2022-12-28
Vue
00

目录

前言
1. 源码阅读
1.1 reactive 部分
1.2 effect 部分
1.3 obj.name = xx 部分
1.4 总结
2. 框架实现
2.1 构建 reactive 函数,获取 proxy 实例
2.2 createGetter && createSetter
2.3 构建 effect 函数,生成 ReactiveEffect 实例
2.4 构建 track 依赖收集
2.5 构建 trigger 触发依赖
2.6 构建 Dep 模块,处理一对多的依赖关系
3. 总结

前言

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

接下来我们看一段代码:

html
<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 9. 可以看到 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 13. 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 14. 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 模块:
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 模块:
js
/** * 响应性的 handler */ export const mutableHandlers: ProxyHandler<object> = {}
  1. 此时我们就已经构建好了一个基本的 reactive 方法,接下来我们可以通过 测试案例 测试一下。

  2. 创建 packages/reactivity/src/index.ts 模块,作为 reactivity 的入口模块

ts
export { reactive } from './reactive'
  1. packages/vue/src/index.ts 中,导入 reactive 模块
ts
export { reactive } from '@vue/reactivity'
  1. 执行 npm run build 进行打包,生成 vue.js

  2. 创建 packages/vue/examples/reactivity/reactive.html 文件,作为测试实例:

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 监听:

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

getter

ts
/** * 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

ts
/** * 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
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 中:

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 函数:
ts
/** * effect 函数 * @param fn 执行方法 * @returns 以 ReactiveEffect 实例为 this 的执行函数 */ export function effect<T = any>(fn: () => T) { // 生成 ReactiveEffect 实例 const _effect = new ReactiveEffect(fn) // 执行 run 函数 _effect.run() }
  1. 接下来我们来实现 ReactiveEffect 的基础逻辑:
ts
/** * 单例的,当前的 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 导出
ts
export { effect } from './effect'
  1. packages/vue/src/index.ts 中 导出
ts
export { reactive, effect } from '@vue/reactivity'

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

html
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 写入如下代码:

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

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 中:

ts
<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 模块:
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 的泛型:
ts
import { Dep } from './dep' type KeyToDepMap = Map<any, Dep>
  1. 修改 track 方法,处理 Dep 类型数据:
ts
/** * 用于收集依赖的方法 * @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 中:
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
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 函数是如何实现的,就留到下一章去学习吧~

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!