#yyds干货盘点#Vue3的reactive

时间:2022-10-07 21:01:44

最近一阶段在学习Vue3,Vue3中用 ​reactive​​ref​ 等方法将数据转化为响应式数据,在获取时使用 ​track​ 往 ​effect​ 中收集依赖,在值改变时,使用 ​trigger​ 触发依赖,执行对应的监听函数,这次就先来看一下 ​reactive​ 的源码。

前置知识:

在 ​​reactive​​ 中会根据传入数据的类型,做一下分类:

function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}

const enum TargetType {
// 无效的 比如基础数据类型
INVALID = 0,
// 常见的 比如object Array
COMMON = 1,
// 集合类型比如 map set
COLLECTION = 2
}

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​,就是常见类型(),传入 ​Map​​Set​等就是集合类型,其他的比如传入基础数据,就是无效类型 ​(INVALID)​,基础数据要用 ​ref​

export const enum ReactiveFlags {
SKIP = '__v_skip', // 标记一个不能转换为响应式数据的对象
IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
IS_READONLY = '__v_isReadonly', // 标记一个只读对象
IS_SHALLOW = '__v_isShallow', // 标记只有一层响应的浅可读写对象
RAW = '__v_raw' // 标记获取原始值
}

还有 Vue3 中数据会有一些标记,比如上面的 ​​getTargetType​​​ 方法,当target被标记为 ​​ReactiveFlags.SKIP​​​ 或是 不可拓展的,则会返回 ​​TargetType.INVALID​​​,无法创建代理,因为Vue需要对Target代理附加很多东西,如果是不可拓展的则会附加失败;或是用户主动调用 ​​markRaw​​ 等方法将数据标记为非响应式数据,那么也无法创建代理。

进入正题:

reactive的源码在官方源码的​​packages/reactivity/src/reactive.ts​​文件中,源码中提供了四个Api来创建reactive类对象:

  • reactive:创建可深入响应的可读写对象
  • readonly:创建可深入响应的只读对象
  • shallowReactive:创建只有第一层响应的浅可读写对象(其他层,值改变视图不更新)
  • shallowReadonly:创建只有一层响应的浅只读对象

它们都是调用 ​​createReactiveObject​​ 方法来创建响应式对象,区别在于传入不同的参数:

function reactive(target: object) {
// 如果是只读的话直接返回
if (isReadonly(target)) {
return target
}
return createReactiveObject(
// 目标对象
target,
// 标识是否是只读
false,
// 常用类型拦截器
mutableHandlers,
// 集合类型拦截器
mutableCollectionHandlers,
// 储了每个对象与代理的map关系
reactiveMap
)
}

function shallowReactive(target) {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
);
}

// readonly、shallowReadonly代码这里省略,区别在于传入的拦截器等参数不同

export const reactiveMap = new WeakMap<Target, any>()

​createReactiveObject​ 代码如下:

function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果代理的数据不是对象,则直接返回原对象
if (!isObject(target)) {
return target
}

// 如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}

// 查看当前代理对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
// proxyMap 是一个全局的缓存WeakMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}

// 如果当前对象无法创建代理,则直接返回源对象
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}

// 根据targetType 选择集合拦截器还是基础拦截器
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

// 向全局缓存Map里存储
proxyMap.set(target, proxy)
return proxy
}

这个方法里最主要的就是使用 ​new Proxy​ 创建代理,根据 ​targat​ 类型不同使用不同的拦截器。其中的 ​getTargetType​方法是用来获取传入target的类型:

export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}

看完了入口函数,接下来就是创建Proxy对象的过程了,Vue3会根据getTargetType返回的数据类型来选择是使用collectionHandlers集合拦截器还是baseHandlers常用拦截器,原因下面讲到集合拦截器的时候再说。

常用拦截器baseHandlers:
  1. ​get​​ 拦截器:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) { // 获取当前是否是reactive
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) { // 获取当前是否是readonly
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) { // 获取当前是否是shallow
return shallow
} else if (
// 如果获取源对象,在全局缓存WeakMap中获取是否有被创建过,如果创建过直接返回被代理对象
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}

// 是否是数组
const targetIsArray = isArray(target)

// arrayInstrumentations相当于一个改造器,里面定义了数组需要改造的方法,进行一些依赖收集等操作
// 如果是数组,并且访问的方法在改造器中,则使用改造器获取
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
}

// 如果不是只读则收集依赖,Vue3中用track收集依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

// shallow只有表层响应式,不需要下面去深度创建响应了
if (shallow) {
return res
}

// 如果获取的值是ref类型
if (isRef(res)) {
// 如果是数组 并且 是int类型的 key,则返回,否则返回.value属性
return targetIsArray && isIntegerKey(key) ? res : res.value
}

if (isObject(res)) {
// *获取时才创建相对应类型的代理,将访问值也转化为reactive,不是一开始就将所有子数据转换
return isReadonly ? readonly(res) : reactive(res)
}

return res
}
}

大致步骤为:
// 1. 一开始的几个 if else 是用来获取特定属性时返回特定值的
// 2. 如果是数组,用 arrayInstrumentations 特殊处理
// 3. 获取结果,如果不是 只读 的,就 track 收集依赖
// 4. 如果获取的值是 对象 ,将访问的值也转化

注意点是当代理类型是 ​readonly​ 时,不会收集依赖。

​Vue3对于深层次的对象是使用时才创建的​,还有如果结果是ref类型,则需要判断是否要获取它的.value类型,举个????:

const Name = ref('张三')
const Array = ref([1])

const data = reactive({
name: Name,
array: Array
})

console.log(Name) // RefImpl类型
console.log(data.name) // 张三
console.log(data.array[0]) // 1

Vue3中使用 ​arrayInstrumentations​对数组的部分方法做了处理,为什么要这么做呢? 对于 ​push​​pop​、 ​shift​、 ​unshift​、 ​splice​ 这些方法,写入和删除时底层会获取当前数组的length属性,如果我们在effect中使用的话,会收集length属性的依赖,当使用这些api是也会更改length,就会造成死循环:

let arr = []
let proxy = new Proxy(arr, {
get: function(target, key, receiver) {
console.log(key)
return Reflect.get(target, key, receiver)
}
})
proxy.push(1)
/* 打印 */
// push
// length


// 当把这个代码注释掉时
// if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
// return Reflect.get(arrayInstrumentations, key, receiver);
// }

const arr = reactive([])

watchEffect(() => {
arr.push(1)
})

watchEffect(() => {
arr.push(2)
// 上面的effect里收集了对length的依赖,push又改变了length,所以上面的又会触发,以此类推,死循环
})

// [1,2,1,2 ...] 死循环
console.log(arr)

对于 ​​includes​​​、 ​​indexOf​​​、 ​​lastIndexOf​​,内部会去获取每一个的值,上面讲到如果获取出来的结果是Obejct,会自动转换为reactive对象:

let target = {name: '张三'}

const arr = reactive([target])

console.log(arr.indexOf(target)) // -1

因为实际上是 ​​reactive(target)​​​ 和 ​​target​​ 在对比,当然查不到。

  1. ​set​​ 拦截器
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
// 获取旧数据
let oldValue = target[key];
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}
// 如果当前不是shallow并且不是只读的
if (!shallow && !isReadonly(value)) {
if (!isShallow(value)) {
// 如果新value本身是响应对象,就把他变成普通对象
// 在get中讲到过如果取到的值是对象,才转换为响应式
// vue3在代理的时候,只代理第一层,在使用到的时候才会代理第二层
value = toRaw(value);
oldValue = toRaw(oldValue);
}
// 如果旧的值是ref对象,新值不是,则直接赋值给ref对象的value属性
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 这里不触发trigger是因为,ref对象在value被赋值的时候会触发写操作,也会触发依赖更新
oldValue.value = value;
return true;
}
}
const hadKey = 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不存在就 触发add类型的依赖更新
trigger(target, "add" /* ADD */, key, value);
}
else if (hasChanged(value, oldValue)) {
// key存在就触发set类型依赖更新
trigger(target, "set" /* SET */, key, value, oldValue);
}
}
return result;
};
}

set中还有一个要注意的地方就是 ​​target === toRaw(receiver)​​,这主要是为了处理代理对象的原型也是代理对象的情况:

const child = reactive({})

let parentName = ''
const parent = reactive({
set name(value) {
parentName = value
},
get name() {
return parentName
}
})

Object.setPrototypeOf(child, parent)

child.name = '张三'

console.log(toRaw(child)) // {name: 张三}
console.log(parentName) // 张三

当这种时候,如果不加上这个判断,由于子代理没有name这个属性,会触发原型父代理的set,加上这个判断避免父代理也触发更新。

集合拦截器collectionHandlers:

集合类型的数据比较特殊,其相关实例方法Proxy没有提供相关的捕获器,但是因为方法调用属于属性获取操作,所以都可以通过捕获get操作来实现,所以Vue3也只定义了get拦截:

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
)
}
}

之前的文章《代理具有内部插槽的内建对象》中说过Proxy代理具有内部插槽的内建对象,访问Proxy上的属性会发生错误。Vue3中是如何解决的呢?

Vue3中新创建了一个和集合对象具有相同属性和方法的普通对象,在集合对象 get 操作时将 target 对象换成新创建的普通对象。这样,当调用 get 操作时 ​​Reflect​​ 反射到这个新对象上,当调用 set 方法时就直接调用新对象上可以触发响应的方法,这样访问的就不是Proxy上的方法,是这个新对象上的方法:

function createInstrumentations() {
const mutableInstrumentations: Record<string, Function> = {
get(key: unknown) {
return get(this, key)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
}

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
})

return [
mutableInstrumentations
]
}

接下来看一看几个具体的拦截器,其实都大同小异了:

  1. ​get​​ 拦截器:
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// 如果出现readonly(reactive())这种嵌套的情况,在readonly代理中获取到reactive()
// 确保get时也要经过reactive代理
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 确保 包装后的key 和 没包装的key 都能访问得到
if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
target.get(key)
}
}

集合拦截器里把 ​​key​​​ 和 ​​rawKey​​ 都做了处理,保证都能取到数据:

let child = {
name: 'child'
}

const childProxy = reactive(child)

const map = reactive(new Map())

map.set(childProxy, 1234)

console.log(map.get(child)) // 1234
console.log(map.get(childProxy)) // 1234
  1. ​set​​ 拦截器:
// Map set拦截器
function set(this: MapTypes, key: unknown, value: unknown) {
// 存origin value
value = toRaw(value);
// 获取origin target
const target = toRaw(this);
const { has, get } = getProto(target);

// 查看当前key是否存在
let hadKey = has.call(target, key);
// 如果不存在则获取 origin
if (!hadKey) {
key = toRaw(key);
hadKey = has.call(target, key);
} else if (__DEV__) {
// 检查当前是否包含原始版本 和响应版本在target中,有的话发出警告
checkIdentityKeys(target, has, key);
}

// 获取旧的value
const oldValue = get.call(target, key);
// 设置新值
target.set(key, value);
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value);
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return this;
}
  1. ​has​​ 拦截器:
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
// 获取代理前数据
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 如果key是响应式的都收集一遍
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

// 如果key是Proxy 那么先访问 proxyKey 在访问 原始key 获取结果
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
  1. ​forEach​​ 拦截器:
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
// 劫持传递进来的callback,让传入callback的数据转换成响应式数据
return target.forEach((value: unknown, key: unknown) => {
// 确保拿到的值是响应式的
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}