上一章:手把手搭建Vue轮子从0到1:3. 响应系统的核心设计原则
响应式核心:
4.1. 阅读源码
4.1.1. Reactive 做了什么?
记得我们在 2.3(手把手搭建Vue轮子从0到1:2. 搭建框架雏形)节 中构建的测试实例 reactive 吗?我们在里面打断点,看看 reactive 究竟做了什么。

- 触发 reactive 方法
- 创建了 reactive 对象:createReactiveObject

- new Proxy(target:传入的对象,handler:baseHandlers )
TargetType.COLLECTION = 2; targetTypye = 1; 所以handler 为 baseHandlers

- baseHandlers(触发 createReactiveObject 时传递的第三个参数:mutableHandlers)
- mutableHandlers 位置:packages/reactivity/src/baseHandlers.ts
- 添加 get、set 方法断点(只有在取值、赋值时执行,所以此时这2个断点还不会执行)
get 方法声明在 BaseReactiveHandler 类里


set 方法声明在 MutableReactiveHandler 类里

- 在 createReactiveObject 方法最后执行了 proxyMap.set(target, proxy) 方法。

- 最后返回代理对象,reactive 方法执行完成。
以上流程可以总结成:
- 创建 proxy
- 把 proxy 加到 proxyMap 里
- 最后返回 proxy
4.1.2. effect
总结:
- 创建 proxy
- 收集 effect 的依赖
- 触发收集的依赖
4.2. 构建 reactive 函数,获取 proxy 实例
packages\reactivity\src\baseHandlers.ts
js
/**
* 响应性的 handler 函数
*/
export const mutableHandlers: ProxyHandler<object> = {}
packages\reactivity\src\reactive.ts
js
import { mutableHandlers } from './baseHandlers'
/**
* 响应性对象的缓存(Map 缓存对象)
* key: target 被代理的对象
* value: proxy 代理对象
*/
export const reactiveMap = new WeakMap<object, any>()
/**
* 为复杂数据类型,创建响应性对象
* @param target 被代理的对象
* @returns 如果 target 是复杂数据类型,则返回代理对象;如果 target 是简单数据类型,则返回 target
*/
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
/**
* 创建响应性对象
* @param target 被代理的对象
* @param baseHandlers 基础的代理处理函数
* @param proxyMap 响应性对象的缓存
* @returns
*/
export function createReactiveObject(target: object, baseHandlers: ProxyHandler<object>, proxyMap: WeakMap<object, any>) {
// 如果该实例已经被代理,则直接读取即可
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 如果该实例没有被代理,则创建代理,生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
// 将 proxy 实例缓存起来
proxyMap.set(target, proxy)
return proxy
}
packages\reactivity\src\index.ts
js
export { reactive } from './reactive'
packages\vue\src\index.ts
js
export { reactive } from '@vue/reactivity'
执行 npm run build 进行打包,生成 vue.js
packages\vue\dist\vue.js
js
var Vue = (function (exports) {
'use strict';
/**
* 响应性的 handler 函数
*/
var mutableHandlers = {};
/**
* 响应性对象的缓存(Map 缓存对象)
* key: target 被代理的对象
* value: proxy 代理对象
*/
var reactiveMap = new WeakMap();
/**
* 为复杂数据类型,创建响应性对象
* @param target 被代理的对象
* @returns 如果 target 是复杂数据类型,则返回代理对象;如果 target 是简单数据类型,则返回 target
*/
function reactive(target) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
/**
* 创建响应性对象
* @param target 被代理的对象
* @param baseHandlers 基础的代理处理函数
* @param proxyMap 响应性对象的缓存
* @returns
*/
function createReactiveObject(target, baseHandlers, proxyMap) {
// 如果该实例已经被代理,则直接读取即可
var existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 如果该实例没有被代理,则创建代理,生成 proxy 实例
var proxy = new Proxy(target, baseHandlers);
// 将 proxy 实例缓存起来
proxyMap.set(target, proxy);
return proxy;
}
exports.reactive = reactive;
return exports;
})({});
//# sourceMappingURL=vue.js.map
创建测试实例:
packages\vue\examples\reactive.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>reactive 测试实例</title>
<script src="../dist/vue.js"></script>
</head>
<body></body>
<script>
const { reactive } = Vue
const obj = reactive({
name: '张三',
age: 18
})
console.log(obj)
</script>
</html>
运行 Live Server 可以看到打印了一个 proxy 对象实例

以上我们已经得到了一个基础的 reactive 函数,但是在 reactive 函数中还存在三个问题:
- WeakMap 是什么?它和 Map 有什么区别?
- mutableHandlers 应该如何实现?
- 如何每次测试时,不用重新打包?
4.3. WeakMap 是什么?它和 Map 有什么区别?
4.4. createGetter 和 createSetter 的实现
对于 Proxy 来说,它的 handler 可以监听代理对象的 getter 和 setter,那么此时 mutableHandlers 就是监听代理对象 getter 和 setter 的核心部分。
4.4.1. track、trigger
packages\reactivity\src\effect.ts
js
/**
* 收集依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
*/
export function track(target: object, key: string | symbol) {
console.log('收集依赖')
}
/**
* 触发依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
* @param newValue 新值
*/
export function trigger(target: object, key?: string | symbol, newValue?: any) {
console.log('触发依赖')
}
4.4.2. getter、setter
packages\reactivity\src\baseHandlers.ts
js
import { track, trigger } from './effect'
/**
* getter 回调方法
*/
const get = createGetter()
/**
* 创建 getter 回调函数
* @returns
*/
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
// 利用 Reflect.get 获取 target 对象的 key 属性值(得到返回值)
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
}
}
/**
* setter 回调方法
*/
const set = createSetter()
/**
* 创建 setter 回调函数
* @returns
*/
function createSetter() {
return function set(target: object, key: string | symbol, value: any, receiver: object) {
// 利用 Reflect.set 设置 target 对象的 key 属性值(设置新值)
const res = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key)
return res
}
}
/**
* 响应性的 handler 函数
*/
export const mutableHandlers: ProxyHandler<object> = {
get,
set
}
4.4.3. 测试
packages\vue\examples\reactive.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>reactive 测试实例</title>
<script src="../dist/vue.js"></script>
</head>
<body></body>
<script>
const { reactive } = Vue
const obj = reactive({
name: '张三',
age: 18
})
console.log(obj.name) // 触发 track
obj.name = '李四' // 触发 trigger
console.log(obj.name) // 触发 track
</script>
</html>
4.5. 热更新的开发时(提升开发体验)
在 package.json 中添加 dev 指令:
rollup -c -w
:-c 读取配置文件,-w 监听源文件是否有改动,如果有改动就重新打包
json
"dev": "rollup -c -w",
执行 npm run dev
,然后修改源代码,就可以发现项目被重新打包了。这样就可以得到一个 dev 的热更新状态。
4.6. 构建 effect 函数,生成 ReactiveEffect 实例
packages\reactivity\src\effect.ts
js
/**
* 创建响应式 effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T>(fn: () => T) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// 执行 ReactiveEffect 实例的 run 方法
_effect.run()
}
/**
* 单例的,当前的 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
js
export { effect } from './effect'
packages\vue\examples
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>reactive 测试实例</title>
<script src="../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三',
age: 18
})
console.log(obj.name) // 触发 track
obj.name = '李四' // 触发 trigger
console.log(obj.name) // 触发 track
// 触发 effect 方法
effect(() => {
document.getElementById('app').innerHTML = obj.name
})
setTimeout(() => {
obj.name = '王五'
}, 3000)
</script>
</html>
至此,已经成功 渲染数据到 html 中了。接下来就是要 当 obj.name 触发 setter 时,修改视图以实现 响应性数据变化。
4.7. 实现 tarck、trigger
- 触发 getter 行为时,触发 track 方法:依赖收集
- 触发 setter 行为时,触发 trigger 方法:触发依赖
4.7.1. 什么是响应性
当响应性数据触发 setter 时执行 fn 函数。
要达到这样的目的,就必须要在 getter 时能够收集当前的fn函数,以便在 setter 的时候可以执行对应的 fn 函数。
但对于收集而言,仅仅把 fn 存起来还不够,还需要知道当前这个 fn 是哪个响应式数据对象的哪个属性所对应的,只有这样在该属性触发 setter 的时候,才能准确的执行响应性。
4.7.2. 如何进行依赖收集
在 packages\reactivity\src\reactive.ts 中我们创建了一个 Map 缓存对象
js
/**
* 响应性对象的缓存(Map 缓存对象)
* key: target 被代理的对象
* value: proxy 代理对象
*/
export const reactiveMap = new WeakMap<object, any>()
WeakMap 的 key 必须是一个对象,并且 key 是一个
- WeakMap:
a. key:响应性对象
b. value:Map 对象
i. key:响应性对象的指定属性
ii . value:指定对象的指定属性的执行函数
关联上 指定对象的指定属性 与 执行函数 fn 之间的关系,当触发 setter 时,直接执行 对应对象的对应属性的 fn 即可。
4.7.3. 构建 track 依赖收集函数
packages\reactivity\src\effect.ts
js
type KeyToDepMap = Map<any, ReactiveEffect>
/**
* 收集所有依赖的 WeakMap 实例:
* 1. key: 响应性对象
* 2. value: 响应性对象的属性与依赖的 Map 实例
* 2.1 key: 响应性对象的指定属性
* 2.2 value: 响应性对象的指定属性对应的依赖(执行函数)
*/
const targetMap = new WeakMap<any, KeyToDepMap>()
/**
* 收集依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
*/
export function track(target: object, key: string | symbol) {
// 如果当前不存在执行函数,则直接返回
if (!activeEffect) return
// 尝试从 targetMap 中,根据 target 获取对应的 map
let depsMap = targetMap.get(target)
// 如果当前不存在 depsMap,则创建 depsMap,并把该对象赋值给对应的 value
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 为指定 map, 指定 key 设置回调函数
depsMap.set(key, activeEffect)
console.log(targetMap)
}

至此,指定对象的指定属性对应的 fn 已经被成功的保存到了 WeakMap 中。
4.7.4. 构建 trigger 触发依赖
packages\reactivity\src\effect.ts
js
/**
* 触发依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
* @param newValue 新值
*/
export function trigger(target: object, key?: string | symbol) {
// 依据 target 获取对应的 map 实例
const depsMap = targetMap.get(target)
// 如果 map 不存在,则直接返回
if (!depsMap) return
// 依据 key ,从 depsMap 中取出value, 该value 是一个 ReactiveEffect 实例
const effect = depsMap.get(key)
// 如果 effect 不存在,则直接返回
if (!effect) return
// 执行 effect 中保存的 fn 函数
effect.fn()
}
运行测试实例(packages\vue\examples\reactive.html),等待3s发现视图发生变化

至此就完成了一个简单的 响应式依赖数据处理。
4.8. 总结:单一依赖的 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 来获取到保存的 依赖,然后只需要触发依赖,即可达到修改数据的效果
4.9. 响应数据对应多个 effect
新增一个 effect 函数,即 name 属性对应两个 DOM 的变化。
但运行该代码发现, p1 的更新渲染无效。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reactive-Dep</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: '张三',
age: 18
})
// 调用 effect 方法
effect(() => {
document.getElementById('p1').innerHTML = obj.name
})
effect(() => {
document.getElementById('p2').innerHTML = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 3000)
</script>
</html>

在构建 KeyToDepMap 对象时,它的 Value 只能是一个 ReactiveEffect,这就导致了一个 key 只能对应一个有效的 effect 函数。
要让一个 key 可以对应多个有效的 effect 函数,需要让 KeyToDepMap 的 Value 对应一个数组。
通过构建一个 Set(set 是一个"数组",值不会重复)类型的对象,作为 Map 的 value。可以把它叫做 Dep,通过 Dep 来保存指定 key 的所有依赖。

4.10. 构建 Dep 模块,处理一对多的依赖关系
- 改造 track 和 trigger
packages\reactivity\src\dep.ts
ts
import { ReactiveEffect } from "./effect"
export type Dep = Set<ReactiveEffect>
/**
* 依据 effects 生成 dep 实例
* @param effects
* @returns
*/
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects)
return dep
}
packages\reactivity\src\effect.ts
ts
import type { Dep } from "./dep"
import { createDep } from "./dep"
type KeyToDepMap = Map<string | symbol | undefined, Dep>
/**
* 收集所有依赖的 WeakMap 实例:
* 1. key: 响应性对象
* 2. value: 响应性对象的属性与依赖的 Map 实例
* 2.1 key: 响应性对象的指定属性
* 2.2 value: 响应性对象的指定属性对应的依赖(执行函数)
*/
const targetMap = new WeakMap<object, KeyToDepMap>()
/**
* 收集依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(被代理对象 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
*/
export function track(target: object, key: string | symbol) {
// 如果当前不存在执行函数,则直接返回
if (!activeEffect) return
// 尝试从 targetMap 中,根据 target 获取对应的 map
let depsMap = targetMap.get(target)
// 如果当前不存在 depsMap,则创建 depsMap,并把该对象赋值给对应的 value
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取指定 key 的 dep
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 为指定 map, 指定 key 设置回调函数
trackEffects(dep)
}
/**
* 利用 dep 一次跟踪指定 key 的所有 effect
* @param dep
*/
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
/**
* 触发依赖
* @param target 被代理的对象(WeakMap 的 key)
* @param key 被代理对象的属性(Map 的 key,当依赖被触发时,需要根据 key 找到对应的 effect 函数)
* @param newValue 新值
*/
export function trigger(target: object, key?: string | symbol) {
// 依据 target 获取对应的 map 实例
const depsMap = targetMap.get(target)
// 如果 map 不存在,则直接返回
if (!depsMap) return
// 依据 key 获取对应的 dep
let dep: Dep | undefined = depsMap.get(key)
// 如果 dep 不存在,则直接返回
if (!dep) return
// 触发 dep 中保存的依赖
triggerEffects(dep)
}
/**
* 依次触发 dep 中保存的依赖
* @param dep
*/
export function triggerEffects(dep: Dep) {
const effects = Array.isArray(dep) ? dep : [...dep]
for (const effect of effects) {
triggerEffect(effect)
}
}
/**
* 触发指定依赖
* @param effect
*/
export function triggerEffect(effect: ReactiveEffect) {
effect.run()
}
/**
* 创建响应式 effect 函数
* @param fn 执行方法
* @returns 以 ReactiveEffect 实例为 this 的执行函数
*/
export function effect<T>(fn: () => T) {
// 生成 ReactiveEffect 实例
const _effect = new ReactiveEffect(fn)
// 执行 ReactiveEffect 实例的 run 方法
_effect.run()
}
/**
* 单例的,当前的 effect
*/
export let activeEffect: ReactiveEffect | undefined
/**
* 响应性触发依赖时的执行类
*/
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) { }
run() {
// 为 activeEffect 赋值
activeEffect = this
// 执行 fn 方法
return this.fn()
}
}
4.11. 总结
reactive 响应性函数的实现:
- 通过 proxy 的 setter 和 getter 来实现的数据监听
- 需要配合 effect 函数进行使用
- 基于 WeakMap 完成的依赖收集和处理
- 可以存在一对多的依赖关系
这里我们需要先思考一下reactive 的不足,然后下一节我们就要思考如何解决这个问题的(ref)。
reactive函数有以下局限:
- 只能对复杂数据类型进行使用:对于 reactive 函数而言,它会把传入的 object 作为 proxy 的 target 参数,而对于 proxy 而言,他只能代理 对象,而不能代理简单数据类型,所以说:不可以使用 reactive 函数构建简单数据的响应性。
- reactive 的响应性数据,不可以进行解构:一个数据是否具备响应性的关键在于:是否可以监听它的 getter 和 setter。而只有 proxy 类型的代理对象才可以被监听 getter 和 setter,而一旦结构,对应的属性将不再是 proxy 类型的对象。所以,解构之后的属性,将不具备响应性。