数据侦测
思路:先侦测对象的一个属性,再侦测对象的全部属性,再递归侦测子属性
在来解决数组的侦测问题
最后收集依赖
侦测一个属性
vue2中使用使用Object.defineProperty
来监测对象的一个属性,
Object的变化侦测
1. 什么是变化侦测
我们在写vue组件的时候,在模板里面使用的变量,当该变量的值改变的时候,能够直接渲染在页面上,这是数据的响应式原理,如何实现这个功能呢?这其实涉及到很多原理和算法,就包括变化侦测,通过变化侦测,我们就可以侦听到该变量的变化,在这个基础上再实现其他的功能
举个例子:
let obj = {
a: 1,
b: 2,
c: {
m: {
n: 4
}
}
}
当对象obj被侦测了的时候,当obj.a
发生变化的时候,我们就可以侦测的到,即能够侦测到obJ的全部属性,不仅如此,如果obj的属性值是一个对象,我们也要能够侦测的到,即能够侦测的到obj.c.m.n
2. 如何实现对象的变化侦测
思路
对一个需要被侦听的对象,先遍历它的所有属性,并用Obj.defineProperty
来侦听每一个属性,然后再判断每个属性值是否是一个对象,如果是的话还需要遍历这个对象的所有属性
具体实现
observe
-
首先需要有Observe这个函数,它算是变化侦测的一个入口,将需要被侦测的对象传给这个函数
比如:
let obj = { a: 1, b: 2, c: { m: { n: 4 } } } Observe(obj) // 此时Obj即被侦测了!
-
需要判断定的参数值是否是对象或者该对象是否已经被侦测了,当该对象具有
__ob__
时,即表示已经被监测了 -
若没有
__ob__
这个属性时,即没有被侦测,此时需要用到Observer
这个对象(注意后面有“r),在这个对象上对这个对象进行监测 -
具体实现代码
export default function (value) { // 如果value不是对象,什么都不做 if (typeof value != 'object') return; // 定义ob var ob; if (typeof value.__ob__ !== 'undefined') { ob = value.__ob__; } else { ob = new Observer(value); } return ob; }
Observer类
上文提到的,Observer类是要监听该对象的所有属性,具体功能如下:
-
Observer接受一个要侦听的对象,先给这个对象添加一个
__ob__
属性,属性值为此次Observer实例
,表示这个对象的所有属性都已经被监听了,并让这个__ob__
不能被枚举 -
定义一个方法
walk
,利用这个方法遍历对象的属性,使用defineReactive
这个函数监听所有属性 -
实现代码:
class Observer { // value就是被侦听的对象 constructor(value) { // 利用这个属性给这个对象添加__ob__属性,函数在下面 def(value, '__ob__', this, false) // 监听每个属性 this.walk(value) } walk(value) { // 遍历属性,通过这个方法侦听每个属性 for(let k in value) { defineReactive(value, k) } } } const def = function (obj, key, value, enumerable) { Object.defineProperty(obj, key, { value, enumerable, writable: true, configurable: true }); };
defineReactive
利用这个函数,通过defineProperty
对传进来的属性进行监听,并判断属性值是否是对象,是的话再调用observe
这个函数监听该对象,类似于递归调用,但不是递归本身,而是observe
、Observer
、defineReactive
三者循环调用。具体功能如下:
-
接三个参数,data是对象,key是属性,value是属性值,默认等于data[key]
-
调用
Object.defineProperty
监听每个属性,并配置get,set方法,在get中返回value,set中修改value,注意此时的value是传进来的参数,但js会分配给这个参数单独的内存空间,所以每调用一次,参数的地址都是不一样的。 -
利用
observe
,将value传进去,value是对象属性的值,这一步是处理属性值是对象的情况,但在这个函数中不用判断value是否是对象,因为在observe中已经有判断若传进来的不是对象,直接return了 -
另外,当属性被设置了新的值(newValue)的时候,此时会触发set方法,需要判断新的值是否是对象,即调用
observe(newValue)
-
具体代码:
function defineReactive(data, key, value = data[key]) { Object.defineProperty(data, key, { // 可枚举 enumerable: true, // 可以被配置,比如可以被delete configurable: true, get() { return value }, set(newValue) { // newValue就是想要设置的新值 // 即此时没有改变属性值 if (val === newValue) { return; } value = newValue // 判断新值是否是对象 observe(newValue) } }) // 判断设置的值是否是对象 observe(value) }
依赖收集
现在我们已经将对象的变化侦测实现了,即对象属性值发生变化时我们能够侦测的到,之后呢,我们是不是就得去通知所以使用这些对象的函数或模板组件做出相应的改变,前提是我们得知道哪些使用了这些对象,我们把使用这些变化侦测对象的模板或组件等称为依赖
什么是依赖
依赖就是使用了这个数据的模板或者组件
举个例子:
<template>
<div>
{{name}}
</div>
</template>
在上面的这个模板中,使用了name这个响应式变量,所以这个模板就是这个变量name的一个依赖。需要注意的是,我们不是直接收集这些模板的,而是收集一个叫做Watcher
的对象实例,Watcher
与真正模板的重新渲染不是这一章要了解的,我们这一章主要是收集Watcher实例
举个例子:
vm.$watch('a.b.c', callback)
在vue组件中,这段代码表示的是当data.a.b.c
属性发生变化时,会触发callabck
这个回调函数
那其实这里就是用到了这个data.a.b.c
属性,相当于这个属性的一个依赖,我们用Watcher
实例作为依赖的话,就相当于
new Watcher(data, 'a.b.c', callback)
如何收集依赖
当执行new Watcher(data, 'a.b.c', callback)
时,会触发data.a.b.c
的get操作,此时就可以把这个依赖通过get方法收集起来,如果修改了这个属性的话,就会触发set操作,此时就需要通知这些依赖。总的来说,就是在getter中收集依赖,在setter中触发(通知)依赖
具体实现
- 首先,需要一个
Dep
类,这个类是用来管理每一个属性的依赖的,包括添加依赖,触发依赖等。所以需要在每个属性上new一个Dep实例 - 还需要一个
Watcher
类,这个类就是依赖,每使用一个被侦测的对象的属性时,就会new一个Watcher
实例,当被通知到属性值修改后,就会执行回调函数
Dep类
-
Dep类是用来管理属性的依赖的,所以每个属性必须拥有一个dep实例,在
defineReactive
这个函数中,我们会监听每个属性,所以在这里给属性加上一个dep实例最好不过了 -
dep类上有一个数组subs,用来添加每个属性的依赖
-
有一个
depend
方法,在这个方法中,会判断是否有Dep.target这个对象,如果有的话,就表示此时有依赖需要添加,Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行,当我们new一个Watcher实例时,就会给Dep.target设置一个值,依赖添加完成之后,再把Dep.target设置为null -
还有一个
notify
方法,当侦听的属性发生变化时,会通知每一个依赖,出发每一个依赖的update方法 -
具体代码:
class Dep { constructor() { // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。 // 这个数组里面放的是Watcher的实例 this.subs = [] } depend() { if(Dep.target) { this.add(Dep.target) } } addSub(sub) { this.subs.push(sub) } notify() { const subs = this.subs.slice() for(let i = 0; i < subs.length; i++) { subs[i].update() } } }
Watcher类
-
Watcher实例就是依赖,接受三个参数,分别为
target, expression, callback
,target就是监听的对象,expression就是监听对象的某个属性,callback就是这个属性改变时会执行的函数new Watcher(data, 'a.b.c', callback)
-
在这个类中,首先会解析
a.b.c
,得到所使用的属性,此时需要一个parsePath
函数,该函数会返回一个函数(赋值在实例的getter上),并获得所使用的属性,但要注意的是,通过这个函数我们并不知道这个属性属于那个对象,只有通过我们传入一个对象后才知道function parsePath(str) { var segments = str.split('.') return (obj) => { for(let i = 0; i < segments.length; i++) { // 若obj不存在,则表示找不到这个属性 if(!obj) return obj = obj[segments[i]] } return obj } }
-
接着,会调用自身的get方法,意味着进入依赖收集阶段,会将全局的
Dep.target
设置为Watcher本身,并调用自身的getter方法,在这个方法中会触发属性的getter操作,并在此操作中将Watcher收集在dep上,收集结束后要记得将Dep.target
设置为null
-
最后,还需要有一个
update
方法,当属性值改变之后,能够通知到该依赖,并执行这个方法 -
代码如下:
var uid = 0; export default class Watcher { constructor(target, expression, callback) { console.log('我是Watcher类的构造器'); this.id = uid++; this.target = target; // parsePath(expression)返回一个函数,放在getter上,所以getter没有什么特别含义 this.getter = parsePath(expression); this.callback = callback; this.value = this.get(); } update() { this.run(); } get() { // 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段 Dep.target = this; const obj = this.target; var value; // 只要能找,就一直找 try { // 此时要找到obj里expression这个属性值 value = this.getter(obj); } finally {zhih1 Dep.target = null; } return value; } run() { this.getAndInvoke(this.callback); } getAndInvoke(cb) { const value = this.get(); if (value !== this.value || typeof value == 'object') { const oldValue = this.value; this.value = value; cb.call(this.target, value, oldValue); } } };
数组的变化侦测
var obj = {
a: {
m: {
n: 5
}
},
b: 10,
c: {
d: {
e: {
f: 6666
}
}
},
g: [22, 33, 44, 55]
};
像上面这段代码中,对象的属性g是数组的情况下,我们对数组进行push等操作时,这个动作是不会被拦截的,所以通过getter/setter的方式行不通
如何追踪变化
前面Object的变化时靠getter来追踪的,一个属性发生变化,就会触发getter。同理,我们只要能在数组中使用push等操作时被通知,就能实现同样的功能
但在es6之前,js没有提供可以拦截原型方法的能力,所以我们只能用自定义的方法去覆盖原型上的方法。
经过整理,我们发现Array原型上有7个方法可以改变数组自身内容,分别为:push, pop, shift, unshift, split, sort, reverse
,所以,我们可以用一个拦截器,每当数组使用这些方法的时候,其实执行的是拦截器里的自定义的方法,如图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdS9oKpH-1649087186200)(变化侦测.assets/image-20220330164500023.png)]
拦截器设计
-
我们要让数组的原型指向拦截器上,并在拦截器上定义push这些方法,再让拦截器指向
Array.prototype
,这样数组还能使用Array
的其他方法 -
拦截器其实就是一个对象,在这个对象上添加push这些方法,并用
Object.defineProperty()
监听这些属性 -
为了保证原来的push等功能不被剥掉,我们需要备份原方法,添加在拦截器的属性上
-
有三种方法push\unshift\splice能够插入新的内容(有可能是对象),所以需要再observe一下这些新元素
-
定义一个函数
array.js
来设计拦截器,并把拦截器暴露出去具体代码如下:
const arrayPrototype = Array.prototype // 将拦截器原型指向数组原型上 export const arrayMethods = Object.create(arrayPrototype) // 要被改写的7个数组方法 const methodsNeedChange = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; methodsNeedChange.forEach((methodName) => { // 保留原先的方法 const original = arrayPrototype[methodName] // 给拦截器定义方法,def函数上文有介绍过 def(arrayMethods, methodName, function() { // 恢复原来的方法,注意这里this指向数组自身,不能用箭头函数 original.apply(this, arguments) // 把类数组对象变为数组 const args = [...arguments]; // 把数组身上的__ob__属性取出来,要记得数组是一个对象,会在observer的时候添加__ob__属 // 性,并且该属性值是observer实例 const ob = this.__ob__ // 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的 let inserted = []; switch (methodName) { case 'push': case 'unshift': inserted = args; break; case 'splice': // splice格式是splice(下标, 数量, 插入的新项) inserted = args.slice(2); break; } // 判断有没有要插入的新项,让新项也变为响应的,ob.observeArray下文会介绍 if (inserted) { ob.observeArray(inserted); } // 触发时表示数组被更改,需要通知依赖,这里要注意的是!!!比如执行obj.g.push()时,是要通知 // obj的,obj身上也有一个dep ob.dep.notify(); }, false) })
使用拦截器覆盖Array原型并挂载在数组上
-
由于对象和数组的监测方式是不同的,所以在new一个observer实例的时候需要判断传入的value是否是一个数组
-
如果是一个数组的话,就需要将原型添加到拦截器上
-
若数组内可存在对象的话,也是需要被监测的,所以需要observe每一个数组元素,但不能像对象一样使用walk()方法,即此时数组内的某个普通元素变化时不能检测到?????????????????
-
具体代码在observer这个类中:
class Observer { constructor(value) { // 每一个Observer的实例身上,都有一个dep this.dep = new Dep(); // 给数组,对象收集依赖 // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例 def(value, '__ob__', this, false); // console.log('我是Observer构造器', value); // 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object // 检查它是数组还是对象 if (Array.isArray(value)) { // 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods // es6写法 Object.setPrototypeOf(value, arrayMethods); value.__proto__ = arrayMethods // 让这个数组变的observe this.observeArray(value); } else { this.walk(value); } } // 遍历 walk(value) { for (let k in value) { defineReactive(value, k); } } // 数组的特殊遍历 observeArray(arr) { for (let i = 0, l = arr.length; i < l; i++) { // 逐项进行observe observe(arr[i]); } } };
数组不支持_proto_
的情况
当不能使用__proto__
的时候,Vue的做法是直接将拦截器上的方法直接设置在被侦测的数组上
function protoAugment(target, src, key) {
target.__proto__ = src
}
function copyAugument(target, src, keys) {
for(let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
// Observer类部分
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
if(Array.isArray(value)) {
const augument = hasProto ? protoAugment : copyAugument
augument(value, arryMethods, arrayKeys)
} else {
this.walk(value)
}