1、什么是变化侦测?
通常,在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染页面,这时如何确定状态中发生了什么变化?
变化侦测就是用来解决这个问题的,它分为两种类型,一种是“推”(push),另一种是拉(pull)。
angular和react的变化侦测属于“拉”,即当状态发生变化时,它不知道那个状态变了,只知道状态可能变了,然后会发送一个信号通知框架,框架内部收到信号后,就会进行对比找出哪些dom需要重新渲染,在angular中称为脏检查,在react中则使用虚拟dom。
而vue框架的变化侦测属于“推”。当状态发生变化时,vue立刻就知道了,而且在一定程度上知道哪些状态变了。因此,它知道的信息更多,也就可以进行更细粒度的更新。
所谓细粒度的更新,也就是说,假如一个状态绑定了好几个依赖,每个依赖表示一个具体的dom节点,那么当这个状态发生变化时,向这个状态的所有依赖发送通知进行dom更新操作,也可以看出“拉”的粒度是最粗的。
2、如何追踪变化?
两种方法,Object.defineProperty和es6的Proxy。由于es6在浏览器中的支持度并不理想,所以vue使用的是第一种。
知道了Object.defineProperty可以侦测对象的变化,那就可以写出以下的代码:
function defineReactive (data, key, val) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { return val }, set: function (newval) { if (val === newval) { return } val = newval } }) }
该函数作用是定义一个响应式数据,也就是在这个函数中进行追踪变化,当从data的key中读取数据时,get函数被触发;当从data的key中设置数据时,set函数被触发。
3、如何收集依赖?
我们之所以要观察数据,目的是当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。
例如:
<template> <h1>{{name}}</h1> </template>
该模版中使用了name,当它发生变化时,要向使用了它的地方发送通知。
实现的方法是,把用到数据name的地方收集起来,然后属性发生变化时,把之前收集好的依赖循环触发一遍。也就是在getter中收集依赖,setter中触发依赖。
4、依赖收集在哪里?
首先想到的是每个key都有一个数组,用来存储当前key的依赖。假设依赖是一个函数,保存在window.target上,现在可以把上面的defineReactive函数改造下:
function defineReactive (data, key, val) { let dep = [] //new Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.push(window.target) return val }, set: function (newval) { if (val === newval) { return } // new for (let i=0;i<dep.length;i++) { dep[i](newval, val) } val = newval } }) }
这里新增了数组dep,用来存储被收集的依赖,然后在set被触发时,循环dep以触发收集到的依赖。
这里可以专门封装一个帮助我们管理依赖的类,用来收集、删除和发送通知的类:
export default class dep { constructor () { this.subs = [] } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if (window.target) { this.addSub(window.target) } } notify () { const subs = this.subs.slice() for (let i=0,l = subs.length;i<1;i++) { subs[i].update() } } } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } }
再改造下之前的函数:
function defineReactive (data, key, val) { let dep = new dep() //update Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.depend() return val }, set: function (newval) { if (val === newval) { return } val = newval // new dep.notify() } }) }
这样就清晰很多了