前言
写这篇文章的痛苦程度堪比申论,恰逢追的小说打更人也完结了,以前觉得这个作者每天就更个几千字,是不是能力不行啊。现在再看看自己,妥妥一个粗鄙的码农,写个代码读后分析都写不出来。
开始
基本的原理,大伙可能或多或少都看到过,什么render时候 get 时候收集依赖,set 时候触发依赖,重新render使视图更新,背都能背出来了。但是对其中的,到底是怎么收集依赖,怎么触发依赖,怎么防止一个组件更新多个 data 数据时重复render,怎么异步渲染这些,可能了解的并不是很清楚。希望通过下面这些苍白无力的文字,能让大伙稍微地,多一些了解。(贴代码时候会省略一些特殊情况,比如readonly,shallow等等这些,不妨碍主流程的。)
track与trigger是一对好兄弟
function track(target, type, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
在track中,targetMap 和 depsMap 分别用来保存一个对象中每个key对应的依赖。以target作为key,对应值为一个新的map => depsMap,在depsMap中,则是以target的每个key作为key值,对应的是effect的副作用依赖数组。通过track,我们就能收集到每个key对应的依赖了。
function trigger(target, type, key, newValue, oldValue, oldTarget) {
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}
const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect);
}
});
}
};
else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key));
}
}
const run = (effect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect);
}
else {
effect();
}
};
effects.forEach(run);
}
在trigger中,通过改变的key,获取我们之前在track中收集的effect依赖数组,然后依次运行这些副作用函数。
effect是个啥
function effect(fn, options = shared.EMPTY_OBJ) {
const effect = createReactiveEffect(fn, options);
effect();
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) {
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn();
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.deps = [];
effect.options = options;
return effect;
}
这是经过删减以后的effect代码,简单来说就是创建了一个副作用的函数,然后effect有自己的依赖 deps,也是一个数组,和track中的 deps 相互依赖。
那我们再哪里会用到effect呢。
const setupRenderEffect = (instance) => {
// create reactive effect for rendering
instance.update = reactivity.effect(function componentEffect() {
if (!instance.isMounted) {
// render mountComponent
}
else {
// render updateComponent
}
}, createDevEffectOptions(instance));
};
我们可以看到,组件对应的 update 函数,其实就是一个 effect ,然后在内部区分组件状态,来判断是运行 mount 还是 update 的逻辑。
其实看到这里,也可以大致明白了和这个原理。首先在 effect 的函数中,我们可以看到,初次创建 effect 时,是会直接运行的,因为当时组件还没 mount ,运行的就是 !instance.isMounted 分支的逻辑。然后通过 track 收集 render 中的依赖。然后,当数据更新时,在 trigger 中通过 key 就能找到这个 effect ,从而触发这个 effect,运行上面函数中的 update 分支的逻辑,更新视图。
effect是怎么避免重复运行的
当时我看到这里的时候,确实也产生了这个疑问,因为有的 handler 中,一次性会改变多个 value ,但是在 track 中每个 value 的 deps 里,都绑定的对应 component 的 render 逻辑,那再 trigger 时,就不可避免地会遇到这个问题。
在 createReactiveEffect 函数中,有这样一句话 cleanup(effect),是在 effect 运行时,触发的。
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
}
逻辑也很好理解,在 track 时,effect 也保存了一份所有依赖它的 deps,在运行时,只要把这些 deps 中的 effect 清除,就可以避免重新运行的问题了。
effect是怎么实现异步运行的
大家都知道,vue是异步更新dom的,那究竟是怎么实现异步的呢。首先,肯定是有一个任务队列存储了所有的effect,然后异步运行。
function createDevEffectOptions(instance) {
return {
scheduler: queueJob,
allowRecurse: true,
onTrack: instance.rtc ? e => shared.invokeArrayFns(instance.rtc, e) : void 0,
onTrigger: instance.rtg ? e => shared.invokeArrayFns(instance.rtg, e) : void 0
};
}
在 trigger 的时候,我们发现有一句 effect.options.scheduler(effect) ,组件在 createEffect 时,就传入了这个 scheduler。
function queueJob(job) {
if ((!queue.length ||
!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
job !== currentPreFlushParentJob) {
const pos = findInsertionIndex(job);
if (pos > -1) {
queue.splice(pos, 0, job);
}
else {
queue.push(job);
}
queueFlush();
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
原理其实很简答,每个 effect 都被添加到了 queue 中,然后,通过 Promise.then 实现异步运行。(flushJobs 就是运行 queue 的具体逻辑)
结语
写的不是很细致,中间有很多细节都省略了,后续可能会更加详细地分析其中的细节部分,我觉得以我的水平能写到这里,已经是谢天谢地了。毕竟读源码是很痛苦的事情,在源码里遨游,很容易就不知所措,被水淹没了。
如果有什么错误,先提前抱歉,因为都是自己看着源码分析出来的。发布完之后,给自己小小地点上一个赞,就lv2了,值得庆祝的事情。当初立的flag就是lv2的时候,就从宁波离职去杭州了。本来以为会拖到年底,想不到4个月就完成了。还是令我这个菜狗子感到欣慰的。要变成来自0574混迹0571了,但是一个合格的MC不会忘记自己来自哪里~
最后,希望自己能拿到心仪的offer,希望每个粗鄙的码农都有美好的职业前景~