大家也可以去我的博客看相关技术文章,欢迎大家,一同进步!!!!
- Vue3 源码分析 01 - 权衡的艺术
- Vue3 源码分析 02 - 框架设计的核心要素
- Vue3 源码分析 03 - vue3的设计思路
- Vue3 源码分析 04 - 响应系统的作用与实现上 - 基本的响应式系统
- Vue3 源码分析 04 - 响应系统的作用与实现下 - 响应式系统的其他细节
vue3 源码分析,第四章 响应系统的作用与实现 下
tips:本章依旧是第四章,只是为了方便阅读,将其拆分了一下,本人博客依旧是连续的,在一章呢
调度执行
什么是调度执行:指的是当trigger动作触发副作用函数重新执行的时候,我们能够自定义
- 比如:副作用函数执行的时机、次数、方式等
举个例子
const data = { foo: 1 };
const obj = new Proxy(data, {...})
effect(( ) => {
console.log(obj.foo)
})
obj.foo++
console.log('结束了')
它的打印是如下的
1 // 副作用函数首次执行
2 // 自增触发trigger
结束了 // 最后打印
如果我们有变动呢?想要
1
结束了
2
我们给effect添加第二个参数,options,运行用户指定调度器
effect(
() => {
console.log(obj.foo);
}, // options
{
// 调度器是一个函数 scheduler
scheduler(fn) {},
}
);
先不管具体实现,我们effect注册的时候也需要给他加上
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
effectFn.options = options; // 把options 挂载到副作用函数上
effectFn.deps = [];
effectFn();
}
好,有了调度函数,我们在trigger的时候就需要把控制权交还给用户了
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects &&
effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
effectsToRun.forEach(effectFn => {
// 新增,如果有调度器
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
在 trigger 动作触发副作用函数执行时,我们优先判断该副作用函数是否存在调度器,如果存在,则直接调用调度器函数,并把当前副作用函数作为参数传递过去,由用户自己控制 如何执行;否则保留之前的行为,即直接执行副作用函数
这样我们就实现了副作用函数可调度
调度器的实际例子
1 上文中的那个打印输出怎么构造调度器能实现呢?
sheduler(fn) {
// 放到宏任务即可
setTimeout(fn)
}
2 控制次数 假设我们不关心trigger多少次,只关心最后一次的结果呢?,又该构造一个怎么样的调度器呢? 参考原文的队列实现
计算属性
计算属性 和 lazy
来实现vue.js 中的计算属性吧
lazy 的含义
effect(() => {
console.log(obj.foo);
});
我们希望lazy执行该如何呢?
effect(
() => {
console.log(obj.foo);
},
{
lazy: true,
}
);
我们需要修改effect的注册机制
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
effectFn.options = options;
effectFn.deps = [];
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值执行
return effectFn;
}
什么时候执行,交给用户了
const effectFn = effect(
() => {
console.log(obj.foo);
},
{
lazy: true,
}
);
effectFn(); // 手动执行
可以把传递给effect的函数看作一个getter,我们每次调用副作用函数都可以获取getter值, 要改造成这样,就需要继续改造
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
const res = fn(); // 将fn执行的结果存储到res中
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res; // 将res作为effectFn的返回值
};
effectFn.options = options;
effectFn.deps = [];
if (!options.lazy) {
effectFn();
}
return effectFn;
}
新的代码可以看到,传递给effect函数的参数fn是真正的副作用函数,而effectFn 是我们包裹后的副作用函数
让我们来试试实现计算属性吧
function computed(getter) {
// 把getter作为一个副作用函数,创建一个lazy的effect
const effectFn = effect(getter, { lazy: true });
const obj = {
// 当读取value时才执行effectFn
get value() {
return effectFn();
},
};
return obj;
}
首先我们定义一个 computed 函数,它接收一个 getter 函数作 为参数,我们把 getter 函数作为副作用函数,用它创建一个 lazy 的 effect。computed 函数的执行会返回一个对象,该对象的 value 属性是一个访问器属性,只有当读取 value 的值时,才会执行 effectFn 并将其结果作为返回值返回
来试一下我们写的computed
const data = { foo: 1, bar: 2 };
const obj = new Proxy(data, {});
const sumRes = computed(() => {
return obj.foo + obj.bar;
});
console.log(sumRes.value); // 3
ok,但是问题时啥,我们只做到了懒计算,但是没有缓存呀
computed 添加上缓存
function computed(getter) {
let value;
let dirty = true; // true 意味着脏,需要重新计算
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
};
return obj;
}
有问题呀,obj.foo 或者 obj.bar 修改了,但是还是走的缓存呀 让我们修正这个问题
function computed(getter) {
let value;
let dirty = true;
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
},
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
};
return obj;
}
还记得我们之前写的调度代码吗?trigger的时候会调用调度器,这里就是trigger,即响应式变量更新时候触发,dirty 设置为 true,这样下次读取 value 的时候就会重新计算
- 这里的getter变化的时候就会按照调度器执行
- 而getter就是副作用函数,里面就是响应式变化的变化会导致从新执行
但是还有问题呀:
看下面
const sumRes = computed(() => {
return obj.foo + obj.bar;
}
effect(() => {
console.log(sumRes.value);
})
obj.foo++
我们注册的这个副作用函数不会重新执行
解决computed嵌套在effect中的问题
解决方案?其实很简单,你执行的时候手动进行跟踪一下
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
if(!dirty) {
// 当计算属性依赖的响应值变化的时候,手动的去trigger触发响应
dirty = true
trigger(obj, 'value')
}
}
}
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
// 当读取 value 时候,手动调用track函数去追逐
track(obj, 'value')
return value
}
}
return obj
}
好啦,这样就可以了
- 当你去读取一个计算属性的value值的时候,我们手动去track下当前的副作用函数就可以了
- 手动调用track,注册当前的副作用函数(嵌套外层的那个副作用函数也注册下)
- 当计算属性依赖的响应式变量变化的时候,触发调度器,去重新执行
- 把dirty设置为true,这样下次读取value的时候就会重新计算
- 重新调用一次trigger, 触发下外层的副作用函数
effect(function effectFn() {
console.log(sumRes.value);
});
- computed(obj)
- value
- effectFn
- value
watch 的实现原理
// watch接受两个参数,source响应式数据,cb回掉函数
function watch(source, cb) {
effect(
// 触发读取操作,从而建立联系
() => source.foo,
{
scheduler() {
// 当数据变化的时,调用回掉函数cb
cb();
},
}
);
}
使用
const data = { foo: 1 };
const obj = new Proxy(data, {});
watch(obj, () => console.log("数据变化了"));
obj.foo++;
封装解决硬编码问题
前面的代码,硬编码了source.xxx,不太合理
function watch(source, cb) {
effect(
// 调用 traverse 递归地读取
() => traverse(source)
),
{
scheduler() {
cb();
},
};
}
function traverse(value, seen = new Set()) {
// 如果读取的值是原始值,或者已经被读取过了,那就什么都不做
if (typeof value !== "object" || value === null || seen.has(value)) return;
// 将当前值加入 seen,表示已经读取过了,避免循环引用
seen.add(value);
// 暂时不考虑数组等其他类型,只考虑对象
for (const k in value) {
traverse(value[k], seen);
}
return value;
}
在 watch 内部的 effect 中调用 traverse 函数进行递归的读取操作,代替硬编码的方式,这样就能读取一个对象上的任意属性,从而当任意属性发生变化时都能够触发回调函数执行。
指定getter
function watch(source, cb) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
effect(
// 执行getter就好
() => getter(),
{
scheduler() {
cb();
}
}
}
新旧值
function watch(source, cb) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
// 定义旧值和新值
let oldValue, newValue;
// 使用effect注册,开启lazy,
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
// 在scheduler中重新执行,获取新值
newValue = effectFn();
// 传递
cb(newValue, oldValue);
// 更新旧值
oldValue = newValue;
},
});
// 手动调用一次,拿到的值就是旧值
oldValue = effectFn();
}
我们手动调用 effectFn 函数得到的返回值就是旧值,即第一次执行得到的值。当变化发生并触发 scheduler 调度函数执行时,会重新调用 effectFn 函数并得到新值,这样我们就拿到了旧值与新值