经过前面几节的学习,我们已经了解了响应式原理中的几个重要知识,其中特别是 Observer、Dep 及 Watcher 类等。这一节我们整体串联起来描述响应式的整个核心过程和原理,所以强烈推荐先学习前几节的内容。文章有点长,但是配了大量图来辅助解说,不用紧张:)。
Observer 类详解、Dep 类详解、Watcher 类详解等,响应式原理的系列文章(非常重要!)
经过前面的学习,我们知道除了每个组件实例都对应一个 watcher 实例外,计算属性(computed)和侦听属性(watch)本质也是依赖 Watcher 实例实现的。为便于描述,我们分别给这三类 Watcher 实例命名为渲染 watcher(renderWatcher),计算属性 watcher(computedWatcher)和侦听属性 watcher(watchWatcher)。
通常我们编写的使用数据、计算属性和侦听属性的代码类似如下:
new Vue({
el: '#app',
// 数据对象
data: {
id: 999,
name: 'java',
subObj: {
code: 'js',
rank: 3
}
},
// 计算属性
computed: {
tipMsg: function() {
return 'hi, '+this.name+', your Uid:'+this.id;
},
fullname: function() {
return 'dev '+this.name;
}
},
// 侦听属性
watch: {
id: function(val, oldVal) {
console.log('Id is changed: '+val)
}
}
})
通过前面 “Dep 类详解” 中我们知道,数据在初始化时经过 observe 和 defineReactive 函数等处理后关联了一些 Observer 对象和 Dep 对象,并且设置了响应式的 getter\setter 方法(如下图)。
其中每个对象关联了 Observer 对象(__ob__),每个对象属性关联了 Dep 对象(dep)。
这里我们主要研究页面渲染时的数据引用,它是典型的使用场景。我们先简单回顾下 renderWatcher,它在 mountComponent 函数中被实例化:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
updateComponent 为最终的渲染函数,在实例化对象时它被当作 expOrFn 传入 Watcher 的构造函数,那么它会被保存在 renderWatcher 的 getter 属性上,并在 renderWatcher.get 成员方法中被调用。在这个 get 方法中,会通过 pushTarget 设置 Dep.target 为本 renderWatcher 对象,然后执行 renderWatcher.getter 属性指向的方法,即执行渲染函数 updateComponent,渲染过程中会引用响应式对象的属性。
get () {
pushTarget(this)
...
// 渲染watcher 中,this.getter 就是 updateComponent
// 计算属性watcher 中,this.getter 就是用户定义的求值函数
value = this.getter.call(vm, vm)
...
popTarget()
this.cleanupDeps()
}
return value
}
页面在被渲染时,会引用对象属性,调用属性的 reactiveGetter 方法,代码如下。在这个场景下 Dep.target 为真,然后执行属性关联的 dep 对象 depend 方法,此时它就等价于执行了 renderWatcher.addDep(dep),这样就把 renderWatcher 依赖的数据属性的 dep 对象保存到了 Watcher.newDeps 里,同时 Dep.subs 里面也保存有该 renderWatcher,这就完成了依赖收集。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 等价于 Dep.target.addDep(dep)
// 或 等价于 renderWatcher.addDep(dep)
dep.depend()
if (childOb) {
// 属性值为对象,收集值对象的 childOb.dep
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {...}
})
在更新对象属性时(this.name = xxx),触发属性的响应式 setter 方法,代码如下,在设置新的值后,调用关联的 dep 对象的 notify 方法派发通知,在这个方法中遍历所有订阅的 Watcher 对象(dep.subs数组变量中)并调用其 update 方法。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {...},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
...
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 对属性值附加 Observer对象,并处理为响应式数据对象
childOb = !shallow && observe(newVal)
// 派发更新通知
dep.notify()
}
})
update 方法根据不同情况做不同响应,renderWatcher 中会在下一个任务调度中执行页面重新渲染;computedWatcher 则把对象状态属性 dirty 设为真,在被引用时再求值;watchWatcher 则会在下一个任务调度中执行用户定义的回调函数,关于具体的任务调度,我们后面会用一个小节来研究学习。
在前面 “计算属性与侦听属性初始化浅析” 和 “Watcher 类详解” 中我们学习了计算属性响应式的处理,用户定义的返回值的求值函数最终赋值给了 computedWatcher.getter 成员变量,最后的本质同样是调用 Object.defineProperty 定制了 getter\setter 方法,我们通过图片简单回顾下:
我们看到计算属性是没有对应的 Dep 对象与之关联的,这与数据对象的处理很大不同,而且它还没有 setter 方法,每个计算属性只有一个 computedWatcher 与之关联。
为了便于描述我们仍然以页面渲染为引用场景,前面章节我们学习了计算属性的初始化流程,这里我们直接上代码,来看看计算属性的 getter 方法:
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 调用 watcher.get 求值
watcher.evaluate()
}
if (Dep.target) {
// Dep.target 收集 watcher.deps 为依赖
watcher.depend()
}
return watcher.value
}
}
在 getter 方法中,获取与计算属性关联的 computedWatcher,如果首次引用或者是依赖的数据有更新时,computedWatcher.dirty 都为 true,这就会执行 computedWatcher,它内部会执行 computedWatcher.get 方法。在这个 get 方法内,会把 computedWatcher 赋值给 Dep.target,然后执行 computedWatcher.getter 求值,这个过程中就会收集到计算属性依赖的变量的 Dep 对象,这就完成了 computedWatcher 的依赖收集。
在计算属性的依赖收集结束后,出栈恢复 Dep.target 为 renderWatcher,然后调用 computedWatcher.depend,它的作用就是让 renderWatcher 收集所有 computedWatcher.deps 为依赖,因为计算属性没有与之关联的 Dep 对象,这个我们在上一节 Watcher 类详解中有详细讲解,通过图片我们简单回顾下。
可以看到,计算属性的依赖收集有两轮,第一轮是 computedWatcher 自身对于数据 Dep 的收集,第二轮才是使用计算属性的其他 Dep.target(比如 renderWatcher)对数据 Dep 的收集。
计算属性因为没有 setter,我们不能主动修改它,它的值只有在它依赖的数据被改变时随之改变。当依赖的数据更新时,数据的 Dep 会 notify 派发通知到订阅的 computedWatcher,在 computedWatcher.update 中设置 computedWatcher.dirty 为 true 即完成了对 computedWatcher 的通知。
在收集依赖环节我们知道 renderWatcher 也会收集对数据的依赖,就是第二轮的收集操作,那么在派发通知的时候肯定也要通知 renderWatcher.update,这个方法中它会把自身放入调度队列,在下一轮任务调度时重新渲染页面,这个流程如上图所示。
在前面 “计算属性与侦听属性初始化浅析” 中我们同样学习了侦听属性的处理,它没有被动的依赖收集环节,在初始化时通过调用 Vue.prototype.$watch 主动收集依赖,我们简单看看它的源码:
Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,
options?: Object): Function {
...
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
在实例化 Watcher 时,侦听属性的键作为 expOrFn 传入构造函数,cb 为用户定义的回调函数,在Watcher 构造函数中把字符串的键通过调用函数 parsePath 转化为 getter 函数,它只是一个简单的访问属性值的函数。经过处理后的侦听属性每个元素都对应一个 watchWatcher,如下图所示:
因为 watchWatcher.lazy 为 false,所以此刻立即执行 watchWatcher.get 方法求值,这样就把依赖的属性的 Dep 对象存入 watchWatcher.deps 中完成依赖收集。这个过程相较于前两种情况来说简单很多。
与前面相同,数据有更新时同样会主动调用 watchWatcher.update 派发通知,它会把自身放入调度队列,在下一轮任务调度中执行 watchWatcher.run 方法:
run () {
if (this.active) {
const value = this.get() // <--------
if (value !== this.value || isObject(value) ||this.deep) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
因为 watchWatcher.user 在实例化时设置为 true,所以在这个方法中会主动执行用户定义的 cb 回调函数,这样就完成了侦听的响应过程。
这节内容主要从多个不同类型的 Watcher 为切入点来研究学习 Vue 的响应式原理,虽然类型不同,但是它们的核心还是不离前面提到的那几个类和函数,比如 Dep\Watcher\Observer 类,Object.defineProperty\observe 等函数,这些是响应式的核心。因为前面有章节已经详细阐述过,且配有大量形象的图片说明,这节内容有些环节简要带过。如果有什么不明白的地方,请先阅读前面几节内容吧~
Vue2源码学习笔记 - 11.响应式原理—Observer 类详解
Vue2源码学习笔记 - 12.响应式原理—Dep 类详解
Vue2源码学习笔记 - 13.响应式原理—Watcher 类详解
或者从本专栏 第七节 的 响应式原来-基础 开始阅读。