阅读目录javascript
一. 什么是响应式?css
咱们能够这样理解,当一个数据状态发生改变的时候,那么与这个数据状态相关的事务也会发生改变。用咱们的前端专业术语来说,当咱们JS中的对象数据发生改变的时候,与JS中对象数据相关联的DOM视图也会随着改变。html
咱们能够先来简单的理解下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
那么Vue中它是如何作到的呢?java
想要完成此过程,咱们须要作以下事情:node
1)侦测对象数据的变化。
2)收集视图依赖了哪些数据。
3)数据变化时,自动通知和数据相关联的视图页面,并对视图进行更新。react
2. 如何侦测数据的变化?git
数据对象侦测也能够叫数据劫持,vue.js 是采用数据劫持及发布者-订阅者模式,经过Object.defineProperty()来劫持各个属性的setter,getter。在数据变更时发布消息给订阅者,触发相应的监听回调。固然咱们也可使用ES6中的Proxy来对各个属性进行代理。github
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部分代码先理解到此了。