Vue3源码学习——响应式原理
最近一段时间一直在学习Vue3相关的内容,本篇是Vue3源码学习系列的第一篇,响应式原理。
样例
这里我们先来看一下Vue3中的基本语法是怎么书写的:
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
这是官网的一个简单的例子,我们通过 reactive 函数定义了响应式对象,当 state.count 发生改变的时候页面展示也会即时的变化。
reactive
这里我们先看一下 vue 提供的 reactive 函数内部是什么样子的,为什么用了 reactive 就可以成为响应式的呢?
function reactive(target: object) {
// 如果对象是只读,则直接返回,不需要进行响应式
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
这里我们可以看到,reactive 函数的关键则在于 createReactiveObject:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
...
// 如果不是对象,直接返回 开发环境下会给出报警提示
// 如果已经是响应式对象,则直接返回
...
// proxyMap 中已经存入过 target,直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
// 是某些特定类型也直接返回
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
这里对于 createReactiveObject 函数只保留了一些关键逻辑,其他内容在注释中已经说明,主要就是判断一些特定情况,在特定情况下直接返回。其中有一个 proxyMap 的容器,这个容器主要用于缓存target,如果我们已经对target代理过了,那么如果再次代理同一个target对象时,则直接从容器中返回不需要再重新进行new Proxy操作了。
具体例子可以是这样的:
var obj = {a: 1}
var x = reactive(obj)
var y = reactive(obj) // 这里y的创建过程走的就是缓存,是从proxyMap中直接取出来的。
关于targetType的值,我们可以看看源码这方面的设定:
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
}
}
这里就可以看出,如果传入的 target 是 Object 类型,对应的 TargetType 就是TargetType.COMMON,那么传入 proxy 的第二个参数 handler 就是 baseHandlers;如果是 Map 这些,则传入的 handler 是 collectionHandlers。
我们先关注 Object 的情况,看一下传入的 baseHandlers:
// 这里mutableHandlers就是createReactiveObject函数中的baseHandlers参数
const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
这里我们重点关注get和set:
get
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 对key的一些判断
...
// 判断是否为数组
const targetIsArray = isArray(target)
if (!isReadonly) {
/*
如果是数组,同时key是存在于arrayInstrumentations中,arrayInstrumentations是一个汇总了
操作数组方法的对象
*/
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 如果是hasOwnProperty,则返回一个重写后的hasOwnProperty函数
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver)
// Symbol Key 不做依赖收集
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// 进行依赖收集
track(target, TrackOpTypes.GET, key)
}
// 如果是shallow则只追踪一层,直接返回
if (shallow) {
return res
}
if (isRef(res)) {
// 如果访问的是数组,key是整数
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
/*
如果是对象,则根据readonly判断,如果readonly为false则将res转变为响应式,
从而能够实现深层次的依赖追踪
*/
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
getter函数中,隐藏掉一些代码只保留下主干内容,我们可以看到这个函数中主要就是对target的类型进行了判断:
- 如果是对象,在非 readonly 的情况下,则会递归的调用 reactive 函数,从而实现深层次的依赖追踪
- 如果是数组,会额外判断一次是否访问了 arrayInstrumentations 中的 key,arrayInstrumentations 部分的代码如下:
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 对数组中的每一项进行依赖收集
track(arr, TrackOpTypes.GET, i + '')
}
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果没有查到,那么将传入的数据还原为原始数据再执行一次
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 暂停追踪
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
// 继续追踪
resetTracking()
return res
}
})
return instrumentations
}
这里的数组方法会分为两类:
- 一类是includes, indexOf, lastIndexOf,这三个方法是数组的查询方法,不会对数组本身造成影响,当调用这些方法的时候,会对数组中的每一项进行依赖收集。
- 第二类是push, pop, shift, unshift, splice,这一类方法会改变原数组,为防止出现死循环,所以在执行前暂停了依赖的追踪,在方法执行之后,恢复追踪功能。
这里提一下 proxy 的优点,在我们使用 push 等方法改变数组的时候,proxy 是可以追踪到数组的 length 的。
track:
根据前面,我们可以发现,target不管是对象,还是数组,在进行逻辑处理的时候都会用到 track 函数,我们大概根据名字也已经猜到 track 就是依赖收集(副作用收集)的函数,那接下来我们具体看一下这个函数是如何工作的:
const targetMap = new WeakMap<any, KeyToDepMap>()
function track(target: object, type: TrackOpTypes, key: unknown) {
// shouldTrack 是否应该收集依赖, activeEffect 当前激活的effect
if (shouldTrack && activeEffect) {
// targetMap是一个存放有所有reactive的依赖容器的容器
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果不存在,就创建一个
targetMap.set(target, (depsMap = new Map()))
}
// dep: 从depsMaps中取出某个属性key所导致的副作用函数集合,如果不存在,那就创建一个,是一个Set
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
track 函数的主要逻辑,就是追踪、收集:
- 当追踪某个对象的某个属性时,首先判断全局容器 targetMap 中是否有这个对象,如果有就取出来,如果没有就创建一个该对象的容器 depsMap,(depsMap 的 key 就是对象的属性,每个 key 对应的value 是 一个收集副作用函数的容器)。
- 然后对于具体访问的是某个属性,判断 depsMap 上是否有这个属性,如果有则取出来,如果没有就创建一个用于收集这个属性所有副作用函数的容器dep,这个dep是一个Set类型。
- 最后就是通过trackEffects将副作用添加进dep中,完成依赖收集。
看一下trackEffects:
function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
...
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
...
}
}
到这里,在get阶段的依赖收集逻辑基本完成,下面可以看一下set阶段的逻辑:
set
const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
// 只读不可修改
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
// 不是浅响应式
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// ...
} else {
// 在浅响应式下,对象被设置为原始值
}
const hadKey =
// 是数组的话,判断key是否超出长度,不是数组判断是否存在于对象上
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 设置修改值
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
if (!hadKey) {
// 如果key不存在于原数据上
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果key存在于原数据上
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
setter函数整体的逻辑是:
- 首先,判断属性值是否只读,是否浅响应式,如果不是浅响应式,则将原值和要修改的值还原为原始数据
- 然后,判断属性是否存在于对象 target 上,根据存在与否,调用 trigger 函数,并传入不同的参数,去派发通知,触发执行副作用函数
trigger函数:
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) {
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
if (key !== void 0) {
deps.push(depsMap.get(key))
}
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
...
// 暂时可以先不管
} else if (isIntegerKey(key)) {
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
...
// 暂时可以先不管
}
break
case TriggerOpTypes.SET:
...
// 暂时可以先不管
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// triggerEffects 触发deps中副作用函数的执行
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
trigger函数代码比较长,但整体在做的事情是明确的:
- 首先,看容器 targetMap 中有没有收集过 target 相关的内容,如果没有收集过,说明还没有关于target 的副作用函数需要执行,直接返回
- 然后,根据传入的 type 类型,分情况来维护 deps 这个用于放置副作用函数的数组。
- 最后,通过 triggerEffects 函数,来执行 deps 中的副作用函数。
总结
这里我们对 reactive函数 的整体逻辑做了分析,其实大体上在做的事情就是在get阶段进行副作用函数的收集,在set阶段去执行收集到的副作用函数,从而实现响应式。
我们平时对于响应式最直观的感受就是,页面和数据的双向绑定。这一功能中涉及到的数据修改,即时反映在界面的逻辑是:
- 首先,将 template模板 编译为 AST,之后会将 AST 转为 render函数。
- 然后,会将 render函数 会作为副作用函数执行,得到vnode节点,在这个过程就会对一些字段进行访问,从而实现了在get阶段对这些字段所导致的副作用函数进行收集。
- 之后,当我们对这些字段进行修改时,也就是在set阶段,会对收集到的副作用函数effect进行依次的执行,而这其中会有上面的render函数重新运行,得到一个新的vnode树。
- 最后,经过patch新老节点,这样,当我们数据层改变之后就能即时的反应在界面上了。
参考文章:
转载自:https://juejin.cn/post/7239111080164261947