Vue3 源码剖析之响应性
所谓的响应性, 就是 Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式的更新 DOM。
我们知道, 在 Vue3 里, 创建响应式数据的方式有:
reactive()
ref()
computed()
另外, watch()
还可以侦听响应式数据从而执行一些副作用。
reactive
我们先来看一下 reactive 函数的具体实现过程:
js
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
js
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
createReactiveObject
可以看到, reactive 内部实际是调用了 createReactiveObject 函数:
js
function createReactiveObject(target: object, baseHandlers: ProxyHandler<any>, proxyMap: WeakMap<object, any>) {
// 如果该实例已经被代理,则直接读取即可
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 未被代理则生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
// 为 Reactive 增加标记
proxy[ReactiveFlags.IS_REACTIVE] = true
// 缓存代理对象
proxyMap.set(target, proxy)
return proxy
}
js
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
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
}
// target already has corresponding Proxy
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
}
const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject 函数最主要的职责就是返回一个 proxy 代理原始对象。
mutableHandlers
接下来,我们继续看 Proxy 处理器对象 mutableHandlers 的实现:
js
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
}
js
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
}
其中最主要的就是 get 和 set 函数:
js
const get = createGetter()
const set = createSetter()
js
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
依赖收集: get 函数
依赖收集发生在数据访问的阶段, 由于我们用 Proxy API 劫持了数据对象, 所以当这个响应式对象属性被访问的时候就会执行 get 函数, 我们来看一下 get 函数的实现:
js
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
// 利用 Reflect 得到返回值
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
}
}
js
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly ? (shallow ? shallowReadonlyMap : readonlyMap) : shallow ? shallowReactiveMap : reactiveMap).get(
target
)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
可以看到, 源码中处理了很多边缘情况, 但其核心就是执行 track 函数收集依赖。
依赖触发: set 函数
依赖触发发生在数据更新的阶段, 由于我们用 Proxy API 劫持了数据对象, 所以当这个响应式对象属性更新的时候就会执行 set 函数:
js
function createSetter() {
return function set(target: object, key: string | symbol, value: unknown, receiver: object) {
// 利用 Reflect.set 设置新值
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key)
return result
}
}
js
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)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
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 函数的核心就是执行 trigger 函数触发依赖。
DANGER
写的好啰嗦, 看源码把。。。有精力再来搞这
所谓的响应性其实指的就是: 当响应式数据触发 setter 时执行 fn 函数
想要达到这样一个目的, 就必须: getter 时能够收集当前的 fn 函数, 以便在 setter 时执行相应的 fn 函数
但是对于收集而言, 如果仅仅是吧 fn 存起来是不够的, 我们还需要知道, 当前的这个 fn 是哪个响应式数据对象的哪个属性对应的, 只有这样, 才可以在该属性触发 setter 时, 准确的执行响应性
WeakMap:
- key: 响应式对象
- value : Map 对象
- key : 响应式对象的指定属性
- value : 指定对象的指定属性的 执行函数 fn
对于 reactive 响应性数据而言, 我们知道它:
- 是通过 proxy 的 setter 和 getter 来实现数据监听
- 需要配合 effect 函数进行使用
- 基于 WeakMap 完成的依赖收集和触发
- 可以存在一对多的依赖关系
不足之处:
- reactive 只能对复杂数据类型进行使用
- reactive 的响应性数据, 不可以进行解构
ref
对于 ref 函数, 会返回 RefImpl 类型实例
在该实例中, 会根据传入的数据类型进行分开处理
- 复杂数据类型: 转化为 reactive 返回的 proxy 实例
- 简单数据类型: 不做处理
无论我们执行 obj.value.name 还是 obj.value.name = xxx 本质上都是触发了 get value
之所以会进行响应性是因为 obj.value 是一个 reactive 函数生成的 proxy
ref 函数本质上是生成了一个 RefImpl 类型的实例对象, 通过 get 和 set 标记处理了 value 函数
为什么 ref 类型的数据, 必须通过 .value 访问值呢?
- 因为 ref 需要处理简单数据类型的响应性, 但是对于简单数据类型而言, 它无法通过 proxy 建立代理
- 所以 vue 通过 get value() 和 set value() 定义了两个属性函数, 通过主动触发这两个函数的形式进行依赖收集和依赖触发
- 所以必须通过 .value 来确保响应性
computed 原理
watch 原理
- 调度器 scheduler 在 watch 中很关键