从使用Vue3到深入原理(二):reactivity模块的实现

前言

:本文过长过干,观看前请备足水分。and,虽然是ts写的,但是并不关心类型,所有的类型注解基本只是为了防报错。

在手写Vue3核心代码之前,作者完全不知道TDD(测试驱动开发)是啥,在完成下文代码之前补习了一下TDD的基本思想以及一款测试框架——jest。所以在阅读之前,最好希望你对jest框架有基本的认知(阅读官方文档半小时能入门),以及明白为何在完成功能之前需要先写测试代码——这有助于减少调试的耗时,细致拆分的测试文件能够将bug扼杀在摇篮之中。

同时,不仅仅是完成代码,为了保证代码的可读性还会对完成功能后的代码进行重构,当然作者的重构能力仅限参考,清谨慎阅读😀。

reactivity的核心流程

以一段reactiveeffect的配合使用的代码来说明响应式的核心流程——依赖收集以及依赖触发。

 const user = reactive({
      age: 10
  })
​
    let nextAge
    effect(() => {
      nextAge = user.age + 1
  })
    console.log(nextAge) //11
    //update
    user.age++
    console.log(nextAge) //12 

这里演示了一个最基本功能的数据响应式案例,effect函数和reactive函数都是reactivity模块中的核心API,reactive函数通过Proxy代理传入的对象,在getter和setter阶段会分别执行依赖收集以及依赖的触发的工作。而effect函数接收一个回调函数,初始默认执行一次,并在回调中对应的依赖更新时再次执行。

当然reactivity还包含其他响应式API如ref等,这里先实现最基本的reactive和effect模块。

reactive和effect的实现

分别测试reactive和effect的逻辑,单元测试(jest) 代码如下:

//effect.spec.ts
//effect测试了reactive的响应式
describe('effect', () => {
  it('happy path', () => {
    const user = reactive({
      age: 10
  })
​
    let nextAge
    effect(() => {
      nextAge = user.age + 1
  })
    expect(nextAge).toBe(11)
    //update
    user.age++
    expect(nextAge).toBe(12)})
}) 

reactive和effect的代理和依赖收集及触发功能的实现

第一步,实现reactive的代理中的get和set

//reactive.ts
export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      //收集依赖 在effect中实现track,因为要获取activeEffect对象
      //...
      return Reflect.get(target, key)
  },
    set(target, key, value) {
      //触发依赖 在effect中实现trigger
      //...
      return Reflect.set(target, key, value)
  }})
} 

在这里拆分并测试一下reactive函数的逻辑,是可以通过的

//reactive.spec.ts
//测试reactive的代理功能
describe('reactive', () => {
  it('happy path', () => {
    const obj = { a: 1 }
    const reactiveObj = reactive(obj)
    expect(reactiveObj).not.toBe(obj)
    expect(reactiveObj.a).toBe(1)})); 

第二步,再get和set中分别收集依赖(track)和收集依赖(trigger),这两个函数的实现放在了effect.ts文件中,因为需要利用外部作用域和effect函数共用activeEffect对象(当前活跃的ReactiveEffect对象),这个点后面就知道为什么了。

//effect.ts中的track和trigger的实现
let activeEffect: ReactiveEffect
const targetsMap = new Map
export function track(target, key) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let deps = depsMap.get(key)
  //将现在的effect对象放入deps
  deps.add(activeEffect)
}
​
export function trigger(target, key, value) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let deps = depsMap.get(key)
  //通知依赖
  for (let dep of deps) {
    dep.run()}
} 

第三步,编写effect函数的功能——执行传入的函数,并且能够配合track和trigger实现依赖的触发与收集

class ReactiveEffect {
  fn: Function
  constructor(fn) {
    this.fn = fn}
  run() {
    this.fn()}
}
​
export function effect(fn: Function) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn)
  activeEffect = _effect
  _effect.run()
} 

这里不是直接在effect函数中执行fn,而是定义了一个ReactiveEffect对象,这个对象其实是观察者模式中的观察者(Observer),观察者上面定义了run函数,在主体(Subjcet)——也就是之前reactive所代理对象的某个key值发生改变时,会通知所有对应的观察者——即track和trigger中的deps集合中所包含的对象执行其run函数。

完善effect的功能

effect返回runner

effect的返回值runner是一个函数,且具有和传入的函数相同的功能和返回值。测试代码如下:

 //测试返回runner的功能
  it('return runner', () => {
    let foo = 1
    let fn = effect(() => {
      foo++
      return 'foo'
  })
    expect(foo).toBe(2)
    const n = fn()
    expect(foo).toBe(3)
    expect(n).toBe('foo')});
}) 

实现也很简单,修改后的代码如下:

class ReactiveEffect {
  private fn: Function
  constructor(fn) {
    this.fn = fn}
  run() {
    activeEffect = this
    return this.fn()}
}
​
export function effect(fn: Function) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn)
  _effect.run()
  //bind是因为run函数内部有this值的指向问题
  return _effect.run.bind(_effect)
} 
scheduler

effect的第二个参数是可选参数——配置对象options,其中的scheduler是一个函数对象,如果有scheduler的话,effect只会在初始化时执行fn,在之后的触发更新阶段都会执行scheduler。测试代码如下:

 //测试scheduler功能
  it('scheduler ', () => {
    let run: any
    let scheduler = function () {
      run = runner
  }
    let foo = reactive({ bar: 1 })
    let bee
    let runner = effect(() => {
      bee = foo.bar + 1
      return 'runner'
  }, {
      scheduler: scheduler
  })
    expect(bee).toBe(2)
    //trigger,执行的不是fn,而是scheduler
    foo.bar++//foo.bar == 2 ,没有执行fn,所以bee还是2 ,执行了scheduler,所以run可以使用
    expect(bee).toBe(2)
    const n = run()//执行run,即执行fn函数,此时bee == foo.bar +1 ==3,且n为fn的返回值
    expect(bee).toBe(3)
    expect(n).toBe('runner')}); 

实现如下,只需要改变effect的二个参数为可选参数,以及改写ReactiveEffect对象的构造函数并向外暴露scheduler,在trigger中判断并调用即可。

//effect第二个参数为可选参数
export function effect(fn: Function, options?: any) {
  //构造一个ReactiveEffect实例
  let _effect = new Re
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值