【Vue3源码】第二章 effect功能的完善上

【Vue3源码】第二章 effect功能的完善

前言

上一章节我们实现了基础版本的effect函数,和reactive函数并且实现依赖收集和依赖触发功能,这一章我们继续完善effect函数的功能。了解vue真正的强大!

如果你还不了解单元测试和环境搭建,请查看我的《【Jest】Jest单元测试环境搭建》内容!

1、实现effect返回runner

实现runner到底有什么作用呢?

我会在后面的文章实现computed详细介绍为什么Vue3源码要这样设计,这里先卖个关子

我们现在只需要知道,runner是effect函数的返回值,并且这个runner会返回effect的第一个参数fn

我们先看下vue3源码中定义的effect返回的类型

export interface ReactiveEffectRunner<T = any> {
    
    
  (): T
  effect: ReactiveEffect
}

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions //options第二节会详细讲,现在不用管它
): ReactiveEffectRunner {
    
     ... }

​ 看了源码中的ReactiveEffectRunner类型,我们已经了解了effect返回的类型,并且知道了这个返回值runner需要实现怎么样的功能。

​ 找到effect函数进行改进。

export const effect = (fn) => {
    
    
  const _effect = new ReactiveEffect(fn);
  _effect.run();
  //通过bind绑定this指向问题,让this显示指向_effect,这样runner就会有返回值。
  const runner = _effect.run.bind(_effect);
  return runner
};

上面代码如果直接返回出 _effect.run() 就会返回 'foo'。因为this指向问题,我们需要重新给run方法绑定this。

我会在单元测试中断点测试这两个内容。

这是ReactiveEffect类中的run方法:

run() {
    
    
 	  activeEffect = this;
 	  return this._fn();
}

​ 这样effect函数就会显示绑定到_effect实例本身,返回_effect 实例中的的run方法。

​ 现在的代码只是实现了effect函数有返回值而已并不能得到runner返回值。我们还需要在ReactiveEffect类中修改下run方法,让run方法可以返回_fn调用后的结果。

直接在run方法中返回函数调用的结果即可

class ReactiveEffect {
    
    
  private _fn;
  constructor(fn) {
    
    
    this._fn = fn;
  }

  run() {
    
    
    activeEffect = this;
    //返回_fn的返回值
    return this._fn();
  }
}

runner的单元测试

编写完代码逻辑后我们就可以开始单元测试了。

在effect.spec.ts中添加以下测试代码

import {
    
     effect } from "../effect";
import {
    
     reactive } from "../reactive";

describe("effect", () => {
    
    
	 //测试runner
  it("should return runner when call effect", () => {
    
    
    //1.effect(fn) => function (runner) => fn => return
    let foo = 10;
    //使用常量接收effect的返回值
    const runner = effect(() => {
    
    
      foo++;
      return "foo";
    });
    expect(foo).toBe(11);
    // 使用r接收runner的返回值
    const r = runner();
    expect(foo).toBe(12);
    //r成功接到了effect中第一个参数的返回值“foo”
    expect(r).toBe("foo");
  });
});

我们在命令行输入 yarn test 测试,通过单元测试!
这是上文提到的断点的截图:

return _effect.run()return _effet.run.bind(_effect) 的区别:

在这里插入图片描述

在这里插入图片描述

我们成功实现了runner的功能,effect方法会返回一个ReactiveEffectRunner类型的返回值,这个返回值(runner)可以再次调用,返回effect传入的第一个参数 即 fn的返回值。

2、scheduler功能

从上文中我们了解到,effect函数其实还传入第二参数options,源码中的options参数是一个ReactiveEffectOptions类型

我们看一下源码中的这个类型

export interface ReactiveEffectOptions extends DebuggerOptions {
    
    
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

export type EffectScheduler = (...args: any[]) => any

ReactiveEffectOptions类型中,找到scheduler,他的值是一个函数。

​ effect第二参数中的scheduler属性的作用:

当effect传入option中包含一个scheduler属性时,那么第一参数fn只会被执行一次,接下来每次的set操作时我们不再去tigger中触发run方法,而是执行我们的scheduler公共方法,我们只有在调用runner时才能继续触发run方法,来实现响应式。

​ 这里我们就知道为什么第一步要封装effect的返回值runner了,为了控制run方法的执行,而不是每次set操作时一直的触发run方法,(当然get操作中我们没有限制去收集依赖),scheduler调度程序)和 runner 帮助 Vue去实现了可以控制的执行run方法的功能!

​ 下面就可以开始一一实现scheduler功能的代码了!

​ 1. 修改effect函数

把effect的第二个参数传入到ReactiveEffect类中。

export const effect = (fn, options:any = {
    
    }) => {
    
    
  //我们实例化时也要携带options
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  return _effect.run.bind(_effect);
};

​ 2. 修改ReactiveEffect类

让该类的构造函数接收scheduler传参,并且定义成public公共属性

class ReactiveEffect {
    
    
  private _fn;
  //在构造函数中我们接收一个公共的scheduler参数,这样外部就能使用它了
  constructor(fn, public scheduler?) {
    
    
    this._fn = fn;
    this.scheduler = scheduler;
  }

  run() {
    
    
    activeEffect = this;
    return this._fn();
  }
}

​ 上面两步的修改主要是为了实现scheduler函数在实例外部也可以访问。

  1. 修改trigger函数

当我们effect函数传入的第二参数options中包含scheduler时,并且reactive函数触发set操作时调用trigger函数中,不让ReactiveEffect的实例去调用run方法,,而是触发scheduler函数来实现控制run执行的目的!

//依赖触发
export function trigger(target, key) {
    
    
  let depsMap = targetMap.get(target);
  let dep = depsMap.get(key);
  for (let effect of dep) {
    
    
    // 当触发set时,如果有scheduler就执行scheduler,收集的依赖永远无法触发
    if (effect.scheduler) {
    
    
      effect.scheduler();
      // 没有就触发ReactiveEffect实例的run方法依赖触发
    } else {
    
    
      effect.run();
    }
  }
}

runner和scheduler一起单元测试

我们对两个功能一起进行单元测试,当然以前写的reactive函数的功能也不会受到影响

import {
    
     effect } from "../effect";
import {
    
     reactive } from "../reactive";

describe("effect", () => {
    
    
  it("happy path", () => {
    
    
    const user = reactive({
    
    
      age: 10,
      name: "www",
      newObj: {
    
    
        objAge: 11,
      },
    });
    let nextAge;
    let age2;
    effect(() => {
    
    
      nextAge = user.age + 1;
    });
    //无法代理深层嵌套的函数
    effect(() => {
    
    
      age2 = user.newObj.objAge;
    });
    expect(nextAge).toBe(11);
    user.age++;
    expect(nextAge).toBe(12);
    user.age = 99;
    expect(nextAge).toBe(100);

    expect(age2).toBe(11);

    //对于深层嵌套的对象由于没有封装递归的逻辑所以监听不到
    user.newObj.objAge++;
    //理论上来说应该变成12,而结果却没有变化
    expect(age2).toBe(11);
  });

  it("should return runner when call effect", () => {
    
    
    //1.effect(fn) => function (runner) => fn => return
    let foo = 10;
    const runner = effect(() => {
    
    
      foo++;
      return "foo";
    });
    expect(foo).toBe(11);
    const r = runner();
    expect(foo).toBe(12);
    expect(r).toBe("foo");
  });

  it("scheduler",() => {
    
    
    // 1. 通过 effect 的第二个参数给定一个 scheduler (是一个函数类型的参数)
    // 2. effect 第一次执行的时候 就会执行第一个参数中的 这个 函数
    // 3. 当响应式对象 set update 不会执行第一个参数的 fn 而是 执行第二个 scheduler 的函数
    // 4. 而当执行runner时才会再次执行 fn
    let dummy;
    let run :any;
    const scheduler = jest.fn(() => {
    
    
      run = runner
    })
    const obj = reactive({
    
    foo:1})
    const runner = effect(
      () => {
    
    
        dummy = obj.foo
      },
      {
    
    scheduler}
    )
    expect(scheduler).not.toHaveBeenCalled()
    expect(dummy).toBe(1)
    //should be called on first trigger
    obj.foo++
    expect(scheduler).toHaveBeenCalledTimes(1)
    //should not run yet
    expect(dummy).toBe(1)
    //manually run
    run()
    //should have run
    expect(dummy).toBe(2)
  })
});

执行yarn test,三次单元测试全部通过!!

我们可以发现结果非常的神奇,有了runner和scheduler,我们实现了一个通过options配置项来控制trigger依赖触发执行run还是scheduler的功能!

在这里插入图片描述

本文篇幅过长,请查看下一章《effect的功能完善下》~

猜你喜欢

转载自blog.csdn.net/m0_68324632/article/details/129002329