🤯vue3核心源码剖析(一)

·  阅读 625
🤯vue3核心源码剖析(一)

🚀reactive & effect 且利用Jest测试实现数据响应式(一)

前言

🎉很高兴在此分享给大家Vue3的数据响应原理,小老弟我表达能力有限,所以

🤣如有错漏,请多指教❤

实现思路

vue3 的数据响应式实现我们想理清楚它的特征,才好往下写。

以下是基本的reactive+effect用法

let count = reactive({ num: 11 })
let result = 0
effect(() => {
  result = count.num + 1
})
// result得到的是12 看起来effect立即执行了呢~
expect(result).toBe(12)
// 相当于count.num = count.num + 1  这里有count.num的get操作和set操作
count.num++
// result得到的是13 看起来他又执行了一遍effect的回调函数了呢~🤯
expect(result).toBe(13)
复制代码

两大点

  • 依赖收集
  • 触发依赖

下面讲一下大体思路:

  1. reactive为源数据创建proxy对象,其中proxygettersetter分别用于数据的依赖收集,数据的依赖触发
  2. effect立即执行一次回调函数,当回调函数内的依赖数据发生变化的时候会再次触发该回调函数
  3. 收集依赖我们可以定义一个track函数,当reactive的数据发生get操作时,track用一个唯一标识(下面会讲这个唯一标识是什么)记录依赖到一个容器里面
  4. 触发依赖我们可以定义一个trigger函数,当reactive的数据发生set操作时,trigger将关于这个数据的所有依赖从容器里面拿出来逐个执行一遍

简单实现(详细代码在最后)

  1. reactive

给源数据创建一个proxy对象,就好像给源数据套上了一层盔甲,敌人只能攻击这层盔甲,无法之间攻击源数据,在这基础之上我们就可以有机会去做数据的拦截。

reactive通过传入一个对象作为参数,返回一个该对象的proxy对象,其中Reflect.get(target, key)返回target的key对应的属性值res,Reflect.set(target, key, value)设置target的key对应的属性值为value

export function reactive(target: Record<string, any>) {
  return new Proxy(target, {
    get(target, key) {
      let res = Reflect.get(target, key)
      return res
    },
    set(target, key, value) {
      let success: boolean
      success = Reflect.set(target, key, value)
      return success
    }
  })
}
复制代码

这时候我们可以编写一个🛠测试用例,跑一跑测试有没有问题

describe('reactive', () => {
  it.skip('reactive test', () => {
    let original = { num: 1 } 
    let count = reactive(original)
    expect(original).not.toBe(count)   ✅
    expect(count.num).toEqual(1)       ✅
  })
})
复制代码

🤮什么?你不是说getter和setter要分别做两件事情吗?😒

  • getter进行依赖收集 👀
  • setter进行触发依赖 🔌

别急!还不是时候!

  1. effect

根据官方给出的介绍:effect会立即触发回调函数,同时响应式追踪其依赖

effect的基本用法:

let result = 0
// 假设count.num == 1
effect(() => {
 result = count.num + 1
})
// 那么输出的result就是2
console.log(result) // output: 2
复制代码

其中count是已经通过了reactive处理的proxy实例对象

根据上述的用法我们可以简单的写出一个effect函数

class ReactiveEffect {
  private _fn: Function
  constructor(fn: Function) {
    this._fn = fn
  }
  run() {
    this._fn()
  }
}
export function effect(fn: Function) {
  let _reactiveFunc = new ReactiveEffect(fn)
  _reactiveFunc.run()
}

复制代码

再写一个测试用例验证一下

describe('effect test', () => {
  it('effect', () => {
    // 创建proxy代理
    let count = reactive({ num: 11 })
    let result = 0
    // 立即执行effect并跟踪依赖
    effect(() => {
      result = count.num + 1
    })
    expect(result).toBe(12)   ✅

    count.num++
    expect(result).toBe(13)   ❌
  })
})

复制代码

欸!我们发现了测试最后一项没有通过,哦原来我们还没实现依赖收集和触发依赖啊。。。

  1. track做依赖收集

我想想,我们应该怎么进行依赖收集?对,上面我们提到过有一个唯一标识和一个容器。我们该去哪找这个依赖啊?欸是容器,那些依赖是我们需要被触发的呢?欸看唯一标识

唯一标识是什么?

假设数据target是一个对象{num: 11},对象的属性名可以绑定很多依赖,这个属性名num+target就可以找到与num相关的所有依赖集合,所以这里的num相关的所有依赖集合的唯一标识就是num+target

容器是什么?

存储不同数据下的所有属性对应的所有依赖集合,我们可以用Map存储不同数据,命名为targetMap,每个数据作为targetMap的键名,再定义一个以属性名key为键名的depsMap作为targetMap的键值,depsMap的键值是一个Set集合,命名作deps,最终deps就是存放特定的key的依赖集合

类型定义:

我相信你们看到targetMap的类型定义的时候应该会理解我上面说的存储结构是怎么样的吧

type EffectKey = string
type IDep = ReactiveEffect
const targetMap = new Map<Record<EffectKey, any>, Map<EffectKey, Set<IDep>>>()
let activeEffect: ReactiveEffect
复制代码

下面我们来写一下track函数的实现,要注意的是我们需要处理一下第一次没有存储Map的情况

export function track(target: Record<EffectKey, any>, key: EffectKey) {
  // 寻找dep依赖的执行顺序
  // target -> key -> dep
  let depsMap = targetMap.get(target)
  // 解决初始化没有depsMap的情况
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // deps是一个Set对象,存放着这个key相对应的所有依赖
  let deps = depsMap.get(key)
  // 如果没有key相对应的Set 初始化Set
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  // 将activeEffect实例对象add给deps
  deps.add(activeEffect)
}
复制代码

解释一下

deps.add(activeEffect)
复制代码

这里的activeEffect就是我们的依赖,怎么获取到的呢?

其实当effect执行的时候,内部 new 了一个ReactiveEffect类,而ReactiveEffect类里面可以通过this获取到activeEffect,因为activeEffect本来就是ReactiveEffect类的实例

我们改写一下ReactiveEffect代码

class ReactiveEffect {
  private _fn: Function
  constructor(fn: Function) {
    this._fn = fn
  }
  run() {
    // 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后,
    // this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机
    activeEffect = this
    this._fn()
  }
}
复制代码
  1. trigger做触发依赖

这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行

  1. 用target作为键名拿到在targetMap里面键值depsMap
  2. 用key作为键名拿到depsMap的键值deps
  3. 然后遍历deps这个Set实例对象,deps里面存的都是ReactiveEffect`实例对象dep,我们依次执行dep.run()就相当于执行了effect的回调函数了。
export function trigger(target: Record<EffectKey, any>, key: EffectKey) {
  const depsMap = targetMap.get(target)
  const deps = depsMap?.get(key)
  // 注意deps可能为undefined的情况
  if (deps) {
    for (let dep of deps) {
      dep.run()
    }
  }
}
复制代码
  1. 添加tracktrigger函数到proxy的getter和setter上
export function reactive(target: Record<string, any>) {
  return new Proxy(target, {
    get(target, key) {
      let res = Reflect.get(target, key)
      // 依赖收集
      track(target, key as string)
      return res
    },
    set(target, key, value) {
      let success: boolean
      success = Reflect.set(target, key, value)
      // 触发依赖
      trigger(target, key as string)
      return success
    }
  })
}
复制代码

最后再用jest运行一下响应式数据的测试用例

describe('effect test', () => {
  it('effect', () => {
    // 创建proxy代理
    let count = reactive({ num: 11 })
    let result = 0
    // 立即执行effect并跟踪依赖
    effect(() => {
      // count.num触发get 存储依赖
      result = count.num + 1
    })
    expect(result).toBe(12)   ✅
    // 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result
    count.num++
    expect(result).toBe(13)   ✅
  })
})
复制代码

总结

上述的测试用例count触发了一次setter操作,两次getter操作。

  1. 第一次getter操作是在effect的回调函数执行的时候发生,effect立即执行,在执行之前我们拿到了activeEffect,之后在proxy的getter中执行了track函数,以num为key的depsMap被第一次初始化,并初始化了targetMap,把activeEffect添加到deps这个Set对象中,这就完成了依赖收集。

  2. 当代码执行到count.num++的时候,我们先执行的是proxy的getter操作,执行 1 的流程,之后执行的是proxy的setter操作

    Reflect.set(target, key, value)
    复制代码

    这段代码把{num: 11} 加一变成了{num: 12},并且在targetMap中寻找{num: 12}为键名的键值,之后进一步获取到了depsMap和deps。通过循环把deps里面的所有activeEffect执行run()方法,这就完成了触发依赖。

最后@感谢阅读!

完整代码

// in effect.ts

class ReactiveEffect {
  private _fn: Function
  constructor(fn: Function) {
    this._fn = fn
  }
  run() {
    // 为什么要在这里把this赋值给activeEffect呢?因为这里是fn执行之前,就是track依赖收集执行之前,又是effect开始执行之后,
    // this能捕捉到这个依赖,将这个依赖赋值给activeEffect是刚刚好的时机
    activeEffect = this
    this._fn()
  }
}
const targetMap = new Map<Record<EffectKey, any>, Map<EffectKey, Set<IDep>>>()
// 当前正在执行的effect
let activeEffect: ReactiveEffect
type EffectKey = string
type IDep = ReactiveEffect
// 这个track的实现逻辑很简单:添加依赖
export function track(target: Record<EffectKey, any>, key: EffectKey) {
  // 寻找dep依赖的执行顺序
  // target -> key -> dep
  let depsMap = targetMap.get(target)
  /**
   * 这里有个疑问:target为{ num: 11 } 的时候我们能获取到depsMap,之后我们count.num++,为什么target为{ num: 12 } 的时候我们还能获取得到相同的depsMap呢?
   * 这里我的理解是 targetMap的key存的只是target的引用 存的字符串就不一样了
   */
  // 解决初始化没有depsMap的情况
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // deps是一个Set对象,存放着这个key相对应的所有依赖
  let deps = depsMap.get(key)
  // 如果没有key相对应的Set 初始化Set
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  // 将activeEffect实例对象add给deps
  deps.add(activeEffect)
}
// 这个trigger的实现逻辑很简单:找出target的key对应的所有依赖,并依次执行
export function trigger(target: Record<EffectKey, any>, key: EffectKey) {
  const depsMap = targetMap.get(target)
  const deps = depsMap?.get(key)
  if (deps) {
    for (let dep of deps) {
      dep.run()
    }
  }
}
// 根据官方给出的介绍:effect会立即触发这个函数,同时响应式追踪其依赖
export function effect(fn: Function, option = {}) {
  let _reactiveFunc = new ReactiveEffect(fn)
  _reactiveFunc.run()
}

复制代码
// in reactive.ts

import { track, trigger } from './effect'

export function reactive(target: Record<string, any>) {
  return new Proxy(target, {
    get(target, key) {
      let res = Reflect.get(target, key)
      // 依赖收集
      track(target, key as string)
      return res
    },
    set(target, key, value) {
      let success: boolean
      success = Reflect.set(target, key, value)
      // 触发依赖
      trigger(target, key as string)
      return success
    }
  })
}

复制代码
// in effect.spec.ts

import { effect } from '../index'
import { reactive } from '../index'
describe('effect test', () => {
  it('effect', () => {
    // 创建proxy代理
    let count = reactive({ num: 11 })
    let result = 0
    // 立即执行effect并跟踪依赖
    effect(() => {
      // count.num触发get 存储依赖
      result = count.num + 1
    })
    expect(result).toBe(12)
    // 这里会先触发proxy的get操作再触发proxy的set操作,触发依赖trigger 更新result
    count.num++
    expect(result).toBe(13)
  })
})

复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改