手写mini-vue3:实现reactivity模块中的-reactive-effect

684 阅读12分钟

实现reactive以及effect

mini-vue3的同步代码实现点击这里

mini-vue3的所有文章点击这里

目录的创建

在src目录下,创建reactivity文件夹,该文件夹用于存放与响应式有关的代码。在reactivity文件夹下创建test文件夹,该文件夹用于存放测试代码。在reactivity文件夹下,创建effect.ts文件reactive.ts文件

image.png

实现reactive

监听数据发生变化

在vue3中reactive函数会给我们返回一个代理对象,该对象的属性发生变化时,vue能够知道该变化发生并且通知对应的视图发生更新。所以想要实现数据发生变化,视图也跟着变化。最重要的一点就是我们需要能够监听到数据发生变化这一行为,而ES6的ProxyAPI就能够做到这一点。

// 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
        }
    })
}

ProxyAPI会返回一个代理对象,修改该代理对象可以映射到原始对象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就是收集的这个属性对应的依赖。

image.png 在解决了依赖收集的数据结构选择的问题后,我们还还有一个问题没有解决,那就是我们如何获取到当前执行的函数? 函数执行时如果访问了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();
  }
}

实现完tracktrigger之后,需要在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

shallowReadonlyreadonly的区别就是当readonly的某个属性是对象时,则该对象也是readonly类型,而当shallowReadonly的某个属性也是对象时,这个属性的对象并不是readonly类型。shallowReactivereactive的区别也是一样的。在上面的实现中,我们并没有对属性也是对象这种情况进行处理。

// 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,
});