一、开篇
想要更加深入了解 Vue3的响应原理,建议看一下 保姆式地带你从0到1实现Vue3 Effect,对你阅读这里的代码更容易理解。
二、响应数据的完成代码
// 存储副作用的桶
const bucket = new WeakMap();
// 原始数据
const data = { text: "Hello World" };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用从桶中取出并执行
trigger(target, key);
return true;
},
});
// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
// 没有 activeEffect 直接 return
if (!activeEffect) return;
// 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,创建一个新的map,并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
// 里面存储着所有与当前 key 相关的副作用函数:effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 effectFn.deps 数组中
activeEffect.deps.push(deps);
}
// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key) {
// 从桶中取出 depsMap,它也是一个map 类型,key-->effects
const depsMap = bucket.get(target);
if (!depsMap) return true;
// 再根据 key 从depsMap 中取得 副作用函数 effects
const effects = depsMap.get(key);
const effects2Run = new Set();
effects &&
effects.forEach((effectFn) => {
// 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effects2Run.add(effectFn);
}
});
effects2Run.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则直接执行副作用函数
effectFn();
}
});
}
// 用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 删除与 effectFn 相关的关系
cleanup(effectFn);
// 调用effect函数的时候,将 effectFn 赋值给 activeEffect
activeEffect = effectFn;
// 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
effectStack.push(effectFn);
// 执行副作用函数
fn();
// 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
// 将 options 挂在到 effectFn 上
effectFn.options = options;
// effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
effectFn.deps.forEach((deps) => {
// 将 effectFn 从依赖集合中删除
deps.delete(effectFn);
});
// 清空 effectFn.deps
effectFn.deps.length = 0;
}
const effectFn = effect(
() => {
document.body.innerText = obj.text;
},
{
lazy: true,
}
);
三、计算属性computed
1. 副作用函数作为getter,得到计算的值
const effectFn = effect(
() => {
return obj.bar + obj.foo
},
{
lazy: true,
}
);
// 手动执行
const value = effectFn()
需要修改effect 注册副作用函数
2. 去除手动执行,待使用的时候,获取计算值
const data = { foo: "foo", bar: "bar" };
const obj = new Proxy(data, {...})
function computed(getter) {
// 把getter 作为副作用函数,创建一个lazy 的effect
const effectFn = effect(getter, { lazy: true });
const obj = {
// 当读取 value 时,才执行effectFn
get value() {
return effectFn();
},
};
// 将 obj 返回
return obj;
}
document.body.innerText = computed(() => {
return obj.bar + obj.foo;
}).value;
效果图
3. 缓存
每次获取value的时候,都会去 执行effectFn(),如果依赖数据没有发生变化,应该获取缓存的数据,而非再次执行effectFn()获取
function computed(getter) {
// 缓存数据
let value;
// dirty 为true,标识需要重新获取计算获取值
let dirty = true;
// 把getter 作为副作用函数,创建一个lazy 的effect
const effectFn = effect(getter, {
lazy: true,
scheduler() {
// 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
dirty = true;
},
});
const obj = {
// 当读取 value 时,才执行effectFn
get value() {
if (dirty) {
value = effectFn();
// 下次获取的时候,直接从缓存中获取数据
dirty = false;
}
return value;
},
};
// 将 obj 返回
return obj;
}
4、computed 计算的值转成响应式
const sum = computed(()=>{
return obj.bar + obj.text;
})
effect(function effectFn(){
document.body.innerText = sum.value
})
更改obj.bar / obj.text值,并不会触发 effectFn 副作用函数重新执行,需要在computed内实现 track 和 trigger
function computed(getter) {
// 缓存数据
let value;
// dirty 为true,标识需要重新获取计算获取值
let dirty = true;
// 把getter 作为副作用函数,创建一个lazy 的effect
const effectFn = effect(getter, {
lazy: true,
scheduler() {
// 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
dirty = true;
// 触发对应的副作用函数
trigger(obj, "value");
},
});
const obj = {
// 当读取 value 时,才执行effectFn
get value() {
if (dirty) {
value = effectFn();
// 下次获取的时候,直接从缓存中获取数据
dirty = false;
}
// 拦截读取操作,将当前的副作用函数收集到 bucket 中
track(obj, "value");
return value;
},
};
// 将 obj 返回
return obj;
}
effect(function effectFn() {
document.body.innerText = computed(() => {
return obj.bar + obj.foo;
}).value;
});
效果
四、完成代码
// 存储副作用的桶
const bucket = new WeakMap();
// 原始数据
const data = { foo: "foo", bar: "bar" };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用从桶中取出并执行
trigger(target, key);
return true;
},
});
// 在get 拦截函数内调用 track 函数 追踪变化
function track(target, key) {
// 没有 activeEffect 直接 return
if (!activeEffect) return;
// 根据 target 从 桶中取出 depsMap,它也是一个map 类型,key-->effects
let depsMap = bucket.get(target);
// 如果 depsMap 不存在,创建一个新的map,并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从depsMap 中取得 deps,它是一个 Set 集合
// 里面存储着所有与当前 key 相关的副作用函数:effects
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 effectFn.deps 数组中
activeEffect.deps.push(deps);
}
// 在 set 拦截函数内调用 trigger 函数 触发变化
function trigger(target, key) {
// 从桶中取出 depsMap,它也是一个map 类型,key-->effects
const depsMap = bucket.get(target);
if (!depsMap) return true;
// 再根据 key 从depsMap 中取得 副作用函数 effects
const effects = depsMap.get(key);
const effects2Run = new Set();
effects &&
effects.forEach((effectFn) => {
// 如果trigger触发执行的副作用函数与当前的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effects2Run.add(effectFn);
}
});
effects2Run.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则直接执行副作用函数
effectFn();
}
});
}
// 用于注册副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
// 删除与 effectFn 相关的关系
cleanup(effectFn);
// 调用effect函数的时候,将 effectFn 赋值给 activeEffect
activeEffect = effectFn;
// 在调用副作用函数之前,将当前副作用函数添加到 effectStack 栈中
effectStack.push(effectFn);
// 执行副作用函数,得到返回值
const res = fn();
// 在调用副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 将返回值 return 出去
return res;
};
// 将 options 挂在到 effectFn 上
effectFn.options = options;
// effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
effectFn.deps.forEach((deps) => {
// 将 effectFn 从依赖集合中删除
deps.delete(effectFn);
});
// 清空 effectFn.deps
effectFn.deps.length = 0;
}
function computed(getter) {
// 缓存数据
let value;
// dirty 为true,标识需要重新获取计算获取值
let dirty = true;
// 把getter 作为副作用函数,创建一个lazy 的effect
const effectFn = effect(getter, {
lazy: true,
scheduler() {
// 依赖的响应数据发生变化的时候,将 dirty 设置为 true,再次获取value的时候,需要重新计算
dirty = true;
// 触发对应的副作用函数
trigger(obj, "value");
},
});
const obj = {
// 当读取 value 时,才执行effectFn
get value() {
if (dirty) {
value = effectFn();
// 下次获取的时候,直接从缓存中获取数据
dirty = false;
}
// 拦截读取操作,将当前的副作用函数收集到 bucket 中
track(obj, "value");
return value;
},
};
// 将 obj 返回
return obj;
}
effect(function effectFn() {
document.body.innerText = computed(() => {
return obj.bar + obj.foo;
}).value;
});
五、总结
- effect 副作用注册函数,不立即执行副作用函数
- 将副作用函数计算的值return 出来
- computed内置实现一个 getter track 和 setter trigger。
六、参考
《Vue.js 设计与实现》