前言
注:本文采用vue版本为3.4.0-alpha.1
在vue.3.4.0-alpha.1
之前,vue3 的响应式大部分是积极的。
什么意思?我们知道,vue3是基于 effect
通过 Proxy
的 get
等拦截器收集依赖,然后通过触发Proxy
的set
,来触发依赖了当前响应数据的 effect
,来实现的响应式的。
但在部分响应式数据中,一次修改可能触发多次响应式更新,虽然存在新旧值对比的机制,但新值不一定是最新的。
比如下面的一个例子。这个例子来自于这个issues
typescript
import { ref, effect } from "vue"
const store = ref([])
let counterForRun = 0
const effectRunner = effect(() => {
console.log(`effect run times is ${counterForRun}`)
if (store.value.length > 0) {
console.log(`store value is ${JSON.stringify(store.value)}`)
store.value.splice(0)
}
counterForRun += 1
})
let intervalTimes = 0
const intervalId = setInterval(() => {
if (intervalTimes === 2) {
clearInterval(intervalId)
return
}
store.value.push(intervalTimes)
intervalTimes += 1
}, 1000)
这个例子做了什么呢?
首先定义了一个响应式空数组,store
。
然后定义了counterForRun
,来记录effectRunner
的触发次数,初始值是0
,effectRunner
每触发一次,counterForRun
就会自增1
。
而effectRunner
会在开始打印counterForRun
的次数,如果发现store
数组长度大于0
,就将他重置为0
,同时这一步收集了数组的length
来作为effectRunner
的依赖。
然后定义了一个定时器,这个定时器的作用是是异步触发store
的更新,从而触发effectRunner
的执行。
这个定时器只会执行2
次。
定时器的逻辑换成一个按钮,然后自己点两下,每次让store
push
任意一个值,结果也是对等的。
那么这个例子的运行结果是什么呢?
typescript
effect run times is 0
effect run times is 1
store value is [0]
effect run times is 2
store value is [null]
effect run times is 3
store value is [1]
effect run times is 4
store value is [null]
我们会发现,effect
执行了4
次,在第5行和第9行出现了 [null]
。
为什么effect
会执行4
次?并且store一度等于 [null]
。
不过略微思考一下就明白了。
我们知道,在ref
中传入对象,最终还是使用reactive
,也就是Proxy
来返回代理对象,而数组是特殊的对象,因此数组的修改,是会触发Proxy
的set
拦截器的,并且可能触发多次。
我们来确定一下。
typescript
const arr = []
const proxyArr = new Proxy(arr,{
set(target, key, value, receiver) {
console.log(key, value)
Reflect.set(target, key, value, receiver)
return true
},
})
proxyArr.push(0)
// 0 0
// length 1
proxyArr.splice(0)
// length 0
push
会触发两次set
,第一次key
是索引,第二次key
是length
。而splice
只会触发key
是length
的set
(当然也会触发deleteProperty
,我们只考虑set
拦截器)。但splice
触发的时候,当前activeEffect
是effectRunner
,所以不会重复触发。
因此会执行四次,第一个是索引触发,第二次是length
触发,第三次是索引触发,第四次是length
。
但是会出现[null]
呢?我们来捋一下逻辑:
当使用push
的时候,第一次索引变更,触发了数组的set
拦截器,通过Reflect.set
更新值后,触发了trigger
,进一步触发了triggerEffects
,遍历依赖这个数组的effect
,从而执行这个effect
的run
方法,run
方法最后执行了effectRunner
包装的函数,可以说这一步触发了effectRunner
。
请注意,这个时候key
是length
的set
依然还没有触发。
但是store
事实上已经是一个非空响应式数组了。
因此顺利通过if
判断条件,执行了数组的splice
方法。
这个时候再次触发了数组的set
拦截器,通过Reflect.set
更新值后,也会走trigger
和triggerEffects
,但是由于activeEffect
是当前effect
,因此不会进入effect
的run
方法。
此时索引变更的拦截器逻辑才执行完,但由于splice
方法,store
事实上又变成了一个空响应式数组了。
但逻辑还没完,因为js
是单线程的,这个时候push
操作导致key
是length
的set
拦截器被触发了。
在set
拦截器中,key
是length
,value
是1
,但此时store
数组事实上已经是一个空响应式数组了。
所以Reflect.set
对store
的length
赋值为1
。从而把store
变成了[null]
。之后还会触发一系列的响应式操作,再次触发ffectRunner
,然后再次触发splice
方法,从而让结果回归正轨,但与我们的结论没什么太大关系了。
我们在这一步就可以得出结论,由于数组的push
操作触发的两次set
,并且其中夹杂一次splice
触发的set
,从而导致数组会暴露出一个[null]
瞬间状态。
而3.4.0
的这个提交可以解决这个问题。
typescript
effect run times is 0
effect run times is 1
store value is [0]
effect run times is 2
store value is [1]
看起来逻辑是符合直觉的,push
只会触发一次counterForRun
,并且splice
也没有触发counterForRun
。
那么当前逻辑是如何呢?
为了方便理解,我们从收集依赖就开始追踪代码,来看看到底存在哪些变化,而这些变化为什么会防止上面的问题出现。
依赖收集
ref
的逻辑没有变动,我们直接略过,在创建effect
的ReactiveEffect
,出现了变化。
我们知道effect
实际是new ReactiveEffect
的封装,但在之前ReactiveEffect
需要的参数分别是get, scheduler, scope
,但现在,在第二个位置增加了一个参数,trigger
,而scheduler
和scope
则后移。
typescript
const _effect = new ReactiveEffect(fn, NOOP, () => {
if (_effect.dirty) {
_effect.run()
}
})
他会检查dirty
是否是true
,true
才会执行run
,也就是effectRunner
。
在创建了一个effect
对象后,会紧接着调用run
方法。
typescript
if (!options || !options.lazy) { // option undefined
_effect.run()
}
我们进入ReactiveEffect
,看看发生了什么变化。
typescript
export class ReactiveEffect<T = any> {
active = true; // 表示当前effect是否处于活动状态
deps: Dep[] = []; // 存储与effect相关的依赖项数组
computed?: ComputedRefImpl<T>; // 表示该effect是由某个计算属性触发的
allowRecurse?: boolean; // 表示该effect是否允许递归运行
onStop?: () => void;
_dirtyLevel = DirtyLevels.Dirty; // 内部属性,表示当前effect的脏状态级别,默认为 Dirty
_trackId = 0; // 内部属性,用于标识追踪的 ID
_runnings = 0; // 内部属性,表示当前正在运行中的effect的数量
_queryings = 0; // 内部属性,表示当前正在查询的effect的数量
_depsLength = 0; // 内部属性,表示当前effect的依赖项数组的长度
constructor(
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope
) {
recordEffectScope(this, scope); // 记录effect的范围信息
}
// 获取当前effect的脏状态
public get dirty() {
// ...
}
// 设置当前effect的脏状态
public set dirty(v) {
// ...
}
run() {
this._dirtyLevel = DirtyLevels.NotDirty; // 将脏状态NotDirty
if (!this.active) {
return this.fn(); // 如果effect不处于活动状态,直接执行
}
let lastShouldTrack = shouldTrack;
let lastEffect = activeEffect;
try {
shouldTrack = true;
activeEffect = this;
this._runnings++; // 增加运行中effect的数量
preCleanupEffect(this); // 执行effect前的清理操作
return this.fn();
} finally {
postCleanupEffect(this); // 执行effect后的清理操作
this._runnings--; // 减少运行中effect的数量
activeEffect = lastEffect; // 恢复之前的活动effect
shouldTrack = lastShouldTrack; // 恢复之前的追踪状态
}
}
stop() {
if (this.active) {
preCleanupEffect(this); // 执行effect前的清理操作
postCleanupEffect(this); // 执行effect后的清理操作
this.onStop?.();
this.active = false;
}
}
}
我们省略了一些属性,这次只看初始化的。在run
方法中,我们会执行传入的函数,然后收集依赖。
在之前的逻辑,会通过parent
来判断是否有循环依赖的问题,同时还有基于effectTrackDepth
的依赖对比和依赖清理逻辑。
但现在更改为基于_runnings
计数来判断是否有循环依赖的问题,执行fun
的时候,_runnings
会自增,执行finally
的时候,_runnings
会自减。
在后面的逻辑中,如果会检测到当前effect
的_runnings
不为0
,说明finally
并没有执行,出现了循环。
而依赖对比和依赖清理,则由preCleanupEffect
和postCleanupEffect
负责。
typescript
function preCleanupEffect(effect: ReactiveEffect) {
effect._trackId++; // 增加effect的追踪ID
effect._depsLength = 0; // 重置依赖项数组长度为 0
}
function postCleanupEffect(effect: ReactiveEffect) {
if (effect.deps && effect.deps.length > effect._depsLength) {
for (let i = effect._depsLength; i < effect.deps.length; i++) {
cleanupDepEffect(effect.deps[i], effect); // 清理多余的依赖项
}
effect.deps.length = effect._depsLength; // 调整依赖项数组的长度
}
}
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
const trackId = dep.get(effect); // 获取依赖项中的追踪 ID
if (trackId !== undefined && effect._trackId !== trackId) {
dep.delete(effect); // 删除无关的依赖项
if (dep.size === 0) {
dep.cleanup(); // 如果依赖项为空,执行清理操作
}
}
}
在执行fun
之前,preCleanupEffect
会永久自增_trackId
,确保了每次effect
的fun
运行时,其_trackId
都是唯一的,避免了在不同追踪周期间引入混淆。同时,重置依赖项数组长度确保了只追踪在本次运行期间访问的新依赖项,避免了旧依赖项的影响。
在finally
中,会执行postCleanupEffect
,由于依赖项数组可能在fun
运行时被动态添加新的依赖,执行后需要清理多余的依赖项,确保依赖项的数量与实际被追踪的依赖一致。这样,便于在下次effect
触发,只追踪新的依赖项,提高了响应式系统的性能。
清理主要靠cleanupDepEffect
,这个函数负责根据_trackId
判断依赖项是否与effect
相关,如果不相关,则删除该关系。
在执行fun
触发store
的get
拦截器,执行trackRefValue
,从而收集依赖,这个逻辑没有什么变化,但初始化依赖的逻辑发生了变化。
typescript
(ref.dep = createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined
)),
我们看一下createDep
是什么样子
typescript
export const createDep = (
cleanup: () => void, // 清理函数,在不再需要依赖项时调用
computed?: ComputedRefImpl<any> // 可选的计算属性实例,表示该依赖项是由计算属性触发的
): Dep => {
const dep = new Map() as Dep; // 创建一个新的 Map 对象,作为依赖项
dep.cleanup = cleanup; // 设置清理函数,用于在不再需要依赖项时执行清理操作
dep.computed = computed; // 设置计算属性实例,表示该依赖项是由计算属性触发的
return dep; // 返回创建的依赖项实例
}
该函数接受两个参数:一个是清理函数cleanup
,用于在不再需要依赖项时执行清理操作;另一个是可选的计算属性实例computed
,表示该依赖项是由计算属性触发的。函数内部创建了一个新的Map
对象,将其类型断言为Dep
类型,并设置了cleanup
和computed
属性。最后,返回创建的依赖项实例。
上文中,cleanupDepEffect
调用的的dep.cleanup
实际执行的就是ref.dep = undefined
。
trackRefValue中
,会检查ref
是不是计算属性,如果是的话,会把计算属性传入第二个参数,而trackRefValue
主要是在ref
和computed
来使用。
在之前的逻辑中,Dep
是一个Set
对象,并定义了n
和w
来识别新旧依赖。
创建并收集完依赖后,会执行之后会执行trackEffect
,在之前的逻辑是执行trackEffects
。逻辑类似,但也进行了变动。我们看一下。
typescript
export function trackEffect(
effect: ReactiveEffect, // 当前活动的effect
dep: Dep, // 当前的依赖项
debuggerEventExtraInfo?: DebuggerEventExtraInfo // 可选的调试事件信息
) {
// 如果当前依赖项中没有记录该effect的追踪 ID,则建立关联
if (dep.get(effect) !== effect._trackId) {
dep.set(effect, effect._trackId); // 在依赖项中记录effect的追踪 ID
// 获取当前effect的依赖项数组中的最后一个依赖项
const oldDep = effect.deps[effect._depsLength];
// 如果最后一个依赖项不是当前依赖项,进行处理
if (oldDep !== dep) {
if (oldDep) {
cleanupDepEffect(oldDep, effect); // 清理旧的依赖项关系
}
effect.deps[effect._depsLength++] = dep; // 将当前依赖项添加到effect的依赖项数组中
} else {
effect._depsLength++; // 如果最后一个依赖项就是当前依赖项,增加依赖项数组长度
}
}
}
该函数的主要作用是建立当前活动的effect
,也就是effectRunner
与指定dep
之间的关系。如果当前依赖项中没有记录该effect
的_trackId
,则在依赖项中记录该_trackId
,并将该依赖项添加到当前effect
的依赖项数组中。与之前不同的是,之前使用effectTrackDepth
,这这里替换为effect
自己的_depsLength
。
当然,还会触发数组length
的get
拦截器,从而再次触发收集依赖,这个也跟之前逻辑相同,只不过track
移到了reactiveEffect.ts
中,初始化依赖的逻辑同样发生了变化。
typescript
// 如果指定键的依赖项不存在,创建一个新的依赖项,并设置清理函数用于删除依赖项
if (!dep) {
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))));
}
至此,依赖收集完毕,我们看触发逻辑。
触发依赖
首先,我们知道,push
实际执行的是经过劫持的push
,在原来push
暂停收集依赖的基础上,增加了暂停调度和重启调度函数
typescript
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling() // 暂停调度
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling() // 恢复调度
resetTracking()
return res
}
})
pauseScheduling
和resetScheduling
是这次新增的函数,并且他们必定成对出现。而他们的逻辑也很简单。
typescript
// 用于存储暂停调度的计数
export let pauseScheduleStack = 0;
// 用于存储effect调度器函数
const queueEffectSchedulers: (() => void)[] = [];
// 用于增加暂停调度的计数
export function pauseScheduling() {
pauseScheduleStack++; // 增加暂停调度的计数
}
// 减少暂停调度的计数,并在计数为零时执行队列中的effect调度器函数
export function resetScheduling() {
pauseScheduleStack--; // 减少暂停调度的计数
// 当暂停调度的计数为零且effect调度器队列不为空时,执行队列中的effect调度器函数并将其移出队列
while (!pauseScheduleStack && queueEffectSchedulers.length) {
queueEffectSchedulers.shift()!();
}
}
也就是说,这两个函数提供了一个机制,可以通过增加和减少pauseScheduleStack
的值来控制调度的暂停和继续,并且在适当的时机执行与这个调度机制相关的effect
调度器函数。
也就是执行resetScheduling
可能并不会让存储的调度函数执行,只有pauseScheduleStack
被减为0
的时候,才会让存储下来的调度器执行。
那么此时pauseScheduleStack
自增成为1
。
然后执行了数组真正的push
,从而触发了set
拦截器。set
拦截器并没有变动,顺利走到trigger
函数里面。
trigger
函数移动了位置,从effect.ts
移动到了reactiveEffect.ts
。
trigger
的逻辑并没有太大的变化,只是最后触发依赖的时候与原先不同。
typescript
pauseScheduling(); // 暂停调度
// 遍历所有需要触发更新的依赖,并触发它们的effect
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty, // 标记为Dirty,表示需要重新计算
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget
}
: void 0
);
}
}
resetScheduling(); // 恢复调度
在原先的逻辑中,如果deps
长度为1
且是有效的是会直接触发,其他情况将deps
进行循环过滤,如果dep
是有效值,会将他进行展开 后推入一个数组,这样这个数组就是存放effect
的一维数组,然后使用createDep
包装成Set
,传入triggerEffects
。然后triggerEffects
会再次转为数组,遍历两次这个数组,使用triggerEffect
提前触发计算属性的effect
,然后触发其他effect
。
而现在的逻辑,先暂停调度,此时pauseScheduleStack
自增成为2
。然后将deps
进行遍历,dep
传入triggerEffects
,而triggerEffects
的逻辑也跟之前不同了。
typescript
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,// 脏状态的级别
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
pauseScheduling(); // 暂停调度
// 遍历依赖项的所有effect
for (const effect of dep.keys()) {
// 如果effect不允许递归运行且正在运行,则跳过此次循环
if (!effect.allowRecurse && effect._runnings) {
continue;
}
// 如果effect的脏状态小于指定的脏状态级别,执行下面的逻辑
if (
effect._dirtyLevel < dirtyLevel &&
(!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
) {
const lastDirtyLevel = effect._dirtyLevel;
effect._dirtyLevel = dirtyLevel; // 更新effect的脏状态
// 如果上一次脏状态为 NotDirty,并且effect没有正在获取脏状态,执行下面的逻辑
if (
lastDirtyLevel === DirtyLevels.NotDirty &&
(!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
) {
effect.trigger(); // 触发effect的trigger
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler); // 将effect的调度器加入调度队列
}
}
}
}
resetScheduling(); // 恢复调度
}
现在triggerEffects
会先暂停调取,此时pauseScheduleStack
自增成为3
。
然后遍历外部传入的dep
,根据脏状态的级别触发相应的更新。并且将调度函数推入queueEffectSchedulers
中。
我们前面讲过,queueEffectSchedulers
会在resetScheduling
中,pauseScheduleStack
为0
的时候触发。
这里,出现了DirtyLevels
这个枚举,我们来看一下DirtyLevels
的具体定义。
typescript
export const enum DirtyLevels {
NotDirty = 0, // 表示数据未脏,即数据没有发生变化。
ComputedValueMaybeDirty = 1, // 表示计算属性可能处于脏状态。当计算属性依赖的数据发生变化时,计算属性可能需要重新计算,但不确定是否真的脏。
ComputedValueDirty = 2, // 表示计算属性处于脏状态。当计算属性依赖的数据发生变化,并且计算属性确实需要重新计算时,它处于脏状态
Dirty = 3 // 表示数据处于脏状态。当普通数据(非计算属性)发生变化时,数据处于脏状态。
}
这就表示,如果数据变化了,也不一定会触发对应的响应式,还需要对应的级别,在原来triggerEffects
中,会优先触发计算属性的effect
和依赖他的响应逻辑。
这是因为如果某个响应数据变动导致effect
的触发,而effect
中恰好存在计算属性,那么此时计算属性的effect
的调度函数还没触发,因此计算属性中的dirty
是false
。所以获取的value
是之前的缓存的。
所以之前triggerEffects
的逻辑是优先触发计算属性的effect
以及对应依赖他的effect
。
而现在triggerEffects
的逻辑是会给予一个对应级别的脏状态,如果effect
自身的脏状态小于这个脏状态,那么就需要更新为最大的脏状态,而执行effect
的run
的时候更改为NotDirty
。
我们接着看,当前脏状态是Dirty
,且dep
自身的脏状态是0
,所以最后会触发effect.trigger()
,在这里,effect的trigger
是空函数,所以会将scheduler
推入queueEffectSchedulers
中。
遍历结束后,执行resetScheduling
恢复调度,pauseScheduleStack
自减变为2
,但因为没有恢复0
,所以不会执行queueEffectSchedulers
队列。
然后执行trigger
的resetScheduling
,pauseScheduleStack
自减变为1
,但因为没有恢复0
,所以不会执行queueEffectSchedulers
队列。
接着结束执行索引触发的set
拦截器。
然后比较关键的是,在之前的逻辑,会触发splice
的set
拦截器,但在新的逻辑,并不会这么做,因为scheduler
并没有执行,而是推入了queueEffectSchedulers
队列。
而最后一个resetScheduling
是在push
之后执行,但此时push
并没有结束,因为还有length
触发的set
拦截器,但因为之前就存在对应的key(length)
,且新值和旧值一样,所以这次只执行反射的赋值,不会进入trigger
。
在上文的例子,因为splice
的执行,导致了新值和旧值的不同,所以会进入trigger
。
结束执行length
触发的set
拦截器。这次我们执行最后的resetScheduling
。pauseScheduleStack
自减变为0
,会执行存储在queueEffectSchedulers
中的scheduler
。
在前文中,我们应该还记得scheduler
是定义effect
的时候传入的。
他会检查dirty
是否是true
,true
才会执行run
,也就是触发effectRunner
。
typescript
public get dirty() {
// 省略
// 返回effect的脏状态是否大于等于 ComputedValueDirty
return this._dirtyLevel >= DirtyLevels.ComputedValueDirty;
}
因为在前面将他的_dirtyLevel
设置为Dirty
所以返回的是true
,其他逻辑我们后面再说。
之后会触发run
,也就是触发了effectRunner
。
而effectRunner
会触发splice
,从而再次触发set
拦截器。
但是最后走到triggerEffects
的时候,由于_runnings
非0
,所以会跳出此次循环,从而没有将scheduler
推入队列。无事发生。
因此splice
也不会触发effectRunner
。
至此,push
会触发一次effect
,而splice
不会触发,原因是索引会触发一次,length
因为新旧值相同不会触发,splice
因为_runnings
非0,也不会触发。
计算属性
对于计算属性的处理,则通过effect
中的get dirty
进行处理。
typescript
// 获取effect的脏状态,表示effect是否需要重新计算
public get dirty() {
// 如果计算属性的脏状态为 ComputedValueMaybeDirty,执行下面的逻辑
if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
this._dirtyLevel = DirtyLevels.NotDirty; // 将脏状态重置为 NotDirty
this._queryings++; // 增加查询状态计数
pauseTracking(); // 暂停依赖追踪
// 遍历effect的依赖,并触发与之相关的计算属性
for (const dep of this.deps) {
if (dep.computed) {
triggerComputed(dep.computed); // 触发依赖的计算属性
// 如果effect的脏状态变为 ComputedValueDirty,跳出循环
if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
break;
}
}
}
resetTracking(); // 恢复依赖追踪
this._queryings--; // 减少查询状态计数
}
// 返回effect的脏状态是否大于等于 ComputedValueDirty
return this._dirtyLevel >= DirtyLevels.ComputedValueDirty;
}
也就是说如果此effect
的脏状态是ComputedValueDirty
或者Dirty
,那么dirty
就是true
,对应的逻辑会走全量更新或者执行effect.run
。
如果此effect
的脏状态是NotDirty
,那么dirty
就是false
,对应的逻辑会使用缓存值,或者跳过effect.run
的执行。
如果此effect
的脏状态是ComputedValueMaybeDirty
,那么将循环执行依赖的计算属性,这个时候会更改此effect
的脏状态,所以这里有个优化,一旦发现当前effect
的脏状态大于等于ComputedValueDirty
,那么跳出循环。
我们直接看个例子。
typescript
const a = ref(0)
const b = computed(() => a.value + 1)
const fun = effect(() => {
console.log(b.value)
})
a.value++
- 因为
a
的值更新,依赖a
的计算属性会被标记为Dirty
。 - 而依赖计算属性的
fun
会被标记为ComputedValueMaybeDirty
。最终被推入queueEffectSchedulers
队列。 queueEffectSchedulers
队列开始依次执行scheduler
,会检查fun
的dirty
,从而触发他的dirty
拦截器。- 而因为他的标记是
ComputedValueMaybeDirty
,从而循环执行他依赖的计算属性,这个计算属性在trackEffect
就传入了。 - 而因为计算属性的
effect
的标记是Dirty
,因此会获取最新的值,计算属性的effect
的标记更改为NotDirty
,然后会触发riggerRefValue
,从而给fun
打上ComputedValueDirty
标记。 - 执行完毕之后,因为
fun
已经是ComputedValueDirty
,所以跳出循环,从而实际触发fun
的响应函数。 - 里面还会再触发一次计算属性,但因为计算属性的
effect
是NotDirty
,所以使用缓存值 - 如果里面还有其它计算属性,没有被
第6步
遍历到。会在获取值的时候,获取这个计算属性的effect
的dirty
,也就是获取他的脏状态,也就是前面的第5步
逻辑,然后根据脏状态来判断计算属性重新计算还是使用缓存的值。
可以看出来,新的响应式逻辑是基于脏状态的的变动,从而选择式做出响应,但也支持外界强制变更,让他在下次必定做出响应。
typescript
instance.effect.dirty = true
instance.update()
public set dirty(v) {
this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}
如果指定dirty
为true
,那么就会给effect
的脏状态设置为Dirty
,从而使下次必定触发effect
。
最后
由于当前并非正式版本,可能存在很多边界问题,也可能存在不少BUG,甚至有可能最后不会被采纳,不过这些更改的思考逻辑是值得学习的。