本文主要通過結合vue官方文檔及源碼,對vue響應式原理進行深入分析。
一、定義
作為vue最獨特的特性,響應式可以說是vue的靈魂了,表面上看就是數據發生變化后,對應的界面會重新渲染,那么響應式系統的底層細節到底是怎么一回事呢?
Tips:vue的響應式系統在vue2.0和vue3.0版本中的底層實現有所不同,簡單了來說就是處理屬性的getter/setter部分從Object.defineProperty替換成了Proxy(不過vue3也保留了Object.defineProperty方式用於支持IE瀏覽器)
vue2.0實現原理
當一個普通的javascript對象傳入vue實例作為data選項時,vue將遍歷data的所有屬性,並使用Object.defineProperty重寫這些屬性的getter/setter方法,這些屬性的getter/setter對於用戶不可見,但是vue可以利用他們來追蹤依賴,在屬性值被訪問和修改時通知變更。每個組件實例都對應一個watcher實例,它會在組件渲染的過程中訪問過的屬性設置為依賴。之后當屬性的setter觸發時,會通知watcher對關聯的組件進行重新渲染。

vue3.0實現原理
當一個普通的javascript對象傳入vue實例作為data選項時,vue會將其轉化為Proxy。首次渲染后,組件將跟蹤在渲染過程中被訪問的屬性,組件就成了這些屬性的訂閱者。當proxy攔截到set操作時,將通知所有訂閱了該屬性的組件進行重新渲染。
二、vue2.0源碼實現
Tips:本節采用的源碼是v2.6.11
在vue2.0中,vue的響應式系統是基於數據攔截+發布訂閱的模式,主要包含三個部分:
- Observer:通過Object.defineProperty攔截data屬性的setter/getter方法,從而使每個屬性都擁有一個Dep,當觸發getter時收集依賴(使用該屬性的watcher),當觸發setter時通知更新;
- Dep:依賴收集器,用於維護依賴data屬性的所有Watcher,並分發更新;
- Watcher:將視圖依賴的屬性綁定到Dep中,當數據修改時觸發setter,調用Dep的notify方法,通知所有依賴該屬性的Watcher進行update更新視圖,使屬性值與視圖綁定起來;
- Compile:模板指令解析器,對模板每個元素節點的指令進行掃描解析,根據指令模板替換屬性數據,同時注入Watcher更新數據的回調方法。(本文不涉及Compile的源碼部分)
Observer
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參數主要做了如下處理:
- 當value為數組時,首先會將數組進行變異,在copyAugment方法中會將數組的'push','pop','shift','unshift','splice','sort','reverse'七個方法觸發dep.notify更新(即通知關聯Watcher重新渲染界面,對這塊感興趣的朋友可以看一下array.js的源碼)。然后再遍歷每個值執行observe方法;
- 當value為對象時,遍歷每個屬性執行defineReactive方法。
針對數組進行處理的boserve方法:
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方法,一層一層向下處理。
總結:Observer主要通過遍歷data對象,並對data對象進行遞歸處理(這點體現在data對象中嵌套對象也是支持響應式的),遍歷遞歸出來的屬性,只針對數組和對象進行下一步處理。其中數組比較特殊,通過數組變異方法監聽了數組的7個操作函數,用於通知Watcher重新渲染界面。如果是對象,將遍歷該對象的屬性,為每個屬性定義getter/setter方法,用於綁定依賴關系和通知Wathcer更新界面。
Dep
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,且同一時間有且只有一個。
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更新模板或表達式的值。
三、vue3.0源碼實現
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執行界面渲染更新。
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
}
}
Track
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
})
}
}
}
Trigger
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)
}
未完待續...