Vue 的响应式原理见
官网的这张图
Vue 的更新是生成render
函数,然后生成虚拟dom
,映射到页面上。左侧的部分其实就是我们watcher
的回调,右下角的data
就是通过我们上面说的Observer
来添加getter
和setter
。watcher
通过dependency
和data
联系在一起,并触发re-render
.
在代码层面上,有 3 个核心的相关类:Observer
、Dep
和Watcher
,Observer
是Dep
和Watcher
之间的桥梁,通过Observer
可以为被观察对象设置Dep
,然后在特定时刻与Dep.target
这个Watcher
互相建立以来,从而建立联系。
Watcher
会在所关注的Dep
发生变化时执行传入的回调,要么是更新数据要么是更新 DOM。
Dep
和Watcher
是通过观察者模式结合的,Dep
是被观察对象,Watcher
是观察者。二者是多对多的关系,一个Dep
可以被多个Watcher
观察,一个Watcher
也可以观察多个Dep
。
Observer
1 | constructor (value: any) { |
上面的arrayMethods
是做了特殊的处理数组Array.prototype
,更改了实现:
1 | const arrayProto = Array.prototype; |
如何监听数组的变化可以参考这个 issue。大体来说是类似Array.prototype
和Object.prototype
这样的原生对象上的 method 是不会受到业务代码的影响的,即使继承一个子类来手动覆盖。
observeArray
用于观察一个数组,为每一个数组元素收集依赖:
1 | observeArray (items: Array<any>) { |
walk
方法用于处理一个 Object,为每一个 key-value 重新定义getter
和setter
:
1 | /** |
defineReactive
在前一篇文章也有所描述。最终达到的效果是在调用new Obersver(value)
或者observe(value)
时,都是重写属性的getter
和setter
。 之后如果有其他地方对 value 的属性值感兴趣,在读取的时候就会收集Watcher
作为依赖,在更改属性值时就会触发watcher
的更新回调。
Dep
这个类比较简单,核心是内部维护一个subs
数组,表示所有对此Dep
感兴趣的Watcher
。
1 | export default class Dep { |
Watcher
先看一下构造函数
1 | constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) { |
看到最后一句会调用get
,它用于执行传入的expOrFn
:
1 | /** |
pushTarget(this)
和popTarget()
是一对操作,前者用于将Dep.target
设置为当前Watcher
,后者用于还原Dep.target
:
1 | const targetStack = []; |
在Dep.depend
方法中会调用Watcher.addDep
,它用于将一个Dep
放到自己的deps
数组,这是这个Watcher
实例所有的依赖Dep
。
1 | /** |
在什么时候会实例化一个Watcher
呢?目前已知的有两处:
在
mountComponent
中会第一次调用new Watcher
,最终的目的是用于更新模板1
2
3
4
5
6
7new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}在
state.js
中的initComputed
也会调用new Watcher
,并会将返回的watcher
实例放入vm._computedWatchers
对象,目的是监听某个值的变化。1
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
接着看一下在Dep.notify
中会调用的Watcher.update
方法:
1 | /** |
update
主要有两个出口,一个是run
另一个是queueWatcher
。如果需要立即执行,那么就会调用run
反之会将其加入一个queue
中。
run
先会调用get
获取最新的值,然后和缓存的 value 对比,发生改变时执行new Watcher
传入的cb
回调函数。
最后看一下queueWatcher
。
queueWatcher
1 | function queueWatcher(watcher: Watcher) { |
会有一个专门的调度器Scheduler
管理queue
,在 flush 时会挨个调用watcher
的run
方法。
1 | /** |
上面的watcher.before()
目前只在mountComponent
时设置:
1 | new Watcher( |
也就是说每次执行watcher.update
之前都会调用一次beforeUpdate
钩子。
还有一个有意思的变量MAX_UPDATE_COUNT
,在开发环境下如果两个watcher
互相触发对方的update
就会陷入死循环,利用这个变量来打破循环。例如下面的代码就会陷入无限循环中:
1 | watch: { |
小结
Vue 的响应式原理核心在于 3 个类,关键之处在于利用Object.defineProperty
来重写属性值的getter
、setter
,利用getter
来收集依赖,利用setter
来触发更新回调,以达到更新 dom 或数据的目的。