在 Vue3 的响应式系统中,计算属性和监听器是我们日常开发中频繁使用的特性。但你知道它们背后的实现原理吗?本文将带你从零开始,手写实现
computed和watch,深入理解其设计思想和实现细节。
引言:为什么需要计算属性和监听器?
在Vue应用开发中,我们经常遇到这样的场景:
- 派生状态:基于现有状态计算新的数据
- 副作用处理:当特定数据变化时执行相应操作
Vue3提供了computed和watch来优雅解决这些问题。但仅仅会使用还不够,深入理解其底层原理能让我们在复杂场景下更加得心应手。
手写实现Computed
computed的核心特性包括:
- 惰性计算:只有依赖的响应式数据变化时才重新计算
- 值缓存:避免重复计算提升性能
- 依赖追踪:自动收集依赖关系
computed函数接收一个参数,类型函数或者一个对象,对象包含get和set方法,get方法是必须得。基本框架就出来了:
js
export function computed(getterOrOptions) {
let getter;
let setter = undefined;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
}
当你使用过computed函数时,你会发现会返回一个ComputedRefImpl类型的实例。代码就可以进一步写成下面的样子:
js
export class ComputedRefImpl {
constructor(getter, setter) {
this.getter = getter;
this.setter = isFunction(setter) ? setter : undefined;
}
}
export function computed(getterOrOptions) {
/* 上述代码实现省略 */
const cRef = new ComputedRefImpl(getter, setter);
return cRef;
}
ComputedRefImpl的实现
在ComputedRefImpl类中有几个主要的属性:
_value:缓存的计算结果_v_isRef:表示这是一个ref对象,可以通过.value访问effect响应式副作用实例_dirty脏值标记,true表示需要重新计算dep依赖收集容器,存储依赖当前计算属性的副作用 在初始化的时候,将会创建一个ReactiveEffect实例,此类型在手写Reactive中实现了。
js
class ComputedRefImpl {
effect = undefined; // 响应式副作用实例
_value = undefined; // 缓存的计算结果
__v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
_dirty = true; // 脏值标记,true表示需要重新计算
dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用
/**
* 构造函数
* @param {Function} getter - 计算属性的getter函数
* @param {Function} setter - 计算属性的setter函数
*/
constructor(getter, setter) {
this.getter = getter;
this.setter = isFunction(setter) ? setter : () => {};
// 创建响应式副作用实例,当依赖的数据变化时会触发调度器
this.effect = new ReactiveEffect(getter, () => {
// 调度器函数 后续处理
});
}
}
通过get value和set value手机依赖和触发依赖
js
class ComputedRefImpl {
/* 上述代码实现省略 */
/**
* 计算属性的getter
* 实现缓存机制和依赖收集
*/
get value() {
// 如果存在激活的副作用,则进行依赖收集
if (activeEffect) {
trackEffects(this.dep || (this.dep = new Set()));
}
// 如果是脏值,则重新计算并缓存结果
if (this._dirty) {
this._value = this.effect.run(); // 执行getter函数获取新值
this._dirty = false; // 清除脏值标记
}
return this._value; // 返回缓存的值
}
/**
* 计算属性的setter
* @param {any} newValue - 新的值
*/
set value(newValue) {
// 如果有setter函数,则调用它
if (this.setter) {
this.setter(newValue);
}
}
}
当依赖值发生变化后,将触发副作用的调度器,触发计算属性的副作用更新。
js
constructor(getter, setter) {
this.getter = getter;
this.setter = isFunction(setter) ? setter : () => {};
// 创建响应式副作用实例,当依赖的数据变化时会触发调度器
this.effect = new ReactiveEffect(getter, () => {
// 调度器函数:当依赖变化时执行
this._dirty = true; // 标记为脏值,下次访问时需要重新计算
triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
});
}
完整代码及用法示例
js
import { isFunction } from "./utils";
import {
activeEffect,
ReactiveEffect,
trackEffects,
triggerEffects,
} from "./effect";
/**
* 计算属性实现类
* 负责管理计算属性的getter、setter以及缓存机制
*/
class ComputedRefImpl {
effect = undefined; // 响应式副作用实例
_value = undefined; // 缓存的计算结果
__v_isRef = true; // 标识这是一个ref对象,可以通过.value访问
_dirty = true; // 脏值标记,true表示需要重新计算
dep = undefined; // 依赖收集容器,存储依赖当前计算属性的副作用
/**
* 构造函数
* @param {Function} getter - 计算属性的getter函数
* @param {Function} setter - 计算属性的setter函数
*/
constructor(getter, setter) {
this.getter = getter;
this.setter = isFunction(setter) ? setter : () => {};
// 创建响应式副作用实例,当依赖的数据变化时会触发调度器
this.effect = new ReactiveEffect(getter, () => {
// 调度器函数:当依赖变化时执行
this._dirty = true; // 标记为脏值,下次访问时需要重新计算
triggerEffects(this.dep); // 触发依赖当前计算属性的副作用更新
});
}
/**
* 计算属性的getter
* 实现缓存机制和依赖收集
*/
get value() {
// 如果存在激活的副作用,则进行依赖收集
if (activeEffect) {
trackEffects(this.dep || (this.dep = new Set()));
}
// 如果是脏值,则重新计算并缓存结果
if (this._dirty) {
this._value = this.effect.run(); // 执行getter函数获取新值
this._dirty = false; // 清除脏值标记
}
return this._value; // 返回缓存的值
}
/**
* 计算属性的setter
* @param {any} newValue - 新的值
*/
set value(newValue) {
// 如果有setter函数,则调用它
if (this.setter) {
this.setter(newValue);
}
}
}
/**
* 创建计算属性的工厂函数
* @param {Function|Object} getterOrOptions - getter函数或包含get/set的对象
* @returns {ComputedRefImpl} 计算属性引用实例
*/
export const computed = (getterOrOptions) => {
let getter; // getter函数
let setter = undefined; // setter函数
// 根据参数类型确定getter和setter
if (isFunction(getterOrOptions)) {
// 如果参数是函数,则作为getter
getter = getterOrOptions;
} else {
// 如果参数是对象,则分别获取get和set方法
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// 创建并返回计算属性实例
const cRef = new ComputedRefImpl(getter, setter);
return cRef;
};
示例用法:
js
import { reactive, computed } from "./packages/index";
const state = reactive({
firstName: "tom",
lastName: "lee",
friends: ["jacob", "james", "jimmy"],
});
const fullName = computed({
get() {
return state.firstName + " " + state.lastName;
},
set(newValue) {
[state.firstName, state.lastName] = newValue.split(" ");
},
});
effect(() => {
app.innerHTML = `
<div> Welcome ${fullName.value} !</div>
`;
});
setTimeout(() => {
fullName.value = "jacob him";
}, 1000);
setTimeout(() => {
console.log(state.firstName, state.lastName); // firstName: jacob lastName: him
}, 2000);
手写实现Watch和WatchEffect
watch函数接收三个参数:
source:要监听的数据源,可以是响应式对象或函数cb:数据变化时执行的回调函数options配置选项:immediate:是否立即执行,deep:是否深度监听等
js
export function watch(source, cb, {immediate = false} = {}) {
// 待后续实现
}
1. watch的实现
首先source是否可以接受多种监听的数据源:响应式对象、多个监听数据源的数组、函数。将不同方式统一起来。
js
export function watch(source, cb, { immediate = false } = {}) {
let getter;
if (isReactive(source)) {
// 如果是响应式对象 则调用traverse
getter = () => traverse(source);
} else if (isFunction(source)) {
// 如果是函数 则直接执行
getter = source;
} else if (isArray(source)) {
// 处理数组类型的监听源
getter = () =>
source.map((s) => {
if (isReactive(s)) {
return traverse(s);
} else if (isFunction(s)) {
return s();
}
});
}
}
/**
* 遍历对象及其嵌套属性的函数
* @param {any} source - 需要遍历的源数据
* @param {Set} s - 用于记录已访问对象的集合,避免循环引用
* @returns {any} 返回原始输入数据
*/
export function traverse(source, s = new Set()) {
// 检查是否为对象类型,如果不是则直接返回
if (!isObject(source)) {
return source;
}
// 检测循环引用,如果对象已被访问过则直接返回
if (s.has(source)) {
return source;
}
// 将当前对象加入已访问集合
s.add(source);
// 递归遍历对象的所有属性
for (const key in source) {
traverse(source[key], s);
}
return source;
}
处理完souce参数后,创建一个ReactiveEffect实例,对监听源产生响应式的副作用。
js
export function watch(source, cb, { immediate = false } = {}) {
/* 上述代码以实现省略 */
let oldValue;
// 定义副作用执行的任务函数
const job = () => {
let newValue = effect.run(); // 获取最新值
cb(oldValue, newValue); // 触发回调
oldValue = newValue; // 新值赋给旧值
};
// 创建响应式副作用实例
const effect = new ReactiveEffect(getter, job);
if (immediate) {
job();
} else {
oldValue = effect.run();
}
}
⚠️ 性能注意
traverse函数会递归遍历对象的所有嵌套属性,在大型数据结构上使用深度监听(deep: true)时会产生显著性能开销。建议:
- 只在必要时使用深度监听
- 尽量使用具体的属性路径而非整个对象
- 考虑使用计算属性来派生需要监听的数据
2. watchEffect的实现
实现了watch函数后,watchEffect的实现就容易了。
js
// watchEffect.js
import { watch } from "./watch";
export function watchEffect(effect, options) {
return watch(effect, null, options);
}
// watch.js
const job = () => {
if (cb) {
let newValue = effect.run(); // 获取最新值
cb(oldValue, newValue); // 触发回调
oldValue = newValue; // 新值赋给旧值
} else {
effect.run(); // 处理watchEffect
}
};
用法示例
js
watch([() => state.lastName, () => state.firstName], (oldValue, newValue) => {
console.log("oldValue: " + oldValue, "newValue: " + newValue);
});
setTimeout(() => {
state.lastName = "jacob";
}, 1000);
setTimeout(() => {
state.firstName = "james";
}, 1000);
/*
1秒钟后:oldValue: lee,tom newValue: jacob,tom
2秒钟后:oldValue: jacob,tom newValue: jacob,james
*/
总结
本文核心内容
通过手写实现Vue3的computed和watch,我们深入理解了:
- 计算属性的惰性计算、值缓存和依赖追踪机制
- 监听器的多数据源处理和深度监听原理
- 响应式系统中副作用调度和依赖收集的完整流程
代码地址
📝 本文完整代码 :
GitHub仓库链接\] \| \[[github.com/gardenia83/...](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fgardenia83%2Fvue-source-code "https://github.com/gardenia83/vue-source-code")
下篇预告
在下一篇中,我们将继续深入Vue3响应式系统,手写实现:
《深入 Vue3 响应式系统:从ref到toRefs的完整实现》
ref和shallowRef的底层机制toRef和toRefs的响应式转换原理- 模板Ref和组件Ref的特殊处理
- Ref自动解包的神秘面纱
敬请期待! 🚀
掌握底层原理,让我们的开发之路更加从容自信