introduction
Last time I read the source code of watch and computed together. In fact, I should first look at the side effects effect, because all responsive APIs are basically used. After the reactive, readonly and ref are finished, let’s take a look at the effect together. This time we want to talk about reactive and readonly, and the two are generally consistent in their implementation. Especially the proxy interception of Map and Set methods is somewhat wonderful.
1. Reactive and readonly
Vue3 uses Proxy to replace Vue2.
const target = { name: 'onlyy~' } // Create a proxy for targetconst proxy = new Proxy(target, { // ...various handlers, such as get, set... get(target, property, receiver){ // Other operations // ... return (target, property, receiver) } })
1. Reactive related types
reactive uses Proxy to define a responsive object.
- Target: The target object, containing several flags, and the __v_raw field, which represents the value of its original non-responsive state;
export interface Target { []?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.IS_SHALLOW]?: boolean []?: any } 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>() const enum TargetType { INVALID = 0, COMMON = 1, COLLECTION = 2 }
2. Related global variables and methods
- ReactiveFlags: defines an enumeration of strings corresponding to various flags (as properties of reactive objects);
- reactiveMap
- shallowReactiveMap
- readonlyMap
- shallowReadonlyMap: These maps are used to store responsive objects generated by the corresponding API (the target object is the key and the proxy object is the value), so as to facilitate the subsequent judgment of whether an object has created responsive objects;
- TargetType: The contents of the enumeration members are used to distinguish whether the proxy target is validated, normal, Set or Map;
// Enumeration of various flagsexport const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', IS_SHALLOW = '__v_isShallow', RAW = '__v_raw' } // ... 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>() const enum TargetType { INVALID = 0, COMMON = 1, COLLECTION = 2 }
Then there are two functions: targetTypeMap is used to determine which of the various JS types belong to; getTargetType is used to obtain the TargetType type corresponding to the target.
function targetTypeMap(rawType: string) { switch (rawType) { case 'Object': case 'Array': return case 'Map': case 'Set': case 'WeakMap': case 'WeakSet': return default: return } } function getTargetType(value: Target) { return value[] || !(value) ? : targetTypeMap(toRawType(value)) }
3. Reactive function
The reactive entry parameter type is object and the return value type is UnwrapNestedRefs, which understands the nested Refs. It means that even if reactive receives a Ref, its return value no longer needs to read the value through .value like Ref. Examples are also given in the comments for the source code.
/* * const count = ref(0) * const obj = reactive({ * count * }) * * ++ * // -> 1 * // -> 1 */
Reactive internally calls createReactiveObject to create reactive objects. There are five ginsenges:
- target: proxy target;
- false: the isReadonly parameter corresponding to createReactiveObject;
- mutableHandlers: A proxy handler for normal objects and arrays;
- mutableCollectionHandlers: Set and Map's proxy handler;
- reactiveMap: The global variable defined previously, collecting the dependencies corresponding to reactive.
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) }
4. Creator createReactiveObject
Whether it is reactive, shallowReactive, readonly and shallowReadonly, they all call createReactiveObject internally to create proxy. There is no operation for createReactiveObject. It mainly determines the type of target and then decides whether to return target directly or return a newly created proxy.
Return to target directly in the following situations:
- target is not an object;
- target is already a responsive object, that is, a proxy created by createReactiveObject;
- target type verification is illegal, such as RegExp, Date, etc.;
When the target-like responsive object already exists in the actual parameters corresponding to the proxyMap (may be reactiveMap, shallowReactiveMap, readonlyMap or shallowReadonlyMap, which corresponds to the four APIs of ractive, shallowReactive, readonly and shallowReadonly), the responsive object is directly taken out and returned;
Otherwise, create a target responsive object proxy, add proxy to proxyMap, and return that proxy.
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { if (!isObject(target)) { if (__DEV__) { (`value cannot be made reactive: ${String(target)}`) } return target } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object if ( target[] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // target already has corresponding Proxy const existingProxy = (target) if (existingProxy) { return existingProxy } // only specific value types can be observed. const targetType = getTargetType(target) if (targetType === ) { return target } const proxy = new Proxy( target, targetType === ? collectionHandlers : baseHandlers ) (target, proxy) return proxy }
We know that the focus of the proxy is actually on the proxy's handler. CreateReactiveObject distinguishes baseHandlers and collectionHandlers based on normal object and array type, Set and Map type.
5. shallowReactive, readonly and shallowReadonly
In fact, the functions of racive, shallowReactive, readonly and shallowReadonly are basically the same in form. They all create reactive objects and store them in the corresponding proxyMap, but there is a difference between the corresponding baseHandlers and collectionHandlers.
// shallowReactive export function shallowReactive<T extends object>( target: T ): ShallowReactive<T> { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap ) } // raedonly // Note that readonly is not responsive, but a read-only copy of the original object// The specific implementation is in the corresponding handlersexport function readonly<T extends object>( target: T ): DeepReadonly<UnwrapNestedRefs<T>> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap ) } // shallowReadonly // It's responsive// Only the outermost layer is read-onlyexport function shallowReadonly<T extends object>(target: T): Readonly<T> { return createReactiveObject( target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap ) }
In fact, the functions of racive, shallowReactive, readonly and shallowReadonly are basically the same in form. They all create reactive objects and store them in the corresponding proxyMap, but there is a difference between the corresponding baseHandlers and collectionHandlers. Then we know that the focus is actually in various handlers.
2. Corresponding Handlers
baseHandlers are used for proxying ordinary objects and arrays, and collectionHandlers are used for proxying Set, Map, etc. Corresponding to four APIs: ractive, shallowReactive, readonly and shallowReadonly, each of which has its own baseHandlers and collectionHandlers.
1. baseHandlers
In the packages/reactivity/src/ file. The baseHandlers corresponding to these 4 APIs are exported respectively.
1.1 reactive
There are 5 agents in reactive baseHandlers.
// reactive export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }
During the interception process, dependency capture is performed in the access programs such as get, has and ownKey, and update is triggered in the two programs used for change, set and deleteProperty.
get and set are created by the functions createGetter and createSetter respectively. These two functions return different get and sets according to the input parameters. Most of the get and sets in baseHandlers of readonly APIs are derived from this, except for the two sets used for alarms in readonly.
(1) get
createGetter two parameters: isReadonly and isShallow, which corresponds to four APIs in combination.
- shallow: When true, it will not enter the recursive link, so it is a shallow processing;
- isReadonly: Influence the selection of proxyMap and the selection of APIs when recursive in createGetter, it mainly plays a role in set.
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // The following if branches determine whether the target is already a proxy object created by these APIs. Only the proxy obtained by the proxy has these keys if (key === ReactiveFlags.IS_REACTIVE) { // Is it a responsive object return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { // Is it a read-only object return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { // Is it a shallow responsive/read-only object return shallow } else if ( // The __v_raw property corresponds to the target object of the proxy object // When this property has a value and there is a proxy object in the corresponding proxyMap, it means that the target is already a proxy // The corresponding value of the __v_raw property is the target itself key === && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target } const targetIsArray = isArray(target) // Proxy several methods of the array, track capture dependencies in 'includes', 'indexOf', 'lastIndexOf' and other methods if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return (arrayInstrumentations, key, receiver) } const res = (target, key, receiver) if (isSymbol(key) ? (key) : isNonTrackableKeys(key)) { return res } // If it is not readonly, the dependency is captured, so readonly is non-responsive if (!isReadonly) { track(target, , key) } if (shallow) { return res } // If the value obtained is a Ref, it will be unpacked directly, and there is no need to use .value to get the value you really need // Unless the target object target is an array, or the current key is an integer // For example, obj[0], even a Ref will not be unpacked directly, and you still need obj[0].value when using it // shallow has not reached this point, so it will not automatically unpack. if (isRef(res)) { // ref unwrapping - skip unwrap for Array + integer key. return targetIsArray && isIntegerKey(key) ? res : } // When the value obtained is an object, recursive operation is required to prevent object references from recursively depending on whether it is readonly. // shallow has not reached this point, so shallow is shallow 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 } }
(2) set
For reactive, it can be said that the main task is to trigger updates in set, which includes adding and modifying attribute values. If the value corresponding to the current key is a Ref and other conditions are met, the operation that triggers the update is inside the Ref. These will be mentioned in the subsequent explanation of Ref.
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = (target as any)[key] // When the current value is Readonly Ref, and the new value is not Ref, modification is not allowed if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false } // If it is a deep modification if (!shallow) { // Solve the original non-proxy value if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue) value = toRaw(value) } // The target object is not an array, the value of the current key is Ref and the new value is not Ref, then the value is assigned through .value // Trigger update inside Ref if (!isArray(target) && isRef(oldValue) && !isRef(value)) { = value return true } } else { // In shallow mode, ignore whether the object is responsive // in shallow mode, objects are set as-is regardless of reactive or not } // Then the update is triggered // Determine whether the current key already exists on the target const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < : hasOwn(target, key) const result = (target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original // If it is a field on the prototype chain, the update will not be triggered if (target === toRaw(receiver)) { if (!hadKey) { // The current key already exists, triggering new updates trigger(target, , key, value) } else if (hasChanged(value, oldValue)) { // The current key does not exist, triggering a modified update trigger(target, , key, value, oldValue) } } return result } }
(3) deleteProperty
The agent for the delete operation, like set, deleteProperty intercepts delete and () operations, and it can also trigger updates.
function deleteProperty(target: object, key: string | symbol): boolean { const hadKey = hasOwn(target, key) const oldValue = (target as any)[key] const result = (target, key) // When the delete is successful and this property is originally present in the target, the deleted update will be triggered if (result && hadKey) { trigger(target, , key, undefined, oldValue) } return result }
(4) has
has been used to determine whether there is the current key in the target, intercepting operations such as a in obj, with(obj){(a)}, etc., which belongs to the access program, where the has operation is collected.
function has(target: object, key: string | symbol): boolean { const result = (target, key) if (!isSymbol(key) || !(key)) { track(target, , key) } return result }
(5) ownKeys
Used to obtain all the keys owned by the target, intercept,,,, belong to the access program, and iterate in iterative dependency collection.
function ownKeys(target: object): (string | symbol)[] { track(target, , isArray(target) ? 'length' : ITERATE_KEY) return (target) }
Now we all understand that for ordinary objects and arrays, reactive creates proxy, and intercepts its property access operations through five proxy handlers: get, set, deleteProperty, has, and ownKeys, and performs dependency collection, intercepts its addition, deletion and modification operations, and triggers updates.
1.2 readonly
There are only three agent handlers for readonly:
- get: created by createGetter(true). Do you still remember the createSetter we mentioned above?
- set
- deleteProperty: These two proxy handlers are used for alarms, after all, readonly cannot be modified.
Pica thought about createGetter(true), and the readonly=true passed in, so that no track operation is performed in the get to collect dependencies, so it is not responsive.
const readonlyGet = /*#__PURE__*/ createGetter(true) export const readonlyHandlers: ProxyHandler<object> = { get: readonlyGet, set(target, key) { if (__DEV__) { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ) } return true }, deleteProperty(target, key) { if (__DEV__) { warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ) } return true } }
1.3 shallowReactive
shallowReactive transplants reactive baseHandlers and updates get and set. For specific implementations, you can also review the createGetter and createSetter mentioned above.
Looking back at createGetter(false, true), isReadonly = false, then track dependency collection can be performed in get; shallow = true, then the top-level Ref will not be unpacked, nor will it be recursively operated in get.
In createSetter(true), the parameter shallow almost only affects whether the original raw value is to be solved. If the new value value is not shallow and is not read-only, it needs to solve its original raw value before the assignment operation can be performed, otherwise our shallowRef will no longer be shallow.
const shallowGet = /*#__PURE__*/ createGetter(false, true) const shallowSet = /*#__PURE__*/ createSetter(true) export const shallowReactiveHandlers = /*#__PURE__*/ extend( {}, mutableHandlers, { get: shallowGet, set: shallowSet } )
1.4 shallowReadonly
The readonly baseHandlers has been ported and the get in it has been updated. This get is also created by createGetter. We know that in readonly's baseHandlers, except for get, the other two are used to intercept and modify operations and alert.
Let’s review createGetter. When isReadonly===true, no track operation will be performed to collect dependencies; when shallow===true, no Ref will be unpacked, nor will it go to the recursive link, that is, shallow readonly.
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) // Props handlers are special in the sense that it should not unwrap top-level // refs (in order to allow refs to be explicitly passed down), but should // retain the reactivity of the normal readonly object. export const shallowReadonlyHandlers = /*#__PURE__*/ extend( {}, readonlyHandlers, { get: shallowReadonlyGet } )
2. cellectionHandlers
For more complex data structures of Set and Map, they have their own methods, so the proxy will be somewhat different. Basically, they are intercepting their original methods and then track or trigger. You can see that among these handlers, only get created by createInstrumentationGetter.
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) }
1.1 createInstrumentationGetter
Because it is a proxy Set and Map, before intercepting their instance methods, access to the instance, i.e. get. This get is not a Map or a get method of the Set instance, but represents access operations to the instance.
For example:
const map = new Map([['name', 'cc']]);
('age', 18);
Here () first accesses the map's set method, and the corresponding key is the string 'set', and this step will be intercepted by the proxy get program, and the real interception of the method is preset in the corresponding instrumentations. After intercepting, if the key exists in the instrumentations, return the preset method and perform track and trigger operations. Otherwise, it is other attributes/methods, and you can return directly, and track and trigger will not be performed.
const [ mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations ] = /* #__PURE__*/ createInstrumentations() 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 === ) { return target } return ( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver ) } }
1.2 instrumentations
Compared with baseHandlers, Proxy cannot directly intercept the call of Map and Set methods, but intercept it through the get program, and then determine whether the key is a method of adding, deleting, modifying and checking, so as to determine whether dependency collection or update is performed. Therefore, you need to preset which keys can trigger track and trigger when they are used as method names. In fact, they are the instance methods and iterator methods of Map and Set. Various Instruments are these preset methods, and track and trigger operations are all included.
function createInstrumentations() { // Corresponding to reactive 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) } // Corresponding shallowReactive 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) } // Corresponding to readonly 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 (this, key, true) }, add: createReadonlyMethod(), set: createReadonlyMethod(), delete: createReadonlyMethod(), clear: createReadonlyMethod(), forEach: createForEach(true, false) } // Corresponding shallowReadonly 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 (this, key, true) }, add: createReadonlyMethod(), set: createReadonlyMethod(), delete: createReadonlyMethod(), clear: createReadonlyMethod(), forEach: createForEach(true, true) } // Use createIterableMethod to hang several iterators for these Instruments const iteratorMethods = ['keys', 'values', 'entries', ] (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 ] }
The function createInstruments is divided into two parts. The first part uses the existing get, set, add, has, clear, etc. to obtain each instrumentation, and the latter part is to update the iterative methods in each instrumentation. As long as it is not isReadonly or is the true value, whether it is methods such as get, set, or iterator interfaces such as keys and values, track or trigger are internally performed. Of course, methods such as get, has, size and several iterator methods are all access operations, so track is used internally to collect dependencies, and trigger occurs in addition, deletion, and modification operations. Of course, it must also be distinguished based on isReadonly and shallow, and the idea is basically the same as baseHandlers.
function get( target: MapTypes, key: unknown, isReadonly = false, isShallow = false ) { // #1772: readonly(reactive(Map)) should return readonly + reactive version // of the value target = (target as any)[] const rawTarget = toRaw(target) const rawKey = toRaw(key) if (!isReadonly) { if (key !== rawKey) { track(rawTarget, , key) } track(rawTarget, , rawKey) } const { has } = getProto(rawTarget) const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive if ((rawTarget, key)) { return wrap((key)) } else if ((rawTarget, rawKey)) { return wrap((rawKey)) } else if (target !== rawTarget) { // #3602 readonly(reactive(Map)) // ensure that the nested reactive `Map` can do tracking for itself (key) } } function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean { const target = (this as any)[] const rawTarget = toRaw(target) const rawKey = toRaw(key) if (!isReadonly) { if (key !== rawKey) { track(rawTarget, , key) } track(rawTarget, , rawKey) } return key === rawKey ? (key) : (key) || (rawKey) } function size(target: IterableCollections, isReadonly = false) { target = (target as any)[] !isReadonly && track(toRaw(target), , ITERATE_KEY) return (target, 'size', target) } function add(this: SetTypes, value: unknown) { value = toRaw(value) const target = toRaw(this) const proto = getProto(target) const hadKey = (target, value) if (!hadKey) { (value) trigger(target, , value, value) } return this } function set(this: MapTypes, key: unknown, value: unknown) { value = toRaw(value) const target = toRaw(this) const { has, get } = getProto(target) let hadKey = (target, key) if (!hadKey) { key = toRaw(key) hadKey = (target, key) } else if (__DEV__) { checkIdentityKeys(target, has, key) } const oldValue = (target, key) (key, value) if (!hadKey) { trigger(target, , key, value) } else if (hasChanged(value, oldValue)) { trigger(target, , key, value, oldValue) } return this } function deleteEntry(this: CollectionTypes, key: unknown) { const target = toRaw(this) const { has, get } = getProto(target) let hadKey = (target, key) if (!hadKey) { key = toRaw(key) hadKey = (target, key) } else if (__DEV__) { checkIdentityKeys(target, has, key) } const oldValue = get ? (target, key) : undefined // forward the operation before queueing reactions const result = (key) if (hadKey) { trigger(target, , key, undefined, oldValue) } return result } function clear(this: IterableCollections) { const target = toRaw(this) const hadItems = !== 0 const oldTarget = __DEV__ ? isMap(target) ? new Map(target) : new Set(target) : undefined // forward the operation before queueing reactions const result = () if (hadItems) { trigger(target, , undefined, undefined, oldTarget) } return result }
1.3 createIterableMethod
Here I will mention the createIterableMethod, which is used to utilize the iterator method of Map and Set itself, and made some modifications, adding tracks to collect dependencies.
function createIterableMethod( method: string | symbol, isReadonly: boolean, isShallow: boolean ) { return function ( this: IterableCollections, ...args: unknown[] ): Iterable & Iterator { const target = (this as any)[] const rawTarget = toRaw(target) const targetIsMap = isMap(rawTarget) const isPair = method === 'entries' || (method === && targetIsMap) const isKeyOnly = method === 'keys' && targetIsMap const innerIterator = target[method](...args) const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive !isReadonly && track( rawTarget, , 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 } = () return done ? { value, done } : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), done } }, // iterable protocol []() { return this } } } }
summary
After analyzing each part, you can see that the ideas are consistent whether they are baseHandlers or collectionHandlers.
However, collectionHandlers only has the get agent, which judges whether it is the method of adding, deleting, modifying and checking that comes with the Map and Set instances, thereby returning the preset hack version method or the original attribute value, and then continuing the subsequent operations. Track and trigger in the hack version method.
The above is the detailed content of Vue3 source code analysis reactive readonly instance. For more information about Vue3 reactive readonly, please follow my other related articles!