总体流程
截取抖音上的渡一教育剪辑号的一张图:
Observer
使用Object.defineProperty将data里的数据循环递归遍历为他们添加访问器属性getter和setter,这是响应式的第一步,数据劫持。当我们访问或修改数据的时候,可以被Vue拦截到,下面是手写Vue2相应式。
function isObject(val) {
return typeof val === "object" && val !== null;
}
function observer(obj) {
let key = Object.keys(obj);
for (let item of key) {
if (isObject(obj[item])) {
observer(obj[item]);
}
Object.defineProperty(obj, item, {
get() {
console.log(`有人读取了属性${item}`);
return obj[key];
},
set(val) {
obj[key] = val;
console.log(`有人修改了属性${item},修改为\${val}`);
},
});
}
}
两个注意点
- 这一套循环递归针对属性设置完了,后续再新加属性、删除属性,Vue监测不到。解决办法就是set和delete前面加个$
- 上面这个是对于对象的监控,不是数组!对于数组,Vue是这样搞的:
Vue把Array的方法重写了,搞了一个自己的对象,可以通知Vue数组发生变化了,为了让开发者使用Array的一些方法,把自己的__proto__指向Array.prototype了。但是还是有问题,单独对某个下标赋值,监控不到。
dep
data返回的是一个对象,dep的作用是为data的每一个属性添加一个dep,dep记录拿一些函数用到了这个属性,举个例子:
const obj = {
a: 1, //dep=[render1,watch1,watch2]
b: 2, //dep=[render2,watch3]
c: {
a: 1,
b: 2,
},
};
依赖收集
在Observer把对象变成响应式对象的时候,为每一个属性添加一个dep,为数组也添加了dep。当读取响应式对象的某个属性的时候,会收集依赖,哪个render或者watch、computed用到了这个属性。
派发更新
当改变属性的时候,就会去通知用到我的render或者watch、computed,数据发生改变了。
watcher
首次页面渲染:每个组件都有一个watch,里面包含了render函数,让render函数运行,会用到数据,被dep记录。watcher会把render先运行一次,收集依赖。
数据发生变化:数据变了,dep去通知用到自己那个属性的render,watch,computed函数。重新运行,重新渲染。
感觉watcher就是把render、watch、computed包起来,dep才可以知道谁在用它。
scheduler(调度器)
考虑一个问题: 如果数据一变,页面就立马去渲染,那效率太低了!所以Vue通过nextTick将用到了watcher(数据变化了的那些dep收集到的watcher)放到了微队列。主线程同步代码执行结束的时候拿出来运行
注意
1.微队列里面不只有watcher,还有一些微任务。
2.重新执行watcher,运行render、watch、computed函数,页面渲染,又用到了响应式数据,重新收集依赖,便于下次派发更新,循环往复。
检验:为什么Proxy比defineProperty好?
1.defineProperty是对象的基本方法,无法对数组的push,shift等方法监听,他只能配置set和get。但是proxy是对整个对象做代理,而且它可以检测到对象属性的删除添加,提供了更多的陷阱函数。
2.defineProperty通过深度遍历,在created生命钩子之前就结束了,无法检测到对象的新增和删除,而且数组的方法那七个方法也检测不到。但是proxy对整个对象代理,可以检测到。