本文使用的
vue
版本为3.2.26
。在阅读 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
拦截器大概职责是在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
时还需要还需要对一些的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
拦截器主要职责是重新触发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
拦截器主要职责也是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
函数。
// 辅助方法,转化为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);
// 检查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
内不是会有重复数据的。
// 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源码解析
©2021 - bill-lai 的小站 -站点源码