本文主要经过结合vue官方文档及源码,对vue响应式原理进行深刻分析。
做为vue最独特的特性,响应式能够说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会从新渲染,那么响应式系统的底层细节究竟是怎么一回事呢?javascript
Tips:vue的响应式系统在vue2.0和vue3.0版本中的底层实现有所不一样,简单了来讲就是处理属性的getter/setter部分从Object.defineProperty替换成了Proxy(不过vue3也保留了Object.defineProperty方式用于支持IE浏览器)vue
当一个普通的javascript对象传入vue实例做为data选项时,vue将遍历data的全部属性,并使用Object.defineProperty重写这些属性的getter/setter方法,这些属性的getter/setter对于用户不可见,可是vue能够利用他们来追踪依赖,在属性值被访问和修改时通知变动。每一个组件实例都对应一个watcher实例,它会在组件渲染的过程当中访问过的属性设置为依赖。以后当属性的setter触发时,会通知watcher对关联的组件进行从新渲染。
当一个普通的javascript对象传入vue实例做为data选项时,vue会将其转化为Proxy。首次渲染后,组件将跟踪在渲染过程当中被访问的属性,组件就成了这些属性的订阅者。当proxy拦截到set操做时,将通知全部订阅了该属性的组件进行从新渲染。
Tips:本节采用的源码是v2.6.11java
在vue2.0中,vue的响应式系统是基于数据拦截+发布订阅的模式,主要包含三个部分:react
- Observer:经过Object.defineProperty拦截data属性的setter/getter方法,从而使每一个属性都拥有一个Dep,当触发getter时收集依赖(使用该属性的watcher),当触发setter时通知更新;
- Dep:依赖收集器,用于维护依赖data属性的全部Watcher,并分发更新;
- Watcher:将视图依赖的属性绑定到Dep中,当数据修改时触发setter,调用Dep的notify方法,通知全部依赖该属性的Watcher进行update更新视图,使属性值与视图绑定起来;
- Compile:模板指令解析器,对模板每一个元素节点的指令进行扫描解析,根据指令模板替换属性数据,同时注入Watcher更新数据的回调方法。(本文不涉及Compile的源码部分)
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data 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) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
在数据处理方法中,vue实例的data会做为value参数传递给Observer构造函数,Observer对value参数主要作了以下处理:typescript
- 当value为数组时,首先会将数组进行变异,在copyAugment方法中会将数组的'push','pop','shift','unshift','splice','sort','reverse'七个方法触发dep.notify更新(即通知关联Watcher从新渲染界面,对这块感兴趣的朋友能够看一下array.js的源码)。而后再遍历每一个值执行observe方法;
- 当value为对象时,遍历每一个属性执行defineReactive方法。
针对数组进行处理的boserve方法:express
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 }
简单来讲,observe方法主要用于判断数组元素类型,若是是数组或对象将再次传入Observer构造函数。数组
针对对象进行处理的defineReactive方法:浏览器
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 } // cater for pre-defined getter/setters 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, { 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 }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
该方法主要经过Object.defineProperty定义了value每一个属性的getter/setter方法,在getter中收集依赖,将属性与Watcher实例相关联;在setter中获取数据变动,通知属性关联得Watcher从新渲染视图。同时也会将属性继续传入Observe方法,一层一层向下处理。async
总结:Observer主要经过遍历data对象,并对data对象进行递归处理(这点体如今data对象中嵌套对象也是支持响应式的),遍历递归出来的属性,只针对数组和对象进行下一步处理。其中数组比较特殊,经过数组变异方法监听了数组的7个操做函数,用于通知Watcher从新渲染界面。若是是对象,将遍历该对象的属性,为每一个属性定义getter/setter方法,用于绑定依赖关系和通知Wathcer更新界面。函数
export default class Dep { static target: ?Watcher; id: number; subs: Array; 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() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
总结:从Observer中咱们能够知道一个数据会对应一个Dep,Dep中的subs数组就是用于收集与该数据关联的Watcher,使二者绑定起来。里面还有个Dep.target是全局共享的,他表示目前正在收集依赖的那个Watcher,且同一时间有且只有一个。
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array; newDeps: Array; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm 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 for batching this.active = true this.dirty = this.lazy // for lazy 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 = noop 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 ) } } this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ 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 } /** * Add a dependency to this directive. */ 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) } } } /** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { 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 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) } } } } }
总结:Watcher主要作了以下工做:
- 初始化属性值,主要是deps、newDeps、depIds、newDepIds,分别表示现有依赖和新一轮要收集的依赖,这里的依赖指的就是上面的Dep;
- 经过传入构造函数的expOrFn参数设置getter属性,若是expOrFn是函数,直接设置为getter,若是expOrFn为表达式,则将表达式传入parsePath设置为getter;
- 执行get方法,这里主要用于收集依赖,并获取属性的值,方法最后要调用cleanupDeps来清除依赖。这是由于数据更新以后依赖有可能发生改变,因此要清除后从新收集依赖;
- update方法是触发更新以后调用,这里分为三种状况(1.lazy延迟加载,暂不调用数据更新;2.sync当即调用更新;3.放入队列中,等待下一个事件循环后执行。),数据更新的时候都会调用run方法,run方法首先会从新调用get收集依赖,而后使用this.cb.call更新模板或表达式的值。
Tips:本节采用的源码是vue3.0.5
vue3.0中响应式系统大体分为如下几个阶段
- 初始化阶段:经过组件初始化方法,将data对象转化为proxy对象,并建立一个负责渲染的effect实例;
- get依赖收集阶段:经过解析组件模板,替换对应的data属性数据,这个过程会触发属性的getter,经过stack方法将proxy对象、key和effect存入dep;
- set派发更新阶段:当data属性数据值变化时,触发属性的getter,经过trigger方法,根据proxy和key找到对应的dep,再调用effect执行界面渲染更新。
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect } export function stop(effect: ReactiveEffect) { if (effect.active) { cleanup(effect) if (effect.options.onStop) { effect.options.onStop() } effect.active = false } } let uid = 0 function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effect.active) { return options.scheduler ? undefined : fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return } let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } }
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target) if (!depsMap) { // never been tracked return } const effects = new Set<ReactiveEffect>() const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } } if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { add(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } } const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type, newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } effects.forEach(run) }
未完待续...