Vue.js设计与实现(第二章)— 响应式之嵌套的 effect 与 effect 栈

899 阅读5分钟

大家好,我是小瑜,通过上一章的学习,知道了Vue3中如何通过Proxy对数据进行响应式处理以及部分优化:

  • 并且对effect取消硬编码
  • 为了针对没有读取或无效属性不触发effect而重新设计响应书数据结构、
  • 对条件判断语句不成立断开副作用与响应式

本章讲述的是effet出现嵌套时的场景, 将会通过输出内容,从而发现不符合预期的部分,并分析问题点并解决, 最终通过巧妙的effect栈来进行优化。

搓搓手准备开始~

出现嵌套的场景

Vue.js的渲染函数就会出现嵌套的场景

// 例如:
// Bar 组件
const Bar = {
 render() {}
}
// Foo 组件渲染了 Bar 组件
const Foo = {
render() {
   return <Bar />
 }
}

用代码模拟出现的嵌套情况

const data = { foo: true, bar: true }
let temp1, temp2

// effectFn1嵌套了effectFn2
effect(function effectFn1() {
  console.log("effectFn1 执行")

  effect(function effectFn2() {
    // effectFn2 会在 effectFn1 执行时执行
    console.log("effectFn2 执行")
    temp2 = obj.bar
  })
  temp1 = obj.foo
})

obj.foo = false

根据输出结果,发现输出不符合预期

在这种情况下,我们希望当修改 obj.foo 时会触发 effectFn1 执行。由于 effectFn2 嵌套在 effectFn1 里,所以会间接触发 effectFn2 执行,而当修改 obj.bar 时,只会触发 effectFn2 执 行。但结果不是这样的,我们尝试修改 obj.foo 的值,会发现输出 为

effectFn1 执行
effectFn2 执行
effectFn2 执行

一共打印了三次,前面两次打印是正常的,但是第三部,当修改foo时,EffectFn1没有被执行,反而只有EffectFn2执行了,这显然不符合预期

分析出现问题的点

let activityFffect
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeityEffect = effectFn
    fn()
  }
  effectfn.deps = []
  effectFn()
}

我们通过全局变量activityEffect来存储通过effect函数注册的副作用函数,这就意味着 activityEffect 在同一时刻所存储的副作用函数有且只有一个。 当副作用函数发生嵌套时,内层副作用函数的执行会覆盖activeEffect的值,并且永远也不会恢复到原来的值。 这时如果再有响应式进行依赖收集,即使这个响应式的数据是在外层副作用函数中读取的,他们收集到的副作用函数也都会是内容副作用函数。 以上就是问题所在。

解决问题

为了解决这个问题,我们需要一个副作用函数栈 effectStack, 在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执 行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作 用函数。也就是始终指向 effectFn1。 这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = [] // 新增

export function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用effect 注册副作用函数时,将副作用函数赋值给activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn) // 新增
    fn()
    console.log(effectStack, "@@")
    // 在当前副作用函数执行完毕后,将当前副作用函数从栈中弹出.并把activeEffect 还原为之前的值
    effectStack.pop() // 新增
    activeEffect = effectStack[effectStack.length - 1] // 新增
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}
  • 现在的效果
effectFn1 执行
effectFn2 执行
effectFn1 执行
effectFn2 执行

画图解释

我们定义了 effectStack 数组,用它来模拟栈, activeEffect 没有变化,它仍然指向当前正在执行的副作用函数。 不同的是,当前执行的副作用函数会被压入栈顶,这样当副作用函数 发生嵌套时,栈底存储的就是外层副作用函数,而栈顶存储的则是内 层副作用函数 image.png

当内层副作用函数 effectFn2 执行完毕后,它会被弹出栈,并 将副作用函数 effectFn1 设置为 activeEffect

133.png 如此一来,响应式数据就只会收集直接读取其值的副作用函数作 为依赖,从而避免发生错乱。

完整代码

index.js

const data = { foo: true, bar: true }

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = [] // 新增

export function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用effect 注册副作用函数时,将副作用函数赋值给activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压入栈中
    effectStack.push(effectFn) // 新增
    fn()
    console.log(effectStack, "@@")
    // 在当前副作用函数执行完毕后,将当前副作用函数从栈中弹出.并把activeEffect 还原为之前的值
    effectStack.pop() // 新增
    activeEffect = effectStack[effectStack.length - 1] // 新增
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

const bucket = new WeakMap()

export const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  },
})

/**
 * 收集依赖
 */
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)
  activeEffect.deps.push(deps)
  console.log(activeEffect.deps)
}

/**
 * 触发依赖
 */
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set(effects) // 新增
  effectsToRun.forEach((effectFn) => effectFn()) // 新增
}

function cleanup(fn) {
  fn &&
    fn.deps.forEach((item) => {
      item.delete(fn) // 将他从集合中删除掉
    })
  fn.deps.length = 0 // 然后清空这个数组
}

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style></style>
  </head>
  <body>
    <script type="module">
      import { effect, obj } from "./index.js"
      /**
       * 需要一个副作用函数栈 effectStack
       * 在副作用函数执行时,将当前的副作用函数压入栈中,待副作用函数执行完毕后,将其从栈中移除
       * 并始终让activeEffect 指向栈顶的副作用函数
       * 这样就能做到一个响应式数据只会收集直接读取其值的副作用函数
       * 而不会出现相互影响的情况
       */

      const data = { foo: true, bar: true }
      let temp1, temp2

      // effectFn1嵌套了effectFn2
      effect(function effectFn1() {
        console.log("effectFn1 执行")

        effect(function effectFn2() {
          // effectFn2 会在 effectFn1 执行时执行
          console.log("effectFn2 执行")
          temp2 = obj.bar
        })
        temp1 = obj.foo
      })

      obj.foo = false
    </script>
  </body>
</html>