实现reactive以及effect
mini-vue3的同步代码实现点击这里
mini-vue3的所有文章点击这里
目录的创建
在src目录下,创建reactivity
文件夹,该文件夹用于存放与响应式有关的代码。在reactivity
文件夹下创建test
文件夹,该文件夹用于存放测试代码。在reactivity
文件夹下,创建effect.ts文件
和reactive.ts文件
。
实现reactive
监听数据发生变化
在vue3中reactive
函数会给我们返回一个代理对象,该对象的属性发生变化时,vue能够知道该变化发生并且通知对应的视图发生更新。所以想要实现数据发生变化,视图也跟着变化。最重要的一点就是我们需要能够监听到数据发生变化这一行为,而ES6的Proxy
API就能够做到这一点。
// reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 当访问target对象的key属性时,会来到这里
// TODO 依赖收集
// track()
return res
},
set(target, key, newValue, receiver) {
// 当修改target对象的key属性时,会来到这里
const res = Reflect.set(target, key, newValue, receiver)
// TODO 触发依赖
// trigger()
return res
}
})
}
Proxy
API会返回一个代理对象,修改该代理对象可以映射到原始对象raw对象。当访问代理对象的某个属性时,会触发getter操作,我们就可以在该getter函数中进行依赖收集;当修改该代理对象的某个值时,会触发setter操作,我们就可以在该setter函数中进行触发依赖。
测试
在test
目录下创建reactive.spec.ts
文件
// reactive.spec.ts文件
import { reactive } from "../reactive";
describe("reactive", () => {
it("happy path", () => {
const obj = {
age: 18,
};
const proxyObj: any = reactive(obj);
// 返回的对象和传入的对象不是同一个对象
expect(obj).not.toBe(proxyObj);
// 修改代理对象
proxyObj.age++;
// 代理对象的age属性值为19
expect(proxyObj.age).toBe(19);
// 原始对象obj的age属性也变为19
expect(obj.age).toBe(19);
});
});
执行yarn test
,发现测试通过,也就是代码没有问题。
实现依赖收集以及触发依赖
实现依赖收集和触发依赖之前首先要知道什么是依赖。现在我们可以通过reactive
函数监听到获取数据和修改数据的行为,知道了数据被修改我们得去重新执行依赖该数据的函数,这样才能做到数据发生变化,视图也发生变化的效果。我们可以简单的把当数据发生变化时,需要重新执行的函数称为依赖。我们得对这些函数进行收集,这样当数据发生变化时,才能通知这些函数重新执行。
要对依赖进行收集,就必须要选择一种合适的数据结构进行保存。当数据发生变化时我们可以获取到发生变化的对象,以及该对象的真正发生变化的key属性。所以我们可以通过一个WeakMap来保存该依赖。我们可以将发生数据变化的这个对象作为WeakMap的key,对应的Value就是一个Map对象。这个Map的key是监听对象的属性,value就是收集的这个属性对应的依赖。。
在解决了依赖收集的数据结构选择的问题后,我们还还有一个问题没有解决,那就是我们如何获取到当前执行的函数? 函数执行时如果访问了reactive对象的属性,那么会执行getter操作,在getter操作中我们如何获取当前执行的函数呢?这个时候我们可以使用一个全局变量
activeEffect
来帮助我们实现获取当前执行的函数。我们只需要在执行函数之前,将该函数赋值给全局变量activeEffect
,这样在进行getter操作时,我们就可以将activeEffect
作为依赖收集起来。
// effect.ts文件
// 存储依赖的weakMap
const targetMap = new WeakMap();
// 保存当前执行的函数的全局变量
let activeEffect = null;
export function track(target, key) {
// 通过target获取到该对象对应的map
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果没有depsMap,那么创建一个并且保存到targetMap中
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 根据key从depsMap中,获取到dep对象
let dep = depsMap.get(key);
if (!dep) {
// 如果dep为undefined,那么创建一个并且保存到depsMap中
dep = new Set();
depsMap.set(key, dep);
}
// 如果当前activeEffect有值,将该activeEffect作为依赖收集起来
if (activeEffect) {
dep.add(activeEffect);
}
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
// 遍历取出dep中的所以依赖,然后执行
for (const effect of dep) {
// 在后面的实现中,被收集的依赖会有run方法,在该方法中会执行真正需要执行的函数
effect.run();
}
}
实现完track
和trigger
之后,需要在getter中调用track
,在setter中调用trigger
// reactive.ts
import { track, trigger } from "./effect";
export function reactive(raw) {
return new Proxy(raw, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver);
// 触发依赖
trigger(target, key);
return res;
},
});
}
实现effect函数
先把effect函数要实现的初步功能通过测试展示出来。在test
目录下创建effect.spect.ts
文件
// effect.spec.ts
import { reactive } from "../reactive";
import { effect } from "../effect";
describe("effect", () => {
it("happy path", () => {
const obj = reactive({
age: 18,
});
let nextAge;
effect(() => {
nextAge = obj.age + 1;
});
// 传递给effect函数的参数会被执行。
expect(nextAge).toBe(19);
// 当响应式对象发生变化时
obj.age++;
// 会重新执行传递给effect函数的参数
expect(nextAge).toBe(20);
});
});
// effect.ts
let activeEffect: ReactiveEffect;
class ReactiveEffect {
// 该参数用于保存effect函数的fn参数
private _fn;
constructor(fn) {
this._fn = fn;
}
run() {
// 在执行函数之前,先将activeEffect赋值为当前的effect,这样后续才能进行收集依赖
activeEffect = this;
return this._fn();
}
}
export function effect(fn) {
// 执行effect函数时,会创建一个ReactiveEffect的实例
const _effect = new ReactiveEffect(fn);
// 执行实例的run方法
_effect.run();
}
effect函数主要做了两件事:创建ReactiveEffect的实例, 调用该实例的run方法。而run方法会先将当前的effect实例赋值给全局变量activeEffect
,然后再执行传递的fn函数。执行fn函数的过程中,因为访问到了obj.age
所以会触发getter
操作,在getter操作中执行了track
函数进行依赖收集。而当我们修改obj.age
的值时,会触发setter
操作,setter操作中会触发trigger
函数,将依赖函数也就是参数fn重新执行,所以nextAge
的值会变成20。
yarn test
执行测试用例,会发现当前的逻辑没有问题。
实现effect函数返回runner
effect函数会返回一个runner函数,该函数实际上就是effect实例的run方法。
// effect.spec.ts
it("runner", () => {
// effect函数会返回一个runner函数,该runner函数实际上就是effect实例的run方法
let foo = 10;
const runner: any = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(11);
const r = runner();
expect(r).toBe("foo");
expect(foo).toBe(12);
});
// effect.ts
...
export functino effect(fn) {
...
// 这里使用bind,将run方法中的this绑定为_effect实例
const runner = _effect.run.bind(_effect)
return runner
}
实现effect函数传入scheduler
effect函数可以接受第一个options参数。如果options中有scheduler
属性,并且该属性是一个函数,那么在第一次会执行effect的第一个参数fn,然后之后如果响应式数据发生改变时,重新执行的是scheduler函数,而不是重新执行effect函数的第一个fn参数。
// effect.spec.ts
...
it("scheduler", () => {
// 当有传递scheduler参数时,第一次执行还是执行effct的第一个参数fn
// 当数据发生变化时,执行的是scheduler函数
let dummy;
let run: any;
// 定义一个scheduler函数
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{
scheduler,
}
);
// 一开始scheduler不会被调用
expect(scheduler).not.toHaveBeenCalled();
// effect函数的第一个参数fn会被调用
expect(dummy).toBe(1);
// 当obj发送变化时
obj.foo++;
// 调用一次scheduler函数
expect(scheduler).toHaveBeenCalledTimes(1);
// 不会执行effect的第一个参数
expect(dummy).toBe(1);
// run函数实际上就是返回的runner函数
run();
expect(dummy).toBe(2);
});
// effect.ts
class ReactiveEffect {
private _fn
constructor(fn, public scheduler) {
this._fn = fn
this.scheduler = scheduler
}
...
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
...
}
export function trigger(target, key) {
...
for (const effect of dep) {
// 当触发依赖时,先判断一下scheduler是否为一个函数,如果是,则执行effect.scheduler方法,
// 如果不是就执行effect.run方法
if (typeof effect.scheduler === 'function') {
effect.scheduler()
} else {
effect.run()
}
}
}
要想实现scheduler
函数,首先要给ReactiveEffect
类添加一个scheduler实例属性,当调用effect函数时,有传递scheduler方法时,就将该scheduler方法作为ReactiveEffect
参数传递进去保存起来。当执行trigger时,先判断该effect的scheduler
属性是否为一个函数,如果是则执行该函数,如果不是则正常执行run方法。
实现stop函数
stop
函数接收一个runner,调用stop
函数后,runner依赖的响应式数据再次发生变化时,不会重新执行effect函数第一个参数fn。也就是说我们要通过runner,消除掉所有dep依赖中该runner对应的effect。
// effect.spec.ts
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
// 执行stop函数前,obj.prop发生变化
obj.prop = 2;
// dummy也会对应发生变化
expect(dummy).toBe(2);
// 调用stop后
stop(runner);
// obj.prop发生变化,dummy不会发生变化了
obj.prop = 3;
expect(dummy).toBe(2);
runner();
expect(dummy).toBe(3);
});
// effect.ts
class ReactiveEffect {
// 添加deps属性,该属性用来反向收集dep
deps: any[] = []
// 该属性用来记录是否调用过stop函数,可以避免多次调用stop
active = true
...
// 添加stop方法
stop() {
// 如果该effect没有调用过stop函数,才进行调用
if (this.active) {
this.deps.forEach(dep => {
if (dep.has(this)) {
dep.delete(this)
}
})
this.active = false
}
}
}
export function effect(fn, options: any = {}) {
...
const runner: any = _effect.run.bind(_effect)
// 将_effect实例作为runner的属性挂载到runner上,这样我们就可以通过runner得到effect实例
runner.effect = _effect
return runner
}
export function stop(runner) {
runner.effect.stop()
}
export function track(target, key) {
...
if (activeEffect) {
dep.add(activeEffect)
// effect反向收集dep
activeEffect.deps.push(dep)
}
}
想要实现stop
方法,关键要能从runner
中得到该runner对应的effect实例,所以在返回runner时,可以将effect实例挂载到runner上。然给ReactiveEffect
类添加stop方法,该方法主要就是遍历deps数组,判断dep中是否收集当前effect实例,如果有则删除。stop
方法仅仅调用runner的effect实例的stop方法即可。同时在收集依赖时,不能简单的将effect作为依赖收集起来,还需要进行effect收集dep操作。
在测试中发现,当调用stop函数后,如果通过obj.prop = 1
这种方式改变响应式对象的数据时,并不会重新执行effect函数的fn参数。但是当通过obj.prop++
这种方式进行修改响应式数据时,会发现effect函数的fn参数会重新执行。这是因为obj.prop++
实际上是等于obj.prop = obj.prop + 1
。所以在赋值之前会访问obj.prop
的getter方法,然后重新进行依赖收集,所以当赋值时会进行触发依赖,而此时依赖中是收集了stop
函数清除的effect的,所以会重新执行effect函数的fn参数。
想要解决这个问题,关键就是在于在依赖收集的时候要进行判断,当前是否应该收集依赖,如果应该才进行收集,如果不需要收集则不进行收集依赖。 所以我们可以通过一个全局变量来控制当前是否需要进行依赖收集。
// effect.ts
let shouldTrack = false
class ReactiveEffect {
...
run() {
activeEffect = this
// 通过run方法进行的getter操作是需要进行依赖收集的
shouldTrack = true
const res = this._fn()
shouldTrack = false
return res
}
}
export function track(target, key) {
if (!shouldTrack) return
...
}
我们可以设置一个shouldTrack
变量来进行控制,当是通过effect.run
方法触发的getter函数时,是需要进行依赖收集的。所以在run方法执行this._fn()
之前将shouldTrack
设置为true,这样当执行this._fn()
时,触发getter时,由于shouldTrack
为true,所以会正常进行依赖收集。当执行完this._fn()
后,将shouldTrack
设置为false。这样当其他操作触发的依赖收集时,由于shouldTrack
为false,所以就不会进行依赖收集。
实现onStop方法
在调用effect时,传递给effect的第二个参数options
中,可以有一个onStop方法,当调用stop
函数后,会回调onStop
方法
// effect.spec.ts
it("onStop", () => {
const obj = reactive({
foo: 1,
});
const onStop = jest.fn();
let dummy;
const runner = effect(
() => {
dummy = obj.foo;
},
{
onStop,
}
);
// 调用stop后,onStop会被执行
stop(runner);
expect(onStop).toBeCalledTimes(1);
});
// effect.ts
class ReactiveEffect {
...
onStop?: () => void
...
stop() {
if (this.active) {
...
this.active = false
this.onStop && this.onStop()
}
}
}
export function effect(fn, options:any = {}) {
...
// 将options上的属性,添加到_effect上
Object.assign(_effect, options)
...
}
实现onStop
方法比较简单,只需要在执行stop
方法时,回调该函数即可。
实现readonly
readonly会返回一个代理对象,但是该对象只能读值,不能改值。因为readonly不能够修改值,所以也就没有必要进行依赖收集,也就不用进行触发依赖。
实现readonly
实际上是比较简单的。
// reactive.ts
export function readonly(raw) {
return new Proxy(raw, {
get(target, key) {
return Reflect.get(target, key)
},
set(target) {
console.warn(`${target} is a readonly can not be set`)
return true
}
})
}
可以发现,readonly的实现代码中,其实是可以复用reactive的代码的。所以我们需要对代码进行稍微的重构。创建baseHandlers.ts
文件
// baseHandlers.ts
import { track, trigger } from "./effect";
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
if (!isReadonly) {
const res = Reflect.get(target, key, receiver)
// 如果不是readonly,则进行依赖收集
track(target, key)
}
return res
}
}
function createSetter(isReadonly = false) {
return set(target, key, newValue, receiver) {
const res = Reflect(target, key, newValue, receiver)
if (!readonly) {
// 如果不是readonly,则进行触发依赖
trigger(target, key)
}
return res
}
}
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set(target) {
console.warn(`${target} is a readonly can not be set`);
return true;
},
}
// reactive.ts
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
function createReactiveObj(raw, baseHanlders) {
if (raw !== null && typeof raw !== 'object') {
console.error(`${raw} must be a object`)
}
return new Proxy(raw, baseHandlers)
}
export function reactive(raw) {
return createReactiveObj(raw, mutableHandlers)
}
export function readonly(raw) {
return createReactiveObj(raw, readonlyHandlers)
}
可以看到经过上面代码的重构后,我们将很多的代码进行了复用,这样在后面实现新的功能时,也能够复用之前的代码。
实现isReadonly,isReactive,isProxy
isReadonly
用于判断一个对象是不是readonly对象,如果是返回true,如果不是返回false。
isReactive
用于判断一个对象是不是reactive对象,如果是返回true,如果不是返回false。
isProxy
用于判断一个对象是不是reactive或者readonly对象,如果是返回true,如果不是返回false。
// effect.ts
export const enum ReactiveFlags = {
IS_READONLY = '__v_isReadonly'
IS_REACTIVE = '__v_isReactive'
}
export function isReadonly(value) {
// 这里使用!!是因为value可能是一个普通对象,会返回undefined
// 所以通过!!将undefined转换为boolean值
return !!value[ReactiveFlags.IS_READONLY]
}
export function isReactive(value) {
return !!value[ReactiveFlags.IS_REACTIVE]
}
export function isProxy(value) {
return isReactive(value) || isReadonly(value)
}
// baseHandlers.ts
import { ReactiveFlags } from "./reactive";
function crateGetter(isReadonly = false) {
return function get(target, key, receiver) {
if (key === ReactiveFlags.IS_READONLY) {
// 如果key === ReactiveFlags.IS_READONLY返回isReadonly即可
return isReadonly;
}
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
}
...
};
}
实现shallowReadonly,shallowReactive
shallowReadonly
和readonly
的区别就是当readonly
的某个属性是对象时,则该对象也是readonly
类型,而当shallowReadonly
的某个属性也是对象时,这个属性的对象并不是readonly
类型。shallowReactive
和reactive
的区别也是一样的。在上面的实现中,我们并没有对属性也是对象这种情况进行处理。
// baseHandler.ts
import { reactive, ReactiveFlags, readonly } from "./reactive";
function createGetter(isReadonly = false, isShallow = false) {
return get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
...
if (!isReadonly) {
track(target, key)
}
// 判断res是否为对象,如果是对象并且不是shallow类型。
// 那么对res对象进行readonly或者reactive处理。
if (res !== null && typeof res === 'object' && !isShallow) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
const shallowReactiveGet = createGetter(false, true);
const shallowReadonlyGet = createGetter(true, true);
export const shallowReactiveHandlers = {
get: shallowReactiveGet,
set,
};
export const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, {
get: shallowReadonlyGet,
});