爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀javascript
若是以前的文章都看過的話,咱們應該已經明白是如何劫持數據了。但還有兩個大問題一直沒解決,即具體是如何收集依賴,又是如何觸發監聽函數的。從前文中,咱們大體能猜到:向effect
函數傳遞一個原始函數,會建立一個監聽函數,而且會當即執行一次。而第一次執行時,就能經過讀操做中的track
收集到依賴,並在寫操做時,經過trigger
時再次觸發這個監聽函數。而這些主要方法的內部邏輯就在 effect 文件中。vue
import { OperationTypes } from './operations'
import { Dep, targetMap } from './reactive'
import { EMPTY_OBJ, extend } from '@vue/shared'
複製代碼
effect 文件的外部引入很少,EMPTY_OBJ
指代一個空對象{}
,extend
是一個擴展對象的方法,相似 lodash 中的_.extend
。而Dep
跟targetMap
正是咱們須要在effect
中探索的。java
歸功於以前看過單測,因此看這裏的類型,會輕鬆不少。若是您沒有看過,直接看結論也行。react
// 迭代行爲標識符
export const ITERATE_KEY = Symbol('iterate')
// 監聽函數的配置項
export interface ReactiveEffectOptions {
// 延遲計算,爲true時候,傳入的effect不會當即執行。
lazy?: boolean
// 是不是computed數據依賴的監聽函數
computed?: boolean
// 調度器函數,接受的入參run便是傳給effect的函數,若是傳了scheduler,則可經過其調用監聽函數。
scheduler?: (run: Function) => void
// **僅供調試使用**。在收集依賴(get階段)的過程當中觸發。
onTrack?: (event: DebuggerEvent) => void
// **僅供調試使用**。在觸發更新後執行監聽函數以前觸發。
onTrigger?: (event: DebuggerEvent) => void
//經過 `stop` 終止監聽函數時觸發的事件。
onStop?: () => void
}
// 監聽函數的接口
export interface ReactiveEffect<T = any> {
// 表明這是一個函數類型,不接受入參,返回結果類型爲泛型T
// T也便是原始函數的返回結果類型
(): T
[effectSymbol]: true
// 暫時未知,猜想是某種開關
active: boolean
// 監聽函數的原始函數
raw: () => T
// 暫時未知,根據名字來看是存一些依賴
// 根據類型來看,存放是二維集合數據,一維是數組,二維是ReactiveEffect的Set集合
deps: Array<Dep> // === Array<Set<ReactiveEffect>>
// 如下同上述ReactiveEffectOptions
computed?: boolean
scheduler?: (run: Function) => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
onStop?: () => void
}
// debugger事件,這個基本不須要解釋
export type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: OperationTypes
key: any
} & DebuggerEventExtraInfo
// debugger拓展信息
export interface DebuggerEventExtraInfo {
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
// 存放監聽函數的數組
export const effectStack: ReactiveEffect[] = []
複製代碼
// 是不是監聽函數
export function isEffect(fn: any): fn is ReactiveEffect {
return fn != null && fn._isEffect === true
}
// 生成監聽函數的effect方法
export function effect<T = any>(
// 原始函數
fn: () => T,
// 配置項
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 若是該函數已是監聽函數了,那賦值fn爲該函數的原始函數
if (isEffect(fn)) {
fn = fn.raw
}
// 建立一個監聽函數
const effect = createReactiveEffect(fn, options)
// 若是不是延遲執行的話,當即執行一次
if (!options.lazy) {
effect()
}
// 返回該監聽函數
return effect
}
// 建立監聽函數的方法
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
// 建立監聽函數,經過run來包裹原始函數,作額外操做
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.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
return effect
}
複製代碼
能夠看到,這兩個方法,其實都沒作什麼關鍵性的邏輯,也都比較易懂。主要是給監聽函數賦一些屬性。核心仍是在那個run
方法中,那裏纔是真正的監聽執行邏輯。不過也有一個不易明白之處是這裏:typescript
// 若是該函數已是監聽函數了,那賦值fn爲該函數的原始函數
if (isEffect(fn)) {
fn = fn.raw
}
複製代碼
這段邏輯表明着,若是傳遞的函數已是監聽函數了,並不會直接返回舊的監聽函數,而是用其原始函數構建一個新的監聽函數,這在咱們的單測篇中略有體現。effect
方法永遠都返回一個新函數,不過暫時不知道這樣設計的緣由是什麼。api
繼續看run
方法。數組
// 監聽函數執行器
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
// 若是這個active開關是關上的,那就執行原始方法,並返回
if (!effect.active) {
return fn(...args)
}
// 若是監聽函數棧中並無此監聽函數,則:
if (!effectStack.includes(effect)) {
// 還不知道具體作什麼用的清除行爲
cleanup(effect)
try {
// 將本effect推到effect棧中
effectStack.push(effect)
// 執行原始函數並返回
return fn(...args)
} finally {
// 執行完之後將effect從棧中推出
effectStack.pop()
}
}
}
// 傳遞一個監聽函數,作某種清除操做
function cleanup(effect: ReactiveEffect) {
// 獲取本effect的deps,而後循環清除存儲了自身effect的引用
// 最後將deps置爲空
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
複製代碼
這個run
方法,因爲引入了cleanup
跟effectStack
,又多了一些判斷,有點兒看不明白。數據結構
問題 1:effect.active
是什麼個邏輯?app
咱們搜索下修改effect.active
的方法,只有一處:async
export function stop(effect: ReactiveEffect) {
// 若是active爲true,則觸發effect.onStop,而且把active置爲false。
if (effect.active) {
cleanup(effect)
if (effect.onStop) {
effect.onStop()
}
effect.active = false
}
}
複製代碼
看過單測篇的話,可能記得有stop
這個 api,向它傳參監聽函數,可使得這個監聽函數失去響應式邏輯。那active
這個邏輯就明白了。
問題 2:既然執行前effectStack.push(effect)
,執行後effectStack.pop()
。那爲何還會存在effectStack.includes(effect)
這種狀況呢?
遇到問題不要慌,記住,咱們有單測大法!咱們把這個 if 邏輯去掉,再跑下 effect 的單測,而後就會發現兩個單測拋錯了。
✕ should avoid implicit infinite recursive loops with itself (26ms)
✕ should allow explicitly recursive raw function loops (12ms)
再搜一下單測代碼,咱們就知道啦,原來是爲了不遞歸循環的。好比在監聽函數中,又改變了依賴數據,按正常邏輯是會不斷的觸發監聽函數的。但經過effectStack.includes(effect)
這麼一個判斷邏輯,天然而然就避免了遞歸循環。
而後還有個更使人不解的cleanup
。不解的核心緣由是不知道這個deps: Array<Set<ReactiveEffect>>
是怎麼寫入的。爲何監聽函數內部會存着一堆監聽函數集合。在這裏爲何又要刪除它。咱們先保留疑問,後面會解答。
另外,從effect
函數到run
函數,咱們能發現一個顯然不合理之處:
// ...監聽函數類型
interface ReactiveEffect<T = any> {
(): T
// ...
}
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
// ...
fn(...args)
複製代碼
能夠看到,新構建的監聽函數reactiveEffect
,居然是有傳參的,而原始函數,以及監聽函數的接口類型類型都是() => T
,是沒有傳參的...這裏產生了不一致。按道理來講,因爲監聽函數的基本套路仍是自動觸發的,因此應該是沒有參數的。因此此處的args
實際上是沒有意義的。真要傳,在 ts 環境下,也會因爲類型不一致而報錯的。
真要想支持傳參的話,爲了保留原始函數類型(目前是 unknow),須要寫很多類型推導。並且還得限制入參函數必須都是可選的,由於傳入的函數會當即執行一次...因此仍是別傳參了...無論怎麼說,這裏感受能夠優化一下。
回過來,如今咱們的最大問題實際上是,這個effect
爲何又會被存在本身的deps
裏,又是如何被觸發。這其中的邏輯顯然是在以前一直看到的track
跟trigger
中。
看 track 以前,還得先複習一下targetMap
,以前咱們大體知道它是這麼一個結構:
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
複製代碼
打平了看,就是這樣:WeakMap<Target, Map<string | symbol, Set<ReactiveEffect>>>
。
這是一個三維的數據結構。Target
咱們以前就知道了,是被劫持的原始數據。根據咱們現有的知識(以及個人提早告知),咱們能知道。二維KeyToDepMap
的key
,就是這個原始對象的屬性 key。而Dep
就是存放着監聽函數effect
的集合。而後再來看track
代碼:
// 收集依賴的函數
export function track( // 原始數據 target: object, // 操做行爲 type: OperationTypes, key?: string | symbol ) {
// 若是shouldTrack開關關閉,或effectStack中不存在監聽函數,則無須要收集
if (!shouldTrack || effectStack.length === 0) {
return
}
// 獲取effect棧最後一個effect
const effect = effectStack[effectStack.length - 1]
// 是迭代操做的話,從新賦值一下key
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
// 獲取二維map,不存在的話,則初始化
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 獲取effect集合,無則初始化
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
// 若是集合中,沒有剛剛獲取的最後一個effect,則將其add到集合dep中
// 並在effect的deps中也push這個effects集合dep
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
// 開發環境下時,觸發track鉤子函數
if (__DEV__ && effect.onTrack) {
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
複製代碼
唔,有點兒繞。心中有很多疑問,一個個來摸索。
問題 1:爲何從effectStack
尾部獲取的effect
就是依賴該target
的監聽函數。
那是由於這段邏輯:
try {
// 將本effect推到effect棧中
effectStack.push(effect)
// 執行原始函數並返回
return fn(...args)
} finally {
// 執行完之後將effect從棧中推出
effectStack.pop()
}
複製代碼
fn
內引用了依賴數據,執行fn
觸發這些數據的get
,進而走到了track
,而此時effectStack
堆棧尾部正好是該effect
。不過這裏就有一個隱藏的限制,fn
,也就是傳給effect
的原始函數,內部的依賴邏輯必須是同步的。好比這樣是行不通的:
let dummy
const obj = reactive({ prop: 1 })
effect(() => {
setTimeout(() => {
dummy = obj.prop
}, 1000)
})
obj.prop = 2
複製代碼
obj.prop
的變動,並不會讓監聽函數從新執行。fn
也不能是一個async
函數。
不過,在 vue3 中的watch
函數是支持async
。這個在此處就不討論了,主要我還沒研究...
問題 2:targetMap
跟effect
的依賴映射究竟是怎麼樣的。
targetMap
的depsMap
中存了effect
的集合dep
,而effect
中又存了這個dep
...乍看有點兒懵,並且爲何要雙向存?
其實剛剛咱們已經看到了一部分緣由,就是在run
方法中執行的cleanup
。每次 run 以前,會執行它:
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
複製代碼
仔細閱讀track
方法,咱們大體能理清targetMap
,effect.deps
中存着的數據具體是怎麼樣的了。分兩步解釋:
targetMap
中存着一個Map
數據(我稱之爲「響應依賴映射」)。這個響應依賴映射的key
是該響應式數據的某個屬性值,value
是全部用到這個響應數據屬性值的全部監聽函數,也便是Set
集合dep
。dep
。那問題來了,effect
爲何要存着這麼個遞歸數據呢?這是由於要經過cleanup
方法,在本身被執行前,把本身從響應依賴映射中刪除了。而後執行自身原始函數fn
,而後觸發數據的get
,而後觸發track
,而後又會把本effect
添加到相應的Set<ReactiveEffect>
中。有點兒神奇啊,每次執行前,把本身從依賴映射中刪除,執行過程當中,又把本身加回去。
對於這種莫名其妙的邏輯,又是使用單測大法的時候了。我把cleanup
的邏輯給註釋了,再跑一會單測,而後會發現以下單測掛了:
✕ should not be triggered by mutating a property, which is used in an inactive branch (3ms)
其單測邏輯爲:
it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
let dummy
const obj = reactive({ prop: 'value', run: true })
const conditionalSpy = jest.fn(() => {
dummy = obj.run ? obj.prop : 'other'
})
effect(conditionalSpy)
expect(dummy).toBe('value')
expect(conditionalSpy).toHaveBeenCalledTimes(1)
obj.run = false
expect(dummy).toBe('other')
expect(conditionalSpy).toHaveBeenCalledTimes(2)
obj.prop = 'value2'
expect(dummy).toBe('other')
expect(conditionalSpy).toHaveBeenCalledTimes(2)
})
複製代碼
喔~~~這下咱們就明白了,原來是爲了這種帶有分支處理的狀況。由於監聽函數中,可能會因爲 if 等條件判斷語句致使的依賴數據不一樣。因此每次執行函數時,都要從新更新一次依賴。因此纔有了cleanup
這個邏輯。
這樣,咱們就基本搞明白track
的套路跟tragetMap
的邏輯了,而後攻讀trigger
。
咱們先大體瞄一眼這個函數。
// 觸發監聽函數的方法
export function trigger( target: object, // 原始數據 type: OperationTypes, // 寫操做類型 key?: unknown, // 屬性key extraInfo?: DebuggerEventExtraInfo // 拓展信息 ) {
// 獲取原始數據的響應依賴映射,沒有的話,說明沒被監聽,直接返回
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
// 聲明一個effect集合
const effects = new Set<ReactiveEffect>()
// 聲明一個計算屬性集合
const computedRunners = new Set<ReactiveEffect>()
// OperationTypes.CLEAR 表明是集合數據的清除方法,會清除集合數據的全部項
// 若是是清除操做,那就要執行依賴原始數據的全部監聽方法。由於全部項都被清除了。
// addRunners並未執行監聽函數,而是將其推到一個執行隊列中,待後續執行
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// key不爲void 0,則說明確定是SET | ADD | DELETE這三種操做
// 而後將依賴這個key的全部監聽函數推到相應隊列中
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// 若是是增長或者刪除數據的行爲,還要再往相應隊列中增長監聽函數
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
// 若是原始數據是數組,則key爲length,不然爲迭代行爲標識符
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
// 聲明一個run方法
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
// 大體翻譯一下:計算屬性的getter函數必須先執行,由於正常的監聽函數,可能會依賴於計算屬性數據
// 運行全部計算數據的監聽方法
computedRunners.forEach(run)
// 運行全部尋常的監聽函數
effects.forEach(run)
}
複製代碼
除了addRunners
跟scheduleRun
是黑盒外,其餘邏輯大體仍是清晰的。惟獨這兒:
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
複製代碼
每次牽扯到數組/集合以及迭代行爲的時候,老是難以理解,核心是由於咱們對這些數據的底層瞭解比較少。在這裏,咱們不解什麼狀況下會走到第二個邏輯,並且這種狀況確定會重複addRunners
,這沒關係嗎?沒事,不懂就跑單測,咱們把第二個 if 註釋了,而後跑一下effect
單測:
✕ should observe iteration (5ms)
✕ should observe implicit array length changes (1ms)
✕ should observe enumeration (1ms)
發現確實跟註釋所示,迭代器相關的單測出錯了,部分狀況下的監聽函數沒有觸發。若是以前精讀過reactvie
的handlers
,咱們能大體猜測到緣由。具體舉例來講,相似單測中這樣的狀況(稍微修改了下單測,更易理解):
it('should observe iteration', () => {
let dummy
const list = reactive<string[]>([])
effect(() => (dummy = list.join(' ')))
expect(dummy).toBe('')
list.push('Hello')
expect(dummy).toBe('Hello')
})
複製代碼
此處的effect
並無用到數組的某個具體下標,handlers
中的track
實際上是劫持了數組的length
屬性(其實還有join方法
,但此處無用),並跟蹤它的變化。在這種狀況下,depsMap
實際上是length
跟effects
的映射關係。(爲何會track
到length
可在上篇文章中尋找答案)
而在上篇文章reactive
篇中咱們又知道。數組push
行爲觸發的 length 變化,是不會再次觸發trigger
的...因而在這個單測中,就只會觸發一次key
爲0
,value
爲Hello
的trigger
。
在這種狀況下,由於key
爲0
,因此if(key !== void 0)
確實爲真值,但depsMap.get(0)
實際上是爲空的。而depsMap.get('length')
纔是真的有相應effect
,所以必需要有第二個邏輯作補充。
那問題又來了...對於這樣的操做怎麼辦?
it('should observe iteration', () => {
let dummy
const list = reactive<number[]>([])
effect(() => {
dummy = list.length + list[0] || 0
})
expect(dummy).toBe(0)
list.push(1)
expect(dummy).toBe(2)
})
複製代碼
這種狀況下,兩個if
邏輯都會跑到,而且depsMap.get(key)
跟depsMap.get(iterationKey)
都有值。是否是會執行兩次 effect 呢?其實並不會。咱們繼續看addRunners
跟scheduleRun
。
// 將effect添加到執行隊列中
function addRunners( effects: Set<ReactiveEffect>, // 監聽函數集合 computedRunners: Set<ReactiveEffect>, // 計算函數集合 effectsToAdd: Set<ReactiveEffect> | undefined // 待添加的監聽函數或計算函數集合 ) {
// 若是effectsToAdd不存在,啥也不幹
if (effectsToAdd !== void 0) {
// 遍歷effectsToAdd
// 若是是計算函數,則推到computedRunners,不然推到effects
effectsToAdd.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
}
function scheduleRun( effect: ReactiveEffect, target: object, type: OperationTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) {
// 開發環境,而且配置了onTrigger,則觸發該函數,傳入相應數據
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(
extend(
{
effect,
target,
key,
type
},
extraInfo
)
)
}
// 若是配置了自定義的執行器方法,則執行該方法
// 不然執行effect
if (effect.scheduler !== void 0) {
effect.scheduler(effect)
} else {
effect()
}
}
複製代碼
這兩個方法,看名字很厲害的樣子,其實作的事情很簡單,就是把依賴這個響應式數據的全部effects
添加到相應的Set
集裏。若是是computed
的計算方法,就推到computedRunners
裏,不然就推正常的effects
集合裏。因爲這兩個都是Set
集合。
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
複製代碼
因此,若是重複添加,是會自動去重的。因此上面兩個if
邏輯中若是獲取到了相同的監聽函數,也是會自動去重的,並不會被執行屢次。整個effect
就是這麼簡單。沒太多花裏胡哨的,run
就是了。
另外讀完之後咱們也能知道,computed
方法就是一類特殊的,有返回值的effect
。那咱們順路看看完。
關於computed
我就不事無鉅細的講了,基本你們都明白了,直接貼核心的重點。
// 函數重載
// 入參爲getter函數
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
// 入參爲配置項
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
// 真正的函數實現
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value: T
const runner = effect(getter, {
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
// expose effect so computed can be stopped
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
值時,若是沒執行過監聽函數,也就是dirty === true
時,執行一遍監聽函數。避免重複獲取時,重複執行。
通常來講,向computed
傳遞的是一個function
類型,只是獲取一個計算類的數據,返回的數據是沒法修改的。但也有例外,若是傳入的是一個配置項,指定了getter
與setter
方法,那也是容許手動變動computed
數據的。
大體邏輯比較簡單,僅有一個trackChildRun
須要多理解一下:
function trackChildRun(childRunner: ReactiveEffect) {
if (effectStack.length === 0) {
return
}
// 獲取父級effect
const parentRunner = effectStack[effectStack.length - 1]
// 遍歷子級,也便是本effect,的deps
for (let i = 0; i < childRunner.deps.length; i++) {
const dep = childRunner.deps[i]
// 若是子級的某dep中沒有父級effect,則將父級effect添加本dep中,而後更新父級effect的deps
if (!dep.has(parentRunner)) {
dep.add(parentRunner)
parentRunner.deps.push(dep)
}
}
}
複製代碼
單看代碼,想去理解意思實際上是比較繞的。咱們先理解trackChildRun
究竟是爲了什麼。一樣的,使用單測,一招鮮吃遍天。註釋掉它再跑下單測:
✕ should trigger effect (3ms)
✕ should work when chained (2ms)
✕ should trigger effect when chained (1ms)
✕ should trigger effect when chained (mixed invocations) (1ms)
✕ should no longer update when stopped (1ms)
再找到相應單測,咱們就瞭解它的用處了,便是爲了讓依賴computed
的effect
實現監聽邏輯。以單測舉例來講:
it('should trigger effect', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
let dummy
effect(() => {
dummy = cValue.value
})
expect(dummy).toBe(undefined)
value.foo = 1
expect(dummy).toBe(1)
})
複製代碼
若是咱們沒有trackChildRun
的邏輯,當value
變動時,cValue
的計算函數確實是能執行的。可是cValue
讀跟寫並無track
跟trigger
的邏輯,當cValue
變動時,天然也沒法觸發監聽函數。爲了解決這個問題,因而就有了trackChildRun
。
監聽函數,也就是單測中的() => { dummy = cValue.value }
,在它第一次執行時,因爲使用到了cValue
,進行了一次計算函數調用,進而走到trackChildRun
。
而此時,這個監聽函數() => { dummy = cValue.value }
還未執行完,所以它還在effectStack
隊列末尾。將其從末尾將其取出,便是所謂的computed
的父級effect
。
而計算函數自身也是一個effect
,以前咱們說過,它的deps
存着全部存着它的dep
。而這個dep
又指向targetMap
中的相應數據。因爲都是引用數據,因此只要把父級effect
補充到computed.deps
,就等同於作到了父級effect
依賴於computed
函數內部依賴的響應數據。
這兩段話提及來確實有點繞,多理解理解就好。但文章到這也差很少結束了,後面我看看,能不能出一張大圖,把整套響應式系統涉及的全部相關數據給繪製清楚,方便你們更直觀的瞭解。
其餘幾篇文章能夠戳專欄主頁自行查看。下週我再彙總一篇,作個引導,並把這過程當中有變動的代碼再調整一下。謝謝您的閱讀。