目前的副作用函式effect
是立即執行的:
effect(() => {
console.log(obj.foo)
})
在某些場景下並不希望effect
立即執行, 因此就可以新增options
新增屬性:
effect(() => {
console.log(obj.foo)
},
// options
{
lazy: true
})
這裡的lazy
就是前面文章介紹的排程, 當options.lazy
為true
時不立即執行副作用函式:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options
options.deps = []
// 只有非懶載入時才執行副作用函式
if (!options.lazy) effectFn()
// 將副作用函式作為返回值返回
return effectFn
}
由於最後一行將副作用函式暴露在了函式外部因此可以手動執行改副作用函式:
const effectFn = effect(() => {
console.log(obj.foo)
},
// options
{
lazy: true
})
// 手動執行
effectFn()
僅僅是這樣的意義其實不太大, 但手動執行後如果可以拿到傳入effect
函式(fn
)的返回值要好的多:
const effectFn = effect(() => obj.foo + obj.bar,
// options
{
lazy: true
})
// val 是 傳函式的返回值
const val = effectFn()
因此需要對effect
函式進行修改:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 將fn的結果返回到res中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 將 res 作為 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
// 只有非懶載入時才執行副作用函式
if (!options.lazy) effectFn()
// 將副作用函式作為返回值返回
return effectFn
}
其實傳遞給effect
的fn
才是真正的副作用函式, 而effectFn
是對fn
的再包裝, 也正因此effectFn
執行後也應該返回fn
得出來的值也就新增了const res = fn()
與return res
說句題外話, 根據前一段時間發的文章<閉包淺談>, 在這裡面新增的res
變數是閉包哦,fn
也正好是回撥函式, 當然effectFn
也是閉包
現在實現了懶執行的副作用函式並且可以拿到副作用函式的執行結果, 可實現計算屬性了:
function computed (getter) {
// 將 getter 作為副作用函式
const effectFn = effect(getter, { lazy: true })
const obj = {
// 當讀取 value 時才執行 effectFn
get value () {
return effectFn()
}
}
return obj
}
現在可以使用computed
函式建立一個計算屬性:
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)
到目前完整的程式碼是:
// 儲存副作用函式的桶
const bucket = new WeakMap()
// 用於儲存被註冊的副作用的函式
let activeEffect = undefined
// 副作用函式棧
const effectStack = []
function cleanup (effectFn) {
for (let itme of effectFn.deps) {
itme.delete(effectFn)
}
effectFn.deps.length = []
}
function effect (fn, options = {}) {
const effectFn = () => {
console.log('effect');
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 將fn的結果返回到res中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 將 res 作為 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
// 只有非懶載入時才執行副作用函式
if (!options.lazy) effectFn()
// 將副作用函式作為返回值返回
return effectFn
}
// const data = {
// text: 'hello world',
// ok: true
// }
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 攔截讀取操作
get (target, key) {
track(target, key)
// 返回屬性值
return target[key]
},
// 攔截設定操作
set (target, key, newVal) {
// 設定屬性值
target[key] = newVal
trigger(target, key)
}
})
function track (target, key) {
// 沒有 activeEffect, 直接 return
if (!activeEffect) return target[key]
// 根據 target 從'桶'中回去 depsMap, 它也是一個 Map 型別: key ---> effects
let depsMap = bucket.get(target)
// 如果 depsMap 不存在, 則新建一個 Map 並與 target 關聯
if (!depsMap) bucket.set(target, (depsMap = new Map()))
// 再根據 key 從depsMap 中去的 deps, 它是一個 Set 型別
// 裡面存貯所有與當前 key 相關的副作用函式: effects
let deps = depsMap.get(key)
// 如果 deps 不存在, 同樣新建一個 Set 並與 key 關聯0
if (!deps) depsMap.set(key, (deps = new Set()))
// 最後將當前啟用的副作用函式新增到'桶'裡
deps.add(activeEffect)
}
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 如果 trigger 觸發執行的副作用函式與當前正在執行的函式相同則不觸發執行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果一個副作用函式有排程器則呼叫改排程器, 並將副作用函式作為引數傳遞
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否則直接執行副作用函式
effectFn()
}
})
}
function computed (getter) {
// 將 getter 作為副作用函式
const effectFn = effect(getter, { lazy: true })
const obj = {
// 當讀取 value 時才執行 effectFn
get value () {
return effectFn()
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
在函式effectFn
中列印了字串'effect'會發現sumRes.value
取了四次, 函式effectFn
就執行了四次:
因此在computed
需要新增值得快取:
function computed(getter) {
// value 用來快取上一次計算的值
let value
// dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
let dirty = true
const effectFn = effect(getter, { lazy: true })
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
dirty = false
}
return value
}
}
return obj
}
此時確實只會計算一次, 且每次訪問不會重新執行副作用函式, 但是相信聰明如你已經發現問題了, 如果我們改變obj
中的值後在訪問sumRes.value
會發現訪問的值沒有變化, 這裡就不做演示了. 解決方法就是當其中的某一個值放生改變時將dirty
重新設為true
就可以了, 這時我們可以新增排程器(請參看: 執行排程):
function computed(getter) {
// value 用來快取上一次計算的值
let value
// dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 新增排程器, 將 dirty 重置
scheduler () {
dirty = true
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
dirty = false
}
return value
}
}
return obj
}
現在基本完美了, 只是在某些情況下出現一個缺陷:
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
// 在副作用函式中讀取計算屬性
console.log(sumRes.value) // 3
})
obj.bar++
在obj.bar++
時期望是觸發計算屬性, 重新渲染, 但實際上並沒有, 原因是因為計算屬性是有自己的effect
並且是懶執行的, 只有真正在讀取計算屬性的值才會執行. 對於計算屬性的getter
函式, 它裡面訪問的響應資料只會把計算屬性函式內部的effect
收集為依賴, 而在上面的例子中把計算屬性用於另一個effect
時就發生了effect
巢狀, 且外層的effect
不會被內層的effect
中的響應式資料收集
從computed
函式中我們也可以看到裡面重新頂一個了一個物件obj
並手動賦予它get
函式, 並沒有像這樣:
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 攔截讀取操作
get (target, key) {
track(target, key)
// 返回屬性值
return target[key]
},
// 攔截設定操作
set (target, key, newVal) {
// 設定屬性值
target[key] = newVal
trigger(target, key)
}
})
使用代理, 兩個其實是完全分開的, 只是使用了同一個副作用函式
解決方法很簡單既然計算屬性沒有繫結跟蹤那就手動呼叫:
function computed(getter) {
// value 用來快取上一次計算的值
let value
// dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 新增排程器, 將 dirty 重置
scheduler () {
dirty = true
// 當計算屬性依賴的響應式資料發生變化時, 手動觸發響應
trigger(obj, 'value')
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
dirty = false
}
// 當讀取 value 時, 手動呼叫 track 函式進行跟蹤
track(obj, 'value')
return value
}
}
return obj
}
到此就完成了!
裡面有許多的閉包...., 及其常見的閉包
目前的完整程式碼為:
// 儲存副作用函式的桶
const bucket = new WeakMap()
// 用於儲存被註冊的副作用的函式
let activeEffect = undefined
// 副作用函式棧
const effectStack = []
function cleanup (effectFn) {
for (let itme of effectFn.deps) {
itme.delete(effectFn)
}
effectFn.deps.length = []
}
function effect (fn, options = {}) {
const effectFn = () => {
console.log('effect');
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 將fn的結果返回到res中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 將 res 作為 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
// 只有非懶載入時才執行副作用函式
if (!options.lazy) effectFn()
// 將副作用函式作為返回值返回
return effectFn
}
// const data = {
// text: 'hello world',
// ok: true
// }
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 攔截讀取操作
get (target, key) {
track(target, key)
// 返回屬性值
return target[key]
},
// 攔截設定操作
set (target, key, newVal) {
// 設定屬性值
target[key] = newVal
trigger(target, key)
}
})
function track (target, key) {
// 沒有 activeEffect, 直接 return
if (!activeEffect) return target[key]
// 根據 target 從'桶'中回去 depsMap, 它也是一個 Map 型別: key ---> effects
let depsMap = bucket.get(target)
// 如果 depsMap 不存在, 則新建一個 Map 並與 target 關聯
if (!depsMap) bucket.set(target, (depsMap = new Map()))
// 再根據 key 從depsMap 中去的 deps, 它是一個 Set 型別
// 裡面存貯所有與當前 key 相關的副作用函式: effects
let deps = depsMap.get(key)
// 如果 deps 不存在, 同樣新建一個 Set 並與 key 關聯0
if (!deps) depsMap.set(key, (deps = new Set()))
// 最後將當前啟用的副作用函式新增到'桶'裡
deps.add(activeEffect)
}
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 如果 trigger 觸發執行的副作用函式與當前正在執行的函式相同則不觸發執行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果一個副作用函式有排程器則呼叫改排程器, 並將副作用函式作為引數傳遞
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否則直接執行副作用函式
effectFn()
}
})
}
function computed(getter) {
// value 用來快取上一次計算的值
let value
// dirty 標誌, 用來標識是否需要重新計算, 為 true 標識需要計算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 新增排程器, 將 dirty 重置
scheduler () {
dirty = true
// 當計算屬性依賴的響應式資料發生變化時, 手動觸發響應
trigger(obj, 'value')
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 將 dirty 設定為 false, 下一次直接訪問 value 中儲存的值
dirty = false
}
// 當讀取 value 時, 手動呼叫 track 函式進行跟蹤
track(obj, 'value')
return value
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
// 在副作用函式中讀取計算屬性
console.log(sumRes.value) // 3
})
obj.bar++
// effect(() => {
// console.log('effect run');
// document.body.innerText = obj.ok ? obj.text : 'not'
// })
// setTimeout(() => {
// obj.ok = false
// }, 2000)