effect 是可以發生嵌套的
vue.js的下渲染函數其實就是在一個effect中執行的
當組件發生嵌套時,例如Foo組件渲染了Bar組件
// Bar組件
const Bar = {
render() { }
}
// Foo組件嵌套渲染了Bar組件
const Foo = {
render() {
return <Bar />
}
}
此時就發生了effect嵌套
effect(() => {
Foo.render()
// 嵌套
effect(() => {
Bar.render()
})
})
我們要搞清楚effect為什麼要嵌套,不嵌套會發生什麼
// 原始數據
const data = { foo: true, bar: true }
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
console.log('effectFn1執行了');
effect(function effectFn2() {
console.log('effectFn2執行了');
// 在effectFn2中讀取了obj.foo
temp2 = obj.bar
})
// 在effectFn1中讀取了obj.foo
temp1 = obj.foo
})
上面的代碼中,effectFn1執行後會導致effectFn2執行
effectFn2執行後會讀取obj.bar的值,effectFn1中也會讀取obj.foo的值
所以他們的結構應該是:
data -> foo -> effectFn1
-> bar -> effectFn2
我們修改foo的時候,會執行effectFn1和effectFn2,而修改bar只會執行effectFn2
但結果卻是修改foo只執行了effectFn2
出現這個問題是因為我們拿activeEffect保存副作用函數,並且它只能保存一個,當effectFn2執行的時候會直接覆蓋effectFn1
解決方法:創建一個副作用函數棧effectStack,副作用函數執行時,將當前副作用函數壓入棧中,在其執行完畢後將它從棧中彈出,並始終讓activeEffect指向棧頂的副作用函數,這樣就可以避免嵌套的副作用函數互相覆蓋的問題
// 定義一個全局變量,用來存儲當前激活的副作用函數
let activeEffect;
// 棧
const effectStack = []
function effect(fn) {
const effectFn = () => {
// 利用 cleanup 函數清除 effectFn 中存儲的副作用函數
cleanup(effectFn);
// 當effectFn執行時,activeEffect保存當前effectFn副作用函數
activeEffect = effectFn
// 在調用副作用函數之前將當前副作用函數壓入棧中
effectStack.push(effectFn);
fn(); // 真正執行的副作用函數
// 當前副作用函數執行完畢後,將當前副作用函數彈出棧,並把activeEffect還原為之前的值
effectStack.pop();
// activeEffect還原為之前的值
activeEffect = effectStack[effectStack.length - 1]
}
// 定義一個deps數組,用來收集與該副作用函數相關的依賴集合
effectFn.deps = []
effectFn()
}
上面的代碼圖解起來大概就是這樣:
activeEffect始終等於棧頂的副作用函數,每次執行完後棧頂的副作用函數就被彈出,這樣就可以避免嵌套的副作用函數互相覆蓋的問題
驗證 :
// 驗證
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
console.log('effectFn1執行了');
effect(function effectFn2() {
console.log('effectFn2執行了');
// 在effectFn2中讀取了obj.foo
temp2 = obj.bar
})
// 在effectFn1中讀取了obj.foo
temp1 = obj.foo
})
setTimeout(() => {
console.log('修改==========');
obj.foo = false
}, 1000);
完整代碼:
// 定義一個全局變量,用來存儲當前激活的副作用函數
let activeEffect;
// 棧
const effectStack = []
function effect(fn) {
const effectFn = () => {
// 利用 cleanup 函數清除 effectFn 中存儲的副作用函數
cleanup(effectFn);
// 當effectFn執行時,activeEffect保存當前effectFn副作用函數
activeEffect = effectFn
// 在調用副作用函數之前將當前副作用函數壓入棧中
effectStack.push(effectFn);
fn(); // 真正執行的副作用函數
// 當前副作用函數執行完畢後,將當前副作用函數彈出棧,並把activeEffect還原為之前的值
effectStack.pop();
// activeEffect還原為之前的值
activeEffect = effectStack[effectStack.length - 1]
}
// 定義一個deps數組,用來收集與該副作用函數相關的依賴集合
effectFn.deps = []
effectFn()
}
// 重置依賴關係
function cleanup(effectFn) {
// 遍歷 effectFn.deps 數組
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依賴集合
const deps = effectFn.deps[i]
// 將 effectFn 從依賴集合中移除
// 也就是在依賴收集的時候的deps,這個deps其實就是key對應的Set集合
// deps.delete(effectFn)也就是刪除key對應的Set集合中的activeEffect
deps.delete(effectFn)
}
// 最後重置 effectFn.deps 數組
effectFn.deps.length = 0
}
// 存儲副作用函數的桶
const bucket = new WeakMap()
function track(target, key) {
// 沒有activeEffect,直接return
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把當前激活的副作用函數添加到依賴集合中:deps -> Set構造函數
// 把activeEffect當作依賴收集給deps
deps.add(activeEffect)
// deps就是與當前副作用函數存在聯繫的依賴集合
// 把依賴集合添加到activeEffect.deps數組中 -> effectFn.deps
activeEffect.deps.push(deps)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
// 獲取依賴集合effects[] -> activeEffect.deps[] -> effectFn.deps
const effects = depsMap.get(key)
// 用一個新Set保存依賴集合,直接使用effects會出現無限循環
// 因為effectFn函數內部調用了cleanup函數在執行刪除操作,而track函數內部的deps.add(activeEffect)又在新增,導致無限循環
const effectsToRun = new Set(effects)
// 調用effectFn函數,並且effectFn函數內部會調用cleanup重置依賴關係
effectsToRun.forEach(effectFn => effectFn())
}
const data = { foo: true, bar: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key);
}
})
// 驗證
// 全局變量
let temp1, temp2;
// effectFn1嵌套effectFn2
effect(function effectFn1() {
console.log('effectFn1執行了');
effect(function effectFn2() {
console.log('effectFn2執行了');
// 在effectFn2中讀取了obj.foo
temp2 = obj.bar
})
// 在effectFn1中讀取了obj.foo
temp1 = obj.foo
})
setTimeout(() => {
console.log('修改==========');
obj.foo = false
}, 1000);
參考資料:vue.js設計與實現
本篇文章為霍春陽老師的《vue.js設計與實現》的核心知識點總結,感興趣的朋友可以支持原版書籍。