距離上一篇過去好久了。你能夠快速瀏覽歷史文章:
你爲何看不懂源碼之Vue 3.0【1】
你爲何看不懂源碼之Vue 3.0 面面俱到【2】react
以前在看 reactive 和 ref 時,總有兩團黑霧籠罩着咱們,一團是 track,一團是 trigger。typescript
兩者都來自同一個文件,effect.ts。數組
在 響應式數據 get 時,track(target, OperationTypes.GET, key)app
在 set 時, trigger(target, OperationTypes.SET, key, extraInfo)。函數
今天,咱們就搞他們兩個!post
接下來看 ref.spec.ts
中的一條用例 (ref 的流程比較簡單,容易理解)性能
it('should be reactive', () => {
const a = ref(1)
let dummy
// 反作用包裝下
effect(() => {
dummy = a.value
})
expect(dummy).toBe(1)
a.value = 2
expect(dummy).toBe(2)
})
複製代碼
effect 接受一個函數,函數返回 dummy 變量,dummy 是響應式對象 a 的值。當改變了 a 的值時,dummy 也 從新計算了遍!測試
這不就是 TMD 計算屬性嗎!接着往下看。ui
首先,須要你人肉調試一遍,順着 effect
函數的軌跡打上備註。spa
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 是false,這裏確定會運行
if (!options.lazy) {
effect()
}
return effect
}
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
// 又用 reactiveEffect 包裝了一層,進去看看
const effect = function reactiveEffect(...args: any[]): any {
return run(effect, fn, args)
} as ReactiveEffect
// 這裏就是一堆參數
effect[effectSymbol] = true
effect.active = true
effect.raw = fn
effect.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
effect.deps = []
// 返回的effect 函數會被 執行掉
return effect
}
複製代碼
接下來到 run
函數了,這裏用了一個巧妙的方法,咱們單拿出來
function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
// 這裏默認進不去
if (!effect.active) {
return fn(...args)
}
// 這裏進去,剛開始確定是 -1
if (activeReactiveEffectStack.indexOf(effect) === -1) {
// clear 操做,暫時不關心
cleanup(effect)
try {
activeReactiveEffectStack.push(effect)
// 這裏執行後,返回結果,fn 就是計算函數
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
複製代碼
後面的 try...finally
執行順序換種寫法是這樣的。
activeReactiveEffectStack.push(effect)
const res = fn(...args)
activeReactiveEffectStack.pop()
return res
複製代碼
爲何要try finally
呢?
我想由於
fn(...args)
是用戶寫的函數。 它有可能報錯,即便它報錯了,也應該被 activeReactiveEffectStack.pop,一是 影響性能,二是 activeReactiveEffectStack 在 track 時,負責綁定 target 和 effect。
繼續往下看, fn(...args)
是 測試用例裏的
() => {
dummy = a.value
}
複製代碼
當執行 a.value 時會發生什麼?固然是 ref 內部的 get
流程,而這個流程是會觸發,track(v, OperationTypes.GET, '')
終於進入 track
時間
track
// ref.ts
track(v, OperationTypes.GET, '')
// effect.ts
export function track( target: any, type: OperationTypes, key?: string | symbol ) {
// 默認 true
if (!shouldTrack) {
return
}
// 這時是有值的,在 try finally 流程中存入的
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (effect) {
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
// targetMap 存入 key 爲 ref 的 空 Map 對象。
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key!)
if (dep === void 0) {
// depsMap 存入 key 爲 '' 的 空 Set 對象
depsMap.set(key!, (dep = new Set()))
}
if (!dep.has(effect)) {
// dep 存入 effect
dep.add(effect)
// dep 入棧
effect.deps.push(dep)
if (__DEV__ && effect.onTrack) {
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
}
複製代碼
track
函數在對象被 set 時調用,它只進行了「記錄」,記錄的值有什麼用呢?應該在 trigger
時會用到。
當前咱們最好能記一下 track
影響了哪些值。
target
,值爲 effect 對象deps
數組存了 effect
,後面應該會有用到。trigger
繼續往下走,
it('should be reactive', () => {
const a = ref(1)
let dummy
// 反作用包裝下
effect(() => {
dummy = a.value
})
expect(dummy).toBe(1)
a.value = 2
expect(dummy).toBe(2)
})
複製代碼
當 a.value = 2
時,確定會調用 ref 對象的 set 方法, 這個時候就走 trigger
流程了: trigger(v, OperationTypes.SET, '')
export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) {
// 還記得嗎,前面 set 過了
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
//addRunners 主要給 computedRunners 和 effects 添加值
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
// 這裏爲 數組 和 delete 服務,暫時不討論
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
// 遍歷執行effect 函數,computedRunners 爲 計算屬性服務,effects 爲 單獨調用 effect.ts 模塊時服務。先談後者。
computedRunners.forEach(run)
effects.forEach(run)
}
複製代碼
trigger 方法主要從 全局 targetMap 對象中 拿出 target 對應的 effect
這兩個函數是重點:addRunners
和 scheduleRun
。
addRunners
將 depsMap 中的 effect 對象賦值給 effects
,以後遍歷 effects
執行 run
方法 effects.forEach(run)
function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) {
if (effectsToAdd !== void 0) {
effectsToAdd.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
}
function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(
extend(
{
effect,
target,
key,
type
},
extraInfo
)
)
}
// 當前用例爲 undefined
if (effect.scheduler !== void 0) {
effect.scheduler(effect)
} else {
effect()
}
}
複製代碼
run 方法 調用了 scheduleRun
函數,直接運行了 effect
,而後會走上文中的 createReactiveEffect
方法中的 effect
函數,直至再次觸發如下函數,從而改變 dummy的值。
effect(() => {
dummy = a.value
})
複製代碼
簡單的 effect
流程到這裏就結束了。 我將其分爲三個階段:
綁定階段:effect 函數會包裝傳入的 方法,將其變成一個 effect 對象,並在綁定階段的最後執行一遍傳入的 方法(初始化)。
收集階段:effect 傳入的方法內部,有響應式對象參與了計算,將觸發
get
操做,會執行track
方法,track 方法的重點是將響應式對象改變的target
與 綁定階段的effect
對象一一對應起來。這兩個階段是同步執行的(activeReactiveEffectStack
協調),值會存在全局的targetMap
。
觸發階段:當 響應式對象
set
時,會觸發trigger
方法,它會從targetMap
中拿到 target 對應的effects
,並遍歷執行。
computed
effect
就是這樣了,但要直接用 effect
仍是有點蛋疼。
它默認反回了 ReactiveEffect
對象,我要這玩意兒幹啥呢,我以前寫計算屬性,直接返回值就是 計算後的值。而如今:
let dummy
const obj = reactive({ prop: 'value' })
effect(() => (dummy = obj.prop))
複製代碼
每次都要定義一個額外變量 dummy
,不只麻煩,還很容易被外界篡改。
因此,終於到了機智的 computed.ts
文件,它的代碼行數很是之少,八十幾行,優秀(廢話,核心功能 effect 都實現了。)。
首先瞅瞅測試用例:
it('should return updated value', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
})
複製代碼
我直接把核心代碼貼過來。compmuted 其實就是 對 effect 進一步封裝
export function computed<T>(
getterOrOptions: (() => T) | WritableComputedOptions<T>
): any {
const isReadonly = isFunction(getterOrOptions)
const getter = isReadonly
? (getterOrOptions as (() => T))
: (getterOrOptions as WritableComputedOptions<T>).get
// 測試環境會給出 computed 屬性不可 set 的提示,正式環境會給一個 空函數
const setter = isReadonly
? __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
: (getterOrOptions as WritableComputedOptions<T>).set
// 保證了在 get 時,只執行第一次 runner
let dirty = true
let value: T
const runner = effect(getter, {
// effect 方法不會當即執行,在 get 時執行
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
dirty = true
}
})
return {
[refSymbol]: true,
// 導出了 runner 讓 computed 能夠被外部暫停
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// When computed effects are accessed in a parent effect, the parent
// should track all the dependencies the computed property has tracked.
// This should also apply for chained computed properties.
trackChildRun(runner)
return value
},
set value(newValue: T) {
setter(newValue)
}
}
}
複製代碼
首先 computed 方法返回了 Ref 對象。
在 get 時,執行了 effect 方法,執行完畢 dirty 爲 false,只有 響應式對象 trigger 後,dirty 纔會爲 true,在這中間,屢次 get 值是同樣的(由於響應式數據沒有改變時,屢次運行 effect 結果是同樣的) 在 set 時,正式環境執行空方法,由於 computed 不支持 set。開發環境直接告警。
備註: 按照 computed 參數約束,是能夠傳入
WritableComputedOptions
對象,這樣就支持 set 了,具體可參考測試用例:should support setter
這個用例讓我讀了許久,很容易被繞進去,你最好用個小本本記錄流程,而後不斷的斷點調試,直至清晰。
it('should work when chained', () => {
const value = reactive({ foo: 0 })
const c1 = computed(() => value.foo)
const c2 = computed(() => c1.value + 1)
// expect(c2.value).toBe(1)
// expect(c1.value).toBe(0)
value.foo++
expect(c2.value).toBe(2)
// expect(c1.value).toBe(1)
})
複製代碼
其實用例在幹什麼很容易看出來, value 是一個響應式數據, c1做爲 計算屬性 引用了它,c2 做爲計算屬性引用了 c1,當 value.foo++ 時,這兩者都要更新。c2 爲 2, c1 爲 1。
我大概描述下整個流程,但願能減輕(增長)你的痛苦。
const value = reactive({ foo: 0 })
-> 建立響應式對象
const c1 = computed(() => value.foo)
-> 建立計算屬性 -> 包裝 effect 對象
const c2 = computed(() => c1.value + 1)
-> 建立計算屬性 -> 包裝 effect 對象
value.foo++
-> 響應式對象 get -> setexpect(c2.value).toBe(2)
-> c2.value -> c2 get -> runner -> activeReactiveEffectStack 存入 c2 effect -> 執行 c2 計算函數 -> 執行 c1.value -> c1 get -> runner -> activeReactiveEffectStack 存入 c1 effect -> 執行 c1 計算函數 -> 調用 value 的 get 方法 -> 觸發 track -> 綁定 effect 和 deep -> activeReactiveEffectStack 彈出 c1 effect -> 執行 trackChildRun -> 返回 c1 計算值 -> activeReactiveEffectStack 彈出 c2 -> 返回 c2 計算值
經過以上步驟,實現了計算屬性的鏈式調用。
這裏重點注意我加粗的地方,trackChildRun
是 computed 中的方法。我打上了運行時備註:
// childRunner 是 c1 effect
function trackChildRun(childRunner: ReactiveEffect) {
// 此時 activeReactiveEffectStack 存在 c2 effect
const parentRunner =
activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (parentRunner) {
for (let i = 0; i < childRunner.deps.length; i++) {
const dep = childRunner.deps[i]
// 綁定 dep 和 c2 effect,這裏的 dep 對應着全局 targetMap 中的 dep
if (!dep.has(parentRunner)) {
dep.add(parentRunner)
parentRunner.deps.push(dep)
}
}
}
}
複製代碼
通過 trackChildRun
的處理,響應式數據不只綁定了 c1 還綁定了 c2,當下次響應式數據變動時,會遍歷與其有關的 dep
,詳見 effect.ts
的 addRunners
方法
終於將文章水完了,要是我也能用當下流行的量子波動閱讀法來讀源碼就行了,溜了溜了......