mini-vue3实现记录 - reactivity
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
对象即可- 值得注意的是,当第二次为
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
。
补充一个超级破产版vue3响应式实现, 实现了reactive
和依赖收集、触发依赖部分.
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key);
// TODO: 依赖收集
track(target, key);
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
//TODO: 收集依赖
trigger(target, key);
return res;
},
});
}
const targetMap = new Map();
// 当前执行的fn,fn执行 => 触发响应式对象的`get` => 依赖收集track
let activeFn = null;
function effect(e) {
activeFn = e;
e();
}
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
// 存入deps
if (activeFn) deps.add(activeFn);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
const deps = depsMap.get(key);
for (const effect of deps) {
effect();
// console.log(effect);
}
}
const foo = { obj: 1 };
const wrapFoo = reactive(foo);
console.log("wrapFoo.obj: " + wrapFoo.obj);
let nextObj = null;
effect(() => {
nextObj = wrapFoo.obj;
});
console.log(nextObj); // 1
wrapFoo.obj = 2;
console.log(nextObj); // 2
wrapFoo.obj++;
console.log(nextObj); // 3
转载自:https://juejin.cn/post/7211713478178930725