第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的
核心是利用 ES5
的 Object.defineProperty
,这也是 Vue.js
为什么不能兼容 IE8
及以下浏览器的原因。
实现一个数据监听器 Observer
Observe
的功能就是用来监测数据的变化。实现方式是给非VNode
的对象类型数据添加一个Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer
对象实例。Observer
是一个类,它的作用是给对象属性添加getter
和setter
,用于依赖收集
和派发更新
依赖收集 getter
- const dep = new Dep() // 实例化一个 Dep 实例
- 在 get 函数中通过 dep.depend 做依赖收集
Dep
是一个 Class
,它定义了一些属性和方法,它有一个静态属性 target
,这是一个全局唯一 Watcher
【同一时间内只能有一个全局的 Watcher
被计算】。Dep
实际上就是对 Watcher
的一种管理,Dep
脱离 Watcher
单独存在是没有意义的。Watcher
和 Dep
就是典型的观察者设计模式。
Watcher
是一个 Class
,在它的构造函数中定义了一些和 Dep
相关的属性:
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
收集过程
当我们实例化一个渲染 watcher
的时候,首先进入 watcher
的构造函数逻辑,然后执行他的 this.get()
方法,进入 get
函数把 Dep.target
赋值为当前渲染 watcher
并压栈(为了恢复用)。接着执行vm._render()
方法,生成渲染 VNode
,并且在这个过程对 vm
上的数据访问,这个时候就触发数据对象的 getter
(在此期间执行 Dep.target.addDep(this)
方法,将 watcher
订阅到这个数据持有的 dep
的 subs
中,为后续数据变化时通知到哪些 subs
做准备)。然后递归遍历添加所有子项的 getter
。
信息
Watcher
在构造函数中初始化两个 Dep
实例数组。newDeps
代表新添加的 Dep
实例数组,deps
代表上一次添加的 Dep
实例数组。 依赖清空:在执行清空依赖(cleanupDeps
)函数时,会首先遍历 deps
,移除对 dep
的订阅,然后把 newDepsIds
和 depIds
交换,newDeps
和 deps
交换,并把 newDepIds
和 newDeps
清空。考虑场景,在条件渲染时,及时对不需要渲染数据的订阅移除,减少性能浪费。
考虑到 Vue
是数据驱动的,所以每次数据变化都会重写 Render
,那么 vm._render()
方法会再次执行,并再次触发数据
收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter
的时候,能知道应该通知哪些订阅者去做相应的逻辑处理【派发更新】
派发过程
当我们组件中对响应的数据做了修改,就会触发 setter
的逻辑,最后调用 dep.notify()
方法,它是 Dep
的一个实例方法。具体做法是遍历依赖收集中建立的 subs
,也就是 Watcher
的实例数组【subs
数组在依赖收集 getter
中被添加,期间通过一些逻辑处理判断保证同一数据不会被添加多次】,然后调用每一个 watcher
的 update
方法。
update
函数中有个 queueWatcher(this)
方法引入了队列的概念,是 vue
在做派发更新时优化的一个点,它并不会每次数据改变都会触发 watcher
回调,而是把这些 watcher
先添加到一个队列中,然后在nextTick
后执行 watcher
的 run
函数
队列排序保证
- 组件的更新由父到子。父组件创建早于子组件,
watcher
的创建也是 - 用户自定义
watcher
要早于渲染watcher
执行,因为用户自定义watcher
是在渲染watcher
前创建的 - 如果一个组件在父组件
watcher
执行期间被销毁,那么它对应的watcher
执行都可以被跳过,所以父组件的watcher
应该先执行。
run 函数解析
先通过 this.get()
得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep
模式任何一个条件,则执行 watcher
的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值 value
和旧值 oldValue
,这就是当我们自己添加 watcher
时候可以在参数中取到新旧值的来源。对应渲染 watcher
而言,在执行 this.get()
方法求值的时候,会执行 getter
方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行 patch
的过程
简单手写 Vue 的响应式
<input id="input" type="text" />
<div id="text"></div>
let input = document.getElementById("input");
let text = document.getElementById("text");
let data = { value: "" };
Object.defineProperty(data, "value", {
set: function(val) {
text.innerHTML = val;
input.value = val;
},
get: function() {
return input.value;
}
});
input.onkeyup = function(e) {
data.value = e.target.value;
};