?最近學習了下Vue3的源碼,抽空寫一些自己對3.x源碼的解讀,同時算是學習的一個總結吧,也能加深自己的印象。
?就先從3.x的響應式系統說起吧。
回憶
?首先大概回憶一下2.x的響應式系統,主要由這幾個模塊組成,Observer,Watcher,Dep。
Observer負責通過defineProperty劫持Data,每個Data都各自在閉包中維護一個Dep的實例,用于收集依賴著它的Watcher。Dep維護一個公共的Target屬性,用于保存當前的需要被收集依賴的Watcher。每次Data被劫持的getter執行的時候,如果Dep.Target!==undefine
, dep和Watcher實例就互相收集對方~
?2.x的響應式系統其實是圍繞著Watcher,也可以說圍繞著watch API的,包括render是一個renderWatcher,computed是通過lazyWatcher實現。這并不是一個好的設計模式,不符合六個設計原則的(單一職責原則,開閉原則)。而響應式系統也無法獨立出來。
對比
?那么3.x是怎樣實現這一塊的內容的呢。
?首先3.x響應式系統相關的代碼在packages/reactivity/src里。3.x的響應式系統的核心由兩個模塊構成: effect, reactive。
?reactive模塊的功能比較簡單,就是給數據設置代理,類似于2.x的Observer,不同的點在于是用的Proxy去做代理。
?effect模塊,傳入一個函數,然后讓這個函數需要被響應式數據影響,目前具體在3.x中包括,watch API,computed API,還有組件的更新都是依賴effect實現的,但是這個模塊沒有暴露在Vue對象上面。所以說effect模塊是一個偏向于底層只有基礎功能的模塊,相比2.x,這明顯是一個較好的設計模式。
Effect
?關于effect模塊,最主要的是里面的effect,track,trigger三個方法。
?effect方法是一個高階函數,或者也可以說是工廠方法,接收一個函數作為參數,返回一個effect實例方法,它使這個函數中的響應式數據可追蹤到這個effect實例,如果有響應式數據發生了改變,就會再次執行這個effect,可以參照源碼中調用這個方法的三個地方computed.ts
,apiWatch.ts
,renderer.ts
。
?首先來看看track:以下是track方法的主要邏輯以及注釋,track方法按字面的解釋就是追蹤,會在數據Proxy的get代理中調用,track這個數據本身。其實簡單說就做了一件事情,把當前的active effect收集到響應式數據的depsMap里面。
其實并不復雜,這里和2.x不同的是,2.x是每個數據各自都在閉包中維護deps對象,這里是用一個全局的Store去保存響應式數據影響的effects,實現了模塊的解耦。
// target為傳入的響應式數據對象,type為操作類型,key為target上被追蹤的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果shouldTrack為false 或者 當前沒有活動中的effect,不需要執行追蹤的邏輯
// shouldTrack為依賴追蹤提供一個全局的開關,可以很方便暫停/開啟,比如用于setup以及生命周期執行的時候
if (!shouldTrack || activeEffect === undefined) {
return
}
// 所有響應式數據都是被封裝的對象,所以用一個Map來保存更方便,Map的key為響應式數據的對象
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 同樣為每個響應式數據按key建立一個Set,用來保存target[key]所影響的effects
let dep = depsMap.get(key)
if (dep === void 0) {
// 用一個Set去保存effects,省去了去重的判斷
depsMap.set(key, (dep = new Set()))
}
// 如果target[key]下面沒有當前活動中的effect,就把這個effect加入到這個deps中
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
?看完track方法的邏輯之后,effect方法的主要邏輯其實就呼之欲出了,那就是啟動響應式追蹤---設置shouldTrack為true,設置activeEffect為當前的effect,然后再調用傳入的方法并追蹤依賴,最后返回一個封裝后的實例effect方法。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// createReactiveEffect是一個工廠方法,返回一個函數實例
const effect = createReactiveEffect(fn, options)
// 如果不是lazy effect(lazy effect主要用于computed),立即執行這個effect
if (!options.lazy) {
effect()
}
return effect
}
// createReactiveEffect是一個工廠方法,返回一個函數實例
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
// 如果effect.active為false,跳過追蹤直接調用傳入的函數
if (!effect.active) {
return fn(...args)
}
if (!effectStack.includes(effect)) {
// 清除effect中之前記錄的deps
cleanup(effect)
try {
// 設置shouldTrack為true
enableTracking()
// 設置activeEffect為當前的effect,另外把當前的effect入棧(比如渲染子組件的時候,這個棧就起作用了)
effectStack.push(effect)
activeEffect = effect
// 執行傳入effect的函數
return fn(...args)
} finally {
effectStack.pop()
// 設置shouldTrack為上一次的shouldTrack(注:和effect一樣,shouldTrack也有一個棧)
resetTracking()
// 設置activeEffect為上一個activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
?最后來看一下trigger方法,trigger方法的調用在Proxy的set代理中,作用就是在修改一個響應式數據的時候,執行這個響應式對象的depsMap中所有的effect。
// target為修改的響應式數據對象,type為操作類型,key為target上具體修改的參數
// newValue,oldValue, oldTarget都很好理解
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 === void 0) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 如果操作類型是CLEAR,說明數據類型是Map,或者Set(注意,3.x的響應式系統是支持Map和Set的)
// CLEAR操作需要觸發集合上的所有屬性的effects
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(dep => {
// addRunners功能其實很簡單,就是區分這個effect是普通的effect還是一個computed effect
addRunners(effects, computedRunners, dep)
})
// 如果是更改length長度,說明是個數組,只需要觸發key在這個新的length之后的數據
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
addRunners(effects, computedRunners, dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// 大部分的情況,觸發這個key下面的effets
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
if (
type === TriggerOpTypes.ADD ||
type === TriggerOpTypes.DELETE ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
// 如果是添加/刪除數組里的項,或者Set,Map的add,delete,set幾個方法,同時也會改變length或者size,
// 在Map和Set里面,受size影響的一些方法(比如size,forEach,entries,keys,values),都會把effect收集到ITERATE_KEY里面。
// 具體可參考packages/reactivity/src/collectionHandler.ts里面的實現
const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
scheduleRun(
effect,
target,
type,
key,
__DEV__
? {
newValue,
oldValue,
oldTarget
}
: undefined
)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
// run每個effect
computedRunners.forEach(run)
effects.forEach(run)
}
// addRunners功能其實很簡單,就是區分這個effect是普通的effect還是一個computed effect
// 普通的effect存在effects里面,computed effect存在computedRunners里面
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 調度將要執行的effect,是否傳入effect.options.scheduler決定了執行的方式
// 若沒有傳入,就立即同步執行,若有,則執行調度方法,傳入effect
// 3.x中關于異步調度方法的實現可以查看packages/runtime-core/src/scheduler.ts中的queueJob方法
function scheduleRun(
effect: ReactiveEffect,
target: object,
type: TriggerOpTypes,
key: unknown,
extraInfo?: DebuggerEventExtraInfo
) {
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect)
} else {
effect()
}
}
?以上源碼都是基于 vue-next-alpha8 版本。
?effect模塊相關的內容就這些,下一篇是關于reactive模塊的。