第四章:响应系统 - 初见 reactivity 模块
01:前言
从本章开始我们将开始实现 Vue3
中的 reactivity
模块,该模块中,将会利用我们在上一章中学到的知识实现响应性数据,如:
reactive
ref
computed
watch
等等
在这些代码的实现中,我们将会参考 vue
的源代码,并在其基础上进行一些适当的 简化 和 修改 。以此来帮助大家掌握 vue3
中响应式的核心逻辑。
那么明确好了这些内容之后,接下来就让我们进入到响应式的实现之中吧。
02:源码阅读:reactive 的响应性,跟踪 Vue 3 源码实现逻辑
我们知道在 vue
中想要实现响应式数据,拥有两种方式:
reactive
ref
在第三章中,我们在 vue
的源码中,创建了 packages/vue/examples/read-test/reactive.html
测试实例,在该实例中,我们通过 reactive
方法声明了一个响应式数据,通过 effect
注册了一个函数。
那么下面,我们就 跟踪 当前的代码,来详细看一下 vue
内容到底做了什么?
看的过程中我们需要时刻记住两点主线:
reactive
做了什么?effect
是什么?
明确好了之后,那么下面我们来去看:
reactive 方法
- 触发
reactive
方法 - 创建
reactive
对象:return createReactiveObject
- 进入
new Proxy
- 第一个参数
target
:为传入的对象 - 第二个参数
handler
:TargetType.COLLECTION = 2
,targetType = 1
,所以handler
为baseHandlers
- 那这个
baseHandlers
是什么呢?
- 第一个参数
- 在
reactive
方法中可知,baseHandlers
是触发createReactiveObject
传递的第三个参数:mutableHandlers
- 而
mutableHandlers
则是packages/reactivity/src/baseHandlers.ts
中导出的对象 - 所以我们到
packages/reactivity/src/baseHandlers.ts
中,为它的get(createGetter)
和set(createSetter)
分别打入一个断点 - 我们知道
get
和set
会在 取值 和 赋值 时触发,所以此时这两个断点 不会执行 - 最后
reactive
方法内执行了proxyMap.set(target, proxy)
方法 - 最后返回了代理对象。
- 那么至此
reactive
方法执行完成。
由以上执行逻辑可知,对于 reactive
方法而言,其实做的事情非常简单:
- 创建了
proxy
- 把
proxy
加到了proxyMap
里面 - 最后返回了
proxy
effect
那么接下来我们 effect
:
- 在
packages/reactivity/src/effect.ts
第170
行可以找到effect
方法,在这里给一个断点 - 执行
new ReactiveEffect(fn)
,其中的fn
就是我们传入的匿名函数:- 这里涉及到了一个类
ReactiveEffect
- 查看该类可知,内部实现了两个方法:
run
stop
- 我们分别为这两个方法 增加断点
- 这里涉及到了一个类
- 代码继续进行
- 可以发现执行了
run
方法,进入方法内部:- 执行
activeEffect = this
,赋值完成之后,activeEffect
为 传入的匿名函数fn
- 然后执行
return this.fn()
触发fn
函数 - 我们知道
fn
函数其实就是 传入的匿名函数,所以document.querySelector('#app').innerText = obj.name
- 执行
- 但是大家不要忘记,
obj
是一个proxy
,obj.name会触发getter
,所以接下来我们就会进入到mutableHandlers
的createGetter
中- 在该代码中,触发了该方法
const res = Reflect.get(target, key, receiver)
- 此时的
res
即为张三
- 注意:接下来触发了
track
函数,该函数是一个重点函数,track
在此为 跟踪的意思,我们来看它内部都做了什么:- 在
4-1
步中,为activeEffect
进行了赋值,我们知道activeEffect
代表的就是fn
函数 - 执行代码可知,
track
内部主要做了两件事情:- 为
targetMap
进行赋值,targetMap
的组成比较复杂:key
:target
value
:Map
key
:key
value
:Set
- 最后执行了
trackEffects(dep, eventInfo)
- 其中
eventInfo
是一个对象,内部包含四个属性:其中effect
即为activeEffect
即fn
函数
- 其中
- 在
trackEffects
函数内部,核心也是做了两件事情:- 为
dep(targetMap[target][key] 得到的 Set 实例)
添加了activeEffect
函数 - 为
activeEffect
函数的 静态属性deps
,增加了一个值dep
- 即:建立起了
dep
和activeEffect
的联系
- 为
- 为
- 在
- 那么至此,整个
track
的核心逻辑执行完成 - 我们可以把整个
track
的核心逻辑说成:收集了activeEffect(即:fn)
- 在该代码中,触发了该方法
- 最后在
createGetter
函数中返回了res(即:张三)
- 至此,整个
effect
执行完成
由以上逻辑可知,整个 effect
主要做了3 件事情:
- 生成
ReactiveEffect
实例 - 触发
fn
方法,从而激活getter
- 建立了
targetMap
和activeEffect
之间的联系dep.add(activeEffect)
activeEffect.deps.push(dep)
那么至此:**页面中即可展示 obj.name
**,但是不要忘记,等待两秒之后,我们会修改 obj.name
的值,我们知道,这样会触发 setter
,那么我们接下来来看 setter
中又做了什么呢?
- 两秒之后触发
setter
,会进入到packages/reactivity/src/baseHandlers.ts
中的的createSetter
方法中 - 创建变量:
oldValue = 张三
- 创建变量:
value = 李四
、 - 执行
const result = Reflect.set(target, key, value, receiver)
,即:修改了obj
的值为 “李四” - 触发:
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
,此时各参数的值为:
trigger
在这里为 触发 的意思,那么我们来看trigger
内部做了什么?首先执行:
const depsMap = targetMap.get(target)
,其中targetMap
即我们在track
函数中,保存activeEffect
的targetMap
然后代码执行到:
deps.push(depsMap.get(key))
。depsMap.get(key)
获取到的即为之前保存的activeEffect
,即fn
函数然后触发
triggerEffects(deps[0], eventInfo)
,我们来看triggerEffects
中做了什么:声明常量:
const effects = isArray(dep) ? dep : [...dep]
,此时的effects
保存的为fn
的集合遍历
effects
,执行:triggerEffect(effect, debuggerEventExtraInfo)
方法,那么我们来看triggerEffect
做了什么执行
effect.run()
方法,已知:effect
是一个ReactiveEffect
类型的对象,则run
方法会触发ReactiveEffect
的run
,那么我们接下来来看 这一次 进入run
方法时,内部做了什么?首先还是为
activeEffect = this
赋值,但是要 注意: 此时的this
不再是一个fn
,而是一个复杂对象:最后执行
this.fn()
即:effect 时传入的匿名函数
至此,
fn
执行,意味着:document.querySelector('#app').innerText = 李四
,页面将发生变化
triggerEffect
完成
triggerEffects
完成
trigger
完成setter
回调完成
由以上逻辑可知,整个 setter
主要做了 2 件事情:
- 修改
obj
的值 - 触发
targetMap
下保存的fn
函数
总结
那么到这里,我们就整个的跟踪了 packages/vue/examples/imooc/reactive.html
实例中:
reactive
函数effect
函数obj.name = xx
表达式
这三块代码背后,vue
究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:
- 创建
proxy
- 收集
effect
的依赖 - 触发收集的依赖
那么接下来我们自己的实现,将会围绕着这三个核心的理念进行。
03:框架实现:构建 reactive 函数,获取 proxy 实例
根据上一小节的内容可知,整个 reactive
函数,本质上是返回了一个 proxy
实例,那么我们这一小节,就先去实现这个 reactive
函数,得到 proxy
实例。
创建
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
模块:ts/** * 响应性的 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
文件,作为测试实例:vue<!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
函数,但是在 reactive
函数中我们还存在三个问题:
WeakMap
是什么?它和Map
有什么区别呢?mutableHandlers
现在是一个空的,我们又应该如何实现呢?- 难不成以后每次测试时,都要打包一次吗?
那么我们一个一个来看~~~
04:框架实现:什么是 WeakMap?它和 Map 有什么区别?
对比 WeakMap 和 Map 的文档可知,他们两个具备一个核心共同点,那就是:都是 {key, value} 的结构对象。
但是对于 WeakMap
而言,他却存在两个不同的地方:
key
必须是对象key
是弱引用的
其中第一个不同点比较好理解,但是第二个不同点是什么意思呢?那么我们本小节就来看一下这个 弱引用 指的是什么?
概念
弱引用:不会影响垃圾回收机制。即:WeakMap 的 key 不再存在任何引用时,会被直接回收。
强引用:会影响垃圾回收机制。存在强应用的对象永远 不会 被回收。
我们来看下面两个例子:
// map
<script>
// target 对象
let obj = {
name: '张三'
}
// 声明 Map 对象
const map = new Map()
// 保存键值对
map.set(obj, 'value')
// 把 obj 置空
obj = null
</script>
在当前这段代码中,如果我们在浏览器控制台中,打印 map
那么打印结果如下:
即:虽然 obj
已经不存在任何引用了,但是它并没有被回收,依然存在于 Map
实例中。这就证明 Map
是强应用的,哪怕 obj
手动为 null
,但是它依然存在于 Map
接下来同样的代码,我们来看 WeakMap
:
// target 对象
let obj = {
name: '张三'
}
// 声明 Map 对象
const wm = new WeakMap()
// 保存键值对
wm.set(obj, 'value')
// 把 obj 置空
obj = null
在当前这段代码中,如果我们在浏览器控制台中,打印 wm
那么打印结果如下:
此时 WeakMap
中不存在任何值,即:obj
不存在其他引用时,WeakMap
不会阻止垃圾回收,基于 obj
的引用将会被清除。这就证明了 WeakMap
的 弱引用特性。
总结
那么由以上可知,对于 WeakMap
而言,它存在两个比较重要的特性:
key
必须是对象key
是弱引用的
05:框架实现:createGetter && createSetter
那么当我们搞明白了 WeakMap
的特性之后,那么接下来我们就来看看 mutableHandlers
如何处理。
我们知道对于 Proxy
而言,它的 handler
可以监听 代理对象 的 getter
和 setter
,那么此时的 mutableHandlers
就是监听 代理对象 getter
和 setter
的核心部分。
所以接下来我们需要创建对应的 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
: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
中:tsconst { reactive } = Vue const obj = reactive({ name: '张三' }) console.log(obj.name); // 此时应该触发 track obj.name = '李四' // 此时应该触发 trigger
重新打包 项目,运行以上测试。
- 在
06:热更新的开发时:提升开发体验
在以上代码中,我们每次想要进行测试,都需要重新执行一遍 npm run build
,那么未免有些过于麻烦了。
那么有没有办法可以让我们的项目提供一个具备 热更新 的开发时呢?
答案当然是可以的,并且非常简单。
目前在 package.json
中,我们只提供了一个 build
指令:
"build": "rollup -c"
使用 build
可以打包项目,那么我们可以在提供一个 dev
指令:
"dev": "rollup -c -w",
rollup -c -w
表示:-c 读取配置文件,-w 监听源文件是否有改动,如果有改动,重新打包
那么这样我们就可以得到一个 dev
的热更新状态。
执行 npm run dev
,然后修改源代码,可以发现:项目就被重新打包了
07:框架实现:构建 effect 函数,生成 ReactiveEffect 实例
根据之前的测试实例我们知道,在创建好了 reactive
实例之后,接下来我们需要触发 effect
:
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.name
})
根据 第二节,中查看的源码可知,在 effect
中,我们生成了 ReactiveEffect
实例,并且触发了 getter(obj.name)
那么接下来我们就要完成这一系列的操作。
在
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() } }
那么根据以上代码可知,最终
vue
会执行effect
传入的 回调函数,即:tsdocument.querySelector('#app').innerText = obj.name
那么此时,
obj.name
的值,应该可以被渲染到html
中所以,我们可以到测试实例中,完成一下测试(PS:不要忘记导出
effect
方法):vue<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
中,那么接下来我们需要做的就是:当obj.name
触发setter
时,修改视图,以此就可实现 响应性数据变化。所以,下面我们就需要分别处理
getter
和setter
对应的情况了。
08:框架实现:track && trigger
根据我们在 packages/reactivity/src/baseHandlers.ts
中的代码可知,当触发 getter
行为时,其实我们会触发 track
方法,进行 依赖收集,当触发 setter
行为时,会触发 trigger
方法,来 触发依赖
那么这里就涉及到了两个概念:
- 依赖收集:
track
- 触发依赖:
trigger
所以接下来如果我们想要实现这两个函数,那么就需要先搞清楚什么是 依赖收集 和 触发依赖。
什么是响应性
根据大家的开发经验和我们在第二小节查看源码时可知,所谓的响应性其实指的就是:当响应性数据触发 setter
时执行 fn
函数
那么想要达到这样的一个目的,那就必须要在:getter
时能够收集当前的 fn
函数,以便在 setter
的时候可以执行对应的 fn
函数
但是对于收集而言,如果仅仅是把 fn
存起来还是不够的,我们还需要知道,当前的这个 fn
是哪个响应式数据对象的哪个属性对应的,只有这样,我们才可以在 该属性 触发 setter
的时候,准确的执行响应性。
那么我们应该如何确定以这一点呢?
如何进行依赖收集
大家还记不记得,我们在 packages/reactivity/src/reactive.ts
中创建过一个 WeakMap
:
export const reactiveMap = new WeakMap<object, any>()
我们知道 WeakMap
它的 key
必须是一个对象,并且 key
是一个弱引用的。
那么大家想一想我们可不可以这样:
WeakMap
:key
:响应性对象value
:Map
对象key
:响应性对象的指定属性value
:指定对象的指定属性的 执行函数
图表表示:
那么这样我们就可以关联上 指定对象的指定属性 与 执行函数 fn
之间的关系,当触发 setter
时,直接执行 对应对象的对应属性的 fn
即可。
那么明确好了这样的一个概念之后,接下来我们就可以根据以上理念进行对应的实现。
09:框架实现:构建 track 依赖收集函数
那么这本小节,我们就来实现 track
函数,明确一下最终的目标,我们期望最终 weakMap
中可以保存一下结构数据:
WeakMap
:
key
:响应性对象value
:Map
对象key
:响应性对象的指定属性value
:指定对象的指定属性的执行函数
在 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)
}
此时运行测试函数,查看打印的 depsMap
,可得以下数据:
那么此时证明,此时:指定对象的指定属性对应的 fn
已经被成功的保存到了 WeakMap
中了。
10:框架实现:构建 trigger 触发依赖
那么在上一小节中,我们已经成功保存依赖到 WeakMap
中了,那么接下来我们就可以在 setter
的时候触发保存的依赖,以此来达到 响应性 数据的效果了。
在 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>
运行测试实例,等待两秒,发现 视图发生变化
那么,至此我们就已经完成了一个简单的 响应式依赖数据处理
11:总结:单一依赖的 reactive
通过以上的努力,我们目前已经构建了一个简单的 reactive
函数,使用 reactive
函数,配合 effect
可以实现出一个 响应式数据渲染功能,那么这一小节,我们把整个的流程做一个总结:
- 首先我们在
packages/reactivity/src/reactive.ts
中,创建了一个reactive
函数,该函数可以帮助我们生成一个proxy
实例对象 - 通过该
proxy
实例的handler
可以监听到对应的getter
和setter
- 然后我们在
packages/reactivity/src/effect.ts
中,创建了一个effect
函数,通过该函数可以创建一个ReactiveEffect
的实例,该实例的构造函数可以接收传入的回调函数fn
,并且提供了一个run
方法 - 触发
run
可以为activeEffect
进行赋值,并且执行fn
函数 - 我们需要在
fn
函数中触发proxy
的getter
,以此来激活handler
的get
函数 - 在
handler
的get
函数中,我们通过WeakMap
收集了 指定对象,指定属性 的fn
,这样的一步操作,我们把它叫做 依赖收集 - 最后我们可以在 任意时刻,修改
proxy
的数据,这样会触发handler
的setter
- 在
handler
的setter
中,我们会根据 指定对象target
的 指定属性key
来获取到保存的 依赖,然后我们只需要触发依赖,即可达到修改数据的效果
12:功能升级:响应数据对应多个 effect
在我们之前的实现中,还存在一个小的问题,那就是:每个响应性数据属性只能对应一个 effect
回调
我们来看下面这个例子 packages/vue/examples/reactivity/reactive-dep.html
:
<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>
在以上的代码中,我们新增了一个 effect
函数,即:name
属性对应两个 DOM
的变化。
但是当我们运行该代码时发现,p1
的更新渲染是无效的!
那么这是因为什么呢?
查看我们的代码可以发现,我们在构建 KeyToDepMap
对象时,它的 Value
只能是一个 ReactiveEffect
,所以这就导致了 一个 key
只能对应一个有效的 effect
函数。
那么假如我们期望:一个 key
可以对应 多个 有效的 effect
函数的话,那么应该怎么做呢?
可能有些同学已经想到了,我们只需要 让 KeyToDepMap
的 Value
可以对应一个数组 不就可以了吗?
如上图所示,我们可以构建一个 Set(set
是一个 “数组”,值不会重复) 类型的对象,作为 Map
的 value
。
我们可以把它叫做 Dep
,通过 Dep
来保存 指定 key
的所有依赖
那么明确好了这样的概念之后,接下来我们到项目中,进行一个对应的实现。
13:框架实现:构建 Dep 模块,处理一对多的依赖关系
想要处理 dep
模块,那么我们需要对 track
和 trigger
进行改造:
track
创建
packages/reactivity/src/dep.ts
模块:jsimport { 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
的泛型:jsimport { Dep } from './dep' type KeyToDepMap = Map<any, Dep>
修改
track
方法,处理Dep
类型数据:tsexport function track(target: object, key: unknown) { ... // 获取指定 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
中保存的依赖。
trigger
- 在
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 = 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
,发现即可实现一对多的依赖关系。
14:reactive 函数的局限性
目前我们已经成功的完成了一个相对完善的 reactive
函数,通过它配合 effect
函数可以实现对应的响应性渲染。
但是对于 reactive
而言,它其实是具备一些局限性的。那么具体都有哪些局限性呢?
我们来思考以下两个问题:
reactive
可以对简单数据类型使用吗?比如:reactive('张三')
- 当我们对
reactive
返回的响应性数据进行解构时,解构之后的属性还会具备响应性吗?
那么下面我们就对这两个问题,一个一个进行解释:
reactive
可以对简单数据类型使用吗?
我们知道,对于 reactive
函数而言,它会把传入的 object 作为 proxy
的 target
参数,而对于 proxy
而言,他只能代理 对象,而不能代理简单数据类型,所以说:我们不可以使用 reactive
函数,构建简单数据类型的响应性。
当我们对 reactive
返回的响应性数据进行解构时,解构之后的属性还会具备响应性吗?
一个数据是否具备响应性的关键在于:**是否可以监听它的 getter
和 setter
**。而根据我们的代码可知,只有 proxy
类型的 代理对象 才可以被监听 getter
和 setter
,而一旦解构,对应的属性将不再是 proxy
类型的对象,所以:解构之后的属性,将不具备响应性。
总结与新的问题
那么到现在我们知道了,reactive
不可以对 简单数据类型使用,并且 不可以解构。那么如果我们期望 简单数据类型也具备响应性,那么我们又应该如何做呢?
熟悉 vue 3
的同学,肯定知道,此时我们可以使用 ref
函数来进行实现。
那么 ref
函数它又是因为什么可以构建简单数据类型的响应性,又为什么必须要通过 .value
访问数据呢?
想要知道以上问题,那么我们就继续往下看~~~
15:总结
在本章,我们初次解除了 reactivity
模块,并且在该模块中构建了 reactive
响应性函数。
对于 reactive
的响应性函数而言,我们知道它:
- 是通过
proxy
的setter
和getter
来实现的数据监听 - 需要配合
effect
函数进行使用 - 基于
WeakMap
完成的依赖收集和处理 - 可以存在一对多的依赖关系
同时我们也了解了 reactive
函数的不足:
reactive
只能对 复杂数据 类型进行使用reactive
的响应性数据,不可以进行解构
因为 reactive
的不足,所以 vue 3
又为我们提供了 ref
函数构建响应性,那么:
ref
函数的内容是如何进行实现的呢?ref
可以构建简单数据类型的响应性吗?- 为什么
ref
类型的数据,必须要通过.value
访问值呢?
带着以上三个问题,我们来看下一章 ref
的响应性 ~~~