前言
在上一篇中,简单的看了一下 reactive实现方法,其中有两个文件,专门给reactive、shallowReactive、readonly、shallowReadonly 提供拦截方法,文件分别在:
给数据提供拦截处理方法:vue-next3.2/packages/reactivity/src/baseHandlers.ts
给数组集合提供拦截处理方法:vue-next3.2/packages/reactivity/src/collectionHandlers.ts
这篇文章是我逐行分析完这两个文件所有的理解和收获,和大家分享,记录我学习的历程
工具函数
在此之前,我们先来看几个工具函数,
- isRef 判断是不是Ref类型
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
- isIntegerKey 是否为数字键
export const isIntegerKey = (key: unknown) =>
isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key
- isObject 是否为对象
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
- hasOwn 判断对象中是否有这个key
// 拿到原型上的hasOwnPrototype 进行柯里化
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
// 转化成响应式代理对象
const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
// 转换成只读代理对象
const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as Record<any, any>) : value
// 不深层次的响应式处理 和直接返回值没啥区别
const toShallow = <T extends unknown>(value: T): T => value
// 拿到方法的原型对象
const getProto = <T extends CollectionTypes>(v: T): any =>
Reflect.getPrototypeOf(v)
baseHandler
数据读取拦截方法
接受两个参数
isReadonly 只读代理对象?
shallow 浅层次代理?
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// reactive 对 readonly 进行了相关校验 readonly中反之
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
// 代理对象已经存在 返回即可 (有四个集合分别存储不同的代理对象)
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
// key 如果是数组方法名称 且是进行过拦截处理的数组原生方法进行操作 arrayInstrumentations
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 映射到原始对象上
const res = Reflect.get(target, key, receiver)
// 中间做了一些验证 不能是Symbol 不能是特殊属性(__proto__,__v_isRef,__isVue)
// 如果值是数组、或者是带有数字为键的对象的ref对象,不能展开直接返回
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 如果是只读就不做依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 如果获取值不是对象直接返回即可
// 否则根据isReadonly返回响应式数据
/*
这里做了懒加载处理 到这里之前获取目标的内部数据都不是响应式,这里是对是对象的内部数据的响应式处理 然后返回代理对象
*/
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
}
}
// 最后通过 这个方法生成一些get拦截方法
const get = /*#__PURE__*/ createGetter() // 可变数据的拦截代理get方法
const shallowGet = /*#__PURE__*/ createGetter(false, true) // 浅层次可变数据的拦截代理get方法
const readonlyGet = /*#__PURE__*/ createGetter(true) // 不可变数据的拦截代理方法
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) // 浅层次的不可变数据的拦截代理方法
懒加载响应式处理很好的避免了循环引用 vue2中都是用Object.defineproperty一把梭,从头处理到尾,但是如果是半路又引用了新数据,单靠这一个无法做到新数据也进行数据代理,Proxy也是无法对嵌套的数据直接完成数据代理,但是可以在获取数据的时候,对返回的数据进行数据代理处理,下面看数据修改拦截
数据修改拦截方法
创建方法接受一个参数:shallow:浅层次的?
返回拦截方法接受4个参数:
target: 目标数据 key: 键值 value:新的属性值 receiver:target的代理对象
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取旧的属性值
let oldValue = (target as any)[key]
// 只有不是浅层次 旧值是ref类型 新值不是 直接在旧值上修改
if (!shallow) {
value = toRaw(value)
oldValue = toRaw(oldValue)
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
}
// 看看key值的存在于对象中?
const hadKey =
// 是数组 且 key是整数类型
isArray(target) && isIntegerKey(key)
// 数组索引不能大于数组的长度
? Number(key) < target.length
// key值存在于存在对象?
: 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
// receiver 必须是target 的代理对象 才会触发 trigger
// Receiver:最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)
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方法
const set = /*#__PURE__*/ createSetter() // 可变数据的拦截set方法
const shallowSet = /*#__PURE__*/ createSetter(true) // 浅层次的可变数据的拦截set方法
// 至于只读的 比较特殊 需要特殊处理
上面有几个点需要详细的解释一下:
-
!isArray(target) && isRef(oldValue) && !isRef(value)前面很好理解 后面的
isRef(oldValue) && !isRef(value)意思是 旧值是ref类型 需要设置的新值不是, 下面这段代码很好的诠释这种情况const {ref, reactive, createApp} = vue setup() { let count = ref(0) const state = reactive({ count }) setTimeout(() => { state.count = 30 }, 1000) return { state } } -
关于
target === toRaw(receiver)这个点的含义可以看看 MDN上的 Proxy 的解释,在以前,我只是认为
receiver是target的代理对象 转换原始对象之后应该会和target绝对相等,在我阅读了一些文章之后,终于找到了详细解答 链接地址:juejin.cn/post/684490… -
如果使用数组原生方法去改变数组,那必然会被会触发两次set 甚至于无限调用,所以vue3.2对数组的5个改变数组本身的方法进行劫持
function createArrayInstrumentations() { const instrumentations: Record<string, Function> = {} // instrument identity-sensitive Array methods to account for possible reactive // values // 3个判断数组中是否存在某值的方法 ;(['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 + '') } // we run the method using the original args first (which may be reactive) // 使用传递进来的参数第一次运行方法 (参数可能是代理对象, 会找不到结果) 找到了结果 返回即可 const res = arr[key](...args) if (res === -1 || res === false) { // 将代理对象转换成原始数据 并再一次运行 且返回 // if that didn't work, run it again using raw values. return arr[key](...args.map(toRaw)) } else { return res } } }) // instrument length-altering mutation methods to avoid length being tracked // which leads to infinite loops in some cases (#2137) // 5个会修改数组本身的方法 ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function(this: unknown[], ...args: unknown[]) { // 在vue3.0版本 vue会对数组的push等方法进行依赖收集和触发 可能产生无限循环调用 这里让数组的push等方法不进行依赖的收集和触发 /** * watachEffect(() => { * arr.push(1) * }) * * watchEffect(() => { * arr.push(2) * }) */ pauseTracking() // 执行数组原生上的方法 将结果返回 const res = (toRaw(this) as any)[key].apply(this, args) resetTracking() return res } }) return instrumentations }也就是说在调用原生方法改变数组时,不会再去收集依赖和触发以来进行更新 而是同意调用 每一个组件唯一的挂载 更新函数
其他的拦截方法
// 拦截删除数据
function deleteProperty(target: object, key: string | symbol): boolean {
// 判断key键是否存在 然后删除操作
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// 只有key键存在 删除成功了才会进行更新
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
// 判断是否存在
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// isSymbol不是唯一值 builtInSymbols 不是Symbol原型上的12个方法
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
// 拿到自身所有属性组成的数组
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
总结
Vue3的响应式是通过代理对象Proxy实现的,任何操作都会通过Reflect进行映射到原始对象,获取的操作一般都是收集依赖,修改或者是新增、删除是触发依赖,对于数组的操作拦截,Proxy并不能很好的处理完美,需要对数组的方法进行劫持,所有的拦截操作都封装进了createReactiveObject 函数中
collectionHandler
vue3对数组集合类型专门写拦截方法,当点开collectionHandlers.ts时
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)
}
会发现vue3只给定义了一个get,那vue3是如何拦截其他操作的?其实这是因为数组集合类型和其他的数据类型有着不同的操作方式:使用自己独有的API进行操作,比如 add、get、等方法,这些操作都被统一的封装进了 createInstrumentations 函数中
为什么要重写
这Set和Map内部的实现原理有关,Set和Map内部数据都是通过this去访问的,被称为内存插槽,在直接通过接口去访问的时候,this指向的是Set, 通过代理对象去访问时,this指向就变成了proxy,也就无法访问。详细解释
集合读操作
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// #1772: readonly(reactive(Map)) should return readonly + reactive version
// of the value
// target可能是:只读代理对象 原始数据可能是一个可变代理对象
// 需要通过Reactive.Flags.RAW拿到只读代理对象原始数据(或许是可变代理对象) 之后在用toRaw获取一次
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 由于Map可以使用对象作为key 有可能会有代理对象作为key 这里拿到原始key
const rawKey = toRaw(key)
// 无论是否 key 和 rawKey是否相同 都去收集依赖
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
// 原始数据原型上的has方法
const { has } = getProto(rawTarget)
// 根据调用的响应式api的不同找到拿到不同的方法
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
// key对应的值存在
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
// rawKey 值存在
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
// key 和 rawKey都不存在 且两个数据不一样(这样代表target是一个可变代理对象) 只能代理对象自己去追踪
target.get(key)
}
}
这个方法有个地方需要注意
如果使用reactive和readonly嵌套使用 readonly(reactive(Map)),(在之前的版本没有这样处理,会导致Map的响应效果消失 请查看这个 issues) 所以target可能是一个代理对象,rawTarget也是Map对象,索性让代理他自己去追踪,
在调用这get方法的时候,第一个入参的this不能和Proxy的get方法的第一个参数弄混,这个this指的是已经代理过的代理对象,拦截方法的第一个参数一般都是target 原始对象,内部的this一般都是代理对象本身
集合写操作
需要拦截的操作有两种,分别对应着Map和Set的set和add方法,都做了不同的处理
- Set、WeakSet写操作
// Set WeakSet 独有
function add(this: SetTypes, value: unknown) {
// value 可能是 代理对象 拿到原始value
value = toRaw(value)
// target 是一个代理对象, 需要拿到原始数据
const target = toRaw(this)
// 获得原型,并使用has方法判断value 是否存在于target中
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
// 不存在 则添加值,并且触发依赖
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
// 返回自己
return this
}
- Map、WeakMap写操作
// Map WeakMap独有
function set(this: MapTypes, key: unknown, value: unknown) {
// value 可能是 代理对象 拿到原始value
value = toRaw(value)
// target 是一个代理对象, 需要拿到原始数据
const target = toRaw(this)
// 原型上的 has、get方法
const { has, get } = getProto(target)
// 判断值是否存在,后面用来判断是修改还是新增
let hadKey = has.call(target, key)
if (!hadKey) {
// 不存在 获取原始key 重新在获取一次
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
// 存在 但是为了防止key和rawKey都存在与target 获取不准确 进行校验
checkIdentityKeys(target, has, key)
}
// 获取旧值
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和WeakSet是存在是修改、不存在是新增,而Map是存在了就不会去依赖,而且相对于baseHandler使用Reflect映射的原始对象上,而这里是用本身的API去操作,
集合迭代器
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function(
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
// target可能是:只读代理对象 原始数据可能是一个可变代理对象
// 需要通过Reactive.Flags.RAW拿到只读代理对象原始数据(或许是可变代理对象)
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 目标是Map类型吗
const targetIsMap = isMap(rawTarget)
// Set是没有entries方法,这是Map的迭代方法 只有Map去调用,isPair为true
// 每一次迭代返回的结构是 [key value] 形式的数组
// 后面的(method === Symbol.iterator && targetIsMap) 是因为Symbol.iterator调用触发的entries
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
// Set是没有keys方法的,这是Map的迭代方法,
const isKeyOnly = method === 'keys' && targetIsMap
// 执行原生的迭代方法 keys values entries
const innerIterator = target[method](...args)
// 根据条件 warp 返回对应的方法 例如 如果是 reactive 返回的就是 toReactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 不是只读 收集依赖
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
// return a wrapped iterator which returns observed versions of the
// values emitted from the real iterator
// 返回一个包转过的迭代器 这个迭代器的返回值都是由默认迭代器返回
return {
// iterator protocol
next() {
// 去除重要的两个值 当前迭代的值、是否迭代完成
const { value, done } = innerIterator.next()
// 如果done最后是true 代表迭代到最后,返回的值是undefined 没有必要做响应式了
return done
? { value, done }
: {
// 如果是有一对 返回的数组中 索引0是key 索引1是value 不是代表是Set和WeakSet 就只有值
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol 返回迭代对象本身
// 自定义迭代器 返回自己迭代对象自己本身
[Symbol.iterator]() {
return this
}
}
}
}
在迭代方面上,Map和Set有所不同,
上面两张图片分别是Map和Set的原型,一共有三个地方不同
1.Set的entries和keys 调用的其实是values方法,而Map是有这两个单独的方法
2.Map的Symbol(symbol.iterator)调用的是entries 而 Set的Symbol(symbol.iterator)调用的还是values()
- 两种数据的添加方式不同,Map使用set方法可以设置key值,Set使用add方法不可以设置key值
正因为这三种不同,才有了上面不同的处理,最主要的逻辑就是包装了迭代器,并且next返回的值每次都对应的响应式api进行处理,
迭代还有一个方法
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)
// 根据条件 warp 返回对应的方法 例如 如果是 reactive 返回的就是 toReactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
// 为了更好的遍历 由内部调用外界进来函数,并且数据是只读或者是响应式的
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
逻辑上并不复杂,最主要的是劫持了方法forEach,并将数据进行代理,
总结
由于数组集合的底层设计的原因,无法通过Proxy进行劫持,只能通过劫持自身接口进行代理,可能反射带劫持方法上,也可能通过映射到原始对象上,
劫持方法,会先拿到原始对象和传递进来的原始数据,再原始对象的原型上的方法,把this绑定为原始对象进行调用。
对于get和has,插入收集依赖的逻辑,然后再将返回值进行转换(因为has返回的是布尔值,不要转换),迭代器也是如此,但是最后需要把迭代过程的数据转换成响应式返回
写操作需要,需要插入触发依赖进行更新的逻辑
最后总结
到这里baseHandler和collectionHandler的逻辑就全部解析完了,也查阅了很多资料,收获了很多底层知识,写了老半天,希望各位大佬能够补充,谢谢