本文主要經過結合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) }
未完待續...