本文是 vue3 源码分析系列的第一篇文章,主要介绍 vue3 的响应式原理,基于项目代码的v3.2.10 版本。本文将通过一个简单的例子,演示vue3 如何使用 reactive 函数和effect 函数实现数据代理、依赖跟踪和自动更新机制。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>reactivetitle>
head>
<body>
<div id="app">div>
<script src="../packages/reactivity/dist/reactivity.global.js">script>
<script>
let { reactive, effect } = VueReactivity;
const user = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
state: 'NY'
}
};
let state = reactive(user)
effect(() => {
app.innerHTML = state.address.city
});
setTimeout(() => {
state.address.city = 'California'
}, 1000);
script>
body>
html>
通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 reactive 函数出发。
reactive 函数是vue3提供的一个核心函数,它可以接收一个对象作为参数,返回一个对象的响应式代理。
function reactive(target) {
// 如果target不是一个对象,直接返回
if (target && typeof target !== 'object') {
console.warn(`value cannot be made reactive: ${String(target)}`)
return target
}
// 调用createReactiveObject函数,根据对象的类型,创建不同的代理处理器
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
reactive 函数首先会判断 target 是否是一个对象,如果不是就直接返回不进行代理。这是因为 Proxy 对象只能代理对象类型的值,不能代理基本类型的值。如果 target 是一个对象,就会调用 createReactiveObject 函数,根据对象的类型,创建不同的代理处理器。
createReactiveObject 函数是一个内部函数,它根据对象的类型,创建不同的代理处理器。
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers
) {
// 根据target的类型,选择合适的代理处理器
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 创建一个Proxy对象,返回给reactive函数
const proxy = new Proxy(target, handlers)
// 返回代理对象
return proxy
}
createReactiveObject 函数会根据 target 的类型,选择合适的代理处理器。接下来,我们来看看代理处理器的定义。
vue3提供了两种代理处理器:baseHandlers 和 collectionHandlers。baseHandlers 用于代理普通对象,collectionHandlers 用于代理数组。我们先来看看 baseHandlers 的定义。
baseHandlers是一个用于代理普通对象的代理处理器,它包含了get、set、deleteProperty等方法,用于拦截对象的属性访问和修改。
const baseHandlers: ProxyHandler<any> = {
get,
set,
deleteProperty,
has,
ownKeys
}
可以看到,baseHandlers 是一个对象,它的属性是一些函数,这些函数就是代理处理器的方法。接下来,我们来看看这些方法的具体实现。
get 方法是一个用于拦截对象的属性读取的方法,它接收三个参数:target、key和receiver。get方法的定义如下:
function get(target: Target, key: string | symbol, receiver: object) {
// 获取target的属性值
const targetIsArray = isArray(target)
const res = targetIsArray ? target[key as number] : Reflect.get(target, key, receiver)
// 如果key是一个symbol,或者是一个不可变的属性,直接返回属性值
if (isSymbol(key) || !isWriteable(key)) {
return res
}
// 调用track函数,建立依赖关系
track(target, TrackOpTypes.GET, key)
// 如果属性值是一个对象,返回一个响应式代理³[3]
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid circular
// dependency
readonly(res)
: reactive(res)
: res
}
get 方法会判断 key 是否是一个 symbol,或者是一个不可变的属性,如果是,就直接返回属性值。这是为了防止对一些内置的或者只读的属性进行代理。然后,get 方法会调用 track 函数,建立依赖关系。如果属性值是一个对象,就返回一个响应式代理。这是为了实现深层次的响应式。接下来,我们来看看 track 函数的定义。
track 函数是一个用于建立依赖关系的函数,它接收三个参数:target、type和key。track函数的定义如下:
function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果当前没有激活的effect函数,直接返回
if (!activeEffect) {
return
}
// 获取targetMap,如果不存在,就创建一个新的WeakMap对象
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取keyMap,如果不存在,就创建一个新的Map对象
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果dep中没有当前的effect函数,就添加到dep中,并将target和key添加到effect函数的依赖集合中
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
track 函数首先会判断当前是否有激活的 effect 函数,如果没有,就直接返回,不进行依赖的建立。这是为了防止对一些不需要响应式的属性进行跟踪。接着,track 函数会获取targetMap,targetMap 是一个全局变量,用于存储对象和属性的映射关系。然后,track 函数会获取 keyMap,keyMap 是一个 Map 对象,用于存储属性和函数的映射关系。最后,track 函数会将 target 和 key 添加到 effect 函数的依赖集合中。这样,就完成了依赖的建立。接下来,我们来看看 effect 函数的定义。
effect 函数是一个用于创建响应式函数的函数,它接收一个函数作为参数,返回一个包装后的函数。effect函数的定义如下:
function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions): ReactiveEffect<T> {
// 创建一个新的effect函数,调用run方法执行原始的函数
const _effect = new ReactiveEffect(fn, NOOP, () => {
if (_effect.dirty) {
_effect.run()
}
})
// 如果options中没有设置lazy为true,就立即执行effect函数
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
effect 函数会创建一个新的 effect 函数,调用 run 方法执行原始的函数。run 方法是一个内部方法,它会设置 activeEffect 为当前的 effect 函数,并使用 try…finally 语句保证 activeEffect 的恢复。函数 ReactiveEffect 不是本文的重点。在 computed 章节会详细解释。接下来,我们来看看 set 方法的定义。
set 方法是一个用于拦截对象的属性修改的方法,它接收四个参数:target、key、value和receiver。set方法的定义如下:
function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
// 获取target的旧值
const oldValue = target[key as any]
// 判断target是否是一个数组,且key是否是一个合法的下标
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 设置target的属性值,如果成功,返回true,否则返回false
const result = Reflect.set(target, key, value, receiver)
// 如果target是一个原始对象,直接返回结果
if (target === toRaw(receiver)) {
if (!hadKey) {
// 如果target之前没有这个属性,就触发添加操作
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果target的属性值发生了变化,就触发更新操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
} else {
// 如果target是一个代理对象,就触发更新操作
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
set 方法会根据 target 是否有这个属性,以及属性值是否发生了变化,触发不同的操作。如果 target 之前没有这个属性,就触发添加操作;如果 target 的属性值发生了变化,就触发更新操作。接下来,我们来看看 trigger 函数的定义。
trigger 函数是一个用于触发响应式更新的函数,它接收五个参数:target、type、key、newValue和oldValue。trigger函数的定义如下:
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown
) {
// 获取targetMap,如果不存在,直接返回
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 创建一个新的Set对象,用于存储需要执行的函数
const effects = new Set<ReactiveEffect>()
// 定义一个函数,用于将dep中的函数添加到effects中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
}
// 根据type和key,选择需要添加的函数
if (type === TriggerOpTypes.CLEAR) {
// 如果type是清空操作,就将所有的函数添加到effects中
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// 如果key是length,且target是一个数组,就将大于新值的下标对应的函数添加到effects中
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// 否则,就将key对应的函数添加到effects中
add(depsMap.get(key))
}
// 遍历effects中的函数,依次执行,完成更新
effects.forEach(effect => {
effect()
})
}
trigger 函数首先会获取 targetMap,如果不存在,就直接返回,不进行更新。这是为了防止对一个没有依赖的对象进行更新。接着,trigger 函数会创建一个新的 Set 对象,用于存储需要执行的函数。然后,trigger函数会定义一个函数,用于将 dep 中的函数添加到 effects 中。最后,trigger函数会遍历effects中的函数,依次执行。
collectionHandlers 是一个用于代理数组的代理处理器,它继承了 baseHandlers,但是重写了get方法,用于拦截数组的特殊方法,如push、pop、splice等。collectionHandlers 的定义如下:
const collectionHandlers: ProxyHandler<any> = extend({}, baseHandlers, {
get(target: Target, key: string | symbol, receiver: object) {
// 获取target的属性值
const res = Reflect.get(target, key, receiver)
// 如果key是一个symbol,或者是一个不可变的属性,直接返回属性值
if (isSymbol(key) || !isWriteable(key)) {
return res
}
// 如果key是一个数组的特殊方法,返回一个包装后的函数
if (mutateMethods.has(key)) {
return function(...args) {
// 获取target的旧值
const oldTarget = toRaw(target)
// 获取target的旧长度
const oldLength = oldTarget.length
// 调用原始的方法,获取返回值
const result = res.apply(target, args)
// 获取target的新长度
const newLength = target.length
// 调用track函数,建立依赖关系
track(target, TrackOpTypes.GET, key)
// 根据key和参数,触发不同的操作
switch (key) {
case 'push':
case 'unshift':
// 如果key是push或unshift,就触发添加操作
trigger(target, TriggerOpTypes.ADD, oldLength, args)
break
case 'pop':
case 'shift':
// 如果key是pop或shift,就触发删除操作
trigger(target, TriggerOpTypes.DELETE, oldLength - 1, void 0)
break
case 'splice':
// 如果key是splice,就根据参数,触发添加或删除操作
if (args.length > 2) {
trigger(target, TriggerOpTypes.ADD, oldLength, args.slice(2))
}
if (args.length > 0) {
trigger(target, TriggerOpTypes.DELETE, oldLength - 1, void 0)
}
break
}
// 返回结果
return result
}
} else {
// 否则,调用baseHandlers的get方法,返回属性值
return baseHandlers.get(target, key, receiver)
}
}
})
可以看到,collectionHandlers 首先会获取 target 的属性值,然后判断key是否是一个 symbol,或者是一个不可变的属性,如果是,就直接返回属性值。接着,collectionHandlers 会判断key是否是一个数组的特殊方法,如果是,就返回一个包装后的函数。这个函数会在调用原始的方法之前,获取 target 的旧值和旧长度,在调用原始的方法之后,获取 target 的新长度,并根据 key 和参数,触发不同的操作。这是为了实现数组的响应式。最后,如果 key 不是一个数组的特殊方法,就调用baseHandlers 的get方法,返回属性值。这是为了复用 baseHandlers 的逻辑。这样,就完成了collectionHandlers 的定义。
总结下以上的内容:
reactive:函数返回一个对象的响应式代理。reactive 函数会调用createReactiveObject函数,根据对象的类型,创建不同的代理处理器。reactive函数的参数必须是一个对象,否则会报错。
createReactiveObject:函数根据对象的类型,创建不同的代理处理器。如果对象是一个数组,会创建一个 collectionHandlers 对象;如果对象是一个普通对象,会创建一个 baseHandlers对象。代理处理器是一个包含 get、set、deleteProperty 等方法的对象,用于拦截对象的属性访问和修改。
effect:函数接收一个函数作为参数,返回一个包装后的函数。effect函数会调用track函数,将当前的函数和当前访问的属性建立依赖关系。effect函数还会调用trigger函数,当依赖的属性发生变化时,触发函数的重新执行。
track:将当前的函数和当前访问的属性建立依赖关系。track函数会使用一个全局变量activeEffect,存储当前的函数。track函数还会使用一个全局变量targetMap,存储对象和属性的映射关系。track函数会检查targetMap中是否有当前对象,如果没有,就创建一个新的Map对象,用于存储属性和函数的映射关系。track函数会检查属性Map中是否有当前属性,如果没有,就创建一个新的Set对象,用于存储依赖的函数。track函数会将activeEffect添加到Set中,完成依赖的建立。
trigger:当依赖的属性发生变化时,触发函数的重新执行。trigger函数会使用targetMap,根据对象和属性,找到对应的函数Set。trigger函数会遍历Set中的函数,依次执行,完成更新。