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

vue-watch-computed原理

引言

之前介绍过了 effect 函数,它用来注册副作用函数,同时它也允许指定一些选项参数 options,例如指定 scheduler 调度器来控制副作用函数的执行时机和方式;也介绍了用来追踪和收集依赖的track 函数,以及用来触发副作用函数重新执行的 trigger 函数。实际上,综合这些内容,我们就可以实现 Vue.js 中一个非常重要并且非常有特色的能力——计算属性。

计算属性与lazy属性

在Vue.js中,effect函数是用来创建响应式副作用的函数。默认情况下,传递给effect的副作用函数会立即执行。例如,下面的代码中,effect函数会立即执行传递给它的副作用函数:

effect(() => {
  console.log(obj.foo);
});

然而,在某些情况下,我们希望副作用函数在需要的时候才执行,而不是立即执行。一个典型的例子是计算属性。为了实现这种延迟执行的效果,我们可以在options中添加一个lazy属性,并将其设置为true。当lazytrue时,副作用函数不会在初始化时立即执行,而是在需要的时候才执行。修正后的代码如下所示:

effect(
  // 这个函数不会立即执行
  () => {
    console.log(obj.foo);
  },
  // options
  {
    lazy: true
  }
);

具体实现中,我们将副作用函数effectFn作为effect函数的返回值返回。这意味着,当我们调用effect函数时,我们会得到对应的副作用函数,并且可以在需要的时候手动执行它。这种机制赋予了我们更多的控制权,允许我们决定何时触发副作用函数的执行,而不是立即执行它。

这种设计模式特别适用于特定场景,例如计算属性。在计算属性中,我们可能希望在特定时刻触发副作用函数的执行,而不是在初始化时立即执行。通过将副作用函数作为effect函数的返回值,我们能够灵活地控制副作用函数的执行时机,以满足不同场景的需求。

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };

  // 设置副作用函数的 options 和 deps
  effectFn.options = options;
  effectFn.deps = [];

  // 只有非 lazy 时执行副作用函数
  if (!options.lazy) {
    effectFn();
  }

  // 将副作用函数作为返回值返回
  return effectFn;
}

在这个代码中,effect函数的第二个参数是一个options对象,其中lazy属性被设置为true。这意味着传递给effect的副作用函数会在需要的时候才执行,例如在计算属性被访问时。这种延迟执行的特性使得effect函数非常适合用于实现计算属性等场景。

在上述代码中,我们通过options参数的lazy属性控制副作用函数的立即执行。如果options.lazytrue,副作用函数将被延迟执行,直到手动触发为止。

现在我们通过计算属性实现了lazy懒加载,那么数据缓存该怎么实现呢。

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value;
  // dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算
  let dirty = true;

  const effectFn = effect(getter, {
    lazy: true
  });

  const obj = {
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn();
        // 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值
        dirty = false;
      }
      return value;
    }
  };

  return obj;
}

解决了懒计算的问题,只有在真正需要计算value的时候,才会执行effectFn。同时,它还引入了一个dirty标志,用于标识当前的计算是否需要重新进行。如果dirtytrue,则重新计算value的值,并将dirty标志设置为false,以便下一次访问时可以直接使用缓存的值。

watch 的实现原理

所谓 watch,其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。举个例子:

watch(obj, () => {
  console.log('数据变了')
})
// 修改响应数据的值,会导致回调函数执行
obj.foo++

假设 obj 是一个响应数据,使用 watch 函数观测它,并传递一个回调函数,当修改响应式数据的值时,会触发该回调函数执行。实际上,watch 的实现本质上就是利用了 effect 以及options.scheduler 选项,如以下代码所示:

effect(() => {
  console.log(obj.foo)
}, {
scheduler() {
// 当 obj.foo 的值变化时,会执行 scheduler 调度函数
}
})

在一个副作用函数中访问响应式数据 obj.foo,通过前面的介绍,我们知道这会在副作用函数与响应式数据之间建立联系,当响应式数据变化时,会触发副作用函数重新执行。但有一个例外,即如果副作用函数存在 scheduler 选项,当响应式数据发生变化时,会触发 scheduler 调度函数执行,而非直接触发副作用函数执行。从这个角度来看,其实 scheduler 调度函数就相当于一个回调函数,而watch 的实现就是利用了这个特点。

下面是最简单的 watch 函数的实现:

// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb) {
  effect(
    // 触发读取操作,从而建立联系
    () => source.foo,
    {
      scheduler: scheduler(),
      // 当数据变化时,调用回调函数 cb
      fn: () => {
        cb();
      },
    }
  );
}

于是一段完整的代码:

// 响应式数据对象
const data = { foo: 1 };

// 创建代理对象,用于监听数据变化
const obj = new Proxy(data, {
  set(target, key, value) {
    target[key] = value;
    // 数据变化时触发回调函数
    watch(obj, () => {
      console.log('数据变化了');
    });
    return true;
  },
});

// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb) {
  effect(() => source.foo, {
    scheduler: scheduler(),
    fn: () => {
      cb();
    },
  });
}

// 模拟 effect 函数
function effect(fn, options) {
  // 在这里执行 effect 相关逻辑
  fn(); // 这里假设执行 fn 会触发响应式数据的读取操作
}

// 模拟 scheduler 函数
function scheduler() {
  // 在这里可以添加调度逻辑
  // 这里返回一个函数作为 scheduler
  return function () {
    // 这里可以添加具体的调度逻辑
    // ...
  };
}

// 数据变化
obj.foo++;

// 输出: 数据变化了

我们首先定义了一个原始的数据对象data,其中有一个属性foo,初始值为1。接着,我们使用Proxy创建了一个代理对象obj,该代理对象会拦截对data的操作。

当你调用obj.foo++时,会触发Proxy的set拦截器。在set拦截器中,我们首先将属性值设置到目标对象上,然后调用watch函数,并传入obj和一个回调函数。在watch函数中,我们使用了一个假设的effect函数(实际开发中可能是框架提供的响应式函数),这个函数用于监听数据的变化。在watch函数中,我们传入了source.foo的读取操作,以及一个包含scheduler和fn属性的配置对象。scheduler可以用于定义调度逻辑(在示例中为空函数),fn则是一个当数据变化时会被调用的回调函数。

当obj.foo++执行时,set拦截器触发,watch函数被调用。

vue-过期的副作用函数 嵌套的 effect 与 effect 栈

評論