vuejs中有很井段的computed、watch用法,那么具体设计思路是什么,今天我们继续探索vuejs一书,看看书中会有什么答案,题外话,本系列属于读书自我感受心得,建议看完前面的部分在进入本期的读书,有助于降低理解难度
1、简易版的computed
在前面的章节我们实现了一个‘toy’级别的响应式系统,那么基于这个响应式系统,我们如何实现一个cumputed计算属性;
首先我们调整一下effect副作用函数,让其不在第一时间触发,方便我们后续处理
function effect(fn, option = {}) {
const effectFn = () => {
clearEffectDeps(effectFn);
activeEffect = effectFn;
// 将当前副作用effectFn放入effectStack中
effectStack.push(effectFn);
// 执行副租用函数
const res = fn()
// 出栈
effectStack.pop();
// 将activeEffect指向上一个effect
activeEffect = effectStack[effectStack.length - 1];
return res
}
effectFn.deps = []
// 增加调度任务模块
effectFn.option = option;
if(!option.lazy) {
effectFn()
}
return effectFn
}
这样当我们配置lazy = true,我们可以在effect首次执行时候就不会触发副作用函数,具体如下:
const effectFn = effect(() => {
return obj.a + obj.b
}, {
lazy: false
})
effectFn()
那么接下来我们实现一下一个简易版的computed计算属性,基本思路就是,生成一个副作用函数,当我们在获取值的时候触发并执行拿到结果,代码如下:
const data = {bar: 1, foo: 1}
const obj = new Proxy(data, {/* 参考之前的代码 */})
function computed(getter) {
const effectFn = effect(getter, {
// 保证副作用不会第一时间被执行
lazy: true
})
const obj = {
// 获取值触发
get value() {
return effectFn();
}
}
return obj
}
const numbers = computed(() => {
return obj.bar + obj.foo
})
console.log(numbers.value) // 2
console.log(numbers.value) // 2
上面基本能达到我们想要的computed计算,但是有一点,我们多次获取值,会发现computed会进行多次计算,用过vue的同学都知道vue中computed会缓存值,原始关联的数据没有发生变化,computed不会计算,只能拿缓存的值去使用,所以我们还需要实现缓存值的功能;
2、带有值缓存版本的computed
那么我们分析一下该如何实现值的缓存呢,很简单我们可以分析出来两个方向:
- 1、我们需要第一个变量,保存上次请求的值(
value) - 2、我们需要一个标志位(
dirty)来去判定是否启用缓存值:
接下来,我们是实现一个基础缓存版本的computed,代码如下:
function computed(getter) {
// 保存值
let value;
// 是否启用缓存
let dirty = true;
const effectFn = effect(getter, {
// 保证副作用不会第一时间被执行
lazy: true
})
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
return value
}
}
return obj
}
调整后的computed看似可以满足我们只计算一次,缓存值的诉求,但是仔细一思考,当我们obj中的值发生变化,computed也不会重新计算新值给我们,这是一个致命的缺陷,所以我们应当保证缓存值的情况下,也不能够失去对于依赖值发生变化的相应。
如此,思路就来了,我们就需要让dirty能够调度执行修改其值,这就用到了我们上一讲提到的scheduler(调度器),具体如下可以实现一个简单的调度器,帮助我们处理computed多次重复计算的问题:
// computed
function computed(getter) {
// 保存值
let value;
// 是否启用缓存
let dirty = true;
const effectFn = effect(getter, {
// 保证副作用不会第一时间被执行
lazy: true,
scheduler() {
dirty = true
}
})
const obj = {
get value() {
console.log('get value', dirty)
if (dirty) {
value = effectFn();
dirty = false;
}
return value
}
}
return obj
}
3、解决effect副作用中不生效问题
上面实现的computed基本可以满足值缓存,依赖值变化,触发调度器调整值,但是当我们在effect中调用计算属性值会,我们发现他并不会随着依赖数据变化而变化,具体一下:
const data = {bar: 1, foo: 1}
const obj = new Proxy(data, {/* 参考之前的代码 */})
const sumRes = computed(() => {
return obj.bar + obj.foo
})
effect(() => {
console.log(sumRes.value)
})
obj.bar++;
// 2
理论上我们需要的是3,实际上我们只得到了2;那么我们分析一下为什么不会更新值:
- 1、sumRes生成一个
computed计算属性,遇到第一个effect执行console.log(sumRes.value),计算值,并且将value值缓存起来,当然这块在细拆分就是effect(() => { effect(...) })嵌套结构; - 2、当我们执行obj.bar++ 他只会执行
bar这个key值对应的副作用函数,也就是外层的effect副作用函数,但是当执行到sumRes.value时,由于第一次执行我们将dirty设置为false,本次没有触发computed内部的effectFn,这样就不发执行调度器scheduler,所以此处仍然拿到的是缓存的value值,所以是2;
针对上面的问题,我们应该如何改进?其实思路也很简单,既然原因是无法触发computed中的effectFn,那么我们是不是让obj也能通过tack和trigger方式进行监听,触发值调度器等相关操作,那么我们就对computed进行改写:具体如下:
function computed(getter) {
// 保存值
let value;
// 是否启用缓存
let dirty = true;
const effectFn = effect(getter, {
// 保证副作用不会第一时间被执行
lazy: true,
scheduler() {
if(!dirty) {
dirty = true;
trigger(obj, 'value');
}
}
})
const obj = {
get value() {
console.log('get value', dirty)
if (dirty) {
value = effectFn();
dirty = false;
}
tack(obj, 'value');
return value
}
}
return obj
}
这样我们就完成了一个穷人版的computed,后面附上完整代码:
// 创建调度任务容器
let workers = new Set();
// 创建一个微任务队列
const p = Promise.resolve();
// 是否标识正在刷新队列
let isFlushing = false;
// 副作用函数
let activeEffect;
let effectStack = [];
// 刷新队列
function flushWorkers() {
if (isFlushing) return
isFlushing = true
p.then(() => {
console.log(workers, 'workers')
workers.forEach(w => w())
}).finally(() => {
isFlushing = false
})
}
function effect(fn, option = {}) {
const effectFn = () => {
clearEffectDeps(effectFn);
activeEffect = effectFn;
// 将当前副作用effectFn放入effectStack中
effectStack.push(effectFn);
// 执行副作用函数,拿到返回值
const res = fn()
// 出栈
effectStack.pop();
// 将activeEffect指向上一个effect
activeEffect = effectStack[effectStack.length - 1];
// 将发回值作为结果抛出去
return res
}
effectFn.deps = [];
// 增加调度任务模块
effectFn.option = option;
if(!option.lazy) {
effectFn()
}
return effectFn
}
// 每次清除副作用函数列表里面的关联关系
function clearEffectDeps(effectFn) {
effectFn.deps.forEach(i => {
i.delete(effectFn)
})
effectFn.deps.length = 0
}
// 数据准备
const data = { bar: 1, foo: 1 };
// 响应式函数容器
const bucket = new WeakMap();
const obj = new Proxy(data, {
get(target, key) {
tack(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
}
});
// 向bucket里面注入副作用函数
function tack(target, key) {
// 没有acticeEffect
if (!activeEffect) {
return;
}
// 判断下面有没有对应的对象相关的内容
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
// 对应key值的内容
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
// 将对应key值的副作用函数相关信息放入副作用函数
activeEffect.deps.push(deps);
}
// 副作用触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) {
return;
}
let deps = depsMap.get(key);
const effectsRun = new Set();
deps && deps.forEach(i => {
if (i !== activeEffect) {
effectsRun.add(i);
}
})
effectsRun.forEach(fn => {
// 执行前,判断是否有调度器
if (fn.option.scheduler) {
fn.option.scheduler(fn);
} else {
fn();
}
});
}
// computed
function computed(getter) {
// 保存值
let value;
// 是否启用缓存
let dirty = true;
const effectFn = effect(getter, {
// 保证副作用不会第一时间被执行
lazy: true,
scheduler() {
if(!dirty) {
dirty = true;
trigger(obj, 'value');
}
}
})
const obj = {
get value() {
console.log('get value', dirty)
if (dirty) {
value = effectFn();
dirty = false;
}
tack(obj, 'value');
return value
}
}
return obj
}
const sumRes = computed(() => {
return obj.bar + obj.foo
})
effect(() => {
console.log(sumRes.value)
})
obj.bar++;
最后希望大家多多支持,我会努力笔耕不辍,更新更多的前端知识给大家~~