In the latest stage of learning Vue3, Vue3 uses reactive
, ref
and other methods to convert data into responsive data, and use track
to go to effect
Collect dependencies in effect
. When the value changes, use trigger
to trigger dependencies and execute the corresponding monitoring functions. This time, let's take a look at the source code of reactive
.
Prerequisite knowledge:
In reactive
, the classification will be made according to the type of incoming data:
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
Type, other such as incoming basic data, is invalid type (INVALID)
, basic data should use ref
.
export const enum ReactiveFlags {
SKIP = '__v_skip', // 标记一个不能转换为响应式数据的对象
IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
IS_READONLY = '__v_isReadonly', // 标记一个只读对象
IS_SHALLOW = '__v_isShallow', // 标记只有一层响应的浅可读写对象
RAW = '__v_raw' // 标记获取原始值
}
There are also some markers in the data in Vue3, such as the above getTargetType
method, when the target is marked as ReactiveFlags.SKIP
or not expandable, it will return TargetType.INVALID
, the proxy cannot be created, because Vue needs to attach a lot of things to the Target proxy. If it is not scalable, the attachment will fail; or the user actively calls markRaw
and other methods to mark the data as non-responsive data, then also Could not create proxy.
To the point:
The reactive source code is in the official source code packages/reactivity/src/reactive.ts
file. The source code provides four APIs to create reactive class objects:
- reactive: create readable and writable objects that can be deeply responsive
- readonly: Create a read-only object that can drill into the response
- shallowReactive: Create shallow readable and writable objects that only respond to the first layer (other layers, the view is not updated when the value changes)
- shallowReadonly: Create a shallow read-only object with only one layer of responses
They all call createReactiveObject
method to create a responsive object, the difference is that different parameters are passed in:
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
code is as follows:
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
}
The main thing in this method is to use new Proxy
to create a proxy, and use different interceptors according to the targat
type. The getTargetType
method is used to obtain the type of the incoming target:
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
After reading the entry function, the next step is to create the Proxy object. Vue3 will choose whether to use the collectionHandlers collection interceptor or the baseHandlers common interceptor according to the data type returned by getTargetType.
Common interceptor baseHandlers:
get
Interceptor: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. 如果获取的值是 对象 ,将访问的值也转化
Note that when the proxy type is
readonly
, no dependencies are collected.
Vue3对于深层次的对象是使用时才创建的
, and if the result is a ref type, you need to determine whether to get its .value type, for example 🌰: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 uses
arrayInstrumentations
to process some methods of the array, why do it?push
、pop
、shift
、unshift
、splice
这些方法,写入和删除时底层会Get the length property of the current array. If we use it in the effect, the dependencies of the length property will be collected. When using these APIs, the length will also be changed, which will cause an infinite loop: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)
For
includes
,indexOf
,lastIndexOf
, each value will be acquired internally. As mentioned above, if the acquired result is Obejct, it will be automatically converted. Object:let target = {name: '张三'} const arr = reactive([target]) console.log(arr.indexOf(target)) // -1
Because it is actually
reactive(target)
andtarget
in the comparison, of course, can't find it.set
Interceptorfunction 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; }; }
Another point to note in the set is
target === toRaw(receiver)
, which is mainly to deal with the situation where the prototype of the proxy object is also the proxy object: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) // 张三
At this time, if this judgment is not added, since the sub-agent does not have the attribute name, the set of the prototype parent agent will be triggered, and this judgment will prevent the parent agent from triggering the update as well.
Collection interceptor collectionHandlers:
The data of the collection type is special, and its related instance method Proxy does not provide relevant catchers, but because the method call belongs to the property acquisition operation, it can be realized by capturing the get operation, so Vue3 also only defines the get interception:
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
)
}
}
In the previous article "Proxying Built-in Objects with Internal Slots" , it was said that Proxy proxies built-in objects with internal slots, and an error will occur when accessing properties on Proxy. How is it solved in Vue3?
In Vue3, a common object with the same properties and methods as the collection object is newly created, and the target object is replaced with the newly created common object when the collection object get operation. In this way, when the get operation is called Reflect
it is reflected on the new object, and when the set method is called, the method on the new object that can trigger the response is called directly, so the access is not the method on the Proxy, but this Methods on the new object:
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
]
}
Next, let's take a look at a few specific interceptors, which are actually similar:
get
Interceptor: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) } }
In the collection interceptor, both
key
andrawKey
have been processed to ensure that the data can be obtained: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
set
Interceptor:// 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; }
has
Interceptor: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) }
forEach
Interceptor: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) }) } }
end
I am Zhou Xiaoyang, a new front-end. I write articles to record the problems I encounter in my daily work and the content of my studies, so as to improve myself. If you think this article is useful to you, please give a like to encourage it~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。