vue系列---响应式原理实现及Observer源码解析(七)
2019-10-28 18:40 龙恩0707 阅读(3098) 评论(2) 编辑 收藏 举报阅读目录
- 一. 什么是响应式?
-
二:如何侦测数据的变化?
- 三. Observer源码解析
一. 什么是响应式?
我们可以这样理解,当一个数据状态发生改变的时候,那么与这个数据状态相关的事务也会发生改变。用我们的前端专业术语来讲,当我们JS中的对象数据发生改变的时候,与JS中对象数据相关联的DOM视图也会随着改变。
我们可以先来简单的理解下Vue中如下的一个demo
<!DOCTYPE html> <html> <head> <title>vue响应性的测试</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div>{{ count }}</div> <button @click="changeValue">点击我自增</button> </div> <script type="text/javascript"> var app = new Vue({ el: '#app', data() { return { count: 1 } }, methods: { changeValue() { this.count++; } } }) </script> </body> </html>
如上demo,当我们点击按钮的时候,我们的count值会自增1,即data对象中的count属性值发生改变,它会重新对html页面进行渲染,因此相关联数据对象属性值的视图也会发生改变。
那么Vue中它是如何做到的呢?
想要完成此过程,我们需要做如下事情:
1)侦测对象数据的变化。
2)收集视图依赖了哪些数据。
3)数据变化时,自动通知和数据相关联的视图页面,并对视图进行更新。
2. 如何侦测数据的变化?
数据对象侦测也可以叫数据劫持,vue.js 是采用数据劫持及发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,触发相应的监听回调。当然我们也可以使用ES6中的Proxy来对各个属性进行代理。
2.1 Object.defineProperty() 侦测对象属性值变化
var obj = {}; var value = '初始化值'; Object.defineProperty(obj, 'name', { get() { console.log('监听getter数据值的变化'); return value; }, set(newVlue) { console.log('监听setter数据值的变化'); value = newVlue; } }); console.log(obj.name); obj.name = 'kongzhi'; console.log(obj.name);
如上代码打印效果如下所示:
如上我们可以看到,当我们执行 console.log(obj.name); 获取 obj对象中属性name的值的时候,Object.defineProperty方法会监听obj对象属性值的变化,自动调用get方法,因此首先会打印 "监听getter数据值的变化" 信息出来,接着打印 "初始化值",当我们给 obj.name 设置值的时候,就会自动调用set方法,因此会打印 "监听setter数据值的变化" 信息出来;然后我们打印 console.log(obj.name); 又会自动调用get方法,因此会打印 "监听getter数据值的变化", 最后更新数据,打印出 "kongzhi" 信息。
如上我们已经了解了 Object.defineProperty()方法的基本使用了,因此我们现在可以封装一个数据监听器函数,比如叫它为 Observer. 它的作用是能够对数据对象的所有属性进行监听。如下代码实现:
function Observer(data) { this.data = data; this.init(); } Observer.prototype.init = function() { var data = this.data; // 遍历data对象 Object.keys(data).forEach((key) => { this.defineReactive(data, key, data[key]); }); }; Observer.prototype.defineReactive = function(data, key, value) { // 递归遍历子对象 var childObj = observer(value); // 对对象的属性进行监听 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: true, // 可删除或可修改目标属性 get: function() { return value; }, set: function(newValue) { if (newValue === value) { return; } value = newValue; // 如果新值是对象的话,递归该对象 进行监听 childObj = observer(newValue); } }); }; function observer (value) { if (!value || typeof value !== 'object') { return; } return new Observer(value); } // 调用方式如下: var data = { "name": "kongzhi", "user": { "name": "tugenhua" } }; observer(data); data.name = 'kongzhi2'; console.log(data.name); // 打印:kongzhi2 data.user.name = 'tugenhua22'; console.log(data.user.name); // 打印:tugenhua22
如上代码我们可以监听每个对象属性数据的变化了,那么监听到该属性值变化后我们需要把该消息通知到订阅者,因此我们需要实现一个消息订阅器,该订阅器的作用是收集所有的订阅者。当有对象属性值发生改变的时候,我们会把该消息通知给所有订阅者。
假如我们把该订阅器函数为Dep; 那么基本代码如下:
function Dep() { this.subs = []; } Dep.prototype.addSub = function(sub) { this.subs.push(sub); } Dep.prototype.removeSub = function(sub) { if (this.subs.length) { var index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } } Dep.prototype.depend = function() { Dep.target.addDep(this); } Dep.prototype.notify = function() { // 遍历,通知所有的订阅者 this.subs.forEach((sub) => { sub.update(); }) } Dep.target = null;
如上代码,我们就可以使用addSub方法来添加一个订阅者,或者使用removeSub来删除一个订阅者, 我们也可以调用 notify 方法来通知所有的订阅者。 如上 Object.prototype.defineReactive 代码中我们能监听对象属性值发生改变,如果值发生改变我们需要来通知所有的订阅者,因此上面的代码我们需要改变一些代码,如下所示:
Object.prototype.defineReactive = function(data, key, value) { ..... // 调用管理所有订阅者的类 var dep = new Dep(); // 对对象的属性进行监听 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: true, // 可删除或可修改目标属性 get: function() { // 新增的 if (Dep.target) { dep.depend(); } return value; }, set: function(newValue) { if (newValue === value) { return; } value = newValue; // 如果新值是对象的话,递归该对象 进行监听 childObj = observer(newValue); // 有值发生改变的话,我们需要通知所有的订阅者 dep.notify(); } }); }
如上面的demo,我们已经改变了数据后,我们会使用getter/setter监听到数据的变化,数据变化后,我们会调用Dep类中 notify方法,该方法的作用是遍历通知所有的订阅者,通知完订阅者后,我们需要做什么呢?就是自动帮我们更新页面,因此每个订阅者都会调用Watcher类中的update方法,来更新数据。
因此我们需要实现一个Watcher类,Watcher的作用是派发数据更新,不过真正修改DOM,还是需要使用VNode. VNode我们后面会讲解到。
Watcher是什么?它和Dep是什么关系?
Dep用于依赖收集和派发更新,它收集所有的订阅者,当有数据变动的时候,它会把消息通知到所有的订阅者,同时它也调用Watcher实列中的update方法,用于派发更新。
Watcher 用于初始化数据的watcher的实列。它原型上有一个update方法,用于派发更新。比如调用回调函数来更新页面等操作。
Watcher 简单实现的代码如下:
function Watcher (obj, expOrFn, cb) { this.obj = obj; this.expOrFn = expOrFn; this.cb = cb; // 如果expOrFn是事件函数的话 if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = this.parseGetter(expOrFn); }; // 触发getter,从而让Dep添加自己作为订阅者 this.value = this.get(); } Watcher.prototype.addDep = function(dep) { dep.addSub(this); }; Watcher.prototype.update = function() { var value = this.get(); var oldValue = this.value; if (oldValue === value) { return; } this.value = value; this.cb.call(this.obj, value, oldValue); } Watcher.prototype.get = function() { Dep.target = this; var value = this.getter.call(this.obj, this.obj); return value; }; /* 如下函数的作用:像vue中的 vm.$watch('xxx.yyy', function() {}); 这样的数据能监听到 比如如下这样的data数据: var data = { "name": "kongzhi", "age": 31, "user": { "name": "tugenhua" } }; 我们依次会把data对象中的 'name', 'age', 'user' 属性传递调用该函数。 如果是 'name', 'age', 'user' 这样的,那么 exp 就等于这些值。因此: this.getter = this.parseGetter(expOrFn); 因此最后 this.getter 就返回了一个函数。 当我们在 Watcher 类中执行 this.value = this.get(); 代码的时候 就会调用 getter方法, 因此会自动执行 parseGetter 函数中返回的函数,参数为 data对象,该函数使用了一个闭包,闭包中保存的 参数 exps 就是我们的 'name', 'age', 'user' 及 'user.name' 其中一个,然后依次执行。最后返回的值: obj = data['name'] 或 data['age'] 等等这些,因此会返回值value了。 */ Watcher.prototype.parseGetter = function(exp) { var reg = /[^\w.$]/; if (reg.test(exp)) { return; } var exps = exp.split('.'); return function(obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) { return; } obj = obj[exps[i]]; } return obj; } }
如上Watcher类,传入三个参数,obj 是一个对象属性,expOrFn 有可能是一个函数或者是其他类型,比如字符串等,cb是我们的回调函数,然后原型上分别有 addDep,update,get方法函数。
现在我们需要如下调用即可:
var data = { "name": "kongzhi", "age": 31, "user": { "name": "tugenhua" } }; // 初始化, 对data数据进行监听 new Observer(data); // 变量data对象的所有属性,分别调用 Object.keys(data).forEach((key) => { if (data.hasOwnProperty(key)) { new Watcher(data, key, (newValue, oldValue) => { console.log('回调函数调用了'); console.log('新值返回:' + newValue); console.log('旧值返回:' + oldValue); }); } });
我们可以在控制台修改下data中的值看下是否要调用回调函数,效果如下所示:
2.2 如何侦测数组的索引值的变化
<!DOCTYPE html> <html> <head> <title>vue响应性的测试</title> <meta charset="utf-8"> <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script> </head> <body> <div id="app"> <div v-if="arrs.length > 0" v-for="(item, index) in arrs"> {{item}} </div> </div> <script type="text/javascript"> var app = new Vue({ el: '#app', data() { return { arrs: ['1', '2', '3'] } }, methods: {} }); app.arrs[1] = 'ccc'; // 改变不了的。不是响应性的 </script> </body> </html>
Vue官网文档建议我们使用 Vue.set(arrs, index, newValue) 方法来达到触发视图更新的效果,比如可以改成如下代码即可生效:
// app.arrs[1] = 'ccc'; Vue.set(app.arrs, 1, 'ccc'); // 会生效的
那么vue为何不能监听数组索引的变化?
Vue官方说明的是:由于Javascript的限制。Vue不能检测以下变动的数组:
当你利用索引直接设置一个项时,比如:vm.items[indexOfItem] = newValue;
当你修改数组的长度时:比如 vm.items.length = newLength;
但是我们自己使用 Object.defineProperty 是可以监听到数组索引的变化的,如下代码:
var arrs = [ { "name": "kongzhi111", "age": 30 }, { "name": "kongzhi222", "age": 31 } ]; function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { console.log('调用了getter函数获取值了'); return value; }, set: function(newValue) { if (value === newValue) { return; } value = newValue; console.log('数据发生改变了'); } }) } // 代码初始化调用 defineReactive(arrs[0], 'name', 'kongzhi111'); /* 会先调用 getter方法,会打印 "调用了getter函数获取值了"信息出来。 然后打印:kongzhi111 值了。 */ console.log(arrs[0].name); // 改变数组中第一项name数据 arrs[0].name = "tugenhua"; /* * 会先调用setter方法,打印:"数据发生改变了" 信息出来。 * 然后打印结果为:{name: 'tugenhua', age: 30} */ console.log(arrs[0]);
如下图所示:
但是Vue源码中并没有对数组进行监听,据说尤大是说为了性能考虑。所以没有对数组使用 Object.defineProperty 做监听,我们可以来看下源码就知道了,源码js地址为:src/core/observer/index.js 代码如下所示:
export class Observer { ..... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } }
如上代码可以看到,如果 Array.isArray(value) 是数组的话,就调用 observeArray函数,否则的话调用walk函数,walk函数代码如下所示:
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } export function defineReactive () { .... Object.defineProperty(obj, key, { get: function reactiveGetter () {}, set: function reactiveSetter (newVal) {} } }
因此如果是数组的话,就没有使用 Object.defineProperty 对数据进行监听,因此数组的改变不会有响应性的。
但是数组的一些push等这样的方法会进行重写的,这个晚点再说。因此改变数组的索引也不会被监听到的。那么既然尤大说为了性能考虑,那么我们就可以来测试下,假如是数组的话,我们也使用 Object.defineProperty 来监听下,看下会怎样影响性能的呢?因此我们需要把源码改成如下测试下:
src/core/observer/index.js 对应的代码改成如下:
export class Observer { .... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { /* if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) */ this.walkTest(value); } else { this.walk(value) } } walkTest(values: Array) { for (let i = 0, l = values.length; i < l; i++) { defineReactive(values, values[i]); } } }
如上代码,如果是数组的话,我们依然监听,我们先把源码注释掉,然后添加 walkTest 函数及调用该函数。
然后我们需要在defineReactive函数中的get/set中打印一些信息出来,代码改成如下所示:
export function defineReactive () { ..... Object.defineProperty(obj, key, { get: function reactiveGetter () { // 如下打印是新增的 typeof key === "number" && console.log('getter'); const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 如下打印是新增的 typeof key === "number" && console.log('setter'); const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } } }
然后我们需要写一个测试代码,我们就在源码中的 example/commit/index.html 代码中测试下即可,改成如下代码:
<!DOCTYPE html> <html> <head> <title>Vue.js github commits example</title> <script src="../../dist/vue.js"></script> </head> <body> <div id="demo"> <span v-for="(item, index) in arrs" @click="clickFunc(item, index)"> {{item}} </span> </div> <script type="text/javascript"> new Vue({ el: '#demo', data: { arrs: [1, 2] }, methods: { clickFunc(item, index) { console.log(item, index); this.arrs[index] = item + 1; } } }) </script> </body> </html>
如上代码,我们改完,等页面打包完成后,我们刷新下页面可以打印信息如下所示:
如上我们可以看到,数组里面只有2个元素,长度为2, 但是从上面结果可以看到,数组被遍历了2次,页面渲染一次。
为什么会遍历2次呢?那是因为 在getter函数内部如果是数组的话会调用dependArray(value)这个函数,在该函数内部又会递归循环判断是不是数组等操作。
现在当我们点击2的时候,那么数字就变为3. 效果如下所示:
如上可以看到,会先调用 clickFunc 函数,打印console.log(item, index)信息出来,然后再调用 this.arrs[index] = item + 1; 设置值,因此会调用 setter函数,然后数据更新了,重新渲染页面,又会调用getter函数,数组又遍历了2次。
如果我们的数组有10000个元素的长度的话,那么至少要执行2次,也就是遍历2次10000的,对性能有点影响。这也有可能是尤大考虑的一个因素,因此它把数组的监听去掉了,并且对数组的一些常用的方法进行了重写。因此数组中 push, shift 等这样的会生效,对数组中索引值改变或改变数组的长度不会生效。但是Vue官方中可以使用 Vue.set() 这样的方法代替。
2.3 如何监听数组内容的增加或减少?
Object.defineProperty 虽然能监听到数组索引值的变化,但是却监听不到数组的增加或删除的。
我们继续看如下demo.
var obj = {}; var bvalue = 1; Object.defineProperty(obj, "b", { set: function(value) { bvalue = value; console.log('监听了setter方法'); }, get: function() { console.log('监听了getter方法'); return bvalue; } }); obj.b = 1; // 打印:监听了setter方法 console.log('-------------'); obj.b = []; // 打印:监听了setter方法 console.log('-------------'); obj.b = [1, 2]; // 打印:监听了setter方法 console.log('-------------'); obj.b[0] = 11; // 打印:监听了getter方法 console.log('-------------'); obj.b.push(12); // 打印:监听了getter方法 console.log('-------------'); obj.b.length = 5; // 打印:监听了getter方法 console.log('-------------'); obj.b[0] = 12;
如上测试代码,我们可以看到,给对象obj中的属性b设置值,即 obj.b = 1; 可以监听到 set 方法。给对象中的b赋值一个新数组对象后,也可以监听到 set方法,如:obj.b = []; 或 obj.b = [1, 2]; 但是我们给数组中的某一项设置值,或使用push等方法,或改变数组的长度,都不会调用 set方法。
也就是说 Object.defineProperty()方法对数组中的push、shift、unshift、等这样的方法是无法监听到的,因此我们需要自己去重写这些方法来实现使用 Object.defineProperty() 监听到数组的变化。
下面先看一个简单的demo,如下所示:
// 获得原型上的方法 var arrayProto = Array.prototype; // 创建一个新对象,该对象有数组中所有的方法 var arrayMethods = Object.create(arrayProto); // 对新对象做一些拦截操作 Object.defineProperty(arrayMethods, 'push', { value(...args) { console.log('参数为:' + args); // 调用真正的 Array.prototype.push 方法 arrayProto.push.apply(this, args); }, enumerable: false, writable: true, configurable: true }); // 方法调用如下: var arrs = [1]; /* 重置数组的原型为 arrayMethods 如果不重置,那么该arrs数组中的push方法不会被Object.defineProperty监听到 */ arrs.__proto__ = arrayMethods; /* * 会执行 Object.defineProperty 中的push方法, * 因此会打印 参数为:2, 3 */ arrs.push(2, 3); console.log(arrs); // 输出 [1, 2, 3];
如上代码,首先我们获取原型上的方法,使用代码:var arrayProto = Array.prototype; 然后我们使用Object.create()方法创建一个相同的对象arrayMethods(为了避免污染全局),因此该对象会有 Array.prototype 中的所有属性和方法。然后对该arrayMethods中的push方法进行监听。监听成功后,调用数组真正的push方法,把值push进去。
注意:我们在调用的时候 一定要 arrs.__proto__ = arrayMethods; 要把数组 arrs 的 __proto__ 指向了 arrayMethods 才会被监听到的。
理解__proto__ 是什么呢?
var Kongzhi = function () {}; var k = new Kongzhi(); /* 打印: Kongzhi { __proto__: { constructor: fn() __proto__: { // ... } } } */ console.log(k); console.log(k.__proto__ === Kongzhi.prototype); // ture
如上代码,我们首先定义了一个Kongzhi的构造函数,然后实列化该构造函数,最后赋值给k, 那么new 时候,我们看new做了哪些事情?
其实我们可以把new的过程拆成如下:
var k = {}; // 初始化一个对象 k.__proto__ = Kongzhi.prototype; Kongzhi.call(k);
因此我们可以把如上的代码改成如下也是可以的:
var Kongzhi = function () {}; var k = {}; k.__proto__ = Kongzhi.prototype; Kongzhi.call(k); console.log(k); console.log(k.__proto__ === Kongzhi.prototype); // ture
和上面的效果一样的。
现在我们来理解下 __proto__ 到底是什么?其实在我们定义一个对象的时候,它内部会默认初始化一个属性为 __proto__; 比如如代码可以验证: var obj = {}; console.log(obj);我们在控制台上看下结果就可以看到,当我们访问对象中的某个属性的时候,如果这个对象内部不存在这个属性的话,那么它就会去 __proto__ 里去找这个属性,这个__proto__又会有自己的 __proto__。因此会这样一直找下去,这就是我们以前常说的原型链的概念。
我们可以再来看如下代码:
var Kongzhi = function() {}; Kongzhi.prototype.age = function() { console.log(31) }; var k = new Kongzhi(); k.age(); // 会打印出 31
如上代码,首先 var k = new Kongzhi(); 因此我们可以知道 k.__proto__ = Kongzhi.prototype;所以当我们调用 k.age()方法的时候,首先 k 中没有age()这个方法,
因此会去它的 __proto__ 中去找,也就是 Kongzhi.prototype中去找,Kongzhi.prototype.age = function() {}; 正好有这个方法,因此就会执行。
对__proto__ 理解概念后,我们再来看上面中这句代码:arrs.__proto__ =arrayMethods;也就是可以继续转化变成如下代码:
arrs.__proto__ = Object.create(Array.prototype); 同样的道理,我们使用Object.defineProperty去监听 arrayMethods这个新数组原型的话,如代码:Object.defineProperty(arrayMethods, 'push', {});因此使用arrs.push(2, 3) 的时候也会被 Object.defineProperty 监听到的。因为 arrs.__proto__ === arrayMethods 的。
如上只是一个简单的实现,为了把数组中的所有方法都加上,因此代码改造成如下所示:
function renderFunc() { console.log('html页面被渲染了'); } // 定义数组的常见有的方法 var methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']; // 先获取原型上的方法 var arrayProto = Array.prototype; // 创建一个新对象原型,并且重写methods中的方法 var arrayMethods = Object.create(arrayProto); methods.forEach((method) => { Object.defineProperty(arrayMethods, method, { enumerable: false, writable: true, configurable: true, value(...args) { console.log('数组被调用了'); // 调用数组中的方法 var original = arrayProto[method]; original.apply(this, args); renderFunc(); } }) }); /* * */ function observer(obj) { if (Array.isArray(obj)) { obj.__proto__ = arrayMethods; } else if (typeof obj === 'object'){ for (const key in obj) { defineReactive(obj, key, obj[key]); } } } function defineReactive(obj, key, value) { // 递归循环 observer(value); Object.defineProperty(obj, key, { get: function() { console.log('监听getter函数'); return value; }, set: function(newValue) { // 递归循环 observer(value); if (newValue === value) { return; } value = newValue; renderFunc(); console.log('监听setter函数'); } }); } // 初始化 var obj = [1, 2]; observer(obj); /* * 调用push方法,会被监听到,因此会打印:数组被调用了 * 然后调用 renderFunc 方法,打印:html页面被渲染了 */ obj.push(3); console.log(obj); // 打印:[1, 2, 3] console.log('-----------'); var obj2 = {'name': 'kongzhi111'}; observer(obj2); // 会调用getter函数,打印:监听getter函数, 同时打印值: kongzhi111 console.log(obj2.name); console.log('-----------'); /* 如下会先调用:renderFunc() 函数,因此打印:html页面被渲染了 同时会打印出:监听setter函数 */ obj2.name = 'kongzhi2222';
如上代码演示可以看到,我们对数组中的 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push' 等方法做了重写操作,会监听到数组中这些方法。observer方法中会判断是否是数组,如果是数组的话,obj.__proto__ = arrayMethods; 让该对象的 __proto__ 指向了原型。因此调用数组上的方法就会被监听到。当然__proto__这边有浏览器兼容问题的,这边先没有处理,待会在Vue源码中我们可以看到尤大是使用什么方式来处理__proto__的兼容性的。同时也对对象进行了监听了。如上代码可以看得到。
2.4 使用Proxy来实现数据监听
Proxy是Es6的一个新特性,Proxy会在目标对象之前架设一层 "拦截", 当外界对该对象访问的时候,都必须经过这层拦截,Proxy就相当于这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。
目前Vue使用的都是Object.defineProperty()方法针对对象通过 递归 + 遍历的方式来实现对数据的监控的。
我们也知道,通过该方法,不能触发数组中的方法,比如push,shift等这些,我们需要在vue中重写该方法,因此Object.defineProperty()方法存在如下缺点:
1. 监听数组的方法不能触发Object.defineProperty方法中set操作(如果我们需要监听的话,我们需要重写数组的方法)。
2. 必须遍历每个对象的每个属性,如果对象嵌套比较深的话,我们需要递归调用。
因此为了解决Object.defineProperty() 如上的缺点,我们监听对象数据的变化时,我们可以使用Proxy来解决,但是Proxy有兼容性问题。我们这边先来了解下Proxy的基本使用方法吧!
Proxy基本语法如下:
const obj = new Proxy(target, handler);
参数说明如下:
target: 被代理的对象。
handler: 是一个对象,声明了代理target的一些操作。
obj: 是被代理完成之后返回的对象。
下面我们来看一个如下简单的demo如下:
const target = { 'name': "kongzhi" }; const handler = { get: function(target, key) { console.log('调用了getter函数'); return target[key]; }, set: function(target, key, value) { console.log('调用了setter函数'); target[key] = value; } }; console.log('------') const testObj = new Proxy(target, handler); console.log(testObj.name); testObj.name = '1122'; console.log(testObj.name);
如上代码,我们调用 console.log(testObj.name); 这句代码的时候,会首先调用get()函数,因此会打印:'调用了get函数'; 然后输出 'kongzhi' 信息出来,当执行 testObj.name = '1122'; 这句代码的时候,会调用set()函数,因此会打印: "调用了setter函数" 信息出来,接着打印 console.log(testObj.name); 又会调用get()函数, 因此会打印 "调用了getter函数" 信息出来,接着执行:console.log(testObj.name); 打印信息 '1122' 出来。
如上:target是被代理的对象,handler是代理target的,handler上有set和get方法,当我们每次打印target中的name属性值的时候会自动执行handler中get函数方法,当我们每次设置 target.name属性值的时候,会自动调用handler中的set方法,因此target对象对应的属性值会发生改变。同时改变后的testObj对象也会发生改变。
我们下面再来看一个使用 Proxy 代理对象的demo,如下代码:
function render() { console.log('html页面被渲染了'); } const obj = { name: 'kongzhi', love: { book: ['nodejs', 'javascript', 'css', 'html'], xxx: '111' }, arrs: [1, 2, 3] }; const handler = { get: function(target, key) { if (target[key] && typeof target[key] === 'object') { return new Proxy(target[key], handler); } return Reflect.get(target, key); }, set: function(target, key, value) { render(); return Reflect.set(target, key, value); } }; let proxy = new Proxy(obj, handler); // 会调用set函数,然后执行 render 函数 最后打印 "html页面被渲染了" proxy.name = 'tugenhua'; // 打印:tugenhua console.log(proxy.name); // 会调用set函数,然后执行 render 函数 最后打印 "html页面被渲染了" proxy.love.xxx = '222'; // 打印:222 console.log(proxy.love.xxx); // 会调用set函数,然后执行 render 函数 最后打印 "html页面被渲染了" proxy.arrs[0] = 4; // 打印:4 console.log(proxy.arrs[0]); // 打印: 3 但是不会调用 set 函数 console.log(proxy.arrs.length);
三. Observer源码解析
<!DOCTYPE html> <html> <head> <title>Vue.js github commits example</title> <!-- 下面的是vue源码 --> <script src="../../dist/vue.js"></script> </head> <body> <div id="demo"> <span v-for="(item, index) in arrs"> {{item}} </span> </div> <script type="text/javascript"> new Vue({ el: '#demo', data: { branches: ['master', 'dev'], currentBranch: 'master', commits: null, arrs: [1, 2] } }); </script> </body> </html>
如上demo代码,我们在vue实例化页面后,会首先调用 src/core/instance/index.js 的代码,基本代码如下:
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
如上Vue构造函数中首先会判断是否是正式环境和是否实例化了Vue。然后会调用 this._init(options)方法。因此进入:src/core/instance/init.js代码,主要代码如下:
import { initState } from './state'; export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this; ..... 省略很多代码 initState(vm); ..... 省略很多代码 } }
因此就会进入 src/core/instance/state.js 主要代码如下:
import { set, del, observe, defineReactive, toggleObserving } from '../observer/index' .... 省略很多代码 export function initState (vm: Component) { ..... if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } ..... } .... 省略很多代码 function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} .... 省略了很多代码 // observe data observe(data, true /* asRootData */) }
如上代码我们就可以看到,首先会调用 initState 这个函数,然后会进行 if 判断 opts.data 是否有data这个属性,该data就是我们的在 Vue实例化的时候传进来的,之前实列化如下:
new Vue({ el: '#demo', data: { branches: ['master', 'dev'], currentBranch: 'master', commits: null, arrs: [1, 2] } });
如上的data,因此 opts.data 就为true,有这个属性,因此会调用 initData(vm) 方法,在 initData(vm) 函数中,如上代码我们也可以看到,最后会调用 observe(data, true /* asRootData */) 方法。该方法中的data参数值就是我们之前 new Vue({ data: {} }) 中的data值,我们通过打断点的方式可以看到如下值:
因此会进入 src/core/observer/index.js 中的代码 observe 函数,代码如下所示:
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
执行 observe 函数代码,如上代码所示,该代码的作用是给data创建一个 Observer实列并返回,从最后一句代码我们可以看得到,如上代码 ob = new Observer(value); return ob;
如上代码首先会if 判断,该value是否有 '__ob__' 这个属性,我们value是没有 __ob__ 这个属性的,如果有 __ob__这个属性的话,说明已经实列化过Observer,如果实列化过,就直接返回该实列,否则的话,就实例化 Observer, Vue的响应式数据都会有一个__ob__的属性,里面存放了该属性的Observer实列,目的是防止重复绑定。我们现在先来看看 代码:
if (hasOwn(value, '__ob__')) {} 中的value属性值如下所示:
如上我们可以看到,value是没有 __ob__ 这个属性的,因此会执行 ob = new Observer(value); 我们再来看看new Observer 实列化过程中发生了什么。代码如下:
export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
如上代码我们可以看得到,首先会调用 this.dep = new Dep() 代码,该代码在 src/core/observer/dep.js中,基本代码如下:
export default class Dep { ...... constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null; ......
Dep代码的作用和我们之前讲的一样,就是消息订阅器,该订阅器的作用是收集所有的订阅者。
代码往下执行,我们就会执行 def(value, '__ob__', this) 这句代码,因此会调用 src/core/util/lang.js 代码,
代码如下:
// ...... 省略了很多的代码 import { arrayMethods } from './array'; // ...... 省略了很多的代码 /** @param obj; obj = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" }; @param key "__ob__"; @param val: Observer对象 val = { dep: { "id": 2, subs: [] }, vmCount: 0, value: { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" } }; */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
如上代码我们可以看得到,我们使用了 Object.defineProperty(obj, key, {}) 这样的方法监听对象obj中的 __ob__ 这个key。但是obj对象中又没有该key,因此Object.defineProperty会在该对象上定义一个新属性为 __ob__, 也就是说,如果我们的数据被 Object.defineProperty绑定过的话,那么绑定完成后,就会有 __ob__这个属性,因此我们之前通过了这个属性来判断是否已经被绑定过了。我们可以看下demo代码来理解下 Object.defineProperty的含义:
代码如下所示:
var obj = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" }; var key = "__ob__"; var val = { dep: { "id": 2, subs: [] }, vmCount: 0, value: { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master" } }; Object.defineProperty(obj, key, { value: val, writable: true, configurable: true }); console.log(obj);
打印obj的值如下所示:
如上我们看到,我们通过 Object.defineProperty()方法监听对象后,如果该对象没有该key的话,就会在该obj对象中添加该key属性。
再接着 就会执行如下代码:
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) }
如上代码,首先会判断该 value 是否是一个数组,如果不是数组的话,就执行 this.walk(value)方法,如果是数组的话,就判断 hasProto 是否为true(也就是判断浏览器是否支持__proto__属性),hasProto 源码如下:
export const hasProto = '__proto__' in {};
如果__proto__指向了对象原型的话(换句话说,浏览器支持__proto__),就调用 protoAugment(value, arrayMethods) 函数,该函数的代码如下:
function protoAugment (target, src: Object) { target.__proto__ = src }
其中 arrayMethods 基本代码在 源码中: src/core/observer/array.js 中,该代码是对数组中的方法进行重写操作,和我们之前讲的是一样的。基本代码如下所示:
import { def } from '../util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) });
现在我们再来看之前的代码 protoAugment 函数中,其实这句代码和我们之前讲的含义是一样的,是让 value对象参数指向了 arrayMethods 原型上的方法,然后我们使用 Obejct.defineProperty去监听数组中的原型方法,当我们在data对象参数arrs中调用数组方法,比如push,unshift等方法就可以理解为映射到 arrayMethods 原型上,因此会被 Object.defineProperty方法监听到。因此会执行对应的set/get方法。
如上 methodsToPatch.forEach(function (method) { } 代码中,为什么针对 方法为 'push, unshift, splice' 等一些数组新增的元素也会调用 ob.observeArray(inserted) 进行响应性变化。inserted 参数为一个数组。也就是说我们不仅仅对data现有的元素进行响应性监听,还会对数组中一些新增删除的元素也会进行响应性监听。...args运算符会转化为数组。
比如如下简单的测试代码如下:
function a(...args) { console.log(args); // 会打印 [1] }; a(1); // 函数方法调用 // observeArray 函数代码如下: observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }
如上代码可以看到,我们对使用 push, unshift, splice 新增/删除 的元素也会遍历进行监听, 再回到代码中,为了方便查看,继续看下代码,回到如下代码中:
if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) }
如果我们的浏览器不支持 hasProto, 也就是说 有的浏览器不支持__proto__这个属性的话,我们就会调用copyAugment(value, arrayMethods, arrayKeys); 方法去处理,我们再来看下该方法的源码如下:
/* @param {target} target = { arrs: [1, 2], branches: ["master", "dev"], commits: null, currentBranch: "master", __ob__: { dep: { id: 2, sub: [] }, vmCount: 0, commits: null, branches: ["master", "dev"], currentBranch: "master" } }; @param {src} arrayMethods 数组中的方法实列 @param {keys} ["push", "shift", "unshift", "pop", "splice", "reverse", "sort"] */ function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }
如上代码可以看到,对于浏览器不支持 __proto__属性的话,就会对数组的方法进行遍历,然后继续调用def函数进行监听:
如下 def代码,该源码是在 src/core/util/lang.js 中:
export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
回到之前的代码,如果是数组的话,就会调用 this.observeArray(value) 方法,observeArray方法如下所示:
observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } };
如果它不是数组的话,那么有可能是一个对象,或其他类型的值,我们就会调用 else 里面中 this.walk(value) 的代码,walk函数代码如下所示:
walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }
如上代码,进入walk函数,obj是一个对象的话,使用 Object.keys 获取所有的keys, 然后对keys进行遍历,依次调用defineReactive函数,该函数代码如下:
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() // 获取属性自身的描述符 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters /* 检查属性之前是否设置了 getter / setter 如果设置了,则在之后的 get/set 方法中执行 设置了的 getter/setter */ const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } /* observer源码如下: export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } let childOb = !shallow && observe(val); 代码的含义是:递归循环该val, 判断是否还有子对象,如果 还有子对象的话,就继续实列化该value, */ let childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 如果属性原本拥有getter方法的话则执行该方法 const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { // 如果有子对象的话,对子对象进行依赖收集 childOb.dep.depend(); // 如果value是数组的话,则递归调用 if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { /* 如果属性原本拥有getter方法则执行。然后获取该值与newValue对比,如果相等的 话,直接return,否则的值,执行赋值。 */ const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { // 如果属性原本拥有setter方法的话则执行 setter.call(obj, newVal) } else { // 如果属性原本没有setter方法则直接赋新值 val = newVal } // 继续判断newVal是否还有子对象,如果有子对象的话,继续递归循环遍历 childOb = !shallow && observe(newVal); // 有值发生改变的话,我们需要通知所有的订阅者 dep.notify() } }) }
如上 defineReactive 函数,和我们之前自己编写的代码类似。上面都有一些注释,可以稍微的理解下。
如上代码,如果数据有值发生改变的话,它就会调用 dep.notify()方法来通知所有的订阅者,因此会调用 Dep中的notice方法,我们继续跟踪下看下该对应的代码如下(源码在:src/core/observer/dep.js):
import type Watcher from './watcher' export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; .... notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } .... }
在notice方法中,我们循环遍历订阅者,然后会调用watcher里面的update的方法来进行派发更新操作。因此我们继续可以把视线转移到 src/core/observer/watcher.js 代码内部看下相对应的代码如下:
export default class Watcher { ... update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ... }
如上update方法,首先会判断 this.lazy 是否为true,该参数的含义可以理解为懒加载类型。
其次会判断this.sync 是否为同步类型,如果是同步类型的话,就会直接调用 run()函数方法,因此就会直接立刻执行回调函数。我们下面可以稍微简单的看下run()函数方法如下所示:
run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
如上代码我们可以看到,const value = this.get(); 获取到了最新值,然后立即调用 this.cb.call(this.vm, value, oldValue); 执行回调函数。
否则的话就调用 queueWatcher(this);函数,从字面意思我们可以理解为队列Watcher, 也就是说,如果某一次数据发生改变的话,我们先把该更新的数据缓存起来,等到下一次DOM更新的时候会执行。我们可以理解为异步更新,异步更新往往是同一事件循环中多次修改同一个值,那么Watcher就会被缓存多次。
理解同步更新和异步更新
同步更新:
上面代码中执行 this.run()函数是同步更新,所谓的同步更新是指当观察者的主体发生改变的时候会立刻执行回调函数,来触发更新代码。但是这种情况,在日常的开发中并不会有很多,在同一个事件循环中可能会改变很多次,如果我们每次都触发更新的话,那么对性能来讲会非常损耗的,因此在日常开发中,我们使用的异步更新比较多。
异步更新:
Vue异步执行DOM更新,只要观察到数据的变化,Vue将开启一个队列,如果同一个Watcher被触发多次,它只会被推入到队列中一次。那么这种缓冲对于去除一些重复操作的数据是很有必要的,因为它不会重复DOM操作。
在下一次的事件循环nextTick中,Vue会刷新队列并且执行,Vue在内部会尝试对异步队列使用原生的Promise.then和MessageChannel。如果不支持原生的话,就会使用setTimeout(fn, 0)代替操作。
我们现在再回到代码中,我们需要运行 queueWatcher (this) 函数,该函数的源码在 src/core/observer/scheduler.js 中,如下代码所示:
let flushing = false; let has = {}; // 简单用个对象保存一下wather是否已存在 export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
如上代码,首先获取 const id = watcher.id; 如果 if (has[id] == null) {} 为null的话,就执行代码,如果执行后会把 has[id] 设置为true。防止重复执行。接着代码又会判断 if (!flushing) {};如果flushing为false的话,就执行代码: queue.push(watcher); 可以理解为把 Watcher放入一个队列中,那为什么要判断 flushing 呢?那是因为假如我们正在更新队列中watcher的时候,这个时候我们的数据又被放入队列中怎么办呢?因此我们加了flushing这个参数来表示队列的更新状态。
如上flushing代表的更新状态的含义,那么这个更新状态又分为2种情况。
第一种情况是:flushing 为false,说明这个watcher还没有处理,就找到这个watcher在队列中的位置,并且把最新的放在后面,如代码:queue.push(watcher);
第二种情况是:flushing 为true,说明这个watcher已经更新过了,那么就把这个watcher再放到当前执行的下一位,当前watcher处理完成后,再会立即处理这个新的。如下代码:
let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher);
最后代码就会调用 nextTick 函数的代码去异步执行回调。nextTick下文会逐渐讲解到,我们这边只要知道他是异步执行即可。因此watcher部分代码先理解到此了。