vue3.4.0-alpha.1 响应式逻辑的变动

前言

注:本文采用vue版本为3.4.0-alpha.1

vue.3.4.0-alpha.1之前,vue3 的响应式大部分是积极的。

什么意思?我们知道,vue3是基于 effect 通过 Proxyget 等拦截器收集依赖,然后通过触发Proxyset,来触发依赖了当前响应数据的 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的触发次数,初始值是0effectRunner每触发一次,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来返回代理对象,而数组是特殊的对象,因此数组的修改,是会触发Proxyset拦截器的,并且可能触发多次。

我们来确定一下。

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是索引,第二次keylength。而splice只会触发keylengthset(当然也会触发deleteProperty,我们只考虑set拦截器)。但splice触发的时候,当前activeEffecteffectRunner,所以不会重复触发。

因此会执行四次,第一个是索引触发,第二次是length触发,第三次是索引触发,第四次是length

但是会出现[null]呢?我们来捋一下逻辑:

当使用push的时候,第一次索引变更,触发了数组的set拦截器,通过Reflect.set更新值后,触发了trigger,进一步触发了triggerEffects,遍历依赖这个数组的effect,从而执行这个effectrun方法,run方法最后执行了effectRunner包装的函数,可以说这一步触发了effectRunner

请注意,这个时候keylengthset依然还没有触发。

但是store事实上已经是一个非空响应式数组了。

因此顺利通过if判断条件,执行了数组的splice方法。

这个时候再次触发了数组的set拦截器,通过Reflect.set更新值后,也会走triggertriggerEffects,但是由于activeEffect是当前effect,因此不会进入effectrun方法。

此时索引变更的拦截器逻辑才执行完,但由于splice方法,store事实上又变成了一个空响应式数组了。

但逻辑还没完,因为js是单线程的,这个时候push操作导致keylengthset拦截器被触发了。

set拦截器中,keylengthvalue1,但此时store数组事实上已经是一个空响应式数组了。

所以Reflect.setstorelength赋值为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的逻辑没有变动,我们直接略过,在创建effectReactiveEffect,出现了变化。

我们知道effect实际是new ReactiveEffect的封装,但在之前ReactiveEffect需要的参数分别是get, scheduler, scope,但现在,在第二个位置增加了一个参数,trigger,而schedulerscope则后移。

typescript 复制代码
  const _effect = new ReactiveEffect(fn, NOOP, () => {
    if (_effect.dirty) {
      _effect.run()
    }
  })

他会检查dirty是否是truetrue才会执行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并没有执行,出现了循环。

而依赖对比和依赖清理,则由preCleanupEffectpostCleanupEffect负责。

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,确保了每次effectfun运行时,其_trackId都是唯一的,避免了在不同追踪周期间引入混淆。同时,重置依赖项数组长度确保了只追踪在本次运行期间访问的新依赖项,避免了旧依赖项的影响。

finally中,会执行postCleanupEffect,由于依赖项数组可能在fun运行时被动态添加新的依赖,执行后需要清理多余的依赖项,确保依赖项的数量与实际被追踪的依赖一致。这样,便于在下次effect触发,只追踪新的依赖项,提高了响应式系统的性能。

清理主要靠cleanupDepEffect,这个函数负责根据_trackId判断依赖项是否与effect相关,如果不相关,则删除该关系。

在执行fun触发storeget拦截器,执行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 类型,并设置了cleanupcomputed属性。最后,返回创建的依赖项实例。

上文中,cleanupDepEffect调用的的dep.cleanup实际执行的就是ref.dep = undefined

trackRefValue中,会检查ref是不是计算属性,如果是的话,会把计算属性传入第二个参数,而trackRefValue主要是在refcomputed来使用。

在之前的逻辑中,Dep是一个Set对象,并定义了nw来识别新旧依赖。

创建并收集完依赖后,会执行之后会执行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

当然,还会触发数组lengthget拦截器,从而再次触发收集依赖,这个也跟之前逻辑相同,只不过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
    }
  })

pauseSchedulingresetScheduling是这次新增的函数,并且他们必定成对出现。而他们的逻辑也很简单。

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中,pauseScheduleStack0的时候触发。

这里,出现了DirtyLevels这个枚举,我们来看一下DirtyLevels的具体定义。

typescript 复制代码
export const enum DirtyLevels {
  NotDirty = 0, // 表示数据未脏,即数据没有发生变化。
  ComputedValueMaybeDirty = 1, // 表示计算属性可能处于脏状态。当计算属性依赖的数据发生变化时,计算属性可能需要重新计算,但不确定是否真的脏。
  ComputedValueDirty = 2, // 表示计算属性处于脏状态。当计算属性依赖的数据发生变化,并且计算属性确实需要重新计算时,它处于脏状态
  Dirty = 3 // 表示数据处于脏状态。当普通数据(非计算属性)发生变化时,数据处于脏状态。
}

这就表示,如果数据变化了,也不一定会触发对应的响应式,还需要对应的级别,在原来triggerEffects中,会优先触发计算属性的effect和依赖他的响应逻辑。

这是因为如果某个响应数据变动导致effect的触发,而effect中恰好存在计算属性,那么此时计算属性的effect的调度函数还没触发,因此计算属性中的dirtyfalse。所以获取的value是之前的缓存的。

所以之前triggerEffects的逻辑是优先触发计算属性的effect以及对应依赖他的effect

而现在triggerEffects的逻辑是会给予一个对应级别的脏状态,如果effect自身的脏状态小于这个脏状态,那么就需要更新为最大的脏状态,而执行effectrun的时候更改为NotDirty

我们接着看,当前脏状态是Dirty,且dep自身的脏状态是0,所以最后会触发effect.trigger(),在这里,effect的trigger是空函数,所以会将scheduler推入queueEffectSchedulers中。

遍历结束后,执行resetScheduling恢复调度,pauseScheduleStack 自减变为2,但因为没有恢复0,所以不会执行queueEffectSchedulers队列。

然后执行triggerresetSchedulingpauseScheduleStack自减变为1,但因为没有恢复0,所以不会执行queueEffectSchedulers队列。

接着结束执行索引触发的set拦截器。

然后比较关键的是,在之前的逻辑,会触发spliceset拦截器,但在新的逻辑,并不会这么做,因为scheduler并没有执行,而是推入了queueEffectSchedulers队列。

而最后一个resetScheduling是在push之后执行,但此时push并没有结束,因为还有length触发的set拦截器,但因为之前就存在对应的key(length),且新值和旧值一样,所以这次只执行反射的赋值,不会进入trigger

在上文的例子,因为splice的执行,导致了新值和旧值的不同,所以会进入trigger

结束执行length触发的set拦截器。这次我们执行最后的resetSchedulingpauseScheduleStack自减变为0,会执行存储在queueEffectSchedulers中的scheduler

在前文中,我们应该还记得scheduler是定义effect的时候传入的。

他会检查dirty是否是truetrue才会执行run,也就是触发effectRunner

typescript 复制代码
public get dirty() {
  // 省略
  // 返回effect的脏状态是否大于等于 ComputedValueDirty
  return this._dirtyLevel >= DirtyLevels.ComputedValueDirty;
}

因为在前面将他的_dirtyLevel设置为Dirty所以返回的是true,其他逻辑我们后面再说。

之后会触发run,也就是触发了effectRunner

effectRunner会触发splice,从而再次触发set拦截器。

但是最后走到triggerEffects的时候,由于_runnings0,所以会跳出此次循环,从而没有将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++
  1. 因为a的值更新,依赖a的计算属性会被标记为Dirty
  2. 而依赖计算属性的fun会被标记为ComputedValueMaybeDirty。最终被推入queueEffectSchedulers队列。
  3. queueEffectSchedulers队列开始依次执行scheduler,会检查fundirty,从而触发他的dirty拦截器。
  4. 而因为他的标记是ComputedValueMaybeDirty,从而循环执行他依赖的计算属性,这个计算属性在trackEffect就传入了。
  5. 而因为计算属性的effect的标记是Dirty,因此会获取最新的值,计算属性的effect的标记更改为NotDirty,然后会触发riggerRefValue,从而给fun打上ComputedValueDirty标记。
  6. 执行完毕之后,因为fun已经是ComputedValueDirty,所以跳出循环,从而实际触发fun的响应函数。
  7. 里面还会再触发一次计算属性,但因为计算属性的effectNotDirty,所以使用缓存值
  8. 如果里面还有其它计算属性,没有被第6步遍历到。会在获取值的时候,获取这个计算属性的effectdirty,也就是获取他的脏状态,也就是前面的第5步逻辑,然后根据脏状态来判断计算属性重新计算还是使用缓存的值。

可以看出来,新的响应式逻辑是基于脏状态的的变动,从而选择式做出响应,但也支持外界强制变更,让他在下次必定做出响应。

typescript 复制代码
instance.effect.dirty = true
instance.update()


public set dirty(v) {
    this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
}

如果指定dirtytrue,那么就会给effect的脏状态设置为Dirty,从而使下次必定触发effect

最后

由于当前并非正式版本,可能存在很多边界问题,也可能存在不少BUG,甚至有可能最后不会被采纳,不过这些更改的思考逻辑是值得学习的。

相关推荐
YBN娜11 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=11 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck15 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!36 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。41 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架