vue3 源码学习,实现一个 mini-vue(二):初见 reactivity 模块

简介: vue3 源码学习,实现一个 mini-vue(二):初见 reactivity 模块

前言

从本章开始我们将开始实现 Vue3 中的 reactivity 模块

接下来我们看一段代码:

<body>
  <div id="app"></div>
</body>
<script>
  // 从 Vue 中结构出 reactie、effect 方法
  const { reactive, effect } = Vue

  // 声明响应式数据 obj
  const obj = reactive({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.name
  })

  // 定时修改数据,视图发生变化
  setTimeout(() => {
    obj.name = '李四'
  }, 2000)
</script>

上面的代码很简单大家应该也都会写,最终的效果就是视图中的 张三2 秒钟之后变成了 李四。但是大家有没有想过这是为什么呢?让我们来从源码中一探究竟吧~

1. 源码阅读

重要提示:我这里的 vue 的版本是 3.2.27,并且本系列中用的都是这个版本

1.1 reactive 部分

  1. 我们直接到 vue 的源码路径 /packages/reactivity/src/reactive.ts 中的第 90 行找到 reactive 方法,并打上断点。

image.png

  1. 发现 reactive 其实直接返回了一个 createReactiveObject ,听名字就这个方法是在创建一个 reactive 对象,接着跳转进这个方法。

image.png

  1. 可以看到 createReactiveObject 这个方法其实就是返回了一个 Proxy 对象
  2. 有两个点可以提的是:一个点是这里维护了一个 proxyMap 对象用来缓存之前已经创建过的响应式对象,而他是一个 WeakMap 类型; 另一个点是在源码第 214 行 创建 proxybaseHandler,它来自上面 reactive 方法中返回的 mutableHandlers,而 mutableHandlers 导入自 baseHandlers.ts 文件,这个我们后面说。

image.png

至此 reactive 方法执行完成。总结 reactive 的逻辑:1. 创建了 proxy。 2.把 proxy 加到了 proxyMap 里面。3. 返回了 proxy

1.2 effect 部分

  1. 接着我们来到 effect 方法,我们直接到 vue 的源码路径 /packages/reactivity/src/effect.ts 中的第 170 行找到 effect 方法,并打上断点。

image.png

  1. 可以发现 effect 方法内其实就只是创建了一个 ReactiveEffect 对象,并且执行了一次它的 run 方法,再将 run 方法返回。我们直接跳到 run 看代码。

image.png

  1. 调试发现,run 方法里只做了上图框框圈出来的两件事。但是大家不要忘记,fn 函数 中的代码为 document.querySelector('#app').innerText = obj.nameobj 是个 proxyobj.name 会触发 getter,所以接下来我们就会进入到 mutableHandlersget 中, 而 getcreateGetter 函数的调用返回值,所以我们直接跳到 createGetter
  2. 调试得知,createGetter 方法中最主要做了两件事,一是调用 const res = Reflect.get(target, key, receiver)res 此时是 张三, 然后将 res 返回。二是触发了 track 函数,这个函数是一个重点函数, track 在此为跟踪的意思。接下来我们看看里面发生了什么。

image.png

  1. 可以看到 track 里面主要做了两件事,一是为 targetMap 赋值,targetMap 的结构是一个 MapSet 的结构(createDep 方法实际是返回了一个 Set);二是执行了 trackEffects 方法。我们来看一下这个方法里做了什么。

image.png

  1. 可以看到在 trackEffects 函数内部,核心也是做了两件事情:一是为 dep(targetMap[target][key] 得到的 Set 实例) 添加了 activeEffect,这个 activeEffect6 步有讲,就一个 ReactiveEffect 对象,里面存了 fn 函数;二是为 activeEffect 函数的 静态属性 deps,增加了一个值 dep,即建立起了 depactiveEffect的联系.

至此,整个 track 的核心逻辑执行完成。我们可以把整个 track 的核心逻辑说成:收集了 activeEffect(即:fn)

  1. 最后在 createGetter 函数中返回了 res(即:张三)

至此,整个 effect 执行完成。总结 effect 的逻辑:1.生成 ReactiveEffect 实例。2.触发 fn 方法,从而激活 getter。3.建立了 targetMapactiveEffect 之间的联系

1.3 obj.name = xx 部分

  1. 接着我们继续调试程序,两秒钟之后,setTimeout 触发,会执行 obj.name = '李四',从而触发 proxyset。所以接下来我们就会进入到 mutableHandlersset 中, 而 setcreateSetter 函数的调用返回值,所以我们直接跳到 createSetter

image.png

  1. createSetter 中主要做是有:1.创建变量: oldValue = 张三。2.创建变量:value = 李四。3.执行 const result = Reflect.set(target, key, value, receiver),即:修改了 obj 的值为 “李四”。4.触发:trigger(target, TriggerOpTypes.SET, key, value, oldValue)trigger 在这里为 触发 的意思,我们来看看 trigger 里面做了什么

image.png

  1. trigger 主要做了上图中框起来的三件事,我们再来看看 triggerEffect 做了什么?

image.png

  1. 可以看到 triggerEffect 其实就是调用了 run 方法,这一次进入 run 方法,执行了一下步骤:1. 首先还是为 activeEffect = this 赋值。2.最后执行 this.fn() 即:effect 时传入的匿名函数。3.至此,fn 执行,意味着: document.querySelector('#app').innerText = 李四,页面将发生变化。
  2. triggerEffect完成 triggerEffects完成 trigger完成 setter回调完成

至此,整个 setter 执行完成。总结 setter:1.修改 obj 的值。2.触发 targetMap 下保存的 fn 函数

1.4 总结

到这里,我们在前言中的代码已经从源码层面上全部分析完了,我们现在总结一下:

  1. reactive 函数
  2. effect 函数
  3. obj.name = xx 表达式

这三块代码背后,vue 究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:

  1. 创建 proxy
  2. 收集 effect 的依赖
  3. 触发收集的依赖

接下来,我们的实现,就将会围绕着这三个核心的理念进行。

2. 框架实现

2.1 构建 reactive 函数,获取 proxy 实例

  1. 创建 packages/reactivity/src/reactive.ts 模块:
import { mutableHandlers } from './baseHandlers'
   
/**
* 响应性 Map 缓存对象
* key:target
* val:proxy
*/
export const reactiveMap = new WeakMap<object, any>()

/**
* 为复杂数据类型,创建响应性对象
* @param target 被代理对象
* @returns 代理对象
*/
export function reactive(target: object) {
    return createReactiveObject(target, mutableHandlers, reactiveMap)
}

/**
* 创建响应性对象
* @param target 被代理对象
* @param baseHandlers handler
*/
function createReactiveObject(
    target: object,
    baseHandlers: ProxyHandler<any>,
    proxyMap: WeakMap<object, any>
) {
    // 如果该实例已经被代理,则直接读取即可
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 未被代理则生成 proxy 实例
    const proxy = new Proxy(target, baseHandlers)

    // 缓存代理对象
    proxyMap.set(target, proxy)
    return proxy
}
  1. 创建 packages/reactivity/src/baseHandlers.ts 模块:
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
  1. 此时我们就已经构建好了一个基本的 reactive 方法,接下来我们可以通过 测试案例 测试一下。
  2. 创建 packages/reactivity/src/index.ts 模块,作为 reactivity 的入口模块
export { reactive } from './reactive'
  1. packages/vue/src/index.ts 中,导入 reactive 模块
export { reactive } from '@vue/reactivity'
  1. 执行 npm run build 进行打包,生成 vue.js
  2. 创建 packages/vue/examples/reactivity/reactive.html 文件,作为测试实例:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="../../dist/vue.js"></script>
  </head>
  <script>
    const { reactive } = Vue

    const obj = reactive({
      name: '张三'
    })
    console.log(obj)
  </script>
</html>
  1. 运行到 Live Server 可见打印了一个 proxy 对象实例

至此我们已经得到了一个基础的 reactive 函数

2.2 createGetter && createSetter

接下来我们需要创建对应的 getset 监听:

/**
 * 响应性的 handler
 */
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set
}

getter

/**
 * getter 回调方法
 */
const get = createGetter()

/**
 * 创建 getter 回调方法
 */
function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 利用 Reflect 得到返回值
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target, key)
    return res
  }
}

setter

/**
 * setter 回调方法
 */
const set = createSetter()

/**
 * 创建 setter 回调方法
 */
function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    // 利用 Reflect.set 设置新值
    const result = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key, value)
    return result
  }
}

track && trigger

gettersetter 中分别调用了 track && trigger 方法,所以我们需要分别创建对应方法:

  1. 创建 packages/reactivity/src/effect.ts
/**
 * 用于收集依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  console.log('track: 收集依赖')
}

/**
 * 触发依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 * @param newValue 指定 key 的最新值
 * @param oldValue 指定 key 的旧值
 */
export function trigger(target: object, key?: unknown, newValue?: unknown) {
  console.log('trigger: 触发依赖')
}

至此我们就可以:

  1. getter 时,调用 track 收集依赖
  2. setter 时,调用 trigger 触发依赖

我们可以在两个方法中分别进行一下打印,看看是否可以成功回调。

测试

packages/vue/examples/reactivity/reactive.html 中:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="../../dist/vue.js"></script>
  </head>
<script>
  const { reactive } = Vue

  const obj = reactive({
    name: '张三'
  })
  console.log(obj.name) // 此时应该触发 track
  obj.name = '李四' // 此时应该触发 trigger
</script>
</html>

2.3 构建 effect 函数,生成 ReactiveEffect 实例

根据之前的测试实例我们知道,在创建好了 reactive 实例之后,接下来我们需要触发 effect

  1. packages/reactivity/src/effect.ts 中,创建 effect 函数:
/**
 * effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
export function effect<T = any>(fn: () => T) {
  // 生成 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn)
  // 执行 run 函数
  _effect.run()
}
  1. 接下来我们来实现 ReactiveEffect 的基础逻辑:
/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * 响应性触发依赖时的执行类
 */
export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}

  run() {
    // 为 activeEffect 赋值
    activeEffect = this

    // 执行 fn 函数
    return this.fn()
  }
}
  1. packages/reactivity/src/index.ts 导出
export { effect } from './effect'
  1. packages/vue/src/index.ts 中 导出
export { reactive, effect } from '@vue/reactivity'

根据以上代码可知,最终 vue 会执行 effect 传入的 回调函数,即:

document.querySelector('#app').innerText = obj.name

那么此时,obj.name 的值,应该可以被渲染到 html 中。

所以,我们可以到测试实例中,完成一下测试

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>

  <script>
    const { reactive, effect } = Vue

    const obj = reactive({
      name: '张三'
    })

    // 调用 effect 方法
    effect(() => {
      document.querySelector('#app').innerText = obj.name
    })
  </script>
</html>

此时,我们成功 渲染了数据到 html,那么接下来我们需要做的就是:obj.name 触发 setter 时,修改视图,以此就可实现 响应性数据变化

所以,下面我们就需要分别处理 gettersetter 对应的情况了。

2.4 构建 track 依赖收集

packages/reactivity/src/effect.ts 写入如下代码:

type KeyToDepMap = Map<any, ReactiveEffect>
/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. `key`:响应性对象
 * 2. `value`:`Map` 对象
 *         1. `key`:响应性对象的指定属性
 *         2. `value`:指定对象的指定属性的 执行函数
 */
const targetMap = new WeakMap<any, KeyToDepMap>()
/**
 * 用于收集依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  // 如果当前不存在执行函数,则直接 return
  if (!activeEffect) return
  // 尝试从 targetMap 中,根据 target 获取 map
  let depsMap = targetMap.get(target)
  // 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的 value
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  //为指定 map,指定key 设置回调函数
  depsMap.set(key, activeEffect)
  // 临时打印
  console.log(targetMap)
}

此时运行测试函数,查看打印的 targetMap,可得以下数据:

image.png

2.5 构建 trigger 触发依赖

packages/reactivity/src/effect.ts

/**
 * 触发依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function trigger(target: object, key?: unknown) {
  // 依据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  // 如果 map 不存在,则直接 return
  if (!depsMap) {
    return
  }
  // 依据 key,从 depsMap 中取出 value,该 value 是一个 ReactiveEffect 类型的数据
  const effect = depsMap.get(key) as ReactiveEffect
  // 如果 effect 不存在,则直接 return
  if (!effect) {
    return
  }
  // 执行 effect 中保存的 fn 函数
  effect.fn()
}

此时,我们就可以在触发 setter 时,执行保存的 fn 函数了。

那么接下来我们实现对应的测试实例,在 packages/vue/examples/reactivity/reactive.html 中:

<script>
  const { reactive, effect } = Vue

  const obj = reactive({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.name
  })

  setTimeout(() => {
    obj.name = '李四'
  }, 2000)
</script>

运行测试实例,等待两秒,发现 视图发生变化

那么,至此我们就已经完成了一个简单的 响应式依赖数据处理

2.6 构建 Dep 模块,处理一对多的依赖关系

在我们之前的实现中,还存在一个小的问题,那就是:每个响应性数据属性只能对应一个 effect 回调

现象:如果我们新增了一个 effect 函数,即:name 属性对应两个 DOM 的变化。更新渲染就会变无效。

原因:因为我们在构建 KeyToDepMap 对象时,它的 Value 只能是一个 ReactiveEffect,所以这就导致了 一个 key 只能对应一个有效的 effect 函数。

解决方法:将 value 变为一个 Set 类型。可以把它叫做 Dep ,通过 Dep 来保存 指定 key 的所有依赖

  1. 创建 packages/reactivity/src/dep.ts 模块:
import { ReactiveEffect } from './effect'

export type Dep = Set<ReactiveEffect>

/**
 * 依据 effects 生成 dep 实例
 */
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  return dep
}
  1. packages/reactivity/src/effect.ts 修改 KeyToDepMap 的泛型:
import { Dep } from './dep'
type KeyToDepMap = Map<any, Dep>
  1. 修改 track 方法,处理 Dep 类型数据:
/**
 * 用于收集依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  // 如果当前不存在执行函数,则直接 return
  if (!activeEffect) return
  // 尝试从 targetMap 中,根据 target 获取 map
  let depsMap = targetMap.get(target)
  // 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的 value
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取指定 key 的 dep
  let dep = depsMap.get(key)
  // 如果 dep 不存在,则生成一个新的 dep,并放入到 depsMap 中
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  trackEffects(dep)
}

/**
 * 利用 dep 依次跟踪指定 key 的所有 effect
 * @param dep
 */
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}

此时,我们已经把指定 key 的所有依赖全部保存到了 dep 函数中,那么接下来我们就可以在 trigger 函数中,依次读取 dep 中保存的依赖。

  1. packages/reactivity/src/effect.ts 中:
export function trigger(target: object, key?: unknown) {
  // 依据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  // 如果 map 不存在,则直接 return
  if (!depsMap) {
    return
  }
  // 依据指定的 key,获取 dep 实例
  let dep: Dep | undefined = depsMap.get(key)
  // dep 不存在则直接 return
  if (!dep) {
    return
  }
  // 触发 dep
  triggerEffects(dep)
}

/**
 * 依次触发 dep 中保存的依赖
 */
export function triggerEffects(dep: Dep) {
  // 把 dep 构建为一个数组
  const effects = Array.isArray(dep) ? dep : [...dep]
  // 依次触发
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

/**
 * 触发指定的依赖
 */
export function triggerEffect(effect: ReactiveEffect) {
  effect.run()
}

至此,我们即可在 trigger 中依次触发 dep 中保存的依赖

测试

  1. 创建 packages/vue/examples/reactivity/reactive-dep.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>

  <script>
    const { reactive, effect } = Vue

    const obj = reactive({
      name: '张三'
    })

    // 调用 effect 方法
    effect(() => {
      document.querySelector('#p1').innerText = obj.name
    })
    effect(() => {
      document.querySelector('#p2').innerText = obj.name
    })

    setTimeout(() => {
      obj.name = '李四'
    }, 2000)
  </script>
</html>

发现两个 p 标签中的内容最后都变成了 李四

3. 总结

在本章,我们初次了解了 reactivity 这个模块,并且在该模块中构建了 reactive 响应性函数。

对于 reactive 的响应性函数而言,我们知道它:

  1. 是通过 proxysettergetter 来实现的数据监听
  2. 需要配合 effect 函数进行使用
  3. 基于 WeakMap 完成的依赖收集和处理
  4. 可以存在一对多的依赖关系

但同时 reactive 函数也存在一些不足,比如:

  1. reactive 只能对 复杂数据 类型进行使用
  2. reactive 的响应性数据,不可以进行解构

因为 reactive 的不足,所以 vue 3 又为我们提供了 ref 函数构建响应性。

关于 ref 函数是如何实现的,就留到下一章去学习吧~

相关文章
|
6天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
3天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
16 7
|
5天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
17 3
|
3天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
19 1
|
3天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
20 1
|
5天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
6天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
6天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
6天前
|
缓存 JavaScript UED
Vue 中异步加载模块的方式
【10月更文挑战第23天】这些异步加载模块的方式各有特点和适用场景,可以根据项目的需求和架构选择合适的方法来实现模块的异步加载,以提高应用的性能和用户体验
|
6天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。