Vue2 响应式主要是 Observer,Dep 和 Watcher 三部分的通信。
Observer
实例化 Observer 时,会遍历对象(即 data),在其每个属性上调用 defineReactive 函数,该函数的作用就是将这些属性转换成 getter 和 setter。这一步是在 beforeCreate 和 created 两个钩子之间执行的。
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// ...
}
接下来看看 defineReactive 。defineReactive 中会实例化一个 Dep,Dep 可以理解为一个消息中心。数据的变更最终是要通知到 Watcher 的,Dep 中收集了这些数据要通知的 Watcher。当 getter 触发时,当前的 Watcher(Dep.target) 会被加入 dep 中;当 setter 触发时,dep 会调用 notify 方法,通知其收集的所有 Watcher。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
// ...
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// ...
dep.notify()
}
})
}
Dep
Dep 就是一个消息中心,代码也很简单,目的就是收集 Watcher,当数据变更时,调用所有 Watcher 的 update 方法。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
// ...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
Watcher 是一个观察者,数据变化时会调用其 update 方法,更新对应视图。在组件 mount 的时候,会实例化一个 Watcher,每个组件对应一个 Watcher。因此 Vue 每次更新都是以组件为单位,diff 算法也是以组件为单位进行对比,这也是组件必须只有一个根元素的原因。
Watcher 实例化之后,会修改 Dep.target,将其指向自己。接着组件挂载,模板渲染,data 中的值被读取,触发其 getter,Dep 收集当前的 Watcher,形成闭环。
下面代码中,get 方法中的 pushTarget 修改了 Dep.target。value = this.getter.call(vm, vm) 这行代码中,getter 方法其实进行了视图的更新。
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
// ...
}
实例分析
下图是在控制台打印的 data 中的某个属性。其中的 __ob__ 属性,就是实例化的 Observer。__ob__ 中有一个 dep 属性,就是实例化的 Dep。dep 中的 subs 就是收集的 Watcher。
当 a 或 b 修改时,会调用其 setter;然后 dep 会通知 subs 中的 Watcher,即调用 Watcher 的 update 方法。然后 Watcher 更新对应的视图(也可能是 computed 或 Watch)。
总结
整个响应式的过程总结下来就是:
在 data 初始化时,将其属性转换成 getter 和 setter,同时实例化一个 Dep。视图渲染时, getter 触发,Dep 会收集 Watcher;在 setter 触发时,Dep 会通知其收集的 Watcher 更新视图。Wathcer 在组件挂载时实例化,Watcher 与组件一一对应。
再看 Vue 官网这张图就会容易理解了。