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,当搜集依靠时,咱们将当时传入的fn
add到该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
。