前言
从本章开始我们将开始实现 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 部分
- 我们直接到
vue
的源码路径/packages/reactivity/src/reactive.ts
中的第90
行找到reactive
方法,并打上断点。
- 发现
reactive
其实直接返回了一个createReactiveObject
,听名字就这个方法是在创建一个reactive
对象,接着跳转进这个方法。
- 可以看到
createReactiveObject
这个方法其实就是返回了一个Proxy
对象。 - 有两个点可以提的是:一个点是这里维护了一个
proxyMap
对象用来缓存之前已经创建过的响应式对象,而他是一个 WeakMap 类型; 另一个点是在源码第214
行 创建proxy
的baseHandler
,它来自上面reactive
方法中返回的mutableHandlers
,而mutableHandlers
导入自baseHandlers.ts
文件,这个我们后面说。
至此 reactive
方法执行完成。总结 reactive
的逻辑:1. 创建了 proxy
。 2.把 proxy
加到了 proxyMap
里面。3. 返回了 proxy
1.2 effect 部分
- 接着我们来到
effect
方法,我们直接到vue
的源码路径/packages/reactivity/src/effect.ts
中的第170
行找到effect
方法,并打上断点。
- 可以发现
effect
方法内其实就只是创建了一个ReactiveEffect
对象,并且执行了一次它的run
方法,再将run
方法返回。我们直接跳到run
看代码。
- 调试发现,
run
方法里只做了上图框框圈出来的两件事。但是大家不要忘记,fn
函数 中的代码为document.querySelector('#app').innerText = obj.name
,obj
是个proxy
,obj.name
会触发getter
,所以接下来我们就会进入到mutableHandlers
的get
中, 而get
为createGetter
函数的调用返回值,所以我们直接跳到createGetter
中 - 调试得知,
createGetter
方法中最主要做了两件事,一是调用const res = Reflect.get(target, key, receiver)
,res
此时是 张三, 然后将res
返回。二是触发了track
函数,这个函数是一个重点函数,track
在此为跟踪的意思。接下来我们看看里面发生了什么。
- 可以看到
track
里面主要做了两件事,一是为targetMap
赋值,targetMap
的结构是一个Map
套Set
的结构(createDep
方法实际是返回了一个Set
);二是执行了trackEffects
方法。我们来看一下这个方法里做了什么。
- 可以看到在
trackEffects
函数内部,核心也是做了两件事情:一是为dep(targetMap[target][key] 得到的 Set 实例)
添加了activeEffect
,这个activeEffect
第6
步有讲,就一个ReactiveEffect
对象,里面存了fn
函数;二是为activeEffect
函数的 静态属性deps
,增加了一个值dep
,即建立起了dep
和activeEffect
的联系.
至此,整个 track
的核心逻辑执行完成。我们可以把整个 track
的核心逻辑说成:收集了 activeEffect
(即:fn)
- 最后在
createGetter
函数中返回了res
(即:张三)
至此,整个 effect
执行完成。总结 effect 的逻辑:1.生成 ReactiveEffect
实例。2.触发 fn
方法,从而激活 getter
。3.建立了 targetMap
和 activeEffect
之间的联系
1.3 obj.name = xx 部分
- 接着我们继续调试程序,两秒钟之后,
setTimeout
触发,会执行obj.name = '李四'
,从而触发proxy
的set
。所以接下来我们就会进入到mutableHandlers
的set
中, 而set
为createSetter
函数的调用返回值,所以我们直接跳到createSetter
中
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
里面做了什么
trigger
主要做了上图中框起来的三件事,我们再来看看triggerEffect
做了什么?
- 可以看到
triggerEffect
其实就是调用了run
方法,这一次进入run
方法,执行了一下步骤:1. 首先还是为activeEffect = this
赋值。2.最后执行this.fn()
即:effect
时传入的匿名函数。3.至此,fn
执行,意味着:document.querySelector('#app').innerText = 李四
,页面将发生变化。 triggerEffect完成
triggerEffects完成
trigger完成
setter回调完成
至此,整个 setter
执行完成。总结 setter
:1.修改 obj 的值。2.触发 targetMap 下保存的 fn 函数
1.4 总结
到这里,我们在前言中的代码已经从源码层面上全部分析完了,我们现在总结一下:
reactive
函数effect
函数obj.name = xx
表达式
这三块代码背后,vue
究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:
- 创建
proxy
- 收集
effect
的依赖 - 触发收集的依赖
接下来,我们的实现,就将会围绕着这三个核心的理念进行。
2. 框架实现
2.1 构建 reactive 函数,获取 proxy 实例
- 创建
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
}
- 创建
packages/reactivity/src/baseHandlers.ts
模块:
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
- 此时我们就已经构建好了一个基本的
reactive
方法,接下来我们可以通过 测试案例 测试一下。 - 创建
packages/reactivity/src/index.ts
模块,作为reactivity
的入口模块
export { reactive } from './reactive'
- 在
packages/vue/src/index.ts
中,导入reactive
模块
export { reactive } from '@vue/reactivity'
- 执行
npm run build
进行打包,生成vue.js
- 创建
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>
- 运行到
Live Server
可见打印了一个proxy
对象实例
至此我们已经得到了一个基础的 reactive
函数
2.2 createGetter && createSetter
接下来我们需要创建对应的 get
和 set
监听:
/**
* 响应性的 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
在 getter
和 setter
中分别调用了 track && trigger
方法,所以我们需要分别创建对应方法:
- 创建
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: 触发依赖')
}
至此我们就可以:
- 在
getter
时,调用track
收集依赖 - 在
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
:
- 在
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()
}
- 接下来我们来实现
ReactiveEffect
的基础逻辑:
/**
* 单例的,当前的 effect
*/
export let activeEffect: ReactiveEffect | undefined
/**
* 响应性触发依赖时的执行类
*/
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
// 为 activeEffect 赋值
activeEffect = this
// 执行 fn 函数
return this.fn()
}
}
- 在
packages/reactivity/src/index.ts
导出
export { effect } from './effect'
- 在
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 时,修改视图,以此就可实现 响应性数据变化。
所以,下面我们就需要分别处理 getter
和 setter
对应的情况了。
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
,可得以下数据:
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
的所有依赖
- 创建
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
}
- 在
packages/reactivity/src/effect.ts
修改KeyToDepMap
的泛型:
import { Dep } from './dep'
type KeyToDepMap = Map<any, Dep>
- 修改
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
中保存的依赖。
- 在
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
中保存的依赖
测试
- 创建
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
的响应性函数而言,我们知道它:
- 是通过
proxy
的setter
和getter
来实现的数据监听 - 需要配合
effect
函数进行使用 - 基于
WeakMap
完成的依赖收集和处理 - 可以存在一对多的依赖关系
但同时 reactive
函数也存在一些不足,比如:
reactive
只能对 复杂数据 类型进行使用reactive
的响应性数据,不可以进行解构
因为 reactive
的不足,所以 vue 3
又为我们提供了 ref
函数构建响应性。
关于 ref
函数是如何实现的,就留到下一章去学习吧~