从零实现Vue响应系统(一、概念与基础架构)

从零实现Vue响应系统(一、概念与基础架构)

这篇文章是我系统学习《Vue设计与实现》一书的笔记开篇,主要介绍了响应式数据与副作用函数的概念,以及如何设计一个完善的响应式系统。后续我会就这一本书,逐章逐节进行阅读,并将阅读笔记整理成文章发布至此。

5351字

在 Vue.js 中,响应系统贯穿了这款框架的设计始终。如果说 React 是遵循 单向数据流 的框架,Vue 则通过 响应式数据绑定 实现视图与数据的自动同步。那么 Vue3 中的响应式系统究竟是怎么实现的呢?我很好奇!

注:本文是 Vue 设计与实现 一书的阅读笔记,包括原文的部分摘抄以及本人的总结,尽可能用自己的理解描述一遍,并附上完整的实现代码。 本文中的完整代码地址:https://github.com/nonhana/demo-vue/blob/main/src/js/responsiveData.js

响应式数据与副作用函数

副作用函数

当我们在接触 React 或者是一些函数式编程概念的时候,我们或多或少都有听过一个名词:副作用。而副作用函数也就是那些会产生副作用的函数。

那么什么是副作用?

javascript
function effect() {
  document.body.textContent = 'Hello World'
}

对于这个函数而言,它的作用是 设置 body 的文本内容,但是 除了 effect 以外的任何函数都可以读取或设置 body 的文本内容。所以这个函数的执行很可能会 直接或间接地影响其他函数的执行。这个时候我们就会说 effect 函数产生了副作用。

因此,副作用函数的本质就是 这个函数内部进行的操作会影响到外部某些事物的变化,而这些外部的事物能够被其他的函数使用。

更直观的例子:

javascript
let val = 1

function effect() {
  val = 2
}

function effect2() {
  console.log(val + 1)
}

如果我们不执行 effect,那么最后会打印 2;如果执行了 effect,最后会打印 3。

而影响打印结果的并不是最终进行打印的 effect2 函数,那么我们就可以说 effect 函数产生了副作用,effect2 函数就是受到了副作用的影响。如果我们不关注具体代码中在哪里调用了 effect 函数而只关注 effect2,那么我们就会发现输出的值是 不可预测的

为了做对比,我们也来探讨一下在函数式编程领域最重要的一个概念:纯函数

纯函数是指:

  1. 给定相同的输入,永远会得到相同的输出。这意味着函数的输出仅仅依赖于它的输入参数,而与外部的状态或变量无关。无论函数被调用多少次,只要输入相同,输出也一定相同。
  2. 没有副作用。副作用指的是函数内部改变了外部世界的状态,或者对外部环境(如全局变量、I/O 操作等)产生影响。纯函数不会修改外部的变量或状态,也不会与外部环境产生任何交互(例如,打印到控制台、写入文件、修改全局变量等)。

举个例子:

javascript
// 纯函数
function add(a, b) {
  return a + b
}

console.log(add(2, 3)) // 输出 5
console.log(add(2, 3)) // 每次调用返回的结果相同

这就是一个标准的简单纯函数,它不修改任何外部状态,也不依赖任何外部的全局变量。

响应式数据

响应式数据简单来说,就是这个数据能够 响应 其发生的变化。当数据变了的时候,会 自动的触发某些操作

javascript
const obj = { text: 'Hello world!' }

function effect() {
  document.body.textContent = obj.text
}

我们假设这个 obj 已经是一个响应式数据,那么我们期望 obj 里面这个 text 属性变了的时候能够重新触发这个 effect 函数,然后重新设置这个 textContent 属性。

响应式数据的基本实现

在刚才的描述中,我们能够发现,响应式数据和副作用函数 是两个相互依赖的关系。

  • 当副作用函数 effect 执行时,会触发 obj.text读取 操作;
  • 当修改 obj.text 的值时,会触发 obj.text设置 操作。

那么,我们只要能够拦截到一个对象的 读取设置 操作,我们就能够在这些操作上面去做手脚了。

当对某个字段属性进行读取时,我们可以把对应的副作用函数存储到一个 bucket 里面,在设置字段属性时我们把副作用函数从 bucket 中取出并重新执行就可以了。

而在 Vue3 中,拦截一个对象属性的读取和设置操作我们都知道是通过 Proxy 来实现的。

可以简单的使用 JavaScript 来进行初步实现:

javascript
// 存储副作用函数的 bucket,这里采用 Set 来做
const bucket = new Set()

// 原始数据
const data = { text: 'Hello world!' }

// 对应的副作用函数
function effect() {
  document.body.textContent = obj.text
}

// 对原始数据使用 Proxy 进行代理
const obj = new Proxy(data, {
  // get 表示拦截读取操作
  get(target, key) {
    bucket.add(effect) // 往 bucket 里面添加副作用函数
    return target[key] // 返回属性值,相当于是 get 的默认操作
  },
  set(target, key, newVal) {
    target[key] = newVal // 设置属性值,相当于是 set 的默认操作
    bucket.forEach(fn => fn()) // 从 bucket 取出函数并执行
    return true // 必须返回一个 boolean 来确认是否操作成功
  },
})

effect()

setTimeout(() => {
  obj.text = 'Hello non_hana!'
}, 2000)

当然,上面这段代码只是最简单的根据固定的对象和函数进行的实现,这种 硬编码 的方式是我们平时在编写代码时应当全力避免的。

那么,如何设计一个完善的响应式系统呢?在 Vue.js 中,一个完善的响应式系统主要需要考虑以下几个因素:

设计一个完善的响应式系统

如何注册函数

我们都已经知道了,响应系统的一般工作流程如下:

  1. 发生 读取 操作时,将副作用函数收集到 bucket 里面
  2. 发生 设置 操作时,从 bucket 里面取出副作用函数,然后依次执行

在上面我们刚刚实现的响应式系统,我们的副作用函数直接就是 effect 命名的,注册也用的这个名字,这肯定不行,我们必须要把 所有依赖于某个属性的函数全部放到 bucket 里面才可以,不管他是不是叫 effect 还是匿名函数。

所以,我们必须要想办法去实现一个机制,这个机制 专门用来把依赖于某个对象的某个属性的副作用函数收集到 bucket 里面,我们也可以直接称其为 注册 机制。

我们用 JavaScript 来实现一下:

javascript
let activeEffect

function effect(fn) {
  activeEffect = fn
  fn()
}

我们用这个 activeEffect 专门来存那些需要被收集的副作用函数,而需要被收集的副作用函数需要通过 effect 来进行注册。effect 直接接受一个函数,所以不管你取什么名字都可以。

而对应的,在执行 fn 的时候会触发 get,这个时候需要收集的不是 effect 而是 activeEffect

javascript
// get 表示拦截读取操作
get(target, key) {
  // 如果 activeEffect 有值,说明是通过 effect 函数进行主动注册的
  if (activeEffect) {
    bucket.add(activeEffect)
  }
  return target[key] // 返回属性值,相当于是 get 的默认操作
},

怎么使用?

javascript
effect(() => {
  document.body.textContent = obj.text
})

这样子算是解决了 如何注册函数 的问题。

响应式系统的数据结构

但是,我们稍微测试一下就可以发现新问题。比如,我们在 obj 这个 Proxy 上面设置一个不存在的属性的时候:

javascript
effect(() => {
  console.log('effect run')
  document.body.textContent = obj.text
})

setTimeout(() => {
  obj.notExist = 'non_hana'
}, 1000)

可以看到,执行了两次这个匿名副作用函数,但是我们的 notExist 属性实际上是不存在的,所以这个属性不应该建立响应联系。

不过这个问题肯定一堆人都看出来了,因为 Proxy 本身就是拦截 某个对象 的操作,并没有细化到 某个对象的某个属性 的操作。所以当对 obj.notExist 进行操作时,肯定会触发 set 操作,然后重新执行,没啥好奇怪的。

所以我们需要 重新设计数据结构,将副作用函数的收集细化到某个对象的某个属性。

我们原本是使用 set,直接把函数往里面一扔一拿就完事了,但是这是不行的。

我们可以看一下我们现在注册副作用函数的代码:

javascript
effect(() => {
  document.body.textContent = obj.text
})

这个函数主要包含了三个部分:

  1. 被操作 & 读取的代理对象 obj
  2. 被操作 & 读取的字段名 text
  3. 副作用函数 effectFn

我们可以根据这三层,来构建一个 树形结构。为什么是树形结构?因为一个代理对象可能有多个属性值,一个属性值又可能有多个副作用函数,所以很容易就联想到树形结构的枝桠。

回到上面的问题,我们只需要将副作用函数和代理对象的关系约束到属性层面,就可以解决问题了。为了适应我们的树形结构,我们将数据结构也改为两层。用 TypeScript 来描述一下:

typescript
const bucket = new WeakMap<object, Map<string | symbol, Set<() => void>>>()

最外层是 WeakMap,其值为一个 Map,这个 Map 保存着对象属性和副作用函数的映射。副作用函数列表本身则保存在 Set 中,需要通过指定的对象和属性方可取出。

为什么是 WeakMap 而不是 Map 呢?

  1. WeakMap 本身的 key 只能存储 object 类型
  2. WeakMap 对于 key 是弱引用,一旦对应 object 的表达式执行完毕,就会将其从内存中移除,不影响垃圾回收器的回收行为。因此特别适合 想要临时在某个对象上挂载数据 的行为。

基于最新的数据结构,我们来改写一下 Proxy 部分的代码:

javascript
const obj = new Proxy(data, {
  get(target, key) {
    if (!activeEffect)
      return target[key]
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)

    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    const depsMap = bucket.get(target)
    if (!depsMap)
      return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
  },
})

代码改写的条理还是很清晰的。

get 部分先根据 target 这个原始对象拿到这个对象内部的属性和副作用函数的映射 Map,如果没有就 new 一个。同理,再根据 key 来拿到副作用函数的 Set,没有就 new 一个,然后往这个 Set 里面把新函数塞进去。

set 部分也是分层拿,没拿到就直接返回,如果拿到了就一个个拿出来执行就行。

可以再进行一层封装:

javascript
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) {
  if (!activeEffect)
    return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  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)
  effects && effects.forEach(fn => fn())
}

这样子拆代码能够减少耦合性,使得各个部分更加的各司其职。

分支切换

这不是 git checkout 那个分支切换,主要指的是在表达式中 根据条件执行不同的代码。我们可以看一下下面:

javascript
const data = { ok: true, text: 'Hello world!' }
const obj = new Proxy({ /* ... */ })
effect(() => {
  document.body.textContent = obj.ok ? obj.text : 'not'
})

这里用了个三元表达式,根据 obj.ok 的值来切换执行不同的代码分支。

**分支切换可能产生遗留的副作用函数。**在上面,需要首先读 obj.ok 才能确定接下来要读 obj.text,然后再读 obj.text,触发了 obj 两个属性的读取操作,所以相当于是给 obj 的两个属性都加上了副作用函数的依赖。

那么,我们把 obj.ok 设置为 false,此时 obj.text 就不会被读取了。理想情况下的依赖收集情况应该是这样:

但是现在很显然是做不到的,切换 obj.ok 后也只能保持第一种的收集方式,那么多出来的那个副作用收集,也就是 obj.text 对应的那个副作用函数收集就是 遗留的

**遗留的副作用函数会导致不必要的更新。**那么怎么解决这个问题呢?

我们可以先想想副作用函数在执行的时候会发生什么?答案是 副作用函数执行的时候会读取某个对象的某个属性的内容。而读取的时候会发生什么?会把这个副作用函数给放到对应 obj 对应 key 的 Set 里面。也就是说,即便副作用函数被注册了,在对某个响应式对象的某个属性进行 set 操作的时候,还是会照样触发,照样走一遍注册的流程。而因为是 Set,Set 保留了这个函数的引用,因此塞不进去了。

那么很简单了,既然每次执行副作用函数都会进行一次注册的操作,那我在执行这个副作用函数之前,先把这个副作用函数从所有依赖的 Set 里面 filter 掉不就可以了?反正你还得重新注册,我全删了你重新来一遍就行。 这巧妙的解决了分支切换的问题,因为在重新执行之前,一些分支可能切换了,导致这个副作用函数读的属性可能发生了变化,那么我重新执行之前清理掉冗余的副作用函数,用全新的空依赖去注册,就行了。

为了实现这个东东,我们需要 明确的知道哪些依赖集合中包括这个副作用函数,所以得重新设计一下这个副作用函数,给这个副作用函数加一个 deps 属性来存所有包含这个副作用函数的依赖集合。相当于 Map 是存了属性和副作用函数 Set,而副作用函数也存一个 数组 来反向标记哪些依赖,是一个双向的过程

好,我们开始改写:

javascript
let activeEffect

function effect(fn) {
  const effectFn = () => {
    activeEffect = fn // 原来的逻辑
    fn() // 原来的逻辑
  }
  effectFn.deps = [] // 函数也是对象哦
  effectFn()
}

很简单吧,把原来的逻辑单独拆到一个函数里面,函数也是个对象,我们可以直接挂一个 deps 的属性,然后执行这个函数就行了。

接下来我们着重处理这个 deps 数组的 依赖收集依赖收集这个词,终于出现了!

在哪里收集呢?在 track 函数里面。

javascript
// 追踪依赖
function track(target, key) {
  if (!activeEffect)
    return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key) // Set<() => void>
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)

  activeEffect.deps.push(deps)
}

可以看到,在最后一步将这个 target 的这个 key 的所有副作用函数 Set 当作依赖项放入 deps 里面,建立了如下的关系:

明确了这个关系之后,我们可以在每次副作用函数执行之前,根据当前这个副作用函数的所有依赖项的副作用函数 Set 来移除它:

javascript
let activeEffectFn // 临时存需要被注册的副作用函数

// 副作用函数注册函数
function effect(effectFn) {
  const fn = () => {
    cleanup(effectFn)
    activeEffectFn = effectFn
    effectFn()
  }
  effectFn.deps = [] // 函数也是对象哦
  fn()
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i] // deps 是 Set<() => void>[] 类型
    deps.delete(effectFn) // 从每个 Set 里面把这个副作用函数删掉
  }
  effectFn.deps.length = 0
}

注意,为啥能这样做,因为 JS 中对于一个对象默认都是用 引用 的方式进行保存的。所以可以直接遍历 effectFn.deps 数组,取出 Set,然后删掉要执行的副作用函数。

这样子能够避免分支切换导致的副作用函数遗留。

不过还是有点问题,现在其实会无限循环运行。可以看一下我们目前的 trigger 函数:

javascript
// 触发副作用函数的重新执行
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap)
    return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

现在的 trigger 函数遍历 effects 集合,集合中的每个副作用函数执行的时候会调用 cleanup 进行清除,实际上就是从 当前 effects 集合 中将当前执行的副作用函数剔除。但是 副作用函数的执行会导致其重新被收集到这个 effects 集合当中,而此时的 forEach 还没遍历完呢,所以会导致一直卡在同一个副作用函数的执行上走不动了。

所以,我们需要在执行的时候,用一个新的集合进行遍历,确保正在遍历的这个集合不是原本的集合,就行了。

javascript
// 触发副作用函数的重新执行
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap)
    return
  const effects = depsMap.get(key)

  const effectsToRun = new Set(effects)
  effectsToRun.forEach(fn => fn())
}

这样子,我们的分支切换算是解决了。

嵌套的 effect 与 effects 栈

在实际的代码编写中,effect 是有可能会发生嵌套的,比如我们有时候会这样子写代码:

javascript
effect(() => {
  effect(() => {
    // ...
  })
  // ...
})

这个场景其实很常见。我们知道 Vue 是可以组件套组件的,Vue 的组件经过编译之后就是一个普通的渲染函数,而这个渲染函数中肯定有一些响应式变量,这个渲染函数就是副作用函数。那么,我们在一个父组件里面套一个子组件,这个时候的渲染函数执行情况就是类似于上面的情况。

所以 effect 必须要设计成可嵌套的。

而我们上面的实现其实并不支持 effect 嵌套。比如这样写:

javascript
// 原始数据
const data = { name: 'non_hana', age: 16 }

// 对原始数据使用 Proxy 进行代理
const obj = new Proxy(data, {
  // ...
})

let temp1, temp2

effect(() => {
  console.log('effectFn1 被执行')
  effect(() => {
    console.log('effectFn2 被执行')
    temp2 = obj.age
  })
  temp1 = obj.name
})

setTimeout(() => {
  obj.name = 'hana'
}, 1000)

如果这么写了,我们想要的结果应该是,修改 obj.name 的值之后,会触发外层函数的执行从而间接触发内层函数的执行;而修改 obj.age 的值之后,只会触发内层函数的执行。但是很明显,结果并没有按我们的期望输出:

前两行是初始化数据,但是到后面我们发现居然内层的执行了,外层的没执行。

不过我们稍微想想,其实能够找到原因。为什么 触发内层的副作用函数呢? 外层的哪里去了? 答案是 被内层的给覆盖了

我们上面的代码,只使用了一个 activeEffect 变量来存需要注册的副作用函数,而且它是一个全局变量,那也就意味着在同一时刻 只能存一个副作用函数。而嵌套 effect 的写法,相当于在第一次初始化的时候,activeEffect 先是被赋值为外层的副作用函数,然后里面又有个 effect,这个 effect 在执行的时候又有个内层的副作用函数,这个时候又重新走了一遍注册副作用函数的流程,所以内层的就会直接把外层的副作用函数给覆盖了。

所以简单来说,我们目前的代码 只能够注册嵌套 effect 中最内层的副作用函数 。而根据我们改完后的依赖收集数据结构,Map 里面的 key 倒都是能够正常一个个注册的,但是由于副作用函数被最内层的覆盖了,所以 每个 key 对应的副作用函数 Set 都会是同一个。所以你无论改了什么属性的内容,都只会触发最内层的副作用函数。举一个更更更直观的例子:

javascript
let temp1, temp2, temp3, temp4, temp5

effect(() => {
  console.log('effectFn1 被执行')
  effect(() => {
    console.log('effectFn2 被执行')
    effect(() => {
      console.log('effectFn3 被执行')
      effect(() => {
        console.log('effectFn4 被执行')
        effect(() => {
          console.log('effectFn5 被执行')
          temp5 = obj.arr3
        })
        temp4 = obj.arr2
      })
      temp3 = obj.arr1
    })
    temp2 = obj.age
  })
  temp1 = obj.name
})

setTimeout(() => {
  obj.name = 'hana'
}, 1000)

所以怎么解决呢?核心问题是现在的方案会导致内层的把外层的副作用函数给 覆盖 并且 无法复原。为了能够保留以前的副作用函数,我们需要用一个 来存这些副作用函数。

当副作用函数执行时,把当前的副作用函数压入栈中 ,待其执行完毕后从栈中弹出,而 activeEffect 始终指向栈顶的副作用函数。这样子能够实现 响应式数据只会收集直接读取它值的副作用函数 而不会相互影响。

所以我们可以改代码,加一个 effectFnStack

javascript
let activeEffectFn // 临时存需要被注册的副作用函数
const effectFnStack = [] // 副作用函数栈,为了解决嵌套 effect 的问题

// 副作用函数注册函数
function effect(effectFn) {
  const fn = () => {
    cleanup(effectFn)
    activeEffectFn = effectFn
    effectFnStack.push(effectFn) // 在调用之前先把这个副作用函数压入栈
    effectFn()
    effectFnStack.pop() // 在调用之后再把这个副作用函数弹出栈
    activeEffectFn = effectFnStack[effectFnStack.length - 1] // 恢复上一个副作用函数
  }
  effectFn.deps = [] // 函数也是对象哦
  fn()
}

我们改完后可以重新模拟一下嵌套 effect 被调用的过程。拿这个例子来:

javascript
effect(() => {
  console.log('effectFn1 被执行')
  effect(() => {
    console.log('effectFn2 被执行')
    temp2 = obj.age
  })
  temp1 = obj.name
})

setTimeout(() => {
  obj.name = 'hana'
}, 1000)

首先,传入的参数 effectFn 是这个函数:

javascript
function fn1() {
  console.log('effectFn1 被执行')
  effect(() => {
    console.log('effectFn2 被执行')
    temp2 = obj.age
  })
  temp1 = obj.name
}
  • activeEffectFn 就等于这个 fn1,然后将 fn1 压入 effectFnStack,然后执行 fn1
    此时的 effectFnStack
    索引
    0fn1
  • 执行 fn1 的时候,遇到了第二个 effect。这个 effect 包含的副作用函数是:
    javascript
    function fn2() {
      console.log('effectFn2 被执行')
      temp2 = obj.age
    }
    

    然后,activeEffectFn 就被 覆盖了。没错,还是会被覆盖的!不过我们之后利用栈能够拿回原来的!
    然后将 fn2 压入 effectFnStack,此时的栈为:
    索引
    0fn1
    1fn2

    压完之后,执行副作用函数。它 读取了 obj.age,触发了我们的 track 函数,实现了依赖追踪,把 fn2obj.age 建立了联系。现在的 Map 里面,存的是 age 和只包含 fn2Set
  • fn2 执行完之后,栈把 fn2 给弹出来,只剩下 fn1,然后把现在是栈顶的 fn1 又重新赋值给 activeEffect
  • 之后,我们才走到 temp1 = obj.name 这一行,读取 obj.name,然后建立起映射。
    所以现在的 Map 映射如下:
    KeyValue
    agefn2
    namefn1

    而我们知道,fn1 实际上是包含了 fn2 的:
    javascript
    function fn1() {
      console.log('effectFn1 被执行')
      effect(fn2)
      temp1 = obj.name
    }
    

    所以现在我们修改 obj.name 的值,就会从上到下依次重新触发 fn1fn2 的内部流程。
    但是,fn1 重新触发的时候还是会执行 effect 函数啊?这有没有问题啊?
    没问题,即使重新执行了一遍,也只是把内部的嵌套函数注册流程又走了一遍,每次执行的时候都会 cleanup 然后再 push 到 Set 里面。不过重复执行 effect,确实有点浪费性能啊!

至此,嵌套 effect 的问题我们算是解决了。

避免无限递归循环

我们看一个例子:

javascript
effect(() => {
  obj.count++
})

这个简单的副作用函数里面写了个自增操作。我们知道自增操作相当于是:

javascript
effect(() => {
  obj.count = obj.count + 1
})

既会读取 obj.count 的值,又会设置 obj.foo 的值 。在执行这个代码的时候,首先是读 obj.count 触发 track 函数来收集依赖,把这个函数本身放到 Set 里面。

放好了之后,就是赋值语句,触发了 trigger 操作,会重新拿出 Set 里面的函数执行。但问题是这个 Set 刚刚被塞进来了你自己 ,所以拿出你自己执行,又会重新走一遍上面的流程,导致 Set 被无限的推进你自己,然后又无限的执行无数个你自己,导致了栈溢出。

这个问题的核心在于 读取和设置操作是在同一个副作用函数里面进行的。所以我们可以加一个条件,如果 trigger 触发执行的副作用函数和当前正在执行的副作用函数( activeEffect )相同,就不触发执行

我们改一下代码:

javascript
// 触发副作用函数的重新执行
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap)
    return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effect
  && effects.forEach((effectFn) => {
    if (effectFn !== activeEffectFn) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => effectFn())
}

这样就可以把 副作用函数数据源副作用函数执行栈 给拆了开来,因此能够确保正常执行。

评论0