前面我们讲了effect的作用,接下来会去深入实现effect相关功能
实现effect返回runner功能
effect(fn)执行会返回一个runner函数;- 执行
runner,相当于重新执行一遍effect里面传入的fn - 最后
runner的返回值就是fn的返回值。
runner对外暴露ReactiveEffect实例的run方法,他可以手动控制响应式的生效与暂停,关于暂停后面会实现stop
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
//合并配置
extend(_effect, options);
_effect.run();
// 把 _effect.run 这个方法返回
// 让用户可以自行选择调用的时机(调用 fn)
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
使用effect函数返回出一个fn函数可以进行继续调用,但是考虑到作用域问题所以采用bind绑定this并返回
实现effect的stop功能
stop的作用是 停止数据响应,只有手动触发run的函数,数据才能够完成响应 封装对外暴露stop
export function stop(runner) {
runner.effect.stop();
}
首先在effect函数编写stop函数,那么内部怎么实现呢? 肯定是需要接收一个参数的,不然都不知道停止哪个,那么可以改写下effect函数
function effect(fn: Function, option: effectOption = {}) {
const scheduler = option.scheduler;
const reactiveEffect = new ReactiveEffect(fn, scheduler);
reactiveEffect.run();
const runner: any = reactiveEffect.run.bind(reactiveEffect);
runner.effect = reactiveEffect;//新增代码
return runner;
}
在这里我们找到了对应停止哪个effect,但是现在问题便是当初添加依赖或者数据发生改变都是通过Proxy的target与key找到对应的Set容器,那么我们可以在收集依赖时收集这个Set容器,当用户调用stop函数时直接清空就可以了。
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
if (!currentEffect) {
return;
}
trackEffects(dep);
}
function trackEffects(dep) {
// 看看 dep 之前有没有添加过,添加过的话 那么就不添加了
if (dep.has(currentEffect)) return;
dep.add(currentEffect);
// depseffect管理Set的容器
currentEffect.deps.push(dep);
}
deps容器是拿给cleanupEffect清空依赖用的
接下来便是内部清空函数逻辑
class ReactiveEffect {
private fn: Function;
deps: Array<Set<this>> = []; //容器
isRunning: boolean = true;
onStop?: Function;
public scheduler: Function | undefined;
constructor(_fn: Function, scheduler?) {
this.fn = _fn;
this.scheduler = scheduler;
}
run() {
currentEffect = this;
return this.fn();
}
stop() {
if (this.isRunning) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.isRunning = false;
}
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
至于isRunning这个变量其实为了做一次优化如果之前已经被调用过了那么就没必要重复执行。
实现effect的任务调度
什么是任务调度
谈起任务调度,这里的任务调度是指当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式 比如说如下
const obj = reactive({a:2})
effect(()=>{
console.log(obj);
})
obj.a++;
obj.a++;
obj.a++;
那么接下来就会执行三次,但是在应用中我绝对不允许这么做,这会大大浪费性能,例如用户修改20次,触发20页面更新显示采用这种方式性能真的太差了。所以vue采用这种方式进行异步更新。
任务调度实现
1. effect接收参数 schedular
interface effectOption {
scheduler?: Function;
}
function effect(fn: Function, option: effectOption = {}) {
const scheduler = option.scheduler;
const reactiveEffect = new ReactiveEffect(fn, scheduler);
reactiveEffect.run();
return reactiveEffect.run.bind(reactiveEffect);
}
2.在修改数据触发effect时如果存在schedular则执行schedular
在ReactiveEffect添加成员scheduler
class ReactiveEffect {
private fn: Function;
public scheduler: Function;
constructor(_fn: Function, scheduler?) {
this.fn = _fn;
this.scheduler = scheduler;
}
run() {
currentEffect = this;
return this.fn();
}
}
在trigger函数里添加执行调度逻辑
function trigger(target, key) {
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for (let effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.fn();
}
}
}
1.ReactiveEffect接收第二个参数 schedular
2.当调用effect函数依旧执行fn函数
3.在修改数据触发effect时如果存在schedular则执行schedular
任务调度用法
const queue: any[] = [];
let isFlushPending = false;
const p = Promise.resolve();
instance.update = effect(componentUpdateFn, {
scheduler: () => {
// 把 effect 推到微任务的时候在执行
// queueJob(effect);
queueJob(instance.update);
},
});
这里举的例子便是组件更新的例子部分代码,当响应式变量更新后,会执行scheduler函数,然后会将用户传递进来的副作用函数放到一个任务队列,等到合适时机再去执行。
其实在vue2当中也有任务调度当触发更新时会通过nextTick去执行,会放到一个异步任务队列,所以说更新机制实现并没有变化,只是改变了实现的细节