淺談vue3中effect與computed的親密關(guān)系
在我剛看完vue3響應(yīng)式的時候,心中就有一個不可磨滅的謎團,讓我茶不思飯不想,總想生病。那么這個謎團是什么呢?就是在響應(yīng)式中一直穿行在tranger跟track之間的effect。如果單純的響應(yīng)式原理根本就用不上effect,那么effect到底是干什么的呢?
船到橋頭自然直,柳岸花明又一村。苦心人天不負(fù),偶然間我看到了effect測試代碼用例!
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})
解釋一下,這段代碼
- 首先聲明dummy變量,然后在effect的回調(diào)中把已響應(yīng)的對象counter的num屬性賦值給dummy
- 然后做斷言判斷 dummy是否等于 0
- 將 counter.num 賦值 7 ,然后 dummy 也變成了 7 !
這,,,讓我想到了什么??
這就是computed的嗎?
趕緊看下 computed 的測試用例!!
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
哈哈哈
阿哈哈哈哈
hhhhhhhhhhhhhhhhhhhh
忍不住想仰天長嘯??!
果然跟我猜想的一樣!?。∥医K于直到effect是個什么鬼了,顧名思義effect是副作用的意思,也就是說它是響應(yīng)式副產(chǎn)品,每次觸發(fā)了 get 時收集effect,每次set時在觸發(fā)這些effects。這樣就可以做一些響應(yīng)式數(shù)據(jù)之外的一些事情了,比如計算屬性computed。
讓我們用effect實現(xiàn)一個computed 可能會更清晰一點
我就不寫一些亂七八糟的判斷了,讓大家能夠看的更加清楚
function computed (fn) {
let value = undefined
const runner = effect(fn, {
// 如果lazy不置為true的話,每次創(chuàng)建effect的時候都會立即執(zhí)行一次
// 而我們要實現(xiàn)computed顯然是不需要的
lazy: true
})
// 為什么要使用對象的形式,是因為我們最后需要得到computed的值
// 如果不用對象的 get 方法的話我們就需要手動再調(diào)用一次 computed()
return {
get value() {
return runner()
}
}
}
// 使用起來是這樣的
const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1
console.log(cValue.value) // 1
這也太簡單了吧,那么重點來了,effect怎么實現(xiàn)的呢?
別著急,我們先捋一下邏輯
- 首先 如果 effect 回調(diào)內(nèi)有已響應(yīng)的對象被觸發(fā)了 get 時,effect就應(yīng)該被儲存起來
- 然后,我們需要一個儲存effect的地方,在effect函數(shù)調(diào)用的時候就應(yīng)該把effect放進(jìn)這個儲存空間,在vue中使用的是一個數(shù)組activeReactiveEffectStack = []
- 再后,每個target被觸發(fā)的時候,都可能有多個effect,所以每個target需要有一個對應(yīng)的依賴收集器 deps,等到 set 時遍歷 deps 執(zhí)行 effect()
- 然而,這個依賴收集器 deps 不能放在 target 本身上,這樣會使數(shù)據(jù)看起來不是很簡潔,還會存在多余無用的數(shù)據(jù),所以我們需要一個 map 集合來儲存 target 跟 deps 的關(guān)系, 在vue中這個儲存集合叫 targetMap 。
幾個概念
track 追蹤器,在 get 時調(diào)用該函數(shù),將所有 get 的 target 跟 key 以及 effect 建立起對應(yīng)關(guān)系
// 比如 const react = reactive({a: { b: 2 })
// react.a 時 target -> {a: { b: 2 } key -> a
// targetMap 儲存了 target --> Map --> key --> Set --> dep --> effect
// 當(dāng)調(diào)用 react.a.b.c.d.e 時 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
if (effect) {
let depsMap = targetMap.get(target);
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key!);
if (!dep) {
depsMap.set(key!, (dep = new Set()));
}
if (!dep.has(effect)) {
dep.add(effect);
effect.deps.push(dep);
}
}
}
trigger 觸發(fā)器,這個就比較好理解了,拿到target key下的對應(yīng)的所有 effect,然后遍歷執(zhí)行 effect()
export function trigger(target: any, key?: string | symbol) {
const depsMap: any = targetMap.get(target);
const effects: any = new Set()
if (depsMap && depsMap.get(key)) {
depsMap.get(key).forEach((dep: any) => {
effects.add(dep)
});
effects.forEach((e: any) => e())
}
}
effect 函數(shù)實現(xiàn)
// 暴露的 effect 函數(shù)
export function effect(
fn: Function,
options: any = EMPTY_OBJ
): any {
if ((fn as any).isEffect) {
fn = (fn as any).raw
}
const effect = createReactiveEffect(fn, options)
// 如果不是 lazy,則會立即執(zhí)行一次
if (!options.lazy) {
effect()
}
return effect
}
// 創(chuàng)建 effect
function createReactiveEffect(
fn: Function,
options: any
): any {
const effect = function effect(...args: any): any {
return run(effect as any, fn, args)
} as any
effect.isEffect = 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 = []
return effect
}
// 執(zhí)行函數(shù),執(zhí)行完之后會將儲存的 effect 刪除
// 這是函數(shù) effect 的所有執(zhí)行,所經(jīng)歷的完整的聲明周期
function run(effect: any, fn: Function, args: any[]): any {
if (!effect.active) {
return fn(...args)
}
if (activeReactiveEffectStack.indexOf(effect) === -1) {
try {
activeReactiveEffectStack.push(effect)
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
一口氣寫了這么多,最后總結(jié)一下。在大家看源碼的時候,如果發(fā)現(xiàn)有哪個地方無從下手的話,可以先從測試用例開始看。因為測試用例可以很清楚的知道這個函數(shù)想要達(dá)到什么效果,然后從效果上想,為什么這么做,如果我自己寫的話應(yīng)該怎么寫,這樣一點點就能揣摩出作者的意圖了。再根據(jù)源碼結(jié)合自己的想法你就能夠?qū)W到很多。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue中el-tab如何點擊不同標(biāo)簽觸發(fā)不同函數(shù)的實現(xiàn)
el-tab本身的功能是點擊之后切換不同頁,本文主要介紹了vue中el-tab如何點擊不同標(biāo)簽觸發(fā)不同函數(shù)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-03-03
vue中的this.$router.push()路由傳值方式
這篇文章主要介紹了vue中的this.$router.push()路由傳值方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
Javascript結(jié)合Vue實現(xiàn)對任意迷宮圖片的自動尋路
本文將結(jié)合實例代碼介紹Javascript結(jié)合Vue實現(xiàn)對任意迷宮圖片的自動尋路,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-06-06

