从头实现 mini-vue(添加effect的runner、scheduler、stop、onStop功能)

138 阅读5分钟

对于希望提升自己前端架构能力,写出更优雅代码的前端同学,
vue源码绝对是你成功路上的垫脚石,但由于尤大实现的vue源码库有许多对于一些边缘情况的判断逻辑,
所有这篇文章希望和大家一起从头实现一个简单版的vue源码库 mini-vue(感谢 阿崔cxr 的 mini-vue)
github源码地址 如果觉着还不错欢迎Star~

实现runner功能

effect再接受用户传入的fn之后会将fn返回let runner=effect(fn)我们称这个返回值为runner,在执行runner()的时候会将fn的返回值作为runner的返回值返回fn的返回值=runner()

1、在src>reactive>test>effect.test.ts中编写runner测试用例

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

describe("effect", () => {
//...other code
  it('runner', () => {
    let bar = 1
    let runner = effect(() => {
      bar++
      return 'bar'
    })
    //验证用户传入的fn执行
    expect(bar).toBe(2)
    let r = runner()
    //验证在执行runner的时候会再次执行用户传入的fn
    expect(bar).toBe(3)
    //验证runner的返回值等于fn的返回值
    expect(r).toBe('bar')
  })
});

从测试用例可以看出我们返回的runner其实就是我们ReactiveEffectrun方法

class ReactiveEffect {
//...other code
  run() {
    activeEffect = this;//用于存储依赖
    this._fn();
  }
}

2、在src>reactive>effect.ts中将run方法返回

export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
  let runner = _effect.run.bind(_effect)// bind用于处理run中的this指向
  return runner
}

3、在ReactiveEffect的run方法中将fn的返回值返回

class ReactiveEffect {
//...other code
  run() {
    activeEffect = this;//用于存储依赖
    return this._fn(); //将fn的返回值返回
  }
}

4、执行一下effect测试用例

yarn test effect.spec.ts

image.png

实现scheduler功能

effect允许传入第二个参数scheduler,当传入scheduler之后,effect初始化会执行fn,而当fn中的reactive值执行set操作时会执行我们传入的scheduler

1、在src>reactive>test>effect.test.ts中编写scheduler测试用例

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

describe("effect", () => {
//...other code
  it('scheduler', () => {
    let dummy
    let info = reactive({ age: 18 })
    //jest 模拟传入的函数
    const scheduler = jest.fn(() => {
      dummy++
    })
    //将scheduler作为第二个参数传入
    effect(() => {
      dummy = info.age
    }, { scheduler })
    //验证第一次是scheduler不被调用
    expect(scheduler).not.toHaveBeenCalled()
    //验证调用传入的fn
    expect(dummy).toBe(18)
    //执行set操作
    info.age = 20
    //验证执行set操作时会执行传入的scheduler
    expect(dummy).toBe(19)
    expect(scheduler).toBeCalledTimes(1)
  })
});

2、在src>reactive>effect.ts中添加传入参数options将options传入ReactiveEffect

export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  let runner = _effect.run.bind(_effect)
  return runner
}
class ReactiveEffect {
  private _fn: any;
  public scheduler: any//添加一个scheduler属性
  constructor(fn, scheduler?) {
    //将传入的scheduler存储起来
    this.scheduler = scheduler
    this._fn = fn;
  }
  run() {
    activeEffect = this;//用于存储依赖
    return this._fn();
  }
}

2、在src>reactive>effect.ts 在trigger是判断effect是否存在scheduler,如果存在执行scheduler否则执行run

export function trigger(target, key) {
//...other code
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run();
    }
  }
}

3、执行一下effect测试用例

yarn test effect.spec.ts

image.png

实现stop功能

effect文件会暴露一个stop方法,如果在stop方法中传入runner,则之后reavtive的set操作不会触发fn的执行,只在运行runner的时执行fn

实现思路:
reactive执行set操作时执行fn的实现原理
(1)在reactive的get操作用将effect存储起来

    get(target, key) {
      const res = Reflect.get(target, key);
      track(target, key);//存储effect
      return res;
    }
  function track(target, key) {
  //other code
  //将activeEffectc存储起来
  dep.add(activeEffect);
}

(2)在set时执行所有effect

    set(target, key, value) {
      const res = Reflect.set(target, key, value);
      trigger(target, key);
      return res;
    }
 function trigger(target, key) {
 //...other code
 //在set中执行effect的run方法
  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run();
    }
  }
}

根据上述原因可知,要实现stop功能只需要将当前的effect从依赖中删除即可。

1、在src>reactive>test>effect.test.ts中编写stop测试用例

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

describe("effect", () => {
//...other code
 it('stop', () => {
    let dummy
    let info = reactive({ age: 18 })
    let runner = effect(() => {
      dummy = info.age
    })
    expect(dummy).toBe(18)
    //将runner传入stop后,更新reactive不会触发fn
    stop(runner)
    info.age = 20
    expect(dummy).toBe(18)
    //再次执行runner的时候会执行fn
    runner()
    expect(dummy).toBe(20)
  })
});

2、在src>reactive>test>effect.ts中将effect挂载到runner上

export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler);
  _effect.run();
  let runner: any = _effect.run.bind(_effect)
  //将effect挂载到runner上,以便在stop函数中使用
  runner._effect = _effect
  return runner
}

export const stop = (runner: any) => {
  runner._effect.stop()
}

2、在src>reactive>test>effect.ts中反向存储track中的依赖以便在stop中将依赖删除

class ReactiveEffect {
//other code
//用于反向存储track中的依赖 
//由于可能是多个依赖所以用[]在存储
  public deps = [] 
  //这样在调用stop时可以将track中存储的对应依赖删除
  stop() {
    this.deps.forEach((dep: any) => {
      dep.delete(this)
    })
  }
}

export function track(target, key) {
  // other code
  //如果当前没有触发的依赖则直接返回否则将activeEffect与dep进行存储
  if (!activeEffect) return 
  dep.add(activeEffect);
  activeEffect.deps.push(dep)
}

3、执行一下effect测试用例

yarn test effect.spec.ts

image.png 4、优化代码 对于这个stop中的代码我们可以重构一下,让他更具有语义化

class ReactiveEffect {
//...other code
  stop() {
    this.deps.forEach((dep: any) => {
      dep.delete(this)
    })
  }
}
class ReactiveEffect {
//...other code
  stop() {
      cleanUpEffect(this);
  }
}
function cleanUpEffect(effect: any) {
  effect.deps.forEach((dep: any) => {
    dep.delete(effect);
  });
}

由于第一次调用stop会将track中的依赖删除,但之后的get操作会将依赖再次添加上去,为了防止这种清空我们可以添加一个active来做判断

class ReactiveEffect {
  private active = true //为防止cleanUpEffect重复调用
  stop() {
    if (this.active) {
      cleanUpEffect(this);
    }
    this.active = false
  }
}

5、再次执行测试

yarn test effect.spec.ts

image.png

实现onStop功能

effect还可以接收一个onStop函数,当用户调用stop功能之后会回到这个传入的onStop函数

1、在src>reactive>test>effect.test.ts中编写onStop测试用例

  it('onStop', () => {
    let dummy
    let info = reactive({ age: 18 })
    //创建一个onStop函数
    const onStop = jest.fn()
    let runner = effect(() => {
      dummy = info.age
      // 将onStop作为第二个参数传入
    }, { onStop })
    expect(dummy).toBe(18)
    //执行stop之后调用onStop
    stop(runner)
    expect(onStop).toHaveBeenCalledTimes(1)
  })

2、在src>reactive>test>effect.ts中将传入的参数赋值给effect

export function effect(fn, options: any = {}) {
  //...other code
  Object.assign(_effect, options)
}

2、在src>reactive>test>effect.ts中的RactiveEffect添加这个onStop的处理逻辑

class ReactiveEffect {
//...other code
  private onStop?: () => void
  stop() {
    if (this.active) {
      cleanUpEffect(this);
      //判断是否存在onStop,有的话在执行cleanUpEffect之后执行
      if (this.onStop) {
        this.onStop()
      }
    }
    this.active = false
  }
}

3、执行测试

yarn test effect.spec.ts

image.png 4、优化代码 对于第二步的处理我们可以让代码更加语义化 在src下创建一个shard文件夹和index文件 重写我们的Object.assign

image.png

image.png

export function effect(fn, options: any = {}) {
  //...other code
  //Object.assign(_effect, options)
  extend(_effect, options)
}

4、再次执行测试

yarn test effect.spec.ts

image.png