mini-vue3: reactive & effect(触发依赖 & 收集依赖)

135 阅读2分钟

vue3响应式核心原理是reactive,reactive的实现是由Proxyeffect组合

reactive:其定义是接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

1.reactive单元测试

// reactivity/tests/reactive.spec.ts
import { reactive } from "../reactive" // 后续创建reactive文件后引入
describe('reactive', () => {
  it('happy path', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(observed.foo).toBe(1)
  })
})

2.实现reactive

  • 使用 Proxy 进行目标对象的代理
  • get时收集依赖
  • set时触发依赖
// reactivity/reactive.ts
export function reactive(raw) {
    // 将传入的对象用Proxy包裹
    return new Proxy(raw, {
      get(target, key) {
         // 不明白Reflect的同学可以MDN搜索哈
         const res = Reflect.get(target, key)

         // get时收集依赖
         track(target, key)
         return res
       },
      set(target, key, value) {
         const res = Reflect.set(target, key, value)

         // 触发依赖
         trigger(target, key)
         return res
      }
    })
}

把收集依赖和触发依赖代码先注释,此时运行yarn test reactive应该可以通过测试

接下来实现effect,实现依赖相关逻辑

3. effect单元测试

 // reactivity/tests/effect.spec.ts
 import { effect } from "../effect"
 import { reactive } from "../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)
   })
 })

4.简易实现effect

// effect.ts
// 抽离类,根据单测的用法,effect传入fn,且立即调用
class ReactiveEffect {
 private _fn: any;
 // 传入fn
 constructor(fn) {
   this._fn = fn
 }
 // 调用run方法去执行fn时,保存this,保存当前实例
 run() {
   activeEffect = this
   this._fn()
 }
}

let targetMap = new Map()// 创建一个Map收集依赖
// 收集依赖,导出track
export function track(target, key) {
 // target -> key -> dep
 let depsMap = targetMap.get(target)
 // 初始化时没有depsMap,建立关系
 if (!depsMap) {
   depsMap = new Map()
   targetMap.set(target, depsMap)
 }

 let dep = depsMap.get(key)
 if (!dep) {
   // fn不能重复,因此使用Set,下方触发依赖时调用effect.run()
   dep = new Set()
   depsMap.set(key, dep)
 }
 // activeEffect 是当前ReactiveEffect实例对象
 // 添加fn
 dep.add(activeEffect)
}

// 触发依赖
export function trigger(target, key) {
 const depsMap = targetMap.get(target)
 const dep = depsMap.get(key)
 for (const effect of dep) {
   effect.run()
 }
}
let activeEffect;
export function effect(fn) {
 const _effect = new ReactiveEffect(fn)
 _effect.run()
}

5.完善effect,返回runner

// reactivity/tests/effect.spec.ts
describe('effect', () => {
    ...// 省略上方测试代码
    
    it('should return runner when call effect', () => {
        // effect传入fn->function(runner) 实际上执行的是effect传入fn,fn的返回值 return
        // 1. effect(fn)->function(runner)->fn -> 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')
    })
}

6. 实现effect返回runner功能

    // effect.ts
    // 返回runner,意味着effect将run方法返回,fn有this指向问题,使用bind解决
    export function effect(fn) {
      const _effect = new ReactiveEffect(fn)
      _effect.run()
      // 返回run方法
      return _effect.run.bind(_effect)
    }
    
    // ReactiveEffect类中的run
    class ReactiveEffect {
      private _fn: any;
      constructor(fn) {
        this._fn = fn
      }
      run() {
        activeEffect = this
        // 返回fn的返回值
        return this._fn()
      }
    }