1. 实现computed源码
实现computed源码首先需要知道computed有那些特性,我们通过案例使用一下
1.1 computed的使用
import { computed, effect, reactive } from "@hpstream/reactivity";
const state = reactive({ flag: true, name: "jw", age: 30 });
let valueStr = computed(() => {
return `姓名:${state.name},年龄:${state.age}`;
});
// console.log(valueStr);
setTimeout(() => {
// state.age = 100;
// state.flag = false;
}, 1000);
effect(() => {
document.body.innerHTML = valueStr.value;
});
根据上面的例子运行例子可知,我们可以得到如下特性:
- computed 也是一个effect, 但是只收集computed里面函数使用的变量。
- computed 拥有缓存的效果,只有当依赖的值发生改变,函数才会重新执行。
根据此特性,我们开始实现computed:
- 由于computed函数的参数有可能是一个函数(代表getter),也有可能是一个对象,即有getter也有setter, 所以我们要对这两种类型做判断处理,如下面代码的情况。
// 使用方式一
let valueStr = computed(() => {
return `姓名:${state.name},年龄:${state.age}`;
});
// 使用方式二
let valueStr = computed({
get:() => {
return `姓名:${state.name},年龄:${state.age}`;
},
set:(val)=>{
return val;
}
});
1.2 computed源码实现
- 判断参数不同的情况
export const isFunction = (value) => {
return typeof value === "function";
};
export function computed(getterOrOptions: any) {
var setter, getter;
// 判断是函数,还是对象
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {};
} else {
setter = getterOrOptions.set;
getter = getterOrOptions.get;
}
return new ComputedRefImpl(getter, setter);
}
- 实现核心逻辑ComputedRefImpl
实现逻辑与reactive类似,进行依赖收集,但使用了dir变量标记依赖值是否发生了变化。
class ComputedRefImpl {
public effect;
public _value;
public dirty = true;
public deps = new Set();
constructor(public getter, public setter) {
// 初始化依赖收集函数
this.effect = new ReactiveEffect(getter, () => {
// 当收集值发生变化时,触发此方法;
if (!this.dirty) {
this.dirty = true;// 修改为true表示需要重新计算;
triggerEffects(this.deps);
}
});
}
get value() {
// 获取值的时候进行依赖收集
trackEffects(this.deps);
if (this.dirty) {
this.dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set value(val) {
this.setter(val);
}
}
- ReactiveEffect 的实现
ReactiveEffect 的第二个函数,我们称之为调度器,当第二个函数存在时,触发更新的时,会直接走调度器函数。
export class ReactiveEffect {
public parent = null;
public deps = [];
public active = true;
constructor(public fn, public scheduler) {}
run() {
if (!this.active) {
return this.fn();
}
try {
this.parent = activeEffect;
activeEffect = this;
cleanupEffect(this);
return this.fn();
} catch (error) {
} finally {
activeEffect = this.parent;
}
}
stop() {
if (this.active) {
this.active = false;
cleanupEffect(this); // 停止effect的收集
}
}
}
export function trackEffects(dep: any) {
if (!activeEffect) return;
let sholdTrack = dep.has(activeEffect);
if (!sholdTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
export function triggerEffects(effects: any) {
if (effects) {
effects = [...effects];
effects.forEach((effect) => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler();// 触发调度器函数
} else {
effect.run(); // 防止循环
}
}
});
}
}
2. watch 源码实现
2.1 watch 的使用
下面代码是我们使用watch的常见的三种方式,解释下他们之间的区别:
- 方式一:直接传递state,或者()=>state,由于他们是引用类型,所以新值和老值他们最终的结果是一样的,也就是说我们无法获取到老值了。
- 方式二:是我们最常用的方式,是可以获取到老值的。
- 方式三:在异步获取请求的时候,保证顺序展示是正常的,不会出现乱序的现象;(详解:第一次发送请求回来需要3s中,第二次发送请求回来需要2s中,那么2s次的结果先渲染,第一次的结果后渲染,这不是我们想要的)
import { reactive, watch } from "@hpstream/reactivity";
const state = reactive({ flag: true, name: "jw", age: 30, adds: { age: 3 } });
// 使用一
watch(state,
async (newValue, oldValue) => {
// newValue === oldValue 为true
console.log(newValue, oldValue);
}
);
// 方式二
watch(()=>stage.age,
async (newValue, oldValue) => {
// newValue === oldValue 为 false
console.log(newValue, oldValue);
}
);
// 方式三 onCleanUp的使用
const state = reactive({ flag: true, name: "jw", age: 30 });
let i = 5000;
function getData(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(timer);
}, timer);
});
}
watch(
() => state.age,
async (newValue, oldValue, onCleanup) => {
let clear = false;
onCleanup(() => {
console.log(1);
clear = true;
});
i -= 1000;
let r: any = await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
if (!clear) {
document.body.innerHTML = r;
}
}
);
state.age = 31;
state.age = 32;
state.age = 33;
state.age = 34;
2.2 源码实现
watch 源码的基础实现
export function watch(source, cb: Function) {
let getter;
if (isReactive(source)) {
getter = () => source; // 存在问题
}
if (isFunction(source)) {
getter = source;
}
let oldValue; // 存储老值
const job = () => {
const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
cb(newValue, oldValue);
oldValue = newValue;
};
const effect = new ReactiveEffect(getter, job);
oldValue = effect.run();
}
但是上诉代码存在问题,无法进行对象的深度收集,因为我们在收集依赖的时候没有去遍历对象的每个属性,所以我们对上面的代码稍作修改。
// .....
let getter;
if (isReactive(source)) {
getter = () => traverse(source);
}
if (isFunction(source)) {
getter = source;
}
//....
// 遍历对象的每个属性
function traverse(value: any, seen = new Set()) {
if (!isObject(value)) {
return value;
}
if (seen.has(value)) {
return value;
}
seen.add(value);
for (const k in value) {
// 递归访问属性用于依赖收集
traverse(value[k], seen);
}
return value;
}
这样子我们就完成了深度依赖收集的处理。
2.3 处理方式三的代码
watch的监听函数增加第三个参数,用来取消直接的代码逻辑。其补充代码如下:
// ....
let oldValue; // 存储老值
let cleanup;
let onCleanup = (fn) => {
cleanup = fn;
};
const job = () => {
const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
if (cleanup) cleanup();
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
};
const effect = new ReactiveEffect(getter, job);
oldValue = effect.run();
// ...
大功造成~!!!
致谢
如果感觉我的文章有用,请关注下我的公众号: 前端小黄。 我将不定期更新我的原创文章。
本文章源码地址:github.com/hpstream/vu…;