这一篇我们来看一下Vue核心的响应性原理,在上一篇我们知道了初始化时Vue会把data选项的数据递归的转换成响应性的数据,具体来说就是给数组和对象创建一个关联的Observer实例,然后对于数组,会拦截它的所有方法,以此来监听数组的变化,对于普通对象,会遍历它自身所有可枚举的属性,将其转换成setter和getter形式,以此来监听某个属性的变化,为什么要这么做呢,我们来看一下。
首先简单介绍一下挂载的逻辑,如果我们传递了模板字符串,那么会进行编译,编译模板这个过程非常复杂,反正最后会将其转换为渲染函数,如果直接是渲染函数了那么就没有这个过程,像我们平时使用Vue单文件的形式开发的话,在打包阶段其实就已经编译好了,渲染函数就是返回虚拟DOM(也就是VNode)的函数,VNode怎么转换成实际的DOM呢,其中涉及到虚拟DOM的patch算法,后面会介绍,只要记得这个过程会获取模板里使用到的数据。接着还会给每个Vue实例创建一个Watcher实例,这部分的代码简化如下:
// 更新组件的方法 let updateComponent = () => { // 执行渲染函数产出VNode,然后调用_update方法进行打补丁 vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
定义了一个更新方法,然后传给Watcher,看Watcher类的实现之前我们先看一下Dep类的实现。
Dep类
dep是一个可观察对象,可以有多个指令订阅它:
let uid = 0 export default class Dep { static target; id; subs; constructor () { this.id = uid++ this.subs = [] } }
定义了两个实例属性,一个静态属性,subs数组就是用来存放订阅它的对象,有四个实例方法:
1.添加订阅者
addSub (sub) { this.subs.push(sub) }
2.删除订阅者
removeSub (sub) { remove(this.subs, sub) }
3.依赖收集
depend () { if (Dep.target) { Dep.target.addDep(this) } }
4.通知订阅者更新
notify () { // 只通知此刻存在的订阅者,调用其update方法 const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
Watcher类
dep的订阅者其实就是Watcher实例:
export default class Watcher { constructor ( vm, expOrFn, cb, options, isRenderWatcher ) { this.vm = vm // 如果是渲染watcher,那么会将该Watcher实例添加到Vue实例的_watcher属性中 if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // 用于批处理的uid this.active = true this.dirty = this.lazy // lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = '' // 从表达式中解析出getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop } } // 执行取值方法,计算当前的值 this.value = this.lazy ? undefined : this.get() } }
watcher做的事情主要是解析表达式、依赖收集、并在表达式的值发生改变时触发回调。Vue把给组件创建的Watcher实例称为渲染watcher。
构造函数里先定义了一堆变量,然后判断表达式的类型,是函数的话那么直接使用该函数作为取值函数,否则调用parsePath方法来解析路径,并返回一个取值函数,这是一个简单的解析方法,只支持对.分隔的字符串进行解析:
const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`) export function parsePath (path) { if (bailRE.test(path)) { return } const segments = path.split('.') return function (obj) { // 遍历路径,一层一层取值 for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
比如实例的data结构如下:
let vm = new Vue({ data: { a: { b: { c: 1 } } } })
那么当我们使用表达式a.b.c来创建Watcher实例的话:
new Watcher(vm, 'a.b.c', () => {})
那么以vm为上下文执行parsePath方法返回的函数时就能成功获取到值1。
构造函数的最后,如果lazy不为true的话会立即执行取值函数get,计算表达式的当前值。
依赖收集
具体到开头的挂载过程,表达式expOrFn就是updateComponent函数,也就是Watcher实例的this.getter,它会在get函数里调用:
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) {} finally { // "touch"每个属性,以便它们都作为依赖项进行跟踪,以便进行深入观察 if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
首先执行了pushTarget方法:
// 当前正在计算执行中的目标watcher // 这是全局唯一的,因为一次只能同时执行一个观察者。 Dep.target = null const targetStack = [] export function pushTarget (target) { targetStack.push(target) Dep.target = target }
Dep.target是一个全局变量,这一步相当于把当前的这个Watcher实例赋值给了Dep.target属性。
随后执行了getter函数,也就是执行updateComponent函数,在vm._render函数中会获取我们定义在data中的数据(如果模板里使用到了的话),此时,就会触发对应
数据的getter函数。
比如:
<div>{{a}}</div>
new Vue({ data: { a: 1 } })
在执行渲染函数的时候会执行vm.a来获取a的值,属性a已经被转换成了getter和setter的形式:
Object.defineProperty(obj, key, { get: function reactiveGetter() { const value = getter ? getter.call(obj) : val // Dep.target就是当前的Watcher实例 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value } })
所以会触发属性a的get函数执行,此时Dep.target就是当前的Watcher实例,那么会执行dep.depend方法,dep是属性a的依赖收集对象,depend方法内会执行Dep.target也就是当前的Watcher实例的addDep方法:
export default class Watcher { addDep (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) } } } }
可以看到这里把属性a的dep添加到了newDeps 集合中,为什么不直接添加到deps中呢?而要区分新旧呢?这其实要结合cleanupDeps方法一起看,这个方法在get方法的最后调用了,用来清理依赖项:
export default class Watcher { cleanupDeps () { let i = this.deps.length // 如果之前依赖某个dep,此时不依赖了,那么要从对应dep里删除该watcher while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // newDepIds转为depIds,并清空旧的depIds let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() // newDeps转为deps,并清空旧的deps tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } }
现在应该很清楚为什么要区分新旧了,因为当前watcher之前的依赖项,现在可能已经不依赖了,那么要找出不再依赖的dep,并从中删除当前watcher,所以需要一个新旧对比的过程。
addDep方法执行完后,属性a的dep里会收集到当前的渲染watcher,渲染watcher里也会保存属性a的dep。
回到a的getter函数,depend函数执行完后,接着:
if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } }
如果属性a的值是数组或对象的话,那么也会创建一个关联的Observer实例:
let childOb = !shallow && observe(val)
这里相当于当前的渲染watcher也会同时订阅a的值对应的dep对象,比如下面这种情况:
new Vue({ data: { a: { b: 1 } } })
渲染watcher的deps数组一共会收集到两个dep实例,一个是属性a的,一个是a的值的。
最后对于a的值为数组的情况还调用了dependArray方法:
function dependArray(value ) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } }
递归遍历数组,对所有对象都进行依赖收集。
总结一下,当我们创建一个Vue实例时,内部会创建一个渲染watcher,watcher实例化时会把自身赋值给全局的Dep.target,然后执行取值函数,也就是会执行渲染函数,如果模板里引用了data的数据,那么就会触发对data数据的读取,相应的会触发对应属性的getter,在getter里会进行依赖收集,也就是会把Dep.target属性指向的watcher收集为依赖添加到它的dep实例中,同样该watcher也会保存该dep,如果该属性存在子observer,那么也会进行依赖收集,如果该属性的值是数组类型,还
会递归遍历数组,给每个对象类型的数组项都进行依赖收集。
接下来回到Watcher实例的get方法,执行完取值方法把依赖收集完毕后:
if (this.deep) { traverse(value) }
如果deep选项配置为true的话,那么会对值调用traverse方法,对于实例化Vue时,该选项是false,所以并不会走这里,我们以一个其他例子来看:
new Vue({ data: { obj: { a: { b: 1 } } }, created() { this.$watch('obj', () => { console.log('变了') }) setTimeout(() => { this.obj.a.b = 2 }, 2000); } })
这个例子中,两秒后我们修改了obj.a.b的属性值,它并不会触发回调,当我们deep设为true时,就会触发:
this.$watch('obj', () => { console.log('变了') }, { deep: true })
这就是上面的traverse函数做的事情,这里的取值函数返回的value就是obj的值。
const seenObjects = new Set() export function traverse (val) { _traverse(val, seenObjects) seenObjects.clear() }
调用了_traverse方法:
export function traverse (val) { _traverse(val, seenObjects) seenObjects.clear() } function _traverse (val, seen) { let i, keys const isA = Array.isArray(val) // 非数组和对象就返回 if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { // 这里我猜测是避免循环引用 const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } // 递归 if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
其实就是一个简单的递归函数,递归遍历了所有的数组项和对象的key,为什么这样就能实现监听深层值的变化呢,很简单,因为读取了所有的属性,也就是触发了它们的getter函数,所以所有属性的dep都收集了当前的watcher实例,那么当然如何一层里的任何一个属性修改了都会触发该watcher实例的更新。
回到get函数,接下来的逻辑:
popTarget() this.cleanupDeps()
get函数的开始调用了pushTarget,依赖收集完毕当然要撤销这个操作:
export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
cleanupDeps方法前面已经说过,不再赘述。
到这里,Watcher实例化的过程就已经结束了,现在我们的依赖都已经收集完毕,那么它们有什么用呢?
触发依赖更新
例子如下:
new Vue({ el: '#app', template: ` <ul> <li v-for="item in list">{{item}}</li> </ul> `, data: { list: [1, 2, 3] }, created() { setTimeout(() => { this.list = [4, 5, 6] }, 5000); } })
先来考考大家,这个例子里面的渲染watcher会收集到几个dep呢?
点击查看答案
2个,list属性一个,list的属性值数组一个
五秒后我们修改了list的值,显然会触发它的setter:
Object.defineProperty(obj, key, { set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val // 值没有变化则直接返回 if (newVal === value || (newVal !== newVal && value !== value)) { return } // #7981: 对于不带setter的访问器属性 if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // 观察新的值 childOb = !shallow && observe(newVal) // 触发更新 dep.notify() } })
首先更新属性值,然后如果新的值是数组或对象的话那么会调用observe方法来将它转成响应式的,这样当我们修改这个新数组或新对象本身时才能触发更新。
最后通知依赖更新,这里的依赖就是渲染watcher,notify方法里会调用watcher的update方法:
export default class Watcher { update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } }
可以看到有三个分支,如果lazy为true的话那么只设置一下dirty属性,这个属性具体有什么作用后面我们如果遇到了再说,如果是同步的,那么会直接执行run函数:
run () { if (this.active) { const value = this.get() if ( value !== this.value || // 即使值相同,深度的watcher和对象/数组上的watcher也应该触发,因为值可能已经发生了变化。 isObject(value) || this.deep ) { // 设置为新值 const oldValue = this.value this.value = value // 用户的watcher,即通过$watch方法或watch选项设置的 if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
这个函数的核心就是调用了get方法更新当前表达式的值,然后当值有变化则会调用回调函数。
当也不是同步的watcher,那么会执行queueWatcher方法,这个涉及到Vue的调度功能,也就是异步批量执行的功能,我们会单独开一篇文章来介绍。
总结一下如何触发依赖更新,当依赖收集完毕后,如果我们更新了某个属性的值,那么该属性值的getter函数会被执行,就会通知保存在该属性值的dep里的所有依赖,调用它们的update方法进行更新。
对于我们开头的例子,也就是会调用渲染watcher的update方法,重新执行取值函数,也就是会执行updateComponent方法来重新执行渲染函数,并进行打补丁更新组件,达到数据更新页面自动更新的效果。