Vue的特色之一是响应式,视图随着数据的更新而更新,在视图中修改数据后Vue实例中的数据也会同步更新。内部借助依赖(下文中的Dep类)来实现,数据的获取(即get操做)会触发收集依赖,而对数据赋值(即set操做)会通知依赖数据更新,从新渲染视图。对数据的get/set操做的拦截借助的是ES5的Object.defineProperty
。html
在Vue源码内,Dep类做为依赖,Watcher类则用来收集依赖和通知依赖从新求值。对于在实例化时传入的数据,使用工厂函数defineReactive
令其响应式。而在实例后再经过Vue.set/vm.$set
添加的响应式数据,则须要借助Observer类来使其成为响应式数据,最后也是经过defineReactive
实现响应式。vue
对于每一个响应式数据,会有两个Dep实例,第一个是在defineReactive
中的闭包遍历,用途显而易见。而第二个Dep则在响应式数组的__ob__
属性值中,这个值是Observer实例,其实例属性dep是Dep实例,在执行Vue.set/vm.$set
添加响应式数据后,会通知依赖更新。react
在讲defineReactive
以前,先讲一下这些辅助类的实现和用处。git
咱们都知道,Vue响应式的实现,会在getter中收集响应式数据的依赖,在setter中通知依赖数据更新,从新计算数据而后来更新视图。在Vue内部,使用Dep实例表示依赖,让咱们看一下Dep类是怎么定义的。github
Dep有两个实例属性,一个静态属性。静态属性target
是Watcher
实例,功能是从新求值和通知视图更新,下文咱们会讲到。实例属性id
是Dep实例的惟一标识,无需多说;属性subs
是Watcher实例数组,用于收集Watcher实例,当依赖更新时,这些Watcher实例就会从新求值。express
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 () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
方法addSub
用于添加Watcher
实例到subs
中,方法removeSub
用于从subs
移除Watcher
实例。数组
方法depond
会在收集依赖的时候调用,实际上执行了Watcher的实例方法addDep
,在addDep
内除了调用dep实例的addSup
方法外,还作了避免重复收集Watcher实例的工做。这个方法会在Vue为响应式数据设置的自定义getter中执行。闭包
notify
方法则遍历subs
,执行Watcher实例方法update来从新求值。这个方法会在Vue为响应式数据设置的自定义setter中执行。架构
有人可能有疑问,target
是静态属性,那不是每一个实例的target都同样的?实际上,从新求值的操做在Watcher实例方法get
内实现。在get方法内,会先调用pushTarget
来更新Dep.target
,使其指向当前Watcher实例,以前的`Dep.target
会被保存targetStack
末尾(至关于入栈操做),完成操做后会执行popTarget
函数,从targetStack
取出最后一个元素来还原Dep.target
(至关于出栈操做)。异步
Dep.target = null const targetStack = [] export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }
当依赖更新时,Watcher类会从新求值,并可能触发重渲染。
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // 与渲染相关的watcher if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } }
构造函数接受五个参数,vm
是挂载的Component实例;expOrFn
是观察的属性,当是字符串时表示属性名,是函数时会被当成属性的get方法;cb
是属性更新后执行的回调函数;options
是配置项;isRenderWatcher
表示当前实例是否与渲染相关。
在构造函数内,先将实例属性vm
指向传入的Component实例vm,若是当前Watcher实例与渲染相关,会将其保存在vm._watcher
中。接着将当前实例添加到vm._watchers
中,同时根据传入的配置项options
初始化实例属性。实例属性getter
是监听属性的getter函数,若是expOrFn
是函数,直接赋值,不然会调用parsePath
来获取属性的getter。
parsePath
内部会先使用正则来判断属性名,若是有除数字、字母、.
和$
之外的字符时视为非法属性名,直接返回,因此属性只能是以.
分隔的属性。若是属性名合法,则parsePath
返回一个闭包函数,调用时会传入vm
,即obj
是vm
的引用,这个闭包函数最终的目的是从vm实例里获取属性。
const bailRE = /[^\w.$]/ export function parsePath (path: string): any { 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 } }
初始化完成以后,若是不是计算属性相关的Watcher实例,会调用实例方法get
求值。
执行getter方法求值,完成依赖收集的过程。
方法开始时,执行pushTarget(this)
,将Dep.target
指向当前Watcher实例。而后执行getter
收集依赖,最后将Dep.target
复原,并执行cleanDeps
遍历deps
。在每次求值以后,都会调用cleanupDeps
方法重置依赖,具体如何重置,稍后再讲。
实际上,Dep.target
指向的实例是即将要收集的目标。
getter
的执行,除了会获取值外,还会触发在defineReactive
中为属性设置的getter,完成依赖的收集。
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
的功能是将当前Watcher实例添加到传入的Dep实例属性subs数组里去。
addDep
接受一个Dep实例做为参数,若是 dep.id
没有在集合 newDepIds
之中,则添加。若是不在集合 depIds
中,则将当前实例添加到 dep.subs
中。 简单来讲,这里的操做会避免重复收集依赖,这也是不直接调用dep.addSub(Dep.target)
的缘由。
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) } } }
从这里能够看出来Dep实例和Watcher实例会相互引用。Dep实例将Watcher实例保存在实例属性subs
中,在响应式属性调用setter
时,执行notify
方法,通知Watcher实例从新求值。
Watcher实例将Dep实例保存在集合newDeps
,目的是避免重复收集依赖,同时会执行Dep实例方法addDep
,将当前Watcher实例添加到Dep实例属性subs
中。
对于Watcher来讲,每次求值的依赖并不必定与上一次的相同,在每次执行get
以后,都会调用cleanupDeps
来重置收集的依赖。Watcher有四个实例属性用于记录依赖,分别是newDeps/newDepIds
与deps/depIds
。newDeps
与deps
是保存依赖的数组,newDepIds
与depIds
是保存依赖Id的集合。记录上一次求值依赖的属性是deps/depIds
,记录下一次求值依赖的属性是newDeps/newDepIds
(执行cleanupDeps
时已经调用过getter
从新求值了,因此说是上一次求值,下一次指的是下一次调用get
的时候)。
cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 交换depIds和newDepIds let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() // 交换deps和newDeps tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
首先遍历deps
,若是这次求值的依赖在下一次求值中并不存在,则须要调用removeSub
方法,从subs
数组中移除当前Watcher实例。
接着交换newDeps/newDepIds
与deps/depIds
,并清空交换后的newDeps/newDepIds
。
Dep类的notify
方法用于通知观察者从新求值,该方法内部实际是遍历subs
数组,执行Watcher的update
方法。
update 方法定义以下。当实例与计算属性相关时,xxx。若是不是计算属性相关时,判断是否须要同步触发,同步触发时调用run
,不然执行queueWatcher(this)
,交由调度模块统一调度。
update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }
销毁当前Watcher实例。$watch
方法返回一个函数,函数内部就是Watcher实例调用teardown
方法。
先判断Watcher实例是否在活跃状态。首先要从Vue实例的观察者队列_watchers
中移除当前实例,若是vm
正在销毁,由于性能的问题会跳过这一操做。接着遍历deps
,取消这些Dep实例对当前Watcher实例的订阅。最后令this.active = false
,表示当前Watcher实例已被销毁。
teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }
不管是同步或异步更新,或者是计算属性相关的Wathcer实例,最终求值都是经过getAndInvoke
方法。
getAndInvoke
接收一个回调函数,会在从新求值且值更新后执行。
当新值与当前值不一样时会被断定为值已更新。当值是对象时且this.deep
为真时也断定为值已更新,尽管引用不发生改变,但其属性却可能发生变化,为避免属性发生改变而Watcher判断未更新的状况出现。
getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }
run方法内部只是对getAndInvoke
的封装,传入的回调函数是实例化时传入的函数。执行以前会先判断Watcher实例是否已弃用。
run () { if (this.active) { this.getAndInvoke(this.cb) } }
因为篇幅的缘由,本文只简单分析了辅助类和工厂函数的源码和功能。干巴巴地讲了这么多,如今来稍微捋一下。
Watcher类会保存响应式数据的getter函数,这个getter函数多是实例化参数expOrFn
(当其是函数类型时),也多是执行parsePath(expOrFn)
获取到的getter函数。实例方法update对外暴露,用于从新求值,实际上执行真正求值操做的get方法。方法addDep接受一个Dep实例参数,在执行订阅操做前还会执行两个if判断,避免重复订阅。
Dep类表明依赖,实例属性subs
是Watcher数组,表明订阅了当前Dep实例的观察者实例,depond方法收集依赖,notify方法通知观察者实例从新求值。订阅列表中可能会有与渲染相关的观察者,因此可能会触发重渲染。
Observer类与Vue.set/vm.$set
的联系比较大,因此分析放在后面。