阅读准备
在阅读 reactive 源码之前,我们需要知道它的特性,了解特性推荐阅读单例测试源码或者是阅读官网的 API,推荐阅读单例,在后面阅读时才能更好理解。vue中响应式数据是通过Proxy
来实现的,可以通过我之前写的Proxy 和 Reflect来了解它的特性和一些注意事项。
在vue3
中使用创建reactive
类对象一共有四种api
,分别对应不同的功能的对象
reactive
创建可深入响应的可读写对象readonly
创建可深入响应的只读对象shallowReactive
创建只有表层(一层)的浅可读写对象shallowReadonly
创建只有表层(一层)的浅只读对象
使用reactive
和readonly
时会自动解构对象且不包括是数组子项的Ref
对象,无论解构有多深,而 shallow
类的对象则不会自动解构,只有一层也不会自动解构。比如
import { reactive, ref } from 'vue'
const observed1 = reactive({
a: ref(1),
b: {
c: ref(1)
},
e: ref({ c: ref(1) })
d: [ref(1), { a: ref(2)}] as const
})
const observed2 = reactive({
d: [ref(1)] as const
})
observed2._a = ref({v: 1})
// Number 1
console.log(observed1.a)
// Number 1
console.log(observed1.b.c)
// Number 1
console.log(observed1.e.c)
// RefImpl value: Number 1
console.log(observed1.d[0])
// Number 1
console.log(observed1.d[1].a)
// RefImpl value: Number 1
console.log(observed2.d[0])
// Number 1
console.log(observed2._a.v)
const shallowObserved = shallowReactive({
a: ref(1)
})
// RefImpl value: Number 1
console.log(observed.a)
reactive
深入对象内部创建子代理时遇到下面这些情况不会创建
- 不可更改属性描述符的对象不会创建,比如调用
Object.prevenExtensions
使对象不可扩展、Object.freeze
冻结对象,Object.seal
封闭对象 - 在对象上声明
__v_skip
属性为true
- 对象调用
vue
的markRaw
reactive
对象与readonly
对象互相转换时,readonly
对象不可转为reactive
;reactive
可以转为readonly
,但是转化的对象使用isReactive
和isReadonly
函数调用时都是返回true
。比如下方代码:
import { reactive, readonly, isReactive, isReadonly } from "vue";
const are = reactive({ a: 2 });
const aro = readonly(are);
// true
console.log(isReactive(aro));
// true
console.log(isReadonly(aro));
const bre = readonly({ b: 2 });
const bro = reactive(bre);
// false
console.log(isReactive(bro));
// true
console.log(isReadonly(bro));
思考实现
如果让我们基于Proxy
来实现vue
的响应式数据的话,我们会怎么设计呢,如果是我会这样设计,因为是响应式的,那么我们必须知道是哪些属性被使用,在它更改时需要重新执行监听函数,我们可以把监听函数跟收集函数统一使用一个函数来执行,而收集的函数里面收集到的key
可能是动态的,所以每次更改都要清空依赖重新收集一遍依赖,加上Proxy
的特性所以当收集函数使用get
获取数据时收集当前使用key
,当外部set
修改数据时查看当前set
是查看key
是否被收集,如果被收集了,则set
之后重新触发收集函数再次收集,而vue3
中这个收集函数就是effect
。当然真实使用时肯定不止get
,set
这里为了方便理解,我们先按简单的看。逻辑如下图所示
为了充分覆盖用户的数据,在Proxy
获取的数据应该始终是Proxy
,否则当用户修改里面的子对象时无法监听,比如下方这种情况
const rt = reactive({a: {b: 3}})
// 代理内部也应该将 rt.a 转化为代理
const ra = ra.a
effect(() => {
// 收集到依赖
console.log(ra.b)
})
// 触发重新执行effect
ra.b = 4
还有一个原则,就是存储到原始数据时始终不应该存储Proxy
后的数据,否则会造成混乱。
入口
在vue3
中创建(除去ref
)中创建响应式对象一共有四种方法,分别是reactive
、shallowReactive
、readonly
、shallowReadonly
这四种方法的入口源码如下
// 创建reactive对象
export function reactive(target: object) {
// 如果reactive进入的是readonly的话直接返回,保持只读
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
// 创建reactive对象
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
// shallowReactive创建,不会自动解包,只有根级别属性才是反应式的
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}
// 创建readonly代理,如果已经是reactive可以附加readonly标识
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
在上面源码我们可以看到,这四种创建响应式对象最终都是使用createReactiveObject
方法来实现的,只是参数不一样, 第一个参数是加工对象,第二个参数是标识只读和可读写,为true
时为只读,第三四个参数是代理的拦截器,一个是常用类型拦截器,一个是集合类型拦截器,这里集合数据标识Map
、Set
、WeakMap
、WeakSet
为什么将他们区分实现,可以查看之前我写的代理具有内部插槽的内建对象,而且集合类型是通过api
来获取和存储数据的,并且存储时不会经过代理,代理只能拦截到获取api
和特定的属性(如size
)。最后一个参数就是每个代理类型(是否shallow(浅处理)
和是否只读)的存储池了,里面存储了每个对象与代理的map
关系。
我们看看四种代理类型的 Map 声明:
// origin与proxy的映射
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()
存储map
都是用WeakMap
当用户丢弃这个代理和代理对象时会自动进行内存回收,方便管理。
接下来我们看看真正的入口源码:
// 创建代理对象
function createReactiveObject(
// 要代理的数据
target: Target,
// 是否是只读
isReadonly: boolean,
// 基础代理器
baseHandlers: ProxyHandler<any>,
// 集合代理器
collectionHandlers: ProxyHandler<any>,
// 映射map
proxyMap: WeakMap<Target, any>
) {
// 如果代理的数据不是obj则直接返回原对象
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// 如果传入的已经是代理了,而且不是readonly -> reactive的转换则直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target;
}
// 查看当前origin对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 如果当前对象无法创建代理则直接返回origin
const targetType = getTargetType(target);
if (targetType === TargetType.INVALID) {
return target;
}
// 查看当前origin type选择集合拦截器还是基础拦截器
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
// 缓存
proxyMap.set(target, proxy);
return proxy;
}
在createReactiveObject
实现用到了ReactiveFlags
,这些都是当前代理的特定标识,在后面代理具体实现中会附加上,稍后讲到具体实现时能看到,这些标识分别是:
// reactive 标志符常量
export const enum ReactiveFlags {
// 是否阻止成为代理属性
SKIP = '__v_skip',
// 是否是reactive属性
IS_REACTIVE = '__v_isReactive',
// 是否是readonly属性
IS_READONLY = '__v_isReadonly',
// mark target
RAW = '__v_raw'
}
创建代理对象的Target
必须为对象,不能为基础对象,reactive
类型可以转化为readonly
但是不能逆转,同一个对象不会创建两次,会在map
中查看当前对象是否已经创建,如果创建了直接返回缓存中的。我们可以看到vue
还会给每个对象打上TargetType
类型,如果为INVALID
的则标识为不可创建,直接返回源对象,TargetType
的源码如下:
// reactive origin类型常量
const enum TargetType {
// 无效的 比如基础数据类型
INVALID = 0,
// 常见的 比如object Array
COMMON = 1,
// 集合类型比如 map set
COLLECTION = 2
}
// 获取origin 类型辅助函数
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
}
}
const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
// 获取origin的类型
function getTargetType(value: Target) {
// 如果mark了不可reactive或者是不可扩展的直接返回无效
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
可以看到Vue
中的Proxy
对象不能创建原型被冻结的对象,这个很好理解,因为Vue
需要对Target
代理附加很多东西,原型被冻结将会附加失败。被用户主动mark
的对象也不能创建。markRaw
的源码如下:
// 记录对象不可代理
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
createReactiveObject
会根据getTargetType
返回的数据类型来选择是使用collectionHandlers
集合拦截器还是baseHandlers
常用拦截器。创建完成后会将代理缓存起来,方便下次获取和查询。到这里入口就已经看完了。接下来我们看看一些辅助函数,这些函数比较简单这里就不再讲解了:
// 是否是reactive
export function isReactive(value: unknown): boolean {
// 如果当前value是readonly则查看raw是不是reactive
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
// 是否是readonly
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
// 查看当前是否是proxy,为reactive 或readonly则为内部代理
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
// 获取数据原始值
export function toRaw<T>(observed: T): T {
// 获取reactive原始对象,因为可能会有 readonly(reactive({}))写法,所以需要深入获取
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
// 将对象转化为reactive
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
// 对象转化为readonly
export const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as Record<any, any>) : value
在思考实现中我们讲到Proxy
会收集和重新调用effect
,在vue
中做了进一步的细分,会用枚举区分因为什么操作收集,因为什么操作重新执行,定义的常量如下
// 因为什么收集
export const enum TrackOpTypes {
// 如 observed.a
GET = 'get',
// 如 a in observed
HAS = 'has',
// 如 Object.keys(observed)
ITERATE = 'iterate'
}
// 因为什么重新触发
export const enum TriggerOpTypes {
// 如修改 observed.a = 1
SET = 'set',
// 如新增 observed.b = 3
ADD = 'add',
// 如 delete observed.a
DELETE = 'delete',
// 在集合时使用 如map.clear()
CLEAR = 'clear'
}
在vue
中收集依赖统一使用track
函数,重新触发effect
是统一使用trigger
函数,track
和trigger
除了收集操作枚举外还需要传入key
,操作什么key
引起的收集和触发,如果是因为ownKey
(如Object.key(...)
)引起的收集会使用一个内置的常量ITERATE_KEY
替代key
,关于track
和trigger
里面具体实现我们讲到effect
章节时再讲解,这里主要将代理的实现,使用方式如下:
// 在vue中除了`Map`的`keys`,其他迭代的都用这个symbol来代替key,
const ITERATE_KEY = Symbol(__DEV__ ? "iterate" : "");
// 收集什么变量,什么操作,收集到什么key
track(target, TrackOpTypes.ITERATE, ITERATE_KEY);
track(target, TrackOpTypes.GET, "a");
track(target, TrackOpTypes.HAS, "a");
// 什么变量引起触发,什么操作,变量什么key引起触发,新值、旧值(如果有)
trigger(target, TriggerOpTypes.ADD, key, value);
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
常用拦截器
在上面中我们看到常用拦截器对象有四种分别是mutableHandlers
、readonlyHandlers
、shallowReactiveHandlers
、shallowReadonlyHandlers
分别对应reactive
、readonly
、shallowReactive
、shallowReadonly
类型的代理,接下来我们看看源码实现:
// 创建get handler
const get = /*#__PURE__*/ createGetter();
// 创shallow的 get handler
const shallowGet = /*#__PURE__*/ createGetter(false, true);
// 创建readonly的 get Handler
const readonlyGet = /*#__PURE__*/ createGetter(true);
// 创建shallowReadonly的 get handler
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);
const set = /*#__PURE__*/ createSetter();
const shallowSet = /*#__PURE__*/ createSetter(true);
// reactive拦截器
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
};
// readonly拦截器
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true;
},
deleteProperty(target, key) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true;
},
};
// shallow reactive拦截器
export const shallowReactiveHandlers = /*#__PURE__*/ extend(
{},
mutableHandlers,
{
get: shallowGet,
set: shallowSet,
}
);
export const shallowReadonlyHandlers = /*#__PURE__*/ extend(
{},
readonlyHandlers,
{
get: shallowReadonlyGet,
}
);
拦截器的get
、set
都是由createGetter
、createSetter
创建的,只是参数差异,在createGetter
函数中第一个参数标识是否只读,第二个参数标识是否shallow
,createSetter
不带是否只读因为,只读的Proxy
不能set
,只需要提示不能更改,同理has
拦截器也是没必要添加。可能你会想只读的proxy
按理来说也不需要get
因为对象不会更改,也就不需要收集数据,对了一半,get
中确实不会收集,但是vue
中代理不仅需要在get
拦截器上收集数据而且需要附加Flags
和创建子对象proxy
(如果不是shallow
的话)。
在上面每个函数前都添加了/*#__PURE__*/
这段注释的作用就是提示打包器这些变量时纯的,当没有使用到这些变量时可以剔除,减小打包后包的大小。
get 拦截器
我们在思考实现中提到get
拦截器大概职责是在effect
时收集依赖,除此之外我们还需要保证Proxy get
获取到的都是proxy
,因为Proxy
只是代理当前对象,子对象也需要深入创建,比如const oa = reactive({a: { b: 2 }})
需要保证oa.a
获取到的也是代理,接下来我们看createGetter
源码:
// 不track收集和创建代理的的keys 包括原型,ref标识 vue标识
const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
// 获取所有内置的Symbol
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(isSymbol)
)
// 创建get handler
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 附加flags
// 获取是否是获取当前是否是reactive | readonly
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
// 如果是获取源对象,通过代理和源数据WeakMap获取是否有被创建过
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)
// 如果当前key是内置symbol key或者是不需要处理的key则直接返回
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 如果不是只读的则track收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// shallow类直接返回,不需要神UR创建
if (shallow) {
return res
}
if (isRef(res)) {
// 是否应该解包ref,如果是不是数组或者是数组但是访问非int key则应该解包
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// 获取时才创建相对应类型的代理,将访问值也转化为reactive
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
createGetter
采用闭包包裹当前代理类型,当使用特定Flag
属性获取时可以方便的返回当前代理的类型。当获取Proxy
代理前数据时必须是已加是加工为代理的对象才能获取。
当代理类型是只读时是不会track(收集)
依赖的,因为只读代理不会更改,收集没意义。如果是shallow
的代理收集后则直接返回结果,因为只浅代理,只创建一层代理,如果不是shallow
并且子属性是对象的话就动态创建当前类型的子对象代理。
vue
的代理深创建子对象proxy
时是使用时才创建,比如const ra = readonly({a: {b: 2}})
使用这段代码时会创建外层{a: ...}
代理,{b: 2}
这层并不会马上创建,而是当你使用ra.b
时会在get
拦截器中创建。
vue
中有一些保留属性是不会收集和创建子代理的例如__proto__(原型)
,__v_isRef(是否是ref)
,__isVue(是否是vue)
。
除此之外还有一些系统自带的Symbol
也不会处理,比如Symbol.toStringTag
、Symbol.iterator
,为什么这里不需要收集呢,因为这里在实现时如果用到代理对象也会被收集到,而比如下面这段代码中,在具体的Symbol
执行时也是在effect
函数内,也会被收集到,而数组内也是通过下标来get
也会收集到具体的index
。
const pig = {
name: '猪',
get [Symbol.toStringTag]() {
return this.name
}
}
effect(() => {
// 经过[Symbol.toStringTag]函数收集到name属性依赖
pig.toString()
})
pig.name = '佩奇'
当获取到的数据是ref
数据时如果不是数组子项的话代理还会自动解包。不知道为啥设计成数组子项不自动解包,我猜测是因为防止reactive([1, ref(2)])
使用时都是数字造成混乱。
Array 改造器
当代理对象是Array
时还需要还需要对一些的api
做特殊处理,因为数组通过一些api
获取会引发混乱,当用户使用indexOf
、lastIndexOf
、includes
时因为底层是通过this[index]
来获取的,this
就是proxy
,在vue
中如果当前子项是对象会转化为代理,就会造成通过原始对象找不到在数组中的位置,比如没处理的话下方代码会出现这种情况
const target = {};
const observed = reactive([target]);
// -1, 因为比对的是 reactive(target) === target
observed.indexOf(target);
在通过push
、pop
、unshift
、splice
写入和删除时底层会获取当前数组的length
属性,如果在effect
中使用时自然也会收集这个属性的依赖,当使用这些api
是也会更改length
,这时容易造成死循环,所以这些方法也需要特殊处理,比如没处理的话下方代码会出现这种情况
const observed: number[] = reactive([]);
// e1: 采集到length依赖,并更改length
effect(() => {
observed.push(1);
});
// e2: 采集到length依赖,并更改length,e1依赖length收到触发重新执行
effect(() => {
observed.push(2);
});
// [ 1, 2, 1 ]
console.log(observed);
下面我们看看Array
改造器源码
// 数组的函数改造器
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// 需要对比获取源数据来比对的api
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 获取源数组
const arr = toRaw(this) as any
// 因为不通过代理获取,所以需要手动track收集每个子项
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
}
}
})
// 为了某些情况会无限循环,对arry的length会改变的阻止收集
// 属于对数组的更改,但是写在effect时互相引用时会容易造成无限循环
;(['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
}
为了确保indexOf
、includes
、lastIndexOf
等 api 能够获取到正确结果会将代理转化为Target
对象,api
传入对象先查找,如果查找不到再讲传入数据转化为raw
查找比对。push
, pop
, shift
, unshift
, splice
等 api 会在调用时暂停收集,因为他们本身也是更改数据,不用收集。
track
采用栈的方式管理,恢复是恢复上一次的状态,我们看一下源码
// 当前是否开启跟踪
let shouldTrack = true;
// 跟踪栈
const trackStack: boolean[] = [];
// 暂停跟踪
export function pauseTracking() {
trackStack.push(shouldTrack);
shouldTrack = false;
}
// 启用跟踪
export function enableTracking() {
trackStack.push(shouldTrack);
shouldTrack = true;
}
// 恢复上一次跟踪,如果没有上一次默认为true
export function resetTracking() {
const last = trackStack.pop();
shouldTrack = last === undefined ? true : last;
}
到这里get
拦截器就看完了,但是没有看到与effect
关联的部分,都是统一track
出去的,可以猜测到区分是否在effect
中get
应该是在effect
这个函数中实现的,这个我们后面再讲。
set 拦截器和 delete 拦截器
在思考实现时我们说到set
拦截器主要职责是重新触发effect
的执行,在vue
中是使用trigger
这个函数来实现的,delete
拦截器也是属于修改同样的职责,接下来我们看看createSetter
和deleteProperty
函数源码:
// 创建set 拦截器
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取旧数据
let oldValue = (target as any)[key]
// 如果当前不是shallow并且不是只读的
if (!shallow && !isReadonly(value)) {
// 获取target属性,如reacitve(ref(1))获取回来就是ref(1)
value = toRaw(value)
oldValue = toRaw(oldValue)
// 并且target不是数组并且旧数据是ref,新数据不是则直接赋值value
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
}
// 查看当前更新key是否存在
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果是通过原型链触发的修改,则不trigger
if (target === toRaw(receiver)) {
// 查看是否存在,存在是否修改,再触发trigger
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
// del 代理器
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
set
和delete
拦截器相对于get
拦截器就简单很多了,set
和delete
拦截器是可读写代理进入的,主要是修改,新增还是删除属性,当trigger
时把具体细节传出。set
还会自动解包ref
并赋值。
set
时如果是shallow
或者readonly
类型时不做任何处理,直接保持原样设置。如果Target
不是数组,旧值是ref
新值不是,则直接更新旧值ref
的value
,这里为什么不用trigger
直接返回,这是是因为ref
里面已经trigger
了,关于ref
的实现我们下一章再讲。
注意:如果是数组,不管是不是下标属性,都是直接赋值,不会解包。上面get
拦截器解包逻辑不太一样,get
拦截器中如果是数组,非下标会解包,而set
拦截器不管是不是下标都不会解包,也就是说会出现下面这种情况,使用感觉不出来,但是还是要注意一下。
const observed1 = reactive([1, 2, 3] as any)
observed1._a = ref(2)
// observed._a 时会自动解包 返回2 实际上存储的是 ref(2)
observed1._a = 4
// observed._a 返回4 实际上存储的是4
const observed2 = reactive({} as any)
observed2._a = ref(2)
// observed._a 时会自动解包 返回2 实际上存储的是 ref(2)
observed2._a = 4
// observed._a 时会自动解包 返回4 实际上存储的是 ref(4)
上面有段target === toRaw(receiver)
来区分是否是原型上触发的判断,它为什么能区分出是否是原型呢,因为在set
拦截器中第四个参数是指向当前操作者的this
的,假如代理是附加在某个对象的原型上,那么指向的就是这个对象而不是代理。
has 拦截器和 ownKeys 拦截器
has
拦截器和ownKeys
拦截器主要职责也是track
,告诉vue
依赖了哪些属性,与get
拦截器一样内置的Symbol
不做收集。当Target
是数组时触发的ownKeys
拦截器会将当前收集的 key 当做是length
属性变化,这是因为用户可能在effect
中使用array.length
和for (const atom of array)
,可以统一使用length
一个属性来标识,这样当length
改变时统一通知就可以了,如果for (const atom of array)
使用ITERATE_KEY
会触发两次,一次时length
修改,一次时ITERATE_KEY
修改。
// has 拦截器
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key);
// 查看是否是内置的symbol如果是则不track
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key);
}
return result;
}
// getOwnPropertyNames getOwnPropertySymbols keys 拦截器
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? "length" : ITERATE_KEY);
return Reflect.ownKeys(target);
}
集合拦截器
在上面我们讲到因为Proxy
代理具有内部插槽的内建对象时和集合获取和存储数据时的限制,所以必须将拦截器与普通对象区分出来。如果是你你会怎么实现呢?
其实在上面代理Array
时已经有例子了,既然只能拦截到方法和属性,那么我们就通过改写集合Proxy
上的方法和属性来实现就行了,下面我们列出集合上的所有的方法和属性,并标注我们需要做的事情。
方法和属性 | Map | WeakMap | Set | WeakSet | 操作 |
---|---|---|---|---|---|
get | Y | Y | N | N | track ,类型:GET ,key :用户传入 |
size | Y | N | Y | N | track ,类型:ITERATE ,key :ITERATE_KEY |
has | Y | Y | Y | Y | track ,类型:HAS ,key :用户传入 |
add | N | N | Y | Y | trigger ,类型: ADD ,key :用户传入(set 中key 就是value ) |
set | Y | Y | N | N | trigger ,类型: SET 或ADD ,key :用户传入 |
delete | Y | Y | Y | Y | trigger ,类型: DELETE ,key :用户传入 |
clear | Y | N | Y | N | trigger ,类型: CLEAR ,key :undefined |
forEach | Y | N | Y | N | track ,类型:ITERATE ,key :ITERATE_KEY |
keys | Y | N | Y | N | track ,类型:ITERATE ,key :MAP_KEY_ITERATE_KEY |
values | Y | N | Y | N | track ,类型:ITERATE ,key :ITERATE_KEY |
entries | Y | N | Y | N | track ,类型:ITERATE ,key :ITERATE_KEY |
Symbol.iterator | Y | N | Y | N | track ,类型:ITERATE ,key :ITERATE_KEY |
在上面中我们看到常用拦截器有四种分别是mutableCollectionHandlers
、readonlyCollectionHandlers
、shallowCollectionHandlers
、shallowReadonlyCollectionHandlers
分别对应reactive
、readonly
、shallowReactive
、shallowReadonly
集合代理类型接下来我们看看源码实现:
// 创建集合get拦截器,附加flags,和改造api
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
);
};
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false),
};
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, true),
};
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(true, false),
};
export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
{
get: /*#__PURE__*/ createInstrumentationGetter(true, true),
};
和我们猜想一样,vue
只代理了集合的get
,因为无法拦截set
,所以通过修改方法和属性可以将我们需要做的操作附加上去。通过闭包将是否只读,是否是shallow
缓存,并生成相对应的修改器。
跟常用拦截器一样也会为集合Proxy
附加上各个flags
属性。当获取某个属性时,如果这个属性是在修改器对象上,并且也在当前集合上,那么就会从修改器中获取这个方法或者属性返回。为什么还要获取是否在是否在集合上呢,因为修改器是通过Proxy
类型分类的,而不是通过集合类型分类的,那么修改器中可能会同时存在add
、set
,如果不判断一遍是否存在集合上那么map.add
也会调用到修改器中的add
方法。接下来我们看看创建修改器的具体实现。
// 只读版本修改方法,只弹出警告
function createReadonlyMethod(type: TriggerOpTypes): Function {
return function (this: CollectionTypes, ...args: unknown[]) {
if (__DEV__) {
const key = args[0] ? `on key "${args[0]}" ` : ``
console.warn(
`${capitalize(type)} operation ${key}failed: target is readonly.`,
toRaw(this)
)
}
return type === TriggerOpTypes.DELETE ? false : this
}
}
// 创建各个版本的拦截处理器
function createInstrumentations() {
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, 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 shallowInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, false, true)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, true)
}
const readonlyInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false)
}
const shallowReadonlyInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, true, true)
},
get size() {
return size(this as unknown as IterableCollections, true)
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true)
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, true)
}
// 各个迭代器拦截器
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
false,
true
)
shallowReadonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
true
)
})
return [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations
]
}
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations
] = /* #__PURE__*/ createInstrumentations()
可以看到构建时大部分方法都是公用的,只是传入参数不同为了区分readonly
、shallow
。只读版本的Proxy
做增删改时都是弹出警告。所以真实的实现只有get
、has
、size
、set
、add
、delete
、clear
、createForEach
、createIterableMethod
等函数。
keys
, values
, entries
, Symbol.iterator
,都使用createIterableMethod
来构建的,为了区分还会将当前的method
传入。
其中集合的size
是属性,为了能够附加操作还将它改造成getter
函数。
get 修改器、size 修改器和 has 修改器
// 辅助方法,转化为shallow 什么都不做
const toShallow = <T extends unknown>(value: T): T => value
// get拦截器
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// 如果出现readonly(reactive(Map)) 的情况,在readonly代理中获取到reactive(Map),
// 确保get时也要经过reactive代理
target = (target as any)[ReactiveFlags.RAW]
// 获取 target
const rawTarget = toRaw(target)
// 获取 key target
const rawKey = toRaw(key)
// 如果key是响应式的
if (key !== rawKey) {
// 再track收集一遍响应式key
// 那么用户不管 trigger的是rawKey 还是 key都会触发得到
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
}
// 收集访问了哪个key
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
// 获取集合原型上的has方法
const { has } = getProto(rawTarget)
// 获取返回值处理函数,根据当前代理类型,将返回值转化为相对应的代理类型
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 确保 包装后的key 和没包装的key都能访问得到
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 !== rawTarget,那么就是如果出现readonly(reactive(Map))的情况,
// 确保也要经过reactive代理处理
target.get(key)
}
}
// has 拦截器 是否shallow处理方式都一样
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, 那么先获取has(keyProxy),再获取has(key)确保获取结果正确
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
// size 拦截器 是否shallow处理方式都一样
function size(target: IterableCollections, isReadonly = false) {
// 获取封装对象
target = (target as any)[ReactiveFlags.RAW]
// 收集获取迭代
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
和常用拦截器附加逻辑差不多,和将获取的值转化为当前proxy
类型的proxy
,track
依赖,不过有细微的区别。
看到(target as any)[ReactiveFlags.RAW]
会获取当前代理前的数据,这是为了防止readonly(reactive(Map))
类型的代理获取的值返回的是readonly
类型的代理,所以获取readonly
前的数据也就是reactive(Map)
进行get
这样获取也会经过reactive
代理修改器,获取回来的就是toReadonly(toReactive(Raw))
。为什么常用处理器不用呢,因为它是通过代理拦截器的第一个参数直接获取的,就是代理前的数据。
还有当Map
和WeakMap
获取数据是,会检测key
是否是Proxy
,如果是的话,会track
两次,这是因为Map
和WeakMap
的key
可以是对象,用户可能将Proxy
当做key
,当然在set
的时候会将key
转化为原始数据存储,但是在初始化的时候不会做检测,所以为了确保能正确的触发,两次收集时必要的,否则当存储是proxy
的key
的话将无法触发。例如:
const map = new Map();
const key = reactive({});
map.set(key, 1);
const rMap = reactive(map);
effect(() => {
map.get(key);
});
// 如果没有收集到 keyProxy的话将无法触发
map.set(key, 2);
set 修改器、delete 修改器和 clear 修改器
// 检查key是否是响应式的
function checkIdentityKeys(
target: CollectionTypes,
has: (key: unknown) => boolean,
key: unknown
) {
const rawKey = toRaw(key);
if (rawKey !== key && has.call(target, rawKey)) {
const type = toRawType(target);
console.warn(
`Reactive ${type} contains both the raw and reactive ` +
`versions of the same object${type === `Map` ? ` as keys` : ``}, ` +
`which can lead to inconsistencies. ` +
`Avoid differentiating between the raw and reactive versions ` +
`of an object and only use the reactive version if possible.`
);
}
}
// 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;
}
// Set add拦截器
function add(this: SetTypes, value: unknown) {
// 存origin value
value = toRaw(value);
// 获取origin target
const target = toRaw(this);
const proto = getProto(target);
// 查看是否存在要添加的value
const hadKey = proto.has.call(target, value);
// 不存在添加,并且trigger
if (!hadKey) {
target.add(value);
trigger(target, TriggerOpTypes.ADD, value, value);
}
return this;
}
// clear拦截器
function clear(this: IterableCollections) {
const target = toRaw(this);
// 获取是否存在数据
const hadItems = target.size !== 0;
// 构建一个map set作为旧数据
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined;
const result = target.clear();
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget);
}
return result;
}
这些修改器也和我们猜想的差不多,主要职责是trigger
。并且会确保set
和add
进去的值是Target
而不是Proxy
。其中Map
和WeakMap
的set
修改器还会检测当前key
是否存储了两份版本即是proxy
和原始版本的key
,如果有的话则弹出警告。Set
和WeakSet
的add
修改器会检测当前是否存在,如果存在则不在触发因为Set
内不是会有重复数据的。
createForEach 修改器和 createIterableMethod 修改器
// 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
// track当前
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
// origin foreach
return target.forEach((value: unknown, key: unknown) => {
// 确保用户拿到的值是响应式的
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
// 创建迭代器拦截器 keys values Symbol.interator使用
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
// 返回迭代器
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
// 获取封装对象
const target = (this as any)[ReactiveFlags.RAW]
// 源对象
const rawTarget = toRaw(target)
// 是否是map
const targetIsMap = isMap(rawTarget)
// 是否需要返回一对[key, val]
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
// 是否只需要返回key
const isKeyOnly = method === 'keys' && targetIsMap
// 获取封装对象迭代器
const innerIterator = target[method](...args)
// 转化函数
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// track当前对象
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
return {
// 迭代器直接使用
next() {
// 直接使用封装对象迭代器,再转化为响应式版本
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// 给 keys values entries 方法调用时创建迭代器
[Symbol.iterator]() {
return this
}
}
}
}
我们先看看forEach
修改器的实现,这里会将获取的每个key
,value
都转化为相对应的代理,确保用户通过Proxy
获取的都是Proxy
,也相对比较简单。
在看迭代器修改器前我们先回忆一下迭代器对象的实现方式,首先迭代器对象上必须返回带有Symbol.iterator
方法,这个方法必须有next
方法,next
方法需要放回带有value
和done
属性的对象,当done
为true
时表示完成迭代到达边界,下面我们实现一个简单的迭代器对象:
let i = 0
const test = {
[Symbol.iterator]() {
return {
next() {
if (i < 5) {
return {
value: i++,
done: false
}
} else {
return {
done: true
}
}
}
}
}
}
// [0,1,2,3,4]
console.log([...test])
接下来我们再看迭代器修改器的实现,keys
, values
, entries
, 等方法都有一个共同的特征就是返回的是迭代器对象,所以当执行的时候要确保能够返回{[Symbol.iterator](): {next: ...}}
,当访问Symbol.iterator
时就直接返回{next: ...}
,可以看到这段代码写的很精妙,为了确保两者都能兼容直接在Symbol.iterator
的实现中返回this
。为了方便理解我将不必要代码剔除掉,如下:
const keyInterator = map.keys()
/**
* keyInterator相当于
* {
* [Symbol.iterator]() {
* return {
* next () {
* ...
* }
* }
* }
* }
**/
vue
通过特定的method
和是否是Map
来判断当前需要返回的是键值对还是单值,同时确认好key
是使用MAP_KEY_ITERATE_KEY
还是ITERATE_KEY
,会用使用集合自身的迭代器实现获取结果,然后转化为与当前Proxy
类型一致的值返回给用户。
到这里我们reactive
中具体的实现就已经看完了,这里我们总结一下重要的知识点。
小结
- 创建代理对象的
Target
必须为对象,不能为基础对象 reactive
类型可以转化为readonly
但是不能逆转- 同一个对象不会创建两次,会在
map
中查看当前对象是否已经创建,如果创建了直接返回缓存中的 Proxy
对象不能创建原型被冻结的和被mark
的对象track
和trigger
需要传入具体细节Proxy
深入创建时是延迟创建的,在get
获取子对象时才会创建子Proxy
readonly
在get
时不会track
属性,因为不可变- 系统自带的
Symbol
和保留属性__proto__
、__v_isRef
、__isVue
不会加入track
和创建Proxy
Proxy
当get
到的是ref
并且Target
不是数组或者是数组但是key
不是数组下标时,会自动解包- 当代理
Array
时,会修改代理上的indexOf
、lastIndexOf
、includes
、push
、pop
、unshift
、splice
方法 - 使用
/*#__PURE__*/
告诉打包器是纯变量,没使用时可删减减少包体积 get
、has
、ownKeys
主要职责是track
set
、delete
主要职责是trigger
- 深入创建
proxy
是在get
拦截器进行的 - 因为局限性所以集合类代理是通过修改
api
来实现的 - 在
Proxy
获取出来的对象始终是与当前Proxy
类型相同的Proxy
- 在
Proxy
存储数据时始终是存储的原始对象
下一章:vue3-effect源码解析