Vue3 源码之 reactivity
注: 为了直观的看到 Vue3 的实现逻辑, 本文移除了边缘情况处理、兼容处理、DEV环境的特殊逻辑等, 只保留了核心逻辑
vue-next/reactivity 实现了 Vue3 的响应性, reactivity 提供了以下接口:
export {
ref, // 代理基本类型
shallowRef, // ref 的浅代理模式
isRef, // 判断一个值是否是 ref
toRef, // 把响应式对象的某个 key 转为 ref
toRefs, // 把响应式对象的所有 key 转为 ref
unref, // 返回 ref.value 属性
proxyRefs,
customRef, // 自行实现 ref
triggerRef, // 触发 customRef
Ref, // 类型声明
ToRefs, // 类型声明
UnwrapRef, // 类型声明
ShallowUnwrapRef, // 类型声明
RefUnwrapBailTypes // 类型声明
} from './ref'
export {
reactive, // 生成响应式对象
readonly, // 生成只读对象
isReactive, // 判断值是否是响应式对象
isReadonly, // 判断值是否是只读对象
isProxy, // 判断值是否是 proxy
shallowReactive, // 生成浅响应式对象
shallowReadonly, // 生成浅只读对象
markRaw, // 让数据不可被代理
toRaw, // 获取代理对象的原始对象
ReactiveFlags, // 类型声明
DeepReadonly // 类型声明
} from './reactive'
export {
computed, // 计算属性
ComputedRef, // 类型声明
WritableComputedRef, // 类型声明
WritableComputedOptions, // 类型声明
ComputedGetter, // 类型声明
ComputedSetter // 类型声明
} from './computed'
export {
effect, // 定义副作用函数, 返回 effect 本身, 称为 runner
stop, // 停止 runner
track, // 收集 effect 到 Vue3 内部的 targetMap 变量
trigger, // 执行 targetMap 变量存储的 effects
enableTracking, // 开始依赖收集
pauseTracking, // 停止依赖收集
resetTracking, // 重置依赖收集状态
ITERATE_KEY, // 固定参数
ReactiveEffect, // 类型声明
ReactiveEffectOptions, // 类型声明
DebuggerEvent // 类型声明
} from './effect'
export {
TrackOpTypes, // track 方法的 type 参数的枚举值
TriggerOpTypes // trigger 方法的 type 参数的枚举值
} from './operations'
一、名词解释
-
target: 普通的 JS 对象
-
reactive:
@vue/reactivity
提供的函数, 接收一个对象, 并返回一个 代理对象, 即响应式对象 -
shallowReactive:
@vue/reactivity
提供的函数, 用来定义浅响应对象 -
readonly:
@vue/reactivity
提供的函数, 用来定义只读对象 -
shallowReadonly:
@vue/reactivity
提供的函数, 用来定义浅只读对象 -
handlers: Proxy 对象暴露的钩子函数, 有
get()
、set()
、deleteProperty()
、ownKeys()
等, 可以参考MDN -
targetMap:
@vue/reactivity
内部变量, 存储了所有依赖 -
effect:
@vue/reactivit
提供的函数, 用于定义副作用,effect(fn, options)
的参数就是副作用函数 -
watchEffect:
@vue/runtime-core
提供的函数, 基于 effect 实现 -
track:
@vue/reactivity
内部函数, 用于收集依赖 -
trigger:
@vue/reactivity
内部函数, 用于消费依赖 -
scheduler: effect 的调度器, 允许用户自行实现
二、Vue3 实现响应式的思路
先看下边的流程简图, 图中 Vue 代码的功能是: 每隔一秒在 id
为 Box
的 div
中输出当前时间
在开始梳理 Vue3 实现响应式的步骤之前, 要先简单理解 effect
, effect
是响应式系统的核心, 而响应式系统又是 Vue3 的核心
上图中从 track
到 targetMap
的黄色箭头, 和从 targetMap
到 trigger
的白色箭头, 就是 effect
函数要处理的环节
effect
函数的语法为:
effect(fn, options)
effect
接收两个参数, 第一个必填参数 fn
是副作用函数
第二个选填 options
的参数定义如下:
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延迟触发 effect
scheduler?: (job: ReactiveEffect) => void // 调度函数
onTrack?: (event: DebuggerEvent) => void // 追踪时触发
onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
onStop?: () => void // 停止监听时触发
allowRecurse?: boolean // 是否允许递归
}
下边从流程图中左上角的 Vue 代码开始
第 1 步
通过 reactive
方法将 target
对象转为响应式对象, reactive
方法的实现方法如下:
import { mutableHandlers } from './baseHandlers'
import { mutableCollectionHandlers } from './collectionHandlers'
const reactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
export function reactive(target: object) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target) // 先忽略, 上边例子中, targetType 的值为: 1
const proxy = new Proxy(
target,
targetType === 2 ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
reactive
方法携带 target
对象和 mutableHandlers
、mutableCollectionHandlers
调用 createReactiveObject
方法, 这两个 handers 先忽略
createReactiveObject
方法通过 reactiveMap
变量缓存了一份响应式对象, reactiveMap
和 readonlyMap
变量是文件内部的变量, 相当于文件级别的闭包变量
其中 targetType 有三种枚举值: 0 代表不合法, 1 代表普通对象, 2 代表集合, 图中例子中, targetType
的值为 1, 对于 { text: '' }
这个普通对象传进 reactive()
方法时, 使用 baseHandlers
提供的 mutableHandlers
最后调用 Proxy 方法将 target 转为响应式对象, 其中 "响应" 体现在 handers 里, 可以这样理解: reactive = Proxy (target, handlers)
第 2 步
mutableHandlers
负责挂载 get
、set
、deleteProperty
、has
、ownKeys
这五个方法到响应式对象上
其中 get
、has
、ownKeys
负责收集依赖, set
和 deleteProperty
负责消费依赖
响应式对象的 get
、has
和 ownKeys
方法被触发时, 会调用 createGetter
方法, createGetter
的实现如下:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
当 { text: '' }
这个普通JS对象传到 createGetter
时, key 的值为: text
, res 的值为: String
类型, 如果 res 的值为 Object
类型则会递归调用, 将 res 转为响应式对象
createGetter
方法的目的是触发 track
方法, 对应本文的第 3 步
响应式对象的 set
和 deleteProperty
方法被触发时, 会调用 createSetter
方法, createSetter
的实现如下:
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
const result = Reflect.set(target, key, value, receiver)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
createSetter
方法的目的是触发 trigger
方法, 对应本文的第 4 步
第 3 步
这一步是整个响应式系统最关键的一步, 即我们常说的依赖收集, 依赖收集的概念很简单, 就是把 响应式数据 和 副作用函数 建立联系
文章一开始流程图的例子中, 就是把 target
对象和 document.getElementById("Box").innerText = date.text;
这个副作用函数建立关联, 这个 "关联" 指的就是上边提到的 targetMap
变量, 后边会详细描述一下 targetMap
对象的结构
第 2 步介绍了 createGetter
方法的核心是调用 track
方法, track
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 track
的实现:
const targetMap = new WeakMap<any, KeyToDepMap>()
// target: { text: '' }
// type: get
// key: text
export function track(target: object, type: TrackOpTypes, key: unknown) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
从 track
方法我们能看到 targetMap
这个闭包变量上储存了所有的 effect
, 换句话说是把能影响到 target
的副作用函数收集到 targetMap
变量中
targetMap 是个 WeakMap, WeakMap 和 Map 的区别在于 WeakMap 的键只能是对象, 用 WeakMap 而不用 Map 是因为 Proxy 对象不能代理普通数据类型
targetMap 的结构:
const targetMap = {
[target]: {
[key1]: [effect1, effect2, effect3, ...],
[key2]: [effect1, effect2, effect3, ...]
}
}
{ text: '' }
这个target 传进来时, targetMap 的结构是:
// 上边例子中用来在 id 为 Box 的 div 中输出当前时间的副作用函数
const effect = () => {
document.getElementById("Box").innerText = date.text;
};
const target = {
"{ text: '' }": {
"text": [effect]
}
}
举三个例子, 来分析一下 targetMap 的结构, 第一个例子是多个 target 情况:
<script>
import { effect, reactive } from "@vue/reactivity";
const target1 = { language: "JavaScript"};
const target2 = { language: "Go"};
const target3 = { language: "Python"};
const r1 = reactive(target1);
const r2 = reactive(target2);
const r3 = reactive(target3);
// effect1
effect(() => {
console.log(r1.language);
});
// effect2
effect(() => {
console.log(r2.language);
});
// effect3
effect(() => {
console.log(r3.language);
});
// effect4
effect(() => {
console.log(r1.language);
console.log(r2.language);
console.log(r3.language);
});
</script>
这种情况下 targetMap 的构成是:
const effect1 = () => {
console.log(r1.language);
};
const effect2 = () => {
console.log(r2.language);
};
const effect3 = () => {
console.log(r3.language);
};
const effect4 = () => {
console.log(r1.language);
console.log(r2.language);
console.log(r3.language);
};
const targetMap = {
'{"language":"JavaScript"}': {
"language": [effect1, effect4]
},
'{"language":"Go"}': {
"language": [effect2, effect4]
},
'{"language":"Python"}': {
"language": [effect3, effect4]
}
}
第二个例子是单个 target 多个属性时:
import { effect, reactive } from "@vue/reactivity";
const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"};
const user = reactive(target);
effect(() => {
console.log(user.name);
console.log(user.age);
console.log(user.email);
});
这种情况下 targetMap 的构成是:
const effect = () => {
console.log(user.name);
console.log(user.age);
console.log(user.email);
};
const targetMap = {
'{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': {
"name": [effect],
"age": [effect],
"email": [effect]
}
}
第三个例子是多维对象时:
import { effect, reactive } from "@vue/reactivity";
const target = {
name: "rmlzy",
skills: {
frontend: ["JS", "TS"],
backend: ["Node", "Python", "Go"]
}
};
const user = reactive(target);
// effect1
effect(() => {
console.log(user.name);
});
// effect2
effect(() => {
console.log(user.skills);
});
// effect3
effect(() => {
console.log(user.skills.frontend);
});
// effect4
effect(() => {
console.log(user.skills.frontend[0]);
});
这种情况下 targetMap 的构成是:
const effect1 = () => {
console.log(user.name);
};
const effect2 = () => {
console.log(user.skills);
};
const effect3 = () => {
console.log(user.skills.frontend);
};
const effect4 = () => {
console.log(user.skills.frontend[0]);
};
const targetMap = {
'{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': {
"name": [effect1],
"skills": [effect2, effect3, effect4]
},
'{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': {
"frontend": [effect3, effect4]
}
}
第 4 步
第 3 步的目的是收集依赖, 这一步的目的是消费依赖
这里要注意, 只有当 target 代理对象的 get
方法被触发时, 才会真正执行 track
, 换句话说, 没有地方需要 get
target 对象时, target 没有依赖, 也就没有收集依赖一说
下边的例子中只是把 target 转换为了响应式对象, 并没有触发依赖收集, targetMap 是空的
const target = {"text": ""};
const date = reactive(target);
effect(() => {
date.text = new Date().toString();
});
第 2 步介绍了 createSetter
方法的核心是调用 trigger
方法, trigger
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 trigger
的实现:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
if (isMap(target)) {
effects.add(depsMap.get(ITERATE_KEY))
}
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
trigger 的实现很简单, 先把 target 相关的 effect 汇总到 effects 数组中, 然后调用 effects.forEach(run)
执行所有的副作用函数
再回顾一下 effect 方法的定义: effect(fn, options)
, 其中 options 有个可选属性叫 scheduler
, 从上边 run
函数也可以看到 scheduler
的作用是让用户自定义如何执行副作用函数
第 5 步
又回到了本文最开始讲的 effect, effect 函数的实现如下:
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
effect 的核心是调用 createReactiveEffect
方法
可以看到 options.lazy
默认为 false
会直接执行 effect, 当设置为 true
时, 会返回 effect 由用户手动触发
createReactiveEffect
函数的实现如下:
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
首先定义了 effect 是个普通的 function
, 先看后边 effect 函数挂载的属性:
effect.id = uid++ // 自增ID, 每个 effect 唯一的ID
effect.allowRecurse = !!options.allowRecurse // 是否允许递归
effect._isEffect = true // 特殊标记
effect.active = true // 激活状态
effect.deps = [] // 依赖数组
effect.raw = fn // 缓存一份用户传入的副作用函数
effect.options = options // 缓存一份用户传入的配置
isEffect
函数用来判断值是否是 effect, 就是根据上边 _isEffect
变量判断的, isEffect
函数实现如下:
function isEffect(fn) {
return fn && fn._isEffect === true;
}
再来看 effect 的核心逻辑:
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
effectStack
用数组实现栈,activeEffect
是当前生效的 effect
先执行 cleanup(effect)
:
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
cleanup
的目的是清空 effect.deps
, deps
是持有该 effect 的依赖数组, deps
的结构如下
清除完依赖后, 开始重新收集依赖, 把当前 effect 追加到 effectStack, 将 activeEffect 设置为当前的 effect, 然后调用 fn 并且返回 fn() 的结果
第 4 步提过到: "只有当 target 代理对象的 get
方法被触发时, 才会真正执行 track
", 至此才是真正的触发了 target
代理对象的 get
方法, 执行了track
方法然后收集到了依赖
等到 fn
执行结束, finally 阶段, 把当前的 effect 弹出, 恢复 effectStack 和 activeEffect, Vue3 整个响应式的流程到此结束
三、知识点
activeEffect 的作用
我的理解是为了暴露给 onTrack
方法, 来整体看一下 activeEffect 出现的地方:
let activeEffect;
function effect(fn, options = EMPTY_OBJ) {
const effect = createReactiveEffect(fn, options);
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 省略部分代码 ...
try {
activeEffect = effect;
return fn();
}
finally {
activeEffect = effectStack[effectStack.length - 1];
}
};
// 省略部分代码 ...
return effect;
}
function track(target, type, key) {
if (activeEffect === undefined) {
return;
}
let dep = targetMap.get(target).get(key); // dep 是存储 effect 的 Set 数组
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
-
在
fn
执行前,activeEffect
被赋值为当前effect
-
在
fn
执行时的依赖收集阶段, 获取 targetMap 中的 dep (存储 effect 的 Set 数组), 并暴露给options.onTrack
接口
effect 和 stop
@vue/reactivity
提供了 stop 函数, effect
可以被 stop 函数终止
const obj = reactive({ foo: 0 });
const runner = effect(() => {
console.log(obj.foo);
});
// effect 被执行一次, 输出 0
// obj.foo 被赋值一次, effect 被执行一次, 输出 1
obj.foo ++;
// 停止 effect
stop(runner);
// effect 不会被触发, 无输出
obj.foo ++;
watchEffect 和 effect
watchEffect
来自@vue/runtime-core
,effect
来自@vue/reactivity
watchEffect
基于effect
实现watchEffect
会维护与组件实例的关系, 如果组件被卸载,watchEffect
会被stop
, 而effect
不会被stop
watchEffect 和 invalidate
watchEffect
接收的副作用函数, 会携带一个 onInvalidate
的回调函数作为参数, 这个回调函数会在副作用无效时执行
watchEffect(async (onInvalidate) => {
let valid = true;
onInvalidate(() => {
valid = false;
});
const data = await fetch(obj.foo);
if (valid) {
// 获取到 data
} else {
// 丢弃
}
});
ref
JS数据类型:
- 基本类型: String、Number、Boolean、Null、Undefined、Symbol
- 引用数据类型: Object、Array、Function
因为 Proxy 只能代理对象, reactive
函数的核心又是 Proxy, 所以 reactive 不能代理基本类型
对于基本类型需要用 ref 函数将基本类型转为对象:
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
其中 __v_isRef
参数用来标志当前值是 ref 类型, isRef
的实现如下:
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
这样做有个缺点, 需要多取一层 .value
:
const myRef = ref(0);
effect(() => {
console.log(myRef.value);
});
myRef.value = 1;
这也是 Vue ref 语法糖提案的原因, 可以参考 如何评价 Vue 的 ref 语法糖提案?
reactive 和 shallowReactive
shallowReactive
用来定义浅响应数据, 深层次的对象值是非响应式的:
const target = {
foo: {
bar: 1
}
};
const obj = shallowReactive(target);
effect(() => {
console.log(obj.foo.bar);
});
obj.foo.bar = 2; // 无效, reactive 则有效
obj.foo = { bar: 2 }; // 有效
readonly 和 shallowReadonly
类似 shallowReactive
, 深层次的对象值是可以被修改的
markRaw 和 toRaw
markRaw 的作用是让数据不可被代理, 所有携带 __v_skip
属性, 并且值为 true
的数据都会被跳过:
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
toRaw 的作用是获取代理对象的原始对象:
const obj = {};
const reactiveProxy = reactive(obj);
console.log(toRaw(reactiveProxy) === obj); // true
computed
const myRef = ref(0);
const myRefComputed = computed(() => {
return myRef.value * 2;
});
effect(() => {
console.log(myRef.value * 2);
});
当 myRef
值变化时, computed 会执行一次, effect 会执行一次
当 myRef
值未变化时, computed 不会执行, effect 依旧会执行
如果你有问题欢迎留言和我交流, 阅读原文