每当问到Vue响应式原理,大家可能都会脱口而出“Vue通过Object.defineProperty方法把data对象的全部属性转化成getter/setter,当属性被访问或修改时通知变化”。然而,其内部深层的响应式原理可能很多人都没有完全理解,网络上关于其响应式原理的文章质量也是参差不齐。
响应式 Vue的响应式原理,可以看作是一种观察者模式。观察者模式是一种实现一对多关系解耦的行为设计模式,它主要涉及两个角色:观察目标、观察者。它的特点是观察者要直接订阅观察目标,观察目标一做出通知,观察者就要进行处理(观察者模式区别于发布/订阅模式,发布/订阅模式中,其解耦能力更近一步,发布者只要做好消息的发布,而不关心消息有没有订阅者订阅。而观察者模式则要求两端同时存在)。
在Vue的响应式原理中,就用到了观察者模式,data即是观察目标,watcher是观察者,依赖收集的过程,其实就是watcher订阅data变化事件的过程。当data(观察目标)发生改变的时候,通过Object.defineProperty定义的setter发送通知给watcher(观察者)。
本文从Vue组件初始化的时候开始讲解,每段代码前都会附带基于Vue v2.6.11 版本的源码Github地址。
initState Vue在初始化组件时,会执行initState,并调用initData对data进行observe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export function initState (vm: Component ) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true ) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
1 2 3 4 5 6 7 8 9 10 function initData (vm: Component ) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} observe(data, true ) }
observe方法会对符合条件的对象执行new Observer(value),包裹成一个响应式对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export function observe (value: any, asRootData: ?boolean ): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__' ) && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array .isArray(value) || isPlainObject(value)) && Object .isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
Observer Observer构造函数会将传入的对象或数组,调用defineReactive并在其中执行Object.defineProperty重写data的setter和getter变成响应式数据。因为Object.defineProperty必须要对对象的每个key使用,因此对象和数组中新添加的下标元素,都不会被Object.defineProperty定义成响应式对象。
不过,Vue会将数组原型上的非纯函数方法,进行了包裹,用于监听数组的变化,包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this .observeArray(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]) } } observeArray (items: Array <any>) { for (let i = 0 , l = items.length; i < l; i++) { observe(items[i]) } } }
到这里位置,data就都被包裹成响应式数据了,接下来就是依赖收集 了
依赖收集 Vue的依赖收集过程,简单的来说,实际就是在组件执行render的时候,通过Object.defineProperty定义data的getter方法,把当前watcher添加到被访问的data的订阅列表中。
computed原理也是类似,在computed执行时,把computed对应的watcher添加到所有被访问过的data的订阅列表中,只要data没有发生变化,那么computed就不会被重新计算,直接使用上次缓存的结果。
组件data初始化完成后,来到了组件mount的阶段,执行new Watcher,监听对象为当前组件的vm,回调为updateComponent。
1 2 3 4 5 6 7 8 Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this , el, hydrating) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el callHook(vm, 'beforeMount' ) let updateComponent updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate' ) } } }, true ) return vm }
Watcher 在Watcher的构造函数中,默认会执行一次this.get(),get方法会将当前Watcher圧栈到targetStack栈顶,并把全局的Dep.target设置为targetStack栈顶元素。
因为组件是递归渲染的,每个子组件开始渲染的时候,都要对应一个新的watcher进行收集,表示当前访问的data都是该子组件的watcher的依赖,当子组件渲染完毕后,后面在访问的data就是父组件的watcher的依赖,所以这里需要采用栈这一数据结构存放当前被收集watcher。
设置好targetStack后,随后执行this.getter.call(vm, vm),也就是之前传入的updateComponent方法,然后调用vm._render方法,在执行render的时候,界面所使用的响应式data的getter就会被访问到,就会被收集到栈顶watcher中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this .getter = expOrFn 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) { } finally { popTarget() } return value } }
1 2 3 4 5 6 7 8 9 10 11 12 13 Dep.target = null const targetStack = []export function pushTarget (target: ?Watcher ) { targetStack.push(target) Dep.target = target } export function popTarget ( ) { targetStack.pop() Dep.target = targetStack[targetStack.length - 1 ] }
来到之前定义响应式数据的defineReactive方法中,看一下getter方法是如何收集依赖的,可以看到,该方法先定义了一个Dep对象,这是用于存储所有订阅了该数据的Watcher的,dep.depend()方法会将当前栈顶watcher放入这个响应式对象的订阅列表中(表示该wathcer订阅了该响应式数据的变化通知),并且也会将当前dep放入栈顶watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() let childOb = !shallow && observe(val) Object .defineProperty(obj, key, { enumerable: true , configurable: true , get : function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array .isArray(value)) { dependArray(value) } } } return value } }) }
到这里依赖收集也就完成了,接下来就是视图更新 了
视图更新 当Vue的响应式数据被修改时,也就是Object.defineProperty的setter方法会被调用,再次来到defineReactive方法中,看一下setter方法是怎样发送通知然后更新视图的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object .getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false ) { return } const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments .length === 2 ) { val = obj[key] } let childOb = !shallow && observe(val) Object .defineProperty(obj, key, { set : function reactiveSetter (newVal) { childOb = !shallow && observe(newVal) dep.notify() } }) }
notify方法中会调用所有watcher的update方法,默认为异步更新,会调用queueWatcher把watcher放入更新队列,在nextTick的时候调用watcher的get()方法更新依赖收集和渲染视图。
1 2 3 4 5 6 7 8 9 notify () { const subs = this .subs.slice() for (let i = 0 , l = subs.length; i < l; i++) { subs[i].update() } }
1 2 3 4 5 6 7 8 9 10 11 update () { if (this .lazy) { this .dirty = true } else if (this .sync) { this .run() } else { queueWatcher(this ) } }
1 2 3 4 5 6 7 8 9 10 11 12 export function queueWatcher (watcher: Watcher ) { const id = watcher.id if (has[id] == null ) { has[id] = true if (!flushing) { queue.push(watcher) } nextTick(flushSchedulerQueue) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function flushSchedulerQueue ( ) { flushing = true let watcher, id queue.sort((a, b ) => a.id - b.id) for (index = 0 ; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } watcher.run() } }
1 2 3 4 5 6 7 run () { if (this .active) { const value = this .get() } }
OK,到此Vue响应式原理和依赖收集的一个完整流程,就走完了。