vue3响应式实现
一、vue中响应式是什么?
在vue的官网中是这样解释的:Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。简单的说Vue中的响应式系统是一种机制,它能够自动追踪数据的变化,并且在数据变化时更新视图。这意味着当应用程序的状态发生变化时,用户界面会自动反映出这些变化,而无需开发者手动操作DOM。
二、响应式是如何实现的?
我们都知道在vue2中响应式的实现是通过 Object.defineProperty
来实现的。通过这种方法实现数据劫持,当使用数据是触发getter,修改数据时触发setter。
const object = {
name: '张三'
}
Object.defineProperty(object,'name',{
// getter
get (){},
// setter
set (){}
})
console.log(object);
我们可以看到通过Object.defineProperty
代理的数据多了两个方法,我们在使用数据和修改数据就会触发这两个方法,从而完成一些事。
然而通过 Object.defineProperty
实现的响应式也是有缺陷的,在vue的官网中是这样说的:由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
这是什么意思呢?我来举个例子:
<template>
<div id="app">
<ul>
<li v-for="(val, key, index) in person" :key="index">
{{ key }} - {{ val }}
</li>
</ul>
<button @click="addProperty">为对象增加属性</button>
<hr />
<ul>
<li v-for="(item, index) in arr" :key="index">
{{ item }}
</li>
</ul>
<button @click="addArrItem">为数组添加元素</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
person: {
name: "张三",
age: 18,
},
arr: ["张三", "李四"],
};
},
methods: {
addProperty() {
this.person.gender = "男";
console.log(this.person);
},
addArrItem() {
this.arr[2] = "王五";
console.log(this.arr);
},
},
};
</script>
我们可以发现:
-
当为 对象 新增一个没有在
data
中声明的属性时,新增的属性 不是响应性 的 -
当为 数组 通过下标的形式新增一个元素时,新增的元素 不是响应性 的
为什么会这样呢?我们知道:Object.defineProperty
只可以监听 指定对象的指定属性的 getter 和 setter,所以我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty
来监听 getter
和 setter
,所以 新增的属性将失去响应性。(如果想要增加具备响应性的新属性,vue官方也知道这样的缺陷,所以他们提供了Vue.set 方法实现为向响应式对象中添加属性)
因为 Object.defineProperty
存在的问题,所以 vue3
中修改了这个核心 API
,改为使用Proxy进行实现。下面是使用Proxy来构建响应式数据:
let product = {
price: 10,
quantity: 2
}
// new Proxy 接收两个参数(被代理对象,handler 对象)。
// 生成 proxy 代理对象实例,该实例拥有《被代理对象的所有属性》 ,并且可以被监听 getter 和 setter
// 此时:product 被称为《被代理对象》,proxyProduct 被称为《代理对象》
const proxyProduct = new Proxy(product, {
// 监听 proxyProduct 的 set 方法,在 proxyProduct.xx = xx 时,被触发
// 接收四个参数:被代理对象 target,指定的属性名 key,新值 newVal,最初被调用的对象 receiver
// 返回值为一个 boolean 类型,true 表示属性设置成功
set(target, key, newVal, receiver) {
// 为 target 附新值
target[key] = newVal
// 触发 effect 重新计算
effect()
return true
},
// 监听 proxyProduct 的 get 方法,在 proxyProduct.xx 时,被触发
// 接收三个参数:被代理对象 target,指定的属性名 key,最初被调用的对象 receiver
// 返回值为 proxyProduct.xx 的结果
get(target, key, receiver) {
return target[key]
}
})
在以上代码中,我们可以发现,Proxy
和 Object.defineProperty
存在一个非常大的区别,那就是:
-
proxy
: -
-
Proxy
将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。 -
当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改
-
代理对象 的任何一个属性都可以触发
handler
的getter
和setter
-
-
Object.defineProperty
: -
-
Object.defineProperty
为 指定对象的指定属性 设置 属性描述符 -
当想要修改对象的指定属性时,可以使用原对象进行修改
-
通过属性描述符,只有 被监听 的指定属性,才可以触发
getter
和setter
-
所以vue3通过Proxy实现响应式就不会存在新增属性时失去响应性的问题。
三、vue3响应系统
下面我们来具体看看vue3是怎么实现响应式的。这里就先说说reactive
reactive方法:
简单的说reactive方法主要做了:创建proxy对象,把 proxy
加到了 proxyMap
里面目的是在下次创建时能够查看当前对象是否已经创建过响应式了,如果创建过就将该proxy对象返回,最后返回了 proxy
。
下面我们根据测试代码来看看vue3源码是怎么实行的:
测试代码:
const { reactive, effect} = Vue
const obj = reactive({
name: '张三'
})
effect(()=> {
document.querySelector('#app').innerHTML = obj.name
})
首先我们们先来说说effect
:effect 意思是影响、作用所以effect是使我们传递的函数发生作用执行函数,通常情况下我们是不会直接使用effect
的,因为effect
是一个底层的API
,在我们使用Vue3
的时候Vue
默认会帮我们调用effect
,所以我们的关注点通常都是在reactive
上。但是reactive
需要和effect
配合使用才会有响应式的效果,所以我们需要了解一下effect
的作用。
源码:
首先来看reactive方法:
export function reactive(target: object) {
// 如果对只读的就直接返回
if (isReadonly(target)) {
return target
}
// 创建响应式对象
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
reactive方法很简单,使用createReactiveObject来创建响应式对象。我们接下来来看createReactiveObject:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果 target 不是对象类型数据,那么直接返回 target
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 从proxyMap里获取该对象,若干获取到该对象的响应式就直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
// 这里说只有特定的值才能进行观测
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 创建Proxy对象,并将其存储到proxyMap里,将proxy对象返回
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
总的来说reactive的逻辑还是很清晰的。我们接下来看看最后返回的proxy对象的创建,targetType
根据枚举值也就只有3
个值,最后走向代理的也就只有两种情况:targetType
为1
的时候,这个时候target
是一个普通的对象或者数组,这个时候使用baseHandlers
;targetType
为2
的时候,这个时候target
是一个集合类型,这个时候使用collectionHandlers
。collectionHandlers
:和baseHandlers
是通过传参传入的,我们来看看源码
baseHandlers
:
export const mutableHandlers: ProxyHandler<object> = {
get, //通过createGetter创建
set, //通过createSetter创建
deleteProperty,
has,
ownKeys
}
-
get
:当访问代理对象的属性时调用。 -
set
:当设置代理对象的属性时调用。 -
deleteProperty
:当删除代理对象的属性时调用。 -
has
:当使用in
操作符检查代理对象属性时调用。 -
ownKeys
:当获取代理对象的所有属性的键时调用。
collectionHandlers
:
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
讲完了reactive我们来说说effect:
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 创建一个响应式副作用函数
const _effect = new ReactiveEffect(fn)
if (options) {
// 将配置项合并到响应式副作用函数上,extend是Object.assign方法
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 如果没有配置项,或者配置项中没有 lazy 属性,或者配置项中的 lazy 属性为 false,就直接调用执行副作用
if (!options || !options.lazy) {
_effect.run()
}
// 将 _effect.run 的 this 指向 _effect
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
这里的关键点有两个部分:创建一个响应式副作用函数const _effect = new ReactiveEffect(fn)
;返回一个runner
函数,可以通过这个函数来执行响应式副作用函数。接下来我们看看ReactiveEffect这个类,类里最重要的是run方法。run
方法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖。
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
// 调度器,用于控制副作用函数何时执行
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
// 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
// 记录父级作用域为当前活动的 ReactiveEffect 对象
this.parent = activeEffect
// 将当前活动的 ReactiveEffect 对象设置为 “自己”
activeEffect = this
// 将 shouldTrack 设置为 true (表示是否需要收集依赖)
shouldTrack = true
// effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行副作用函数,并返回执行结果
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
// 执行完毕会将 effectTrackDepth 减 1
trackOpBit = 1 << --effectTrackDepth
// 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”
activeEffect = this.parent
// 将 shouldTrack 设置为上一个值
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
我们知道 fn
函数其实就是effect 传入的匿名函数,run方法最重要的作用就是执行该函数,所以 document.querySelector('#app').innerHTML= obj.name
就会被执行,obj.name 就会会 触发 getter
,所以接下来我们就会进入到 mutableHandlers
的 createGetter
中 在该代码中,触发了该方法 const res = Reflect.get(target, key, receiver)
,注意:接下来触发了 track
函数,该函数是一个重点函数, track
在此为 跟踪 的意思,我们来看它内部都做了什么。
const targetMap = new WeakMap();
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
// 如果 targetMap 中没有 target对应的键值对,就会创建一个 Map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 如果 depsMap 中没有 key,就会创建一个 Set
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 其中 effect 即为 activeEffect 即 fn 函数
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
// 在 trackEffects 函数内部,核心也是做了两件事情:为 dep(targetMap[target][key] 得到的 Set 实例) 添加了 activeEffect函数;b. 为 activeEffect 函数的 静态属性 deps,增加了一个值 dep,即:建立起了 dep 和 activeEffect 的联系
trackEffects(dep, eventInfo)
}
}
由以上逻辑可知,整个 effect
主要做了3 件事情:
-
生成
ReactiveEffect
实例 -
触发
fn
方法,从而激活getter
-
建立了
targetMap
和activeEffect
之间的联系
-
-
dep.add(activeEffect)
-
activeEffect.deps.push(dep)
-
接下来就是触发依赖trigger
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 获取 targetMap 中的 depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
// 创建一个数组,用来存放需要执行的 ReactiveEffect 对象
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 执行所有的 副作用函数
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// 执行 add、delete、set 操作时,就会触发的依赖变更
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
// 创建一个 eventInfo 对象,主要是调试的时候会用到
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// 如果 deps 的长度为 1,就会直接执行
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
// 如果 deps 的长度大于 1,这个时候会组装成一个数组,然后再执行
// 这个时候调用就类似一个调用栈
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
tigger
函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给triggerEffects
函数去执行。
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
// 执行 computed 依赖
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 执行其他依赖
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 最主要的是以下代码,这里是执行依赖函数:如果 effect 是一个调度器,就会执行 scheduler
if (effect.scheduler) {
effect.scheduler()
} else {
// 否则直接执行 effect.run()
effect.run()
}
}
}
总结
到这里我们了解了 :reactive
;effect
函数;obj.name = xx
表达式。这三块代码背后,vue
究竟都做了什么。虽然整个的过程比较复杂,但是如果我们简单来去看,其实内部的完成还是比较简单的:
-
创建
proxy
-
收集
effect
的依赖 -
触发收集的依赖