?最近學(xué)習(xí)了下Vue3的源碼,抽空寫一些自己對3.x源碼的解讀,同時算是學(xué)習(xí)的一個總結(jié)吧堤魁,也能加深自己的印象蜜暑。
?就先從3.x的響應(yīng)式系統(tǒng)說起吧铐姚。
回憶
?首先大概回憶一下2.x的響應(yīng)式系統(tǒng),主要由這幾個模塊組成肛捍,Observer隐绵,Watcher,Dep拙毫。
Observer負(fù)責(zé)通過defineProperty劫持Data依许,每個Data都各自在閉包中維護一個Dep的實例,用于收集依賴著它的Watcher恬偷。Dep維護一個公共的Target屬性悍手,用于保存當(dāng)前的需要被收集依賴的Watcher。每次Data被劫持的getter執(zhí)行的時候袍患,如果Dep.Target!==undefine
, dep和Watcher實例就互相收集對方~
?2.x的響應(yīng)式系統(tǒng)其實是圍繞著Watcher坦康,也可以說圍繞著watch API的,包括render是一個renderWatcher诡延,computed是通過lazyWatcher實現(xiàn)滞欠。這并不是一個好的設(shè)計模式,不符合六個設(shè)計原則的(單一職責(zé)原則肆良,開閉原則)筛璧。而響應(yīng)式系統(tǒng)也無法獨立出來。
對比
?那么3.x是怎樣實現(xiàn)這一塊的內(nèi)容的呢惹恃。
?首先3.x響應(yīng)式系統(tǒng)相關(guān)的代碼在packages/reactivity/src里夭谤。3.x的響應(yīng)式系統(tǒng)的核心由兩個模塊構(gòu)成: effect, reactive。
?reactive模塊的功能比較簡單巫糙,就是給數(shù)據(jù)設(shè)置代理朗儒,類似于2.x的Observer,不同的點在于是用的Proxy去做代理参淹。
?effect模塊醉锄,傳入一個函數(shù),然后讓這個函數(shù)需要被響應(yīng)式數(shù)據(jù)影響浙值,目前具體在3.x中包括恳不,watch API,computed API开呐,還有組件的更新都是依賴effect實現(xiàn)的烟勋,但是這個模塊沒有暴露在Vue對象上面规求。所以說effect模塊是一個偏向于底層只有基礎(chǔ)功能的模塊,相比2.x神妹,這明顯是一個較好的設(shè)計模式颓哮。
Effect
?關(guān)于effect模塊,最主要的是里面的effect鸵荠,track冕茅,trigger三個方法。
?effect方法是一個高階函數(shù)蛹找,或者也可以說是工廠方法姨伤,接收一個函數(shù)作為參數(shù),返回一個effect實例方法庸疾,它使這個函數(shù)中的響應(yīng)式數(shù)據(jù)可追蹤到這個effect實例乍楚,如果有響應(yīng)式數(shù)據(jù)發(fā)生了改變,就會再次執(zhí)行這個effect届慈,可以參照源碼中調(diào)用這個方法的三個地方computed.ts
,apiWatch.ts
,renderer.ts
徒溪。
?首先來看看track:以下是track方法的主要邏輯以及注釋,track方法按字面的解釋就是追蹤金顿,會在數(shù)據(jù)Proxy的get代理中調(diào)用臊泌,track這個數(shù)據(jù)本身。其實簡單說就做了一件事情揍拆,把當(dāng)前的active effect收集到響應(yīng)式數(shù)據(jù)的depsMap里面渠概。
其實并不復(fù)雜,這里和2.x不同的是嫂拴,2.x是每個數(shù)據(jù)各自都在閉包中維護deps對象播揪,這里是用一個全局的Store去保存響應(yīng)式數(shù)據(jù)影響的effects,實現(xiàn)了模塊的解耦筒狠。
// target為傳入的響應(yīng)式數(shù)據(jù)對象猪狈,type為操作類型,key為target上被追蹤的key
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果shouldTrack為false 或者 當(dāng)前沒有活動中的effect辩恼,不需要執(zhí)行追蹤的邏輯
// shouldTrack為依賴追蹤提供一個全局的開關(guān)雇庙,可以很方便暫停/開啟,比如用于setup以及生命周期執(zhí)行的時候
if (!shouldTrack || activeEffect === undefined) {
return
}
// 所有響應(yīng)式數(shù)據(jù)都是被封裝的對象运挫,所以用一個Map來保存更方便,Map的key為響應(yīng)式數(shù)據(jù)的對象
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 同樣為每個響應(yīng)式數(shù)據(jù)按key建立一個Set套耕,用來保存target[key]所影響的effects
let dep = depsMap.get(key)
if (dep === void 0) {
// 用一個Set去保存effects谁帕,省去了去重的判斷
depsMap.set(key, (dep = new Set()))
}
// 如果target[key]下面沒有當(dāng)前活動中的effect,就把這個effect加入到這個deps中
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
?看完track方法的邏輯之后冯袍,effect方法的主要邏輯其實就呼之欲出了匈挖,那就是啟動響應(yīng)式追蹤---設(shè)置shouldTrack為true碾牌,設(shè)置activeEffect為當(dāng)前的effect,然后再調(diào)用傳入的方法并追蹤依賴儡循,最后返回一個封裝后的實例effect方法舶吗。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// createReactiveEffect是一個工廠方法,返回一個函數(shù)實例
const effect = createReactiveEffect(fn, options)
// 如果不是lazy effect(lazy effect主要用于computed)择膝,立即執(zhí)行這個effect
if (!options.lazy) {
effect()
}
return effect
}
// createReactiveEffect是一個工廠方法誓琼,返回一個函數(shù)實例
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,跳過追蹤直接調(diào)用傳入的函數(shù)
if (!effect.active) {
return fn(...args)
}
if (!effectStack.includes(effect)) {
// 清除effect中之前記錄的deps
cleanup(effect)
try {
// 設(shè)置shouldTrack為true
enableTracking()
// 設(shè)置activeEffect為當(dāng)前的effect肴捉,另外把當(dāng)前的effect入棧(比如渲染子組件的時候腹侣,這個棧就起作用了)
effectStack.push(effect)
activeEffect = effect
// 執(zhí)行傳入effect的函數(shù)
return fn(...args)
} finally {
effectStack.pop()
// 設(shè)置shouldTrack為上一次的shouldTrack(注:和effect一樣,shouldTrack也有一個棧)
resetTracking()
// 設(shè)置activeEffect為上一個activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
?最后來看一下trigger方法齿穗,trigger方法的調(diào)用在Proxy的set代理中傲隶,作用就是在修改一個響應(yīng)式數(shù)據(jù)的時候,執(zhí)行這個響應(yīng)式對象的depsMap中所有的effect窃页。
// target為修改的響應(yīng)式數(shù)據(jù)對象跺株,type為操作類型,key為target上具體修改的參數(shù)
// 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,說明數(shù)據(jù)類型是Map胚嘲,或者Set(注意作儿,3.x的響應(yīng)式系統(tǒng)是支持Map和Set的)
// CLEAR操作需要觸發(fā)集合上的所有屬性的effects
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(dep => {
// addRunners功能其實很簡單,就是區(qū)分這個effect是普通的effect還是一個computed effect
addRunners(effects, computedRunners, dep)
})
// 如果是更改length長度馋劈,說明是個數(shù)組攻锰,只需要觸發(fā)key在這個新的length之后的數(shù)據(jù)
} 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) {
// 大部分的情況,觸發(fā)這個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)
) {
// 如果是添加/刪除數(shù)組里的項妓雾,或者Set娶吞,Map的add,delete械姻,set幾個方法妒蛇,同時也會改變length或者size,
// 在Map和Set里面楷拳,受size影響的一些方法(比如size绣夺,forEach,entries欢揖,keys陶耍,values),都會把effect收集到ITERATE_KEY里面她混。
// 具體可參考packages/reactivity/src/collectionHandler.ts里面的實現(xiàn)
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功能其實很簡單烈钞,就是區(qū)分這個effect是普通的effect還是一個computed effect
// 普通的effect存在effects里面泊碑,computed effect存在computedRunners里面
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
// 省略
}
// 調(diào)度將要執(zhí)行的effect,是否傳入effect.options.scheduler決定了執(zhí)行的方式
// 若沒有傳入毯欣,就立即同步執(zhí)行馒过,若有,則執(zhí)行調(diào)度方法酗钞,傳入effect
// 3.x中關(guān)于異步調(diào)度方法的實現(xiàn)可以查看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模塊相關(guān)的內(nèi)容就這些,下一篇是關(guān)于reactive模塊的算吩。