为了更好的作解释,我会调整源码中的接口、类型、函数等声明顺序,并会增长一些注释方便阅读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
函数内部依赖的响应数据。
这两段话提及来确实有点绕,多理解理解就好。但文章到这也差很少结束了,后面我看看,能不能出一张大图,把整套响应式系统涉及的全部相关数据给绘制清楚,方便你们更直观的了解。
其余几篇文章能够戳专栏主页自行查看。下周我再汇总一篇,作个引导,并把这过程当中有变动的代码再调整一下。谢谢您的阅读。