我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
Vue 最终渲染的真实 DOM 是由模板字符串和数据(状态)决定的,一旦状态改变,页面就会自动重新渲染。这一特性是 Vue 的响应式系统赋予的,响应式系统主要围绕数据(状态)做了两件事:(1)收集依赖;(2)变化侦测。
1,什么是收集依赖
如果组件 componentA 使用了数据 dataB 的话,我们就可以说组件 componentA 依赖了数据 dataB。此时,应该把这个依赖收集保存起来,以供后续变化侦测调用触发。
2,什么是依赖
在 Vue 中,有一个专门的类用来表示依赖,Watch,他被定义在 core/observer/watcher.js 中。这个类封装了当状态改变时,Vue 应该做的操作。例如与渲染有关的渲染 watcher。
let updateComponent = () => {
// vm._render() 函数的执行结果是一个 VNode
// vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
vm._update(vm._render(), hydrating)
}
// 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
vm._watcher = new Watcher(vm, updateComponent, noop)
上面这个 watcher 就封装了当组件使用的状态变化时,Vue 应该做的操作 updateComponent,该函数首先执行 render 函数获取最新的虚拟 DOM,然后调用 _update 进行页面的渲染,_update 函数会对新旧虚拟 DOM 进行对比,然后对有差异的地方进行最小程度 DOM 操作。
3,依赖收集到哪里
在 Vue 中,有一个专门用于收集依赖的类,Dep 类,它被定义在 core/observer/dep.js 中。其内部维护了一个数组,用于存储依赖(Watcher 类的实例)。一个 Dep 的实例和一个属性对应,也就是说这个 Dep 实例存储了这个属性的依赖,如果这个属性的值变化了的话,Vue 就会执行这个属性对应 Dep 实例的 notify 方法,该方法会遍历执行 subs 数组中 Watcher 实例的 update 方法。
class Dep {
// 用于收集依赖的数组
subs: Array<Watcher>;
constructor () {
// 初始化保存依赖的数组 subs
this.subs = []
}
// 触发 subs 数组中依赖的更新操作
notify () {
// 数组的 slice 函数具有拷贝的作用
const subs = this.subs.slice()
// 遍历 subs 数组中的依赖项
for (let i = 0, l = subs.length; i < l; i++) {
// 执行依赖项的 update 函数,触发执行依赖
subs[i].update()
}
}
}
4,依赖收集的时机
依赖收集发生在使用这个数据的时候,例如在模板中使用某个数据渲染页面。在 JS 中,有两种方案监控数据被使用,第一种是借助 Object.defineProperty,第二种是借助 ES6 中的 Proxy,Proxy 的解决方案很完美,但是由于浏览器的支持并不理想,所以 Vue 2 中使用的是 Object.defineProperty。
如果数据的某个属性被使用了的话,JS 会调用这个属性对应的 get 函数,我们可以在这个 get 函数中进行依赖的收集,简要的代码如下:
// 原始的 get 操作
const getter = property && property.get
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 触发执行上面拿到的 getter
const value = getter ? getter.call(obj) : val
/ 下面是依赖收集的操作 /
// 如果 Dep 上的静态属性 target 存在的话
if (Dep.target) {
// 向 dep 中添加依赖,依赖是 Watcher 的实例
dep.depend()
if (childOb) {
// childOb.dep 用来存储数组类型值的依赖
childOb.dep.depend()
}
}
// getter 返回值
return value
},
})
上面代码中很有意思的一点是:数组类型值和对象类型值依赖存储的位置并不一样,这是因为数组类型值和对象类型值变化侦测的位置不同,接下来说说变化侦测。
5,变化侦测
依赖收集完成之后,如果数据变化了的话,会触发这个数据对应 dep 实例的 notify 方法,在 notify 方法中执行该数据依赖(Watcher 实例)的 update 方法。
为完成上面的操作,我们需要知道什么时候数据发生了变化,这就是变化侦测。
对象类型值和数组类型值的变化侦测采用了不同的方式。对象类型值借助 Object.defineProperty 中的 set 实现变化侦测。但是由于数组类型的值可以使用原型上的方法(push、pop、sort 等)变更数组,Object.defineProperty 无法监控数组原型方法的使用,所以数组类型值的变化侦测无法使用 Object.defineProperty。Vue 的做法是重写数组类型值的原型方法,这样当调用数组的原型方法时,我们就可以在重写的原型方法中执行相应的操作
6,总结
- 收集依赖和变化侦测都是围绕数据(状态)展开的。
- 对象类型和数组类型的依赖收集都是借助 Object.defineProperty 中的 get 实现的。
- 对象类型和数组类型的变化侦测使用了不同的方案。对象类型使用 Object.defineProperty 中的 set 实现,数组类型的实现方法是重写了原型上的方法,在重写的原型方法中执行相应的变化侦测的逻辑。