从本章开始我们将开始实现 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
秒钟之后变成了 李四
。但是大家有没有想过这是为什么呢?让我们来从源码中一探究竟吧~
重要提示:我这里的 vue
的版本是 3.2.27
,并且本系列中用的都是这个版本
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
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
在此为跟踪的意思。接下来我们看看里面发生了什么。
9. 可以看到
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
之间的联系
setTimeout
触发,会执行 obj.name = '李四'
,从而触发 proxy
的 set
。所以接下来我们就会进入到 mutableHandlers
的 set
中, 而 set
为 createSetter
函数的调用返回值,所以我们直接跳到 createSetter
中
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
里面做了什么
14.
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 函数
到这里,我们在前言中的代码已经从源码层面上全部分析完了,我们现在总结一下:
reactive
函数effect
函数obj.name = xx
表达式这三块代码背后,vue
究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:
proxy
effect
的依赖接下来,我们的实现,就将会围绕着这三个核心的理念进行。
packages/reactivity/src/reactive.ts
模块:tsimport { 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
模块:js/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
此时我们就已经构建好了一个基本的 reactive
方法,接下来我们可以通过 测试案例 测试一下。
创建 packages/reactivity/src/index.ts
模块,作为 reactivity
的入口模块
tsexport { reactive } from './reactive'
packages/vue/src/index.ts
中,导入 reactive
模块tsexport { reactive } from '@vue/reactivity'
执行 npm run build
进行打包,生成 vue.js
创建 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>
Live Server
可见打印了一个 proxy
对象实例至此我们已经得到了一个基础的 reactive
函数
接下来我们需要创建对应的 get
和 set
监听:
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
在 getter
和 setter
中分别调用了 track && trigger
方法,所以我们需要分别创建对应方法:
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: 触发依赖')
}
至此我们就可以:
getter
时,调用 track
收集依赖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>
根据之前的测试实例我们知道,在创建好了 reactive
实例之后,接下来我们需要触发 effect
:
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()
}
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()
}
}
packages/reactivity/src/index.ts
导出tsexport { effect } from './effect'
packages/vue/src/index.ts
中 导出tsexport { reactive, effect } from '@vue/reactivity'
根据以上代码可知,最终 vue 会执行 effect 传入的 回调函数,即:
htmldocument.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
对应的情况了。
在 packages/reactivity/src/effect.ts
写入如下代码:
tstype 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
,可得以下数据:
在 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>
运行测试实例,等待两秒,发现 视图发生变化
那么,至此我们就已经完成了一个简单的 响应式依赖数据处理
在我们之前的实现中,还存在一个小的问题,那就是:每个响应性数据属性只能对应一个 effect 回调
现象:如果我们新增了一个 effect
函数,即:name
属性对应两个 DOM
的变化。更新渲染就会变无效。
原因:因为我们在构建 KeyToDepMap
对象时,它的 Value
只能是一个 ReactiveEffect
,所以这就导致了 一个 key
只能对应一个有效的 effect
函数。
解决方法:将 value
变为一个 Set
类型。可以把它叫做 Dep
,通过 Dep
来保存 指定 key
的所有依赖
packages/reactivity/src/dep.ts
模块:tsimport { 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
的泛型:tsimport { Dep } from './dep'
type KeyToDepMap = Map<any, Dep>
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
中保存的依赖。
packages/reactivity/src/effect.ts
中:tsexport 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
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
标签中的内容最后都变成了 李四
在本章,我们初次了解了 reactivity
这个模块,并且在该模块中构建了 reactive
响应性函数。
对于 reactive
的响应性函数而言,我们知道它:
proxy
的 setter
和 getter
来实现的数据监听effect
函数进行使用WeakMap
完成的依赖收集和处理但同时 reactive
函数也存在一些不足,比如:
reactive
只能对 复杂数据 类型进行使用reactive
的响应性数据,不可以进行解构因为 reactive
的不足,所以 vue 3
又为我们提供了 ref
函数构建响应性。
关于 ref
函数是如何实现的,就留到下一章去学习吧~
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!