在Vue3中,reactive和effect是两个非常重要的API,用于实现响应式数据和副作用函数。本文将介绍它们的基本用法,以及简单的实现原理。
由于源码中逻辑分支较多,代码示例大多都是采用简单实现,帮助理解
reactive是一个工厂函数,用于创建响应式对象。使用reactive函数创建的对象,其属性可以被自动追踪,当属性发生变化时,会自动更新相关的视图。
下面是reactive函数的简单实现:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key); // 追踪属性访问
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key); // 触发更新
},
};);
}
上述代码中,使用Proxy对象实现了对目标对象的代理。当访问代理对象的属性时,会自动调用get方法;当设置代理对象的属性时,会自动调用set方法。
在get方法中,我们调用了track函数,用于追踪属性的访问。track函数的实现可以参考Vue3源码中的effect.ts文件,不在本文讨论范围内。
在set方法中,我们先更新了目标对象的属性值,然后调用了trigger函数,用于触发更新。trigger函数的实现也可以参考Vue3源码中的effect.ts文件。
effect是一个函数,用于创建副作用函数。使用effect函数创建的副作用函数,可以自动追踪其内部响应式数据的变化,当数据变化时,会自动重新执行副作用函数。
下面是effect函数的简单实现:
class reactiveEffect {
private _fn: any
constructor(fn: Function) {
this._fn = fn
}
run(){
activeEffect = this
this._fn()
}
}
let activeEffect
export function effect(fn) {
const _effect = new reactiveEffect(fn)
activeEffect = _effect
// 在这里我们将当前effect赋值到全局属性,当我们调用传入的副作用函数时,也就会访问响应式数据,这时在get方法中收集当前effect,就完成了effect的依赖收集。
_effect.run()
activeEffect = null;
}
function effect(fn) {
const runner = () => {
fn();
};
runner();
return runner;
}
上述代码中,我们创建了一个名为runner的函数,用于执行副作用函数。在创建runner函数后,我们立即执行了一次副作用函数,用于初始化副作用函数的状态。
在副作用函数中访问的响应式数据,会被track函数自动追踪。当响应式数据发生变化时,trigger函数会自动调用runner函数,重新执行副作用函数。
const targetMap = new WeakMap() // 用于存储所有的目标对象(即响应式对象)以及它们对应的 depsMap
export function track(target, key) {
let depsMap = targetMap.get(target) //用于存储目标对象的所有响应式属性以及它们对应的依赖列表 dep。
if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)// dep 是一个 Set 对象,用于存储所有依赖于某个响应式属性的 effect 函数
if(!dep) {
dep = new Set()
}
dep.push(activeEffect.run) // 将当前effect收集到dep中
}
function trigger(target, key = null) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const effects = new Set()
// 收集所有与 target[key] 相关的 effect
const addEffects = (dep) => {
dep.forEach((effect) => {
effects.add(effect)
})
}
if (key !== null) {
const dep = depsMap.get(key)
if (dep) {
addEffects(dep)
}
} else {
// 如果不指定 key,则会遍历 depsMap 中所有的 dep,收集它们中的 effect 并执行。
depsMap.forEach(addEffects)
}
// 执行所有相关的 effect
effects.forEach((effect) => {
effect()
})
}
从以上可以看出,当我们显式调用effect时,很容易就能将副作用函数收集为当前依赖,而在模版内的响应式数据应该如何收集依赖呢?
简单来说,Vue 的模板编译器会将模板解析成抽象语法树(AST),然后生成渲染函数。在生成渲染函数时,模板中的数据绑定和指令会被编译成 JavaScript 代码,并将其包装在 render 函数中。render 函数是一个纯函数,它接收一个上下文对象作为参数,返回一个 VNode 对象。在执行 render 函数期间,访问响应式数据的属性会自动触发依赖追踪,将当前正在执行的 render 函数添加到该属性对应的 dep 中。
因此,虽然在 Vue 模板中的响应式数据没有明显的 effect 函数,但是它们的响应式更新机制和通过 effect 函数实现的响应式数据是类似的,都是通过依赖追踪和响应式更新实现的。
reactive和effect是Vue3中实现响应式数据和副作用函数的核心API,学习并理解这部分内容对于深入Vue核心逻辑是很有帮助