手写vue3源码——reactive, effect ,scheduler, stop 等

简介: 在上面的测试用例中,有两个关键的函数reactive和effect,一个是创建响应式对象,另一个则是收集依赖,这个测试用例有点大,一次性实现不太方便,咋们可以把这任务拆分为更小的模块(任务拆分),分别写两个测试用例来测试reactive和effect

reactive, effect 大家都清除 ,但是对于scheduler, stop等方法是需要看源码咋们才能明白的😃😃😃,在上一节中,咋们用 pnpm 搭建了一个和vue3一样的monorepo,这一节中,就使用这个方式在里面填充vue3的源码吧!本节的源码请查看


目标


本次目标主要是实现,reactive,effect stop, onstop, scheduler 等


为了方便大家的理解,这一次咋们就从 测试用例的角度,来写出源码,vue3的响应式相信大家都用过,那么用测试用例来描述则是这样的。


test('响应式数据测试', () => {
     // 创建一个响应式对象
    const origin = reactive({ num: 1 })
    let newVal;
    // 依赖收集
    effect(() => {
      newVal = origin.num
    })
    expect(newVal).toBe(1)
    // update 更新阶段
    origin.num = 2
    expect(newVal).toBe(2)
  })


在上面的测试用例中,有两个关键的函数reactive和effect,一个是创建响应式对象,另一个则是收集依赖,这个测试用例有点大,一次性实现不太方便,咋们可以把这任务拆分为更小的模块(任务拆分),分别写两个测试用例来测试reactive和effect


reactive


看到reactive 想必都不陌生,传入一个对象,返回一个代理对象即可。那测试用例如下:


test('测试reactive', () => {
    let obj = { num: 1 }
    const proxyObj = reactive(obj)
    expect(obj).not.toBe(proxyObj)
    expect(proxyObj.num).toBe(1)
    // update set
    proxyObj.num = 2
    expect(obj.num).toBe(2)
  })


需求: 根据测试用例可以看出,调用reactive后,返回的结果和原对象不是同一个,并且将代理对象数据发生改变后,原对象的数据也会相应改变


export function reactive(obj) {
  if (!isObj(obj)) return obj;
  return new Proxy(obj, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      // todo 依赖收集
      if (isObj(value)) {
        return reactive(value)
      }
      return value
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      // todo 触发依赖
      return result
    },
  })
}


根据上面代码,可以运行测试用例,发现是没有问题的ヾ(≧▽≦*)o,但是在这里还有两个todo没有实现,分别是依赖收集和触发依赖


effect


effect函数可能有小伙伴不清除,这里解释下它的作用:调用effect后,里面的函数会立马执行一次哦,根据这个需要咋们写出以下测试用例:


test('effect是接受一个函数,当执行effect的时候,内部的函数会执行', () => {
    const fn = jest.fn();
    effect(fn)
    expect(fn).toBeCalledTimes(1)
  })


根据需要来实现下effect函数


export function effect(fn){
  fn()
}


上面的函数运行测试用例是没有问题的,但是咋们在深入一点,effect的作用是在trigger的时候来收集当前的fn,并且对外提供一个run函数,我想啥时候调用就啥时候调用,那么咋们是不是可以对fn进行包装一下。


class EffectReactive {
  fn: Function
   constructor(fn) {
    this.fn = fn
  }
  run(){
    this.fn()
  }
}
export function effect(fn){
  const _effect = new EffectReactive(fn)
  _effect.run()
}


对于effect的需要先到这儿,既然effect可以进行run函数了,接下来实现下trigger 和 track


trigger 和 track


需求:


1.trigger是在get到时候进行依赖收集

2.track 是在set的时候进行依赖触发,执行每一个fn


依赖收集收集的是fn, 那么在执行run的时候是不是可以来进行收集呢?,所以定义一个全局变量activeEffect,来保存,方便后续进行收集。


在class EffectReactive 里面的run 加上:

activeEffect = this;


然后在来实现trigger和track


// 用于保存每一个target,提高效率
const targetMap = new WeakMap();
/**
 * 收集依赖 target(map) ---> key(map) ---> fn(set)
 * @param target 
 * @param key 
 */
export function trigger(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 获取key
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  // 如果activeEffect不存在就不需要进行收集了
if (!activeEffect) return
  // 收集依赖
  deps.add(activeEffect)
}
/**
 * 依赖触发
 * @param target 
 * @param key 
 * @returns 
 */
export function track(target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  let deps = depsMap.get(key);
  if (!deps) {
    return
  }
  // 依赖触发的时候进行变量每一个fn,进行执行,就可以完成响应式的数据更新
  deps.forEach(effect => {
   effect.run()
  })
}


到了这一步,就可以发现咋们一开始的那个测试用例就可以通过啦😄


返回runner


在effect函数中,咋们可以返回一个runner函数,runner可以进行手动调用,并且拿到runner里面函数的结果,测试用例如下:


test('effect 有返回值', () => {
    let num = 10;
    // effect有返回值
    const runner = effect(() => {
      num++;
      return 'num'
    })
    // effect 在一开始的时候会调用
    expect(num).toBe(11)
    // 执行runner,并且拿到返回值
    const r = runner()
    // effect内部也会执行
    expect(num).toBe(12)
    // 验证返回值
    expect(r).toBe('num')
  })


咋们来改造下代码,对于effect函数需要返回值,是不是直接在effect里面做这样的操作


return  _effect.run.bind(_effect)


注意: 在class EffectReactive 中存在this绑定,所以出处需要使用bind来绑定this


面试的话,一般到这里就结束了,但是咋们是手写源码,肯定还需要往下走😎🤞😎


scheduler


scheduler的意思是调度者,作用是 当scheduler存在的时候,一开始scheduler不执行,当数据改变到时候,scheduler执行,run函数不执行,当手动调用scheduler里面的run函数的时候,直接看测试用例


test('scheduler 调度器', () => {
    let dummy;
    let run;
    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);
  })


根据需求来改造现有代码


在effect当中新增一个参数options,并且需要控制run函数的执行,run函数咋们是在track中进行执行的,所以咋们需要把scheduler传入到 EffectReactive 里面,给this进行绑定


// effect 函数
export function effect(fn, options: any = {}) {
  const _effect = new EffectReactive(fn, options.scheduler)
  ... 省略其他
  }
  // class EffectReactive 中做以下修改
  constructor(fn, public scheduler?) {
    this.fn = fn
    // 把scheduler 绑定在this当中,方便track中调用
    this.scheduler = scheduler
  }
  // track 函数做以下修改
  deps.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  })


这样的话,scheduler 的测试用例就能通过了, scheduler 的作用个人觉得可以用于 频繁修改数据,需要响应式,有点类似节流操作


stop


stop的作用是 停止数据响应,只有手动触发run的函数,数据才能够完成响应

,请查看测试用例


test("stop 停止响应", () => {
    let dummy;
    const obj = reactive({ prop: 1 });
    const runner = effect(() => {
      dummy = obj.prop;
    });
    obj.prop = 2;
    expect(dummy).toBe(2);
    stop(runner);
    obj.prop = 3
    expect(dummy).toBe(2);
    // stopped effect should still be manually callable
    runner();
    expect(dummy).toBe(3);
  });


看到stop是需要传入一个runner,这个runner是啥,就是咋们的effect函数的返回值,所以先改造下effect函数


  const runner = _effect.run.bind(_effect)
  return runner


还需要一个stop函数


/**
 * 停止响应式更新
 * @param runner 
 */
export function stop(runner) {
// 这里临时代码
  runner.stop()
}


这里请思考, runner 是effect, 控制run 是在 class EffectActive中,所以咋们还需要来改造下effect函数,把effect绑定在runner上


  const runner = _effect.run.bind(_effect)
  // 这样就可以在class EffectActive中 进行stop控制了
  runner.effect = _effect
  return runner


对于对外暴露的stop也需要做相应的变化


 runner.effect.stop()


接下来在class EffectActive中 实现stop函数,请分析下stop函数应该怎么实现?


1.需要控制run函数的执行,是不是只需要把trigger中收集到的依赖进行清空哇,😄


2.trigger中只会收集依赖,咋们怎么进行反向收集呢? 只需要在class EffectActive中用一个数组来接收,然后在trigger中来进行反向收集


3.在class EffectActive 中来实现清空操作即可


修改代码


// 在class EffectActive 中增加以下代码
export class EffectReactive {
  fn: Function;
  // 保存正则执行的effect,用于清除
  deps = []
  // 省略构造函数和run方法
  stop(){
   effect.deps.forEach(effectSet => {
    effectSet.delete(effect)
  })
  effect.deps.length = 0
  }
 }
 // 在trigger中进行反向收集
 // 收集依赖
  deps.add(activeEffect)
  activeEffect.deps.push(deps)


这样的话就可以完成测试用例了😄


在这里还可以进行优化下stop的调用,就是说在同一个 EffectActive实例中只调用一次,解决办法则是在class 中加一个变量(active)锁即可,详情查看


插曲


这里有一个问题,如果测试用例的 obj.prop = 3 改成 obj.prop++,测试用例就会有问题啦🙄🙄🙄,分析下问题,obj.prop = 3 和 obj.prop++的区别是,前者只触发set方法,而后者是先触发get方法,然后在触发set方法,触发了get方法是不是又会触发trigger来收集依赖哇,所以 obj.prop++ 在测试用例是会报错的哦!


那么如何解决呢?


咋们一起来分析下:


1.咋们是不是需要在trigger中来控制是否需要依赖收集,这里是不是可以定义一个全局变量(shouldTrack)默认是false


2.在 class EffectActive 中的run方法里面来控制变量,在run之前需要,run完之后就不需要了,如果是调用了stop后调用run就直接执行fn即可


改造源码


// 在track中加上一个控制条件
 if (!shouldTrack) return
 // 修改 class EffectActive 
 // 调用stop后不需要收集依赖
    if (!this.active) {
      activeEffect = this;
      return this.fn()
    }
    // 收集依赖
    shouldTrack = true;
    activeEffect = this;
    const result = this.fn();
    // 执行fn后停止收集依赖
    shouldTrack = false;
    return result


这样就可以通过测试用例了


onStop


onStop 是一个stop后的回调函数,这个功能我把测试用例写出来,实现留个各位看官老爷


test("events: onStop", () => {
    const onStop = jest.fn();
    const runner = effect(() => {}, {
      onStop,
    });
    stop(runner);
    expect(onStop).toHaveBeenCalled();
  });


结果


f9d24f3981f33f5637a61239e9940442.png


所有测试都完成通过

相关文章
|
23小时前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
102 64
|
23小时前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
26天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
22天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
50 7
|
24天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
41 3
|
22天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
39 1
|
22天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
44 1
|
25天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
26天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
JSON 前端开发 API
Vue3入门到精通--reactive以及reactive相关函数
Vue3入门到精通--reactive以及reactive相关函数
下一篇
无影云桌面