Vue监听数据对象变化源码
监听数据对象变化,最容易想到的是建立一个需要监视对象的表,定时扫描其值,有变化,则执行相应操作,不过这种实现方式,性能是个问题,如果需要监视的数据量大的话,每扫描一次全部的对象,需要的时间很长。当然,有些框架是采用的这种方式,不过他们用非常巧妙的算法提升性能,这不在我们的讨论范围之类。
vue 中数据对象的监视,是通过设置 es5 的新特性(es7 都快出来了,es5 的东西倒也真称不得新)object.defineproperty() 中的 set、get 来实现的。
目标
与官方文档第一个例子相似,不过也有简化,因为这篇只是介绍下数据对象的监听,不涉及文本解析,所以文本解析相关的直接舍弃了:
<div id="app"></div> var app = new vue({ el: 'app', data: { message: 'hello vue!' } });
浏览器显示:
hello vue!
在控制台输入诸如:
app.message = 'changed!'
之类的命令,浏览器显示内容会跟着修改。
object.defineproperty
引用 mdn 上的定义:
object.defineproperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
与此相生相伴的还有一个 object.getownpropertydescriptor():
object.getownpropertydescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
下面的例子用一种比较简单、直观的方式来设置 setter、getter:
var dep = []; function definereactive(obj, key, val) { // 有自定义的 property,则用自定义的 property var property = object.getownpropertydescriptor(obj, key); if(property && property.configurable === false) { return; } var getter = property && property.get; var setter = property && property.set; object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function() { var value = getter ? getter.call(obj) : val; dep.push(value); return value; }, set: function(newval) { var value = getter ? getter.call(obj) : val; // set 值与原值相同,则不更新 if(newval === value) { return; } if(setter) { setter.call(obj, newval); } else { val = newval; } console.log(dep); } }); }
var a = {}; definereactive(a, 'a', 12); // 调用 getter,12 被压入 dep,此时 dep 值为 [12] a.a; // 调用 setter,输出 dep ([12]) a.a = 24; // 调用 getter,24 被压入 dep,此时 dep 值为 [12, 24] a.a;
observer
简单说过 object.defineproperty 之后,就要开始扯 observer 了。observer,中文解释为“观察者”,观察什么东西呢?观察对象属性值的变化。故此,所谓 observer,就是给对象的所有属性加上 getter、setter,如果对象的属性还有属性,比如说 {a: {a: {a: 'a'}}},则通过递归给其属性的属性也加上 getter、setter:
function observer(value) { this.value = value; this.walk(value); } observer.prototype.walk = function(obj) { var keys = object.keys(obj); for(var i = 0; i < keys.length; i++) { // 给所有属性添加 getter、setter definereactive(obj, keys[i], obj[keys[i]]); } }; var dep = []; function definereactive(obj, key, val) { // 有自定义的 property,则用自定义的 property var property = object.getownpropertydescriptor(obj, key); if(property && property.configurable === false) { return; } var getter = property && property.get; var setter = property && property.set; // 递归的方式实现给属性的属性添加 getter、setter var childob = observe(val); object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function() { var value = getter ? getter.call(obj) : val; dep.push(value); return value; }, set: function(newval) { var value = getter ? getter.call(obj) : val; // set 值与原值相同,则不更新 if(newval === value) { return; } if(setter) { setter.call(obj, newval); } else { val = newval; } // 给新赋值的属性值的属性添加 getter、setter childob = observe(newval); console.log(dep); } }); } function observe(value) { if(!value || typeof value !== 'object') { return; } return new observer(value); }
watcher
observer 通过设置数据对象的 getter、setter 来达到监听数据变化的目的。数据被获取,被设置、被修改,都能监听到,且能做出相应的动作。
现在还有一个问题就是,谁让你监听的?
这个发出指令的就是 watcher,只有 watcher 获取数据才触发相应的操作;同样,修改数据时,也只执行 watcher 相关操作。
那如何讲 observer、watcher 两者关联起来呢?全局变量!这个全局变量,只有 watcher 才做修改,observer 只是读取判断,根据这个全局变量的值不同而判断是否 watcher 对数据进行读取,这个全局变量可以附加在 dep 上:
dep.target = null;
根据以上所述,简单整理下,代码如下:
function watcher(data, exp, cb) { this.data = data; this.exp = exp; this.cb = cb; this.value = this.get(); } watcher.prototype.get = function() { // 给 dep.target 置值,告诉 observer 这是 watcher 调用的 getter dep.target = this; // 调用 getter,触发相应响应 var value = this.data[this.exp]; // dep.target 还原 dep.target = null; return value; }; watcher.prototype.update = function() { this.cb(); }; function observer(value) { this.value = value; this.walk(value); } observer.prototype.walk = function(obj) { var keys = object.keys(obj); for(var i = 0; i < keys.length; i++) { // 给所有属性添加 getter、setter definereactive(obj, keys[i], obj[keys[i]]); } }; var dep = []; dep.target = null; function definereactive(obj, key, val) { // 有自定义的 property,则用自定义的 property var property = object.getownpropertydescriptor(obj, key); if(property && property.configurable === false) { return; } var getter = property && property.get; var setter = property && property.set; // 递归的方式实现给属性的属性添加 getter、setter var childob = observe(val); object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function() { var value = getter ? getter.call(obj) : val; // 如果是 watcher 监听的,就把 watcher 对象压入 dep if(dep.target) { dep.push(dep.target); } return value; }, set: function(newval) { var value = getter ? getter.call(obj) : val; // set 值与原值相同,则不更新 if(newval === value) { return; } if(setter) { setter.call(obj, newval); } else { val = newval; } // 给新赋值的属性值的属性添加 getter、setter childob = observe(newval); // 按序执行 dep 中元素的 update 方法 for(var i = 0; i < dep.length; i++) { dep[i].update(); } } }); } function observe(value) { if(!value || typeof value !== 'object') { return; } return new observer(value); }
var data = {a: 1}; new observer(data); new watcher(data, 'a', function(){console.log('it works')}); data.a =12; data.a =14;
上面基本实现了数据的监听,bug 肯定有不少,不过只是一个粗糙的 demo,只是想展示一个大概的流程,没有扣到非常细致。
dep
上面几个例子,dep 是个全局的数组,但凡 new 一个 watcher,dep 中就要多一个 watcher 实例,这时候不管哪个 data 更新,所有的 watcher 实例的 update 都会执行,这是不可接受的。
dep 抽象出来,单独搞一个构造函数,不放在全局,就能解决了:
function dep() { this.subs = []; } dep.prototype.addsub = function(sub) { this.subs.push(sub); }; dep.prototype.notify = function() { var subs = this.subs.slice(); for(var i = 0; i < subs.length; i++) { subs[i].update(); } }
利用 dep 将上面的代码改写下就好了(当然,此处的 dep 代码也不完全,只是一个大概的意思罢了)。
vue 实例代理 data 对象
官方文档中有这么一句话:
每个 vue 实例都会代理其 data 对象里所有的属性。
var data = { a: 1 }; var vm = new vue({data: data}); vm.a === data.a // -> true // 设置属性也会影响到原始数据 vm.a = 2 data.a // -> 2 // ... 反之亦然 data.a = 3 vm.a // -> 3
这种代理看起来很麻烦,其实也是可以通过 object.defineproperty 来实现的:
function vue(options) { var data = this.data = options.data; var keys = object.keys(data); var i = keys.length; while(i--) { proxy(this, keys[i]; } } function proxy(vm, key) { object.defineproperty(vm, key, { configurable: true, enumerable: true, // 直接获取 vm.data[key] 的值 get: function() { return vm.data[key]; }, // 设置值的时候直接设置 vm.data[key] 的值 set: function(val) { vm.data[key] = val; } }; }
捏出一个 vue,实现最初目标
var vue = (function() { var watcher = function watcher(vm, exp, cb) { this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); }; watcher.prototype.get = function get() { dep.target = this; var value = this.vm._data[this.exp]; dep.target = null; return value; }; watcher.prototype.adddep = function adddep(dep) { dep.addsub(this); }; watcher.prototype.update = function update() { this.run(); }; watcher.prototype.run = function run() { this.cb.call(this.vm); } var dep = function dep() { this.subs = []; }; dep.prototype.addsub = function addsub(sub) { this.subs.push(sub); }; dep.prototype.depend = function depend() { if(dep.target) { dep.target.adddep(this); } }; dep.prototype.notify = function notify() { var subs = this.subs.slice(); for(var i = 0; i < subs.length; i++) { subs[i].update(); } }; dep.target = null; var observer = function observer(value) { this.value = value; this.dep = new dep(); this.walk(value); }; observer.prototype.walk = function walk(obj) { var keys = object.keys(obj); for(var i = 0; i < keys.length; i++) { definereactive(obj, keys[i], obj[keys[i]]); } }; function definereactive(obj, key, val) { var dep = new dep(); var property = object.getownpropertydescriptor(obj, key); if(property && property.configurable === false) { return; } var getter = property && property.get; var setter = property && property.set; var childob = observe(val); object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function reactivegetter() { var value = getter ? getter.call(obj) : val; if(dep.target) { dep.depend(); if(childob) { childob.dep.depend(); } } return value; }, set: function reactivesetter(newval) { var value = getter ? getter.call(obj) : val; if(newval === value) { return; } if(setter) { setter.call(obj, newval); } else { val = newval; } childob = observe(newval); dep.notify(); } }); } function observe(value) { if(!value || typeof value !== 'object') { return; } return new observer(value); } function vue(options) { var vm = this; this._el = options.el; var data = this._data = options.data; var keys = object.keys(data); var i = keys.length; while(i--) { proxy(this, keys[i]); } observe(data); var elem = document.getelementbyid(this._el); elem.innerhtml = vm.message; new watcher(this, 'message', function() { elem.innerhtml = vm.message; }); } function proxy(vm, key) { object.defineproperty(vm, key, { configurable: true, enumerable: true, get: function proxygetter() { return vm._data[key]; }, set: function proxysetter(val) { vm._data[key] = val; } }); } return vue; })();
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script type="text/javascript" src="vue.js"></script> </head> <body> <div id="app"></div> <script type="text/javascript"> var app = new vue({ el: 'app', data: { message: 'aaaaaaaaaaaaa' } }); </script> </body> </html>
参考资料:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。