Vue3对Vue2中的响应式原理使用Proxy
进行了重写,本文我们将对Vue3响应式的源码进行分析。
Vue3中提供了四种创建不同类型的响应式数据的方式,分别是:
在Vue3中我们可以通过Composition API而不是Options API去显示的创建一个响应式对象.
setup(){
const orginData = {count: 1}
const state = reactive(orginData)
return {
state
}
}
当我们使用reactive函数创建一个响应式数据的流程如下:
下面我们对其中涉及的关键函数进行解析。
export function reactive(target: T): UnwrapNestedRefs
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果传入的是只读类型的响应式对象 直接返回该响应式对象
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
首先对传入的参数进行类型判断,如果传入的是一个只读类型的响应式对象,则直接返回该对象,否则就进入createReactiveObject函数,这个函数的主要功能就是根据传入的不同参数,创建不用类型的响应式对象。接着我们来详细分析一下createReactiveObject的创建流程:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler
) {
if (!isObject(target)) {
return target
}
// 如果已经是proxy对象则直接返回,有个例外,如果是readOnly作用于响应式 则继续
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
const proxyMap = isReadonly ? readonlyMap : reactiveMap
// 已经有了对应的proxy映射 直接
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
// 只有在白名单中的数据类型才可以被响应式
if (targetType === TargetType.INVALID) {
return target
}
// 通过Proxy API劫持target对象,把它变成响应式
const proxy = new Proxy(
target,
// Map Set WeakMap WeakSet用collectionhandlers代理 Object Array用baseHandlers代理
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 存储一个原始类型和proxy数据类型的映射
proxyMap.set(target, proxy)
return proxy
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
在这个函数前面有对目标类型的限制判断函数,如果响应式的数据类型是以上六种则在可响应式处理的白名单中,否则目标对象的类型是无效的。
Object
和Array
数据类型则TargetType=COMMON
对应的处理函数就是baseHandlers
,如果目标对象是Map
、Set
、WeakMap
、WeakSet
这四种数据类型则对应的处理函数是collectionHandlers
。 用一个流程图表示:以上是对创建响应式函数的分析,接下来我们将分析针对Object的数据类型使用baseHandlers的处理流程。
对应的源码文件: vue-next/packages/reactivity/src/reactive.ts
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
前面的流程中我们分析到如果目标对象是Object
和Array
会调用baseHandlers,我们是通过reactive函数向createReactiveObject中传入参数,实际上我们执行的是mutableHandlers
。
这个方法实际上就是对目标对象的一些访问、删除、查询、设置的操作的劫持,这里我们着重分析一些set和get函数,因为这两个函数中涉及到依赖收集和派发更新的操作。分析的时候我们会删除部分代码,只分析主流程。
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
export const mutableHandlers: ProxyHandler
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
}
}
当我们访问对象的属性的时候会触发get函数,这个函数中的主要步骤有三个,首先会使用Reflect.get进行求值, 然后判断是否是只读的,如果不是就调用track
进行依赖收集,然后对求值的结果进行判断,如果是对象则递归调用reactive
或者readonly
对结果继续进行响应式处理,最后将获取的结果返回。
注意:这里和Vue2响应式处理的方式有所不同,这也是Vue3响应式在初始化的时候性能优化的一个点。
Object
类型,如果是的话就会递归的调用Observer
将子对象也变成响应式。function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 1.先获取oldValue
const oldValue = (target as any)[key]
// 2.设置新值
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 派发更新
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
当我们更新响应式对象的属性的时候会触发set
函数,set
函数内部的主要步骤也是三个,首先获取这个属性的oldValue
,然后通过Reflect.set对属性进行赋值操作,最后调用trigger
进行派发更新,在派发更新阶段如果是新增属性则trigger
的type是add
,如果value!==oldValue
则trigger
的type是set
。
源代码文件:vue-next/packages/reactivity/src/baseHandlers.ts
在进行分析依赖收集的流程之前我们要先弄明白一个概念targetMap
, 它是一个WeakMap
的数据结构,主要用于存放用来存储原始数据->key->deps
这样的一个映射关系,比如:
const orginData = {count:1,number:0}
const state = reactive(orginData)
const ef1 = effect(() => {
console.log('ef1:',state.count)
})
const ef2 = effect(() => {
console.log('ef2:',state.number)
})
const ef3 = effect(() => {
console.log('ef3:', state.count)
})
state.number = 2
首先orginData
作为targetMap的键存储,value是一个depsMap它是一个Map数据结构,用于存放对这个原始数据的所有依赖,depsMap中的key是原始数据对应的key,value是deps它是一个Set数据结构,用于存放所有对这个Key的依赖。我们用一张图来表示上面的依赖映射关系:
理清完上述对应关系之后我们来分析一下track内部的实现机制:
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 获取当前target对象对应depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取当前key对应dep依赖
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前的effect作为依赖
dep.add(activeEffect)
// 当前的effect收集dep集合作为依赖
activeEffect.deps.push(dep)
}
}
依赖收集的流程很简单,先获取当前target
对应的依赖映射,如果没有就以当前的target
为键,Map数据结构为值设置一个, 然后根据depsMap
获取当前key所对应的依赖集合,如果没有就以当前的key为键,Set数据结构为值设置一个,然后判断当前正在激活的副作用函数在不在当前key对应的依赖集合中,如果不在,就将当前激活的副作用函数(activeEffect
)push到当前key对应的依赖集合中,弄清楚依赖映射表的对应关系之后,在分析依赖收集的流程就简单很多。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map | Set
) {
// 获取当前target的依赖映射表
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
// 声明一个集合和方法,用于添加当前key对应的依赖集合
const effects = new Set()
const add = (effectsToAdd: Set | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect))
}
}
// 根据不同的类型选择使用不同的方式将当前key的依赖添加到effects
if (type === TriggerOpTypes.CLEAR) {
...判断逻辑省略
}
// 声明一个调度方法
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// 循环遍历 按照一定的调度方式运行对应的依赖
effects.forEach(run)
}
当响应是对象的属性值发生改变的话,就会触发set函数,在对属性设置新值的时候会调用trigger进行派发更新,派发更新的逻辑也很简单:
上述的示例代码我们打开控制台可以发现:
当我们向effect中传递一个原始的函数的时候,会立即执行一次,如果函数中有对响应式数据的访问操作的话,此时会将当前的effect作为依赖收集到访问属性的依赖集合中。那么effect内部的运行机制究竟如何呢?下面我们来具体分析一下。
export function effect(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
通过上述源码我们可以看到,effect在响应式中起到了桥梁的作用, 主要功能就是对原始函数进行包装,将它变成响应式的副作用函数。主要接受两个参数,第一个参数就是我们上面传递的函数,第二个参数是配置参数,用于控制第一个参数的调度行为。首先判断当前的函数是否已经是副作用函数,如果是就将获取当前响应式函数的原始函数。然后将参数传递给createReactiveEffect
来创建响应式副作用函数。最后执行返回的响应式副作用函数。下面我们来看一下createReactiveEffect
函数是如何创建响应式副作用函数的:
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined // 用于保存当前激活的副作用函数
function createReactiveEffect(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect {
const effect = function reactiveEffect(): unknown {
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
// 开启全局shouldtrack,允许依赖收集
enableTracking()
// 将当前的effect压栈
effectStack.push(effect)
activeEffect = effect
// 执行原始的函数
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
effectStack
栈用于存放被激活的effect和activeEffect
用于存放当前被激活的effect。activeEffect
,并将传入的原始函数执行一遍, 执行完毕之后将当前的effect从effectStack
栈中弹出,并将activeEffect
的值改为栈中的最后一个。raw
用于保存当前副作用函数的原始函数,_isEffect
是否是副作用函数,deps
副作用函数中订阅的属性。 为什么要用一个栈保存激活的副作用函数呢?直接使用一个变量保存被当前的effect不是更好吗?
我们先看一下下面的示例:const parent = effect(() => {
const child= effect(() => {
console.log('child:', state.count)
})
console.log('parent:', state.number)
})
state.count = 2
当一个副作用函数嵌套一个副作用函数的时候,当我们执行到内部的副作用函数的时候,如果仅仅是使用一个变量保存当前的激活副作用函数,当内部的副作用函数执行完毕之后,当我们执行下面的代码的时候此时的activeEffect
指向就不正确了,所以我们这里使用一个栈的结构来保存激活的副作用函数, 因为函数的执行也是一个出栈入栈的顺序,因此我们设计一个栈来保存激活的副作用函数,当我们执行这个副作用函数内部嵌套的副作用函数时候,此时的activeEffect
就是里面嵌套的副作用函数,内部的副作用函数执行完毕之后,从effectStack
栈中将内部的副作用函数弹出,此时的activeEffect
就指向了外部的副作用函数,这样指向就是正确的。
对应的源码文件: vue-next/packages/reactivity/src/effect.ts
至此对于一个Object类型的响应式数据的创建流程已经分析完毕,在分析源码的过程中,我们可以发现: