Vue3 中 reactivity 模块 reactive 介绍和实现

在 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) {
      // ...
    }
  })
}

 

  • 38
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3 的响应式系统是其最重要的特性之一,它使得我们可以更加方便地处理数据的变化,同时也是 Vue3 许多其他特性的基础。下面我将详细介绍 Vue3 响应式系统的原理和实现方式。 ## 响应式系统的原理 Vue3 的响应式系统基于 ES6 的 Proxy 实现,具体而言,当我们创建一个响应式对象时,会使用 Proxy 对这个对象进行包装,并拦截这个对象上的所有属性的 get 和 set 操作,当属性被获取或者修改时,会触发相应的操作。同时,Vue3 还会维护一个依赖收集的系统,用于收集属性的依赖关系,当属性被修改时,会自动触发依赖关系的更新操作。 举个例子,我们可以通过以下方式创建一个响应式对象: ```javascript import { reactive } from 'vue' const state = reactive({ count: 0 }) ``` 这个对象被 reactive 包装后,我们可以像访问普通对象一样访问它的属性: ```javascript console.log(state.count) // 0 ``` 但是,当我们修改这个对象的属性时,Vue3 会自动检测到这个修改,并触发相应的更新操作: ```javascript state.count++ ``` ## 响应式系统的实现方式 在 Vue3 ,响应式系统的核心代码位于 `@vue/reactivity` 模块,包括以下几个部分: 1. `reactive` 函数:用于将一个普通对象转换为响应式对象。 2. `effect` 函数:用于创建一个响应式的副作用函数,会自动收集依赖关系并在依赖发生变化时重新执行。 3. `ref` 函数:用于创建一个可变的响应式对象。 4. `computed` 函数:用于创建一个计算属性,会自动收集依赖关系并在依赖发生变化时重新计算。 其,`reactive` 函数是最核心的部分,它的实现方式如下: ```javascript export function reactive(target) { if (target && typeof target === 'object') { if (target instanceof Array) { target.forEach((item, index) => { target[index] = reactive(item) }) } else { Object.keys(target).forEach(key => { target[key] = reactive(target[key]) }) } return new Proxy(target, { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) trigger(target, key) return result }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key) trigger(target, key) return result } }) } return target } ``` 这段代码会递归地将一个对象的所有属性都转换为响应式对象,并使用 Proxy 对这个对象进行包装。在 `get` 和 `set` 操作,我们会调用 `track` 和 `trigger` 函数来进行依赖收集和触发更新。 具体而言,`track` 函数用于收集依赖关系,它会将当前正在执行的副作用函数和当前属性的键值对存储到一个全局变量。而 `trigger` 函数则用于触发更新操作,它会遍历存储的依赖关系,并依次执行相应的副作用函数。 除了 `reactive` 函数之外,`effect` 函数、`ref` 函数和 `computed` 函数的实现方式也都基于这个原理,它们都会使用 `track` 和 `trigger` 函数进行依赖收集和触发更新。 综上所述,Vue3 的响应式系统基于 ES6 的 Proxy 实现,并通过依赖收集的方式自动触发更新操作。这个系统的实现方式相对简单,但非常高效且易于扩展,是 Vue3 最重要的特性之一。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值