Vue3 中 reactivity 模块 reactive 介绍和实现

文章介绍了如何在Vue3中利用Proxy对象实现对象属性的访问和设置,以及如何通过getter和setter收集依赖并触发响应式更新。重点讲解了ReactiveEffect和effect的使用,以及如何处理嵌套effect和分支依赖问题。

在 Vue3 中, 通过 Proxy 对象来实现对一个对象属性 的访问和设置,从而达到依赖收集和触发的功能

示例

这是一个 Proxy 的使用示例:

const target = { a: 1 }

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    // target: 源对象
    // key: 访问的属性名称
    // receiver:代理对象,即 proxy

    if (key in target) {
      return target[key]
    }

    return -1
  },
  set(target, key, value, receiver) {
    // target: 源对象
    // key: 访问的属性名称
    // value 设置的属性值
    // receiver:代理对象,即 proxy

    target[key] = value
  }
})

proxy.a // 1
proxy.b // -1
proxy.c // -1

proxy.b = 2

proxy.b // 2
proxy.c // -1

可以看到,使用 Proxy 对象生成的代理对象可以检测到对源对象的任意属性值访问,即使是不存在的属性,这与 Vue2 中使用的 Object.defineProperty  是不同的,因此 Vue2 中在初始化数据时,需要对对象进行遍历来重新定义属性,Vue3 中不需要,这是一个性能提升点

了解了这个对象的使用方法后,可以很快的写出 Vue3 中的核心 API:effect  和 reactive  :

初步实现一个响应式对象

// 存储当前正在执行的的 ReactiveEffect
let activeEffect: ReactiveEffect | undefined = undefined

// 使用 “对象 => 对象的 key => Set<ReactiveEffect>” 结构来存储对象的key对应的多个 ReactiveEffect
type KeyToDepMap = Map<any, Set<ReactiveEffect>>
const targetMap = new WeakMap<any, KeyToDepMap>()

class ReactiveEffect {
  constructor(public fn: () => any) {}

  run() {
    try {
      activeEffect = this
      return this.fn()
    } finally {
      activeEffect = undefined
    }
  }
}

function reactive<T extends object>(object: T) {
  return new Proxy(object, {
    get(target, key, receiver) {
      // 在这里进行依赖的收集
      if (activeEffect) {
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          depsMap = new Map()
          targetMap.set(target, depsMap)
        }
        let dep = depsMap.get(key)
        if (!dep) {
          dep = new Set()
          depsMap.set(key, dep)
        }
        dep.add(activeEffect)
      }

      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // 记得先设置值,再触发副作用,这样副作用中才能访问到最细的值
      const res = Reflect.set(target, key, value, receiver)

      // 在这里就行触发收集到的副作用
      const depsMap = targetMap.get(target)

      if (depsMap) {
        const dep = depsMap.get(key)

        if (dep) {
          dep.forEach(effect => effect.run())
        }
      }

      return res
    }
  })
}

// effect 是一个在文档没有介绍的 API
function effect(fn: () => any) {
  const _effect = new ReactiveEffect(fn)

  // effect 会立即运行一次,在这个时候来收集依赖
  _effect.run()
}

// 使用示例
const state = reactive({ a: 1 })

effect(() => {
  console.log(state.a)
})

state.a = 2

运行这一段代码,会有两次输出,分别是 1 和 2,第一次输出原因为 effect 立即执行的一次回调,也是在这个时候进行了依赖收集的工作,第二次输出是在给属性 a 重新设置值时,此时触发了代理对象的 setter,从而找出收集到的 ReactiveEffects 执行

在 getter 和 setter 中,使用了 Reflect 对象,Reflect 和 Proxy 一样也是 JavaScript 提供的原生对象,Reflect 功能是可以在取值或设置值时候,修改属性访问器等中的 this 指向,看下面一个案例:

const target = {
  name: 'zhangsan',

  get alias() {
    return this.name
  }
}

const proxy = new Proxy(target, {
  get(target, key, receiver) {

    target[key] // this 指向 target,即源对象

    Reflect.get(target, key, receiver) // this 指向 receiver ,即 proxy
  }
})

// 通过代理对象访问 alias 属性
proxy.alias

因此使用 Reflect 来读取对象属性、设置对象属性值可以正确的追踪到每一个依赖,因为通过 target[key] 是不经过代理对象的,不经过代理对象就不会触发 getter,就不会被收集

嵌套的 effect

在实际的开发中,effect 是可以嵌套使用的,在 Vue3 中,有以下写法:

const state = reactive({ a: 1, b: 2, c: 3 })

effect(() => {
  console.log(state.a)

  effect(() => {
    console.log(state.b)
  })

  console.log(state.c)
})

外层 effect 会收集到属性的 a、c 作为依赖, 内层的 effect 会收集到属性 b 作为依赖,在属性 a/c 值变化时,外层 effect 会重新执行,当属性 b 值变化时,内层的 effect 重新执行

回想上面我们拿到当前正在执行的 ReactiveEffect 的地方(可以看下面代码),按照嵌套 effect 来分析我们的代码:

  1. 运行外层 effect 时,activeEffect = effect外,effect外 收集到属性 a
  2. 在执行过程中,遇到了内层 effect,即会将 activeEffect 设置为 effect内,effect内 收集到属性 b
  3. effect内 执行完毕时,将 activeEffect 设置为了 undefined
  4. 回到外层执行console.log(state.c)  时,由于 activeEffect 为 undefined,即在我们的代码中,effect外 收集不到属性 c 作为依赖
class ReactiveEffect {
  constructor(public fn: () => any) {}

  run() {
    try {
      activeEffect = this
      return this.fn()
    } finally {
      activeEffect = undefined
    }
  }
}

解决办法也肯简单,很容易想到我们可以模拟栈结构来存储正在执行的 effect,当一个内部 effect 执行完成后,弹栈再取外层的 effect,如下面这个实现(部分代码):

let activeEffects: ReactiveEffect[] = []

class ReactiveEffect {
  constructor(public fn: () => any) {}

  run() {
    try {
      activeEffects.push(this)
      return this.fn()
    } finally {
      activeEffects.pop()
    }
  }
}

// 在 getter 中

const proxy = new Proxy({}, {
  get(target, key, receiver) {
    if (activeEffects.length > 0) {
      // ...
      dep.add(activeEffects[activeEffects.length - 1])
    }
  }
})

在 Vue3 中,通过在 ReactiveEffect 对象中记录 parent 属性来解决,以下是 Vue3 的实现方式:

let activeEffect: ReactiveEffect | undefined = undefined

class ReactiveEffect {
  parent: ReactiveEffect | undefined

  constructor(public fn: () => any) {}

  run() {
    try {
      this.parent = activeEffect
      activeEffect = this
      return this.fn()
    } finally {
      activeEffect = this.parent
      this.parent = undefined
    }
  }
}

// 在 getter 中使用方式同最开始的判断,不变

分支依赖处理

有以下代码:

const state = reactive({ flag: true, a: 1, b: 2 })

effect(() => {
  console.log(state.flag ? state.a : state.b)
})

state.flag = false

state.a = 10

在上述实现的代码中:

  1. effect 首次运行回调时,收集到了属性 flag 和 属性 a 作为依赖
  2. 在更新了 flag 值之后,effect 重新执行,又收集到了属性 b 作为依赖,此时 effect 收集到了 flag、a、b 三个依赖
  3. 更新属性 a 的值,会重新执行回调,但事实上是不需要重新执行的,因为这个时候 effect 因 flag 的值是 false,根本不会使用到属性 a 的值

根据前面逻辑,在触发 setter 时,会找到这个属性名对应 Set<ReactiveEffect>,依次去执行里边的每个 ReactiveEffect,为了解决上述问题,我们需要在执行 ReactiveEffect 的回调之前(每个),清除掉当前执行的 ReactiveEffect 与触发属性的 key 的对应关系

在 ReactiveEffect 上新增一个属性 deps,用于收集所有包含自己的属性对应的依赖集合,即所有包含自己的 Set<ReactiveEffect>

+ function cleanupEffect(effect: ReactiveEffect) {
+   const { deps } = effect
+   deps.forEach(dep => {
+     dep.delete(effect)
+   })
+   deps.length = 0
+ }

class ReactiveEffect {
  parent: ReactiveEffect | undefined
+ deps: Set<ReactiveEffect>[] = []

  constructor(public fn: () => any) {}

  run() {
    try {
      this.parent = activeEffect
      activeEffect = this
      cleanupEffect(this)
      return this.fn()
    } finally {
      activeEffect = this.parent
      this.parent = undefined
    }
  }
}

function reactive<T extends object>(object: T) {
  return new Proxy(object, {
    get(target, key, receiver) {
      if (activeEffect) {
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          depsMap = new Map()
          targetMap.set(target, depsMap)
        }
        let dep = depsMap.get(key)
        if (!dep) {
          dep = new Set()
          depsMap.set(key, dep)
        }
        dep.add(activeEffect)
+       activeEffect.deps.push(dep)
      }

      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // ...
    }
  })
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值