最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

嵌套的 effect 与 effect 栈

引言

effect 副作用函数是可以发生嵌套的,至于为什么要设计成这样呢

嵌套的 effect

effect(function effectFn1() {
  effect(function effectFn2() { /* ... */ })
    /* ... */
})

在上面这段代码中,effectFn1 内部嵌套了 effectFn2,effectFn1 的执行会导致 effectFn2 的执行。那么,什么场景下会出现嵌套的 effect 呢?拿 Vue.js 来说,实际上 Vue.js 的渲染函数就是在一个 effect 中执行的.

当组件发生嵌套时,例如 Foo 组件渲染了 Bar 组件:

// Bar 组件
const Bar = {
  render()  {/* ... */ },
}
// Foo 组件渲染了 Bar 组件
const Foo = {
  render() {
    return <Bar /> }// jsx 语法
}

此时就发生了 effect 嵌套,它相当于:

effect(() => {
  Foo.render()
// 嵌套
  effect(() => {
    Bar.render()}
)}
)

effect函数可以嵌套使用,也就是说,一个effect函数内部可以包含另一个effect函数。当外部effect函数依赖于内部effect函数创建的响应式数据时,内部effect函数会被自动追踪,确保外部effect函数在内部effect函数发生变化时得以执行。

这种嵌套的effect函数用于创建依赖关系链,确保当某个响应式数据变化时,所有依赖于它的effect函数都能够被触发执行,从而保持应用的响应性。

而”effect 栈”,在Vue 3的内部实现中,Vue使用了一个effect栈来追踪当前正在执行的effect函数,这个栈的作用类似于函数调用栈,用于管理effect函数的执行顺序和依赖关系。

现在有一个不使用栈结构的嵌套的effect函数的例子,但是他并不能实现嵌套的功能。假设我们有两个响应式数据count1count2,其中count2的值依赖于count1的值。我们可以使用嵌套的effect函数来实现这种依赖关系。

// 原始数据
const data = { foo: true, bar: true };

// 代理对象
const obj = new Proxy(data, {
  get(target, key) {
    console.log(`读取属性: ${key}`);
    return target[key];
  }
});

// 全局变量
let temp1, temp2;

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

  effect(function effectFn2() {
    console.log('effectFn2 执行');
    // 在 effectFn2 中读取 obj.bar 属性
    temp2 = obj.bar;
  });

  // 在 effectFn1 中读取 obj.foo 属性
  temp1 = obj.foo;
});

effectFn1是外部的effect函数,它依赖于obj.foo的值,并且在内部包含了一个innerEffect,内部的effect函数依赖于obj.bar的值。当我们修改obj.foo时,我们希望外部的effect函数被触发执行,并且输出obj.foo的值,然后触发内部的依赖函数。当我们修改obj.bar时,内部的effect函数被触发执行,并且输出obj.bar的值。

我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;

function effect(fn) {
  // 定义副作用函数
  const effectFn = () => {
    // 调用 cleanup 函数,具体实现需要根据需求补充
    cleanup(effectFn);
    // 将副作用函数赋值给 activeEffect
    activeEffect = effectFn;
    // 执行副作用函数
    fn();
    // 将当前副作用函数的依赖集合存储在 effectFn.deps 中(需要根据实际逻辑补充)
    effectFn.deps = []; // 这里需要根据实际逻辑设置依赖集合
  };
  
  // 执行副作用函数
  effectFn();
}

但其实只使用一个变量储存而不使用栈结构,当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,也就是说我在读取obj.foo的时候,activeEffect还只是innerEffect的值,并且只触发了innerEffect的效果。

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

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;

// effect 栈
const effectStack = [];

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn); // 调用 cleanup 函数,具体实现需要根据需求补充
    activeEffect = effectFn;

    // 将当前副作用函数压入栈中
    effectStack.push(effectFn);

    fn();

    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };

  // 初始化副作用函数的依赖集合
  effectFn.deps = [];

  // 执行副作用函数
  effectFn();
}
vue-watch-computed原理 Vue.js 响应式系统的作用与实现

評論