引言
之前介绍过了 effect 函数,它用来注册副作用函数,同时它也允许指定一些选项参数 options,例如指定 scheduler 调度器来控制副作用函数的执行时机和方式;也介绍了用来追踪和收集依赖的track 函数,以及用来触发副作用函数重新执行的 trigger 函数。实际上,综合这些内容,我们就可以实现 Vue.js 中一个非常重要并且非常有特色的能力——计算属性。
计算属性与lazy
属性
在Vue.js中,effect
函数是用来创建响应式副作用的函数。默认情况下,传递给effect
的副作用函数会立即执行。例如,下面的代码中,effect
函数会立即执行传递给它的副作用函数:
effect(() => {
console.log(obj.foo);
});
然而,在某些情况下,我们希望副作用函数在需要的时候才执行,而不是立即执行。一个典型的例子是计算属性。为了实现这种延迟执行的效果,我们可以在options
中添加一个lazy
属性,并将其设置为true
。当lazy
为true
时,副作用函数不会在初始化时立即执行,而是在需要的时候才执行。修正后的代码如下所示:
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.lazy
为true
,副作用函数将被延迟执行,直到手动触发为止。
现在我们通过计算属性实现了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
标志,用于标识当前的计算是否需要重新进行。如果dirty
为true
,则重新计算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函数被调用。