vue3的模块组成
- vue:即咱们平常导入运用的vue
- compiler-sfc 依靠于 compiler-dom 和 compiler-core 将
template模板转化成对应的render函数,- runtime 模块担任履行
render函数
effect & reactive & 依靠搜集 & 触发依靠
- 关于一个呼应式目标,其内部存在一个容器寄存依靠,经过
effect函数搜集依靠 -
effect函数接纳一个参数函数fn,即为依靠,运转fn会触发封装的署理目标proxy的get,get除了完结赋值操作,还有进行track依靠搜集 -
track函数的作用便是保存依靠。咱们需求两个表,一个根据target获得该target一切key即key对应的deps,一个Set根据key获得对应deps,当搜集依靠时,咱们将当时传入的fnadd到该set即可? 但是咱们的入参只要target和key,怎么获得这个fn呢? 咱们能够创建一个大局目标activeEffect,因为咱们是先履行的fn,再触发track,这意味着咱们能够先保存这个fn,再在track中获得。
export function track(target, key) {
// 咱们需求一个容器,存储呼应式目标的特点对应的一切依靠,关于target
// 那么这个对应关系便是: target -> key -> deps
// 所以咱们需求一个Map,寄存一切target, 还需求一个表来寄存该`target`对应`key`的一切dep
// 考虑到一个`key`可能有相同依靠,关于`dep`的搜集,咱们运用Set数据结构
let depsMap = targetMap.get(target);
// depsMap: `key`为呼应式目标的键`key`, `value`为这个`key`对应的依靠
if (!depsMap) {
// init
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
// init
dep = new Set();
depsMap.set(key, dep);
}
// 咱们需求将fn存入,怎么获得?
// 因为咱们是先进行fn的履行,所以咱们能够创建一个大局目标,在fn履行时使其指向当时的ReactiveEffect目标,然后在track中即可获得
dep.add(activeEffect);
}
- 当咱们修正呼应式目标时,则需求
触发依靠,触发依靠的逻辑很简略,取出一切dep循环调用即可。
// 触发依靠,根据`target`和`key`去除dep表,遍历履行即可
export function trigger(target, key) {
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for (const effect of dep) {
effect.run();
}
}
runner
runner用于保存传入effect的fn函数,当调用effect时,fn会作为回来值回来。
scheduler
scheduler作为effect函数的第二个参数,能够到达以下作用:
effect函数第一次履行时,仍会履行fn函数,进行依靠搜集- 当改动呼应式目标的值时,不会触发
fn,而是会履行scheduler- 因为
runner保存了fn,所以能够经过runner调用fn
stop功用
stop函数能够完结将依靠从依靠表中删去的功用,先从单测下手

stop(runner)后,改动呼应式目标的值不会导致dummy的值变化,这是因为依靠现已被删去,而从头履行runner后,因为依靠被从头搜集,改动呼应式目标的值能够使dummy同步变化。
完结
runner.effect保存了当时履行的依靠目标,调用该目标上的stop办法stop办法主要完结两件事:1. 履行cleanupEffect函数,铲除当时依靠 2. 假如scheduler目标中有传入回调函数就履行该函数cleanupEffect函数接纳一个目标effect,即当时的依靠目标,要在依靠表中删去该目标,咱们首先要在依靠目标上绑定依靠表,activeEffect指向当时的依靠目标,创建特点deps数组来保存dep依靠, 在track搜集依靠时保存。- 咱们还能够进行优化:当用户重复调用
stop时,咱们只履行一次逻辑,在这儿咱们创建active特点并初始化为true,当stop被调用,将其赋为false防止其再次履行
修正stop上的bug
还是从单测下手

obj.prop++,而不是obj.prop = 3这一简略赋值操作的话(只触发set),咱们会一起触发set和get,因为obj.prop++ => obj.prop = obj.prop + 1,那么这导致的问题便是,调用stop(runner)删去的依靠从头被搜集,即stop函数失效。
完结
- 由所以
Track的问题,咱们能够考虑在Track前添加是否需求搜集依靠的逻辑判别- 何时不需求搜集依靠?1. 仅仅单纯的拜访呼应式目标的特点时,即未履行
effect时,activeEffect=== undefined; 2. 当现已调用过stop时,即this.active = false时, 当这两者符合任一种,咱们在Track前直接return- 因此在
run中,当当时处于stop状态时,咱们直接履行fn而不进行下面的逻辑, 此时Track通道处于封闭状态,而当处于非stop状态时,咱们打开Track通道,因为履行_fn会触发get->Track,所以能够正常搜集
ref
为什么咱们需求ref? 咱们知道,对根本数据类型进行呼应式处理的时分,咱们都会运用ref而不是reactive,这是reactive底层基于的proxy只能署理目标,那怎么处理根本数据类型呢?咱们仍需求将其转化为一个目标,这便是ref里.value的必要性。
完结
从单测下手
- 能够看到,想要完结
ref,实际上便是要完结一个只要value这一个key的reactive目标即可- 值得注意的是,当第2次为
a.value赋值为2时,不会从头触发依靠,因为其与旧值相同,所以咱们需求添加新旧值的判别- 据此,咱们能够创建一个目标,经过对其的键
value进行get和set的阻拦, 当令的进行相关依靠搜集和触发依靠即可
class RefImpl {
private _value: any;
public dep;
private _rawValue: any;
constructor(value) {
this._rawValue = value;
this._value = convert(value);
this.dep = new Set();
}
get value() {
// 依靠搜集
/**
* 因为`ref`目标只要`value`一个key
* 所以咱们搜集时只需求一个Set存储每次的`activeEffect`即可
* 假如咱们不需求进行依靠搜集,就直接return this._value即可,不然会存入`activeEffect = undefined`
*/
trackRefValue(this);
return this._value;
}
set value(newValue) {
// 假如set的值与本来的值相同,则无需重复触发依靠
if (hasChanged(newValue, this._rawValue)) {
this._rawValue = newValue;
this._value = convert(newValue);
// 触发依靠
triggerEffects(this.dep);
}
}
}
function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
- 咱们需求对其依靠搜集和触发依靠进行特别处理,因为该目标只对一个key上的依靠进行处理,所以咱们创建私有特点
dep进行保存。 然后运用从reactive抽离出来的逻辑进行依靠处理即可- 针对
get的阻拦,咱们需求对是否需求进行依靠搜集进行判别,不然可能dep存入undefined- 针对
set的阻拦,咱们运用变量rawValue对保存转化前的值(因为假如传入目标需求将其进行reactive处理), 和新值进行比照,假如改动了再进行触发依靠
- 当咱们传入目标时,咱们也需求将其转化为呼应式,这儿咱们直接将传入的
value运用reactive包裹使其成为呼应式即可。
computed
computed特点在于其的缓存特性,即只要第一次触发get和computed``依靠的值产生改动时才会进行触发依靠操作。

.value拜访到computed目标的值,且再不拜访computed目标的值时,咱们传入的getter不会被调用。
其次,当依靠的呼应式目标值未被改动时,咱们在阻拦对computed目标的get时直接回来value即可,无需触发依靠, 即无需调用getter。
最后,当依靠的呼应式值被改动时,getter需求从头被触发。
import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
private _getter: any;
private _dirty: boolean = true;
private _value: any;
private _effect: any;
constructor(getter) {
this._getter = getter;
this._effect = new ReactiveEffect(getter, () => {
if (!this._dirty) this._dirty = true;
});
}
get value() {
// computed取值时,经过get对其进行一个阻拦
/**
* 何时需求调用`getter`?
* 1. 第一次触发`get` 2. 依靠呼应式目标的值改动后
* 怎么知道依靠的呼应式值产生改动? 经过引进effect
* 流程:
* 1. 第一次进入,经过用effect上的`run`函数完结`getter`的调用,完结赋值操作,并封闭调用`getter`的开关,到达缓存作用
* 2. 当依靠变化时,因为trigger, 咱们传入的scheduler被触发,`getter`触发的通道从头被打开
* 3. 再次拜访computed目标,触发get value()阻拦,再次调用`getter`完结赋值操作,并封闭调用`getter`的开关。
*/
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
- 为了到达控制依靠是否被触发的作用,且咱们需求知道依靠呼应式的值产生改动,咱们需求在
computed目标内运用effect,而因为咱们还需求进行其他特别处理,这儿咱们创建ReactiveEffect目标, 经过调用其上的run和传入scheduler到达该作用。- 咱们需求一个变量控制是否调用
getter函数,这儿咱们运用dirty变量,初始化为true,当其为true时调用getter函数- 当第一次触发
get,dirty为true,调用getter,将dirty设为false, 之后当依靠的呼应式目标值未改动时,因为dirty为false,只会直接回来value。- 当依靠的呼应式值产生改动,
computed目标上的ReactiveEffect目标触发trigger, 因为传入scheduler,不会履行getter,而是履行scheduler,然后将dirty设为true,当下次computed目标get被触发时,会再次履行getter。









