vue-3.Array的变化侦测
3.1 如何追踪变化?
使用自定义的方法覆盖原生的原型方法。
我们可以用一个拦截器覆盖Array.prototype.之后每当使用Array原型上的方法操作数组时,其实执行的都是拦截器中提供的方法,比如push方法。然后在拦截器中使用原生Array的原型方法去操作数组。
3.2 拦截器
拦截器其实就是一个和Array.prototype一样的object,里面包含的属性一摸一样,只不过这个object中某些可以改变数组自身内容的方法是我们处理过的。
Array原型中可以改变数组自身内容的方法有7个,分别是push,pop,unshift,shift,splice,sort,reverse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto); [ 'push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse' ].forEach(function (method){ const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args){ return original.apply(this, args); }, enumerable: false, writable: true, configurable: true }) })
|
变量arrayMethods继承自Array.prototype,具备其所有功能,在arrayMethods上使用Object.defineProperty方法将那些可以改变数组自身内容的方法进行封装。
所以当使用push方法,实际上使用的是arrayMethods.push,也就是函数mutator。因此我们就可以在mutator函数作一些其他的事,比如发送变化通知。
3.3 使用拦截器覆盖Array原型
有了拦截器之后,想要使他生效,需要去覆盖Array.prototype,但是又不能直接去覆盖,因为这样会污染全局Array。我们只希望拦截那些响应式数组的原型。将数据转换为响应式的,需要通过Observer,所以只需要在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型的数据的原型就好了:
1 2 3 4 5 6 7 8 9 10
| export class Observer{ constructor(value){ this.value = value; if(Array.isArray(value)){ value.__proto__ = arrayMethods; }else{ this.walk(value); } } }
|
3.4 将拦截器方法挂载到数组的属性上
因为不是所以浏览器都支持__proto__属性,因此,如果不能使用__proto__属性,就直接将arrayMethods身上的这些方法设置到被侦测的数组上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { arrayMethods } from './array'
const hasProto = '__proto__' in {}; const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{ constructor(value){ this.value = value; if(Array.isArray(value)){ const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys ); }else{ this.walk(value); }
} } function protoAugment(target, src, keys){ target.__proto__ = src } function copyAugment(target, src, keys){ for(let i=0,l=keys.length; i<l; i++){ const key = keys[i]; def(target, key, src[key]) } }
|
使用hasProto判断浏览器是否支持__proto__:如果支持,则使用protoAugment函数来覆盖原型;如果不支持,则调用copyAugment函数将拦截器中的方法挂载到value上。
3.5 如何收集依赖?
list:[1,2,3,4,5]
不管value是什么,想要获取一个object某个属性的数据,要通过key来读取value,因此在读取list的时候,会触发这个名字叫做list的属性的getter,比如:this.list
Array的依赖和Object一样,也在defineReactive中收集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function defineReactive(data, key, val){ if(typeof val === 'object'){ new Observer(val) } let dep = new Dep(); Object.definedProperty(data, key, { enumerable: true, configurable: true, get: function(){ dep.depend(); return val; }, set: function(newVal){ if(val === newVal){ return; } val = newVal; dep.notify(); } }) }
|
所以,Array在getter中收集依赖,在拦截器中触发依赖
3.6 依赖列表存在哪儿?
vue.js把Array的依赖存放在Observer中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export class Observer{ constructor(value){ this.value = value; this.dep = new Dep();
if(Array.isArray(value)){ const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys ); }else{ this.walk(value); }
} }
|
数组在getter中收集依赖,在拦截器中触发依赖,所以这个依赖保存位置很关键,必须在getter和拦截器中都可以访问到。
之所以将依赖保存在Observer,是因为在getter中可以访问到Observer实例,在Array拦截中可以访问到Observer实例。
3.7 收集依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function defineReactive(data, key, val){ let childOb = observe(val);
let dep = new Dep(); Object.definedProperty(data, key, { enumerable: true, configurable: true, get: function(){ dep.depend(); if(childOb){ childOb.dep.depend(); } return val; }, set: function(newVal){ if(val === newVal){ return; } val = newVal; dep.notify(); } }) } export function observe(value, asRootData){ if(!isObject(value)) { return } let ob; if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){ ob = value.__ob__; }else{ ob = new Observer(value); } return ob }
|
3.8 在拦截器中获取Observer实例
因为Array拦截器是对原型的一种封装,所以可以在拦截器中访问到this。而dep保存在Observer中,所以需要在this上读到Observer的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function def(obj, key, val, enumerable){ Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
export class Observer{ constructor(value){ this.value = value; this.dep = new Dep(); def(value, '__ob__', this); if(Array.isArray(value)){ const augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); }else{ this.walk(value); } } }
|
def函数在value上新增一个不可枚举的属性__ob__,这个属性就是当前Observer实例。这个属性既可以用来在拦截器中访问Observer实例,还可用来标记是否已被Observer转换成响应式数据。
当value被标记来__ob__后可以通过value.__ob__来访问observer实例,如果是Array拦截器,拦截器是原型方法,可以通过this.__ob__来访问Observer实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| [ 'push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse' ].forEach(function (method){ const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args){ const ob = this.__ob__; return original.apply(this, args); }, enumerable: false, writable: true, configurable: true }) })
|
在mutator函数中可以通过this.__ob__来获取Observer实例。
3.9 向数组的依赖发送通知
当侦测到数组变化时,需要向依赖发送通知,首先要能访问到依赖。前面已可以在拦截器中访问Observer实例,只需要在Observer实例中拿到dep属性就可以发送通知了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| [ 'push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse' ].forEach(function (method){ const original = arrayProto[method]; Object.defineProperty(arrayMethods, method, { value: function mutator(...args){ let result = original.apply(this, args); const ob = this.__ob__; ob.dep.notify(); return result }, enumerable: false, writable: true, configurable: true }) })
|
3.10 侦测数组元素的变化
前面说侦测数组的变化指的是数组自身的变化,比如是否新增一个元素,是否删除一个元素,其实数组中object上某个属性发送变化也需要发送通知。比如使用push新增一个元素,这个元素的变化也需要侦测。
在observer新增一些处理,让它可以将array也转换响应式的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export class Observer{ constructor(value){ this.value = value; def(value, '__ob__', this);
if(Array.isArray(value)){ this.observerArray(value); }else{ this.walk(value); } } }
function observerArray(items){ for(let i = 0, l = items.length; i < l; i++){ observe(items[i]) } }
|
observerArray方法作用是循环Array中的每一项,执行observe函数来侦测变化。
observe函数就是将数组的每个元素都执行一遍new Observer,是一个递归过程。
3.11 侦测新增元素的变化
数组中一些方法比如push可以新增内容,新增的内容也需要转换成响应式的来侦测变化,否则出现修改数组无法触发消息等问题。
只需要获取新增元素并使用Observer来侦测他们就行。
3.11.1 获取新增元素
获取新增元素需要在拦截器中数组方法的类型进行判断。如果数组方法是push,unshift,splice(可以新增数组元素的方法),则把参数中新增的元素拿过来,用Observer侦测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| [ 'push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse' ].forEach(function (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; } ob.dep.notify(); return result; })
|
通过swtich对method进行判断,将新增元素取处理,暂存在inserted中
3.11.2 使用observer侦测新增元素
Observer会将自身实例附加到value的ob__属性上,所以被侦测了变化的数据都有一个__ob__属性,数组元素也不例外。
因此可以在拦截器中访问到this.__ob,然后调用__ob__上的observeArray方法就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| [ 'push', 'pop', 'unshift', 'shift', 'splice', 'sort', 'reverse' ].forEach(function (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); ob.dep.notify(); return result; })
|
3.12 关于Array的问题
Array的变化侦测是通过拦截原型的方式实现的。所以:
this.list[0] = 2;
this.list.length = 0;
以上2中修改数组不会触发重新渲染和watch。
vue.js的实现方法决定了无法对以上2中作拦截也就没办法响应。但可以通过:
this.list.splice(0,0,item);
vm.$set(list,0,item);
实现数据响应
3.13 总结
Array追踪变化的方式和Object不一样,他是通过创建拦截器去覆盖数组原型的方式来追踪变化。
为了不污染全局Array.prototype,在Observer中只针对需要侦测数据变化的数组使用__proto__来覆盖原型方法。但__proto__不是所有浏览器都支持它,针对不支持它的浏览,直接循环拦截器,把拦截器中方法设置到数组身上来拦截Array.prototype上的原生方法。
Array收集依赖方式和Object一样,都在getter中收集。但是因为数组要在拦截器中向依赖发送消息,所以把依赖保存在来Observer实例上。
在Observer中对每个侦测数据变化的数据加上__ob__标记,并把this保存在__ob__上,一方面为了标记数据已被侦测(防止重复侦测),另一方面可以通过__ob__拿到Observer实例,从而获取实例上的依赖,以便在拦截器中发送通知向依赖。
除了数组自身变化外,使用observeArray方法将数组每个元素都转换为响应式的并侦测变化。
除了侦测已有数据外,当新增元素时也需要进行变化侦测,根据数组方法提取新增元素,然后使用observeArray方法对新增元素进行变化侦测。
根据下标修改数组元素或者使用length清空数组操作无法拦截。