“This is the 9th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”
Vue3 advantages
Before reading more about Vue3’s responsive system, it is important to know that Vue3 uses Proxy for two-way data binding, while Vue2 uses Object.defineProperty. The advantages and disadvantages of Proxy compared with Object. DefineProperty feature are as follows:
Proxy
Is a proxy for the entire object, andObject.defineProperty
Only one attribute can be proxy.- New property on object,
Proxy
You can listen in,Object.defineProperty
Can’t. - Array new modification,
Proxy
You can listen in,Object.defineProperty
Can’t. - If the internal properties of an object are to be recursively brokered,
Proxy
You can recurse only when called, andObject.definePropery
You need to do all the recursion at once, performance ratioProxy
Poor. Proxy
Not compatible with IE,Object.defineProperty
Incompatible with IE8 and belowProxy
Use more thanObject.defineProperty
More convenient.
If you don’t know about Proxy and Object.defineProperty, read this article
Next, formally read the code for Vue3’s reactive correlation implementation.
PS: recently is reading Vue3 source code, will launch a series of articles, interested, point attention, learn together ~
Source directory Description
This is the Vue3 source directory structure downloaded from GitHub. The red border reActivity directory is a code package that implements responsiveness and can be built separately for use as a standalone package.
The following describes the main files in the reactivity directory:
- Tests: Describes test cases related to code packages.
- Index. ts: Used to export package-specific implementation methods.
- Reactive. Ts: describes how to use a Proxy to Proxy objects and hijack them.
The principle is as follows: create a Proxy object with Proxy, and perform track operation when the Proxy object performs get trap function to read values, and trigger operation when the set trap function writes values. Note that this is an object-only proxy, not a primitive data type.
- Refs.ts: Describes how to solve the problem of primitive data type proxies.
Principle is: the use of the object itself get | set function, and track the get function for operation, the set function for the trigger operation.
- Computed. Ts: Describes the implementation of computed properties. Actually with
lazy
Properties of theeffect
. - Effect. ts: describes how to track property changes and execute callback functions.
- BaseHandlers. Ts: Custom interception behavior specified by the agent for Object and Array data types.
- CollectionHandlers. Ts: Self-defined interception behavior specified by the agent for Set, Map, WeakMap, WeakSet data types.
Use case
Here’s a look at two scenarios, one using the vue3 library directly, and the other using packages built separately from the ReActivity directory.
Scenario 1: Vue3 library
Import {reactive, effect} from 'vue3.js' const obj = reactive({x: 1 }) effect(() => { patch() }) setTimeout(() => { obj.x = 2 }, 1000) function patch() { document.body.innerText = obj.x }Copy the code
Obviously, after 1s, the page displays 2. Reactive hijacks the setter method of an OBj object and triggers the effect function when assigning a new value.
Scenario 2: The ReActivity package
To build the reactivity library, download vue3 from VUE-Next and run the following command in the root directory. I used YARN.
Dist directory is generated in the reactivity package, which contains the reactivity.glob.js file. This file exposes the global VueReactivity object. Mapdev reActivity: mapDev reactivity: mapDev reactivity: mapDev reactivity: mapDev reactivity: mapDev reactivity: mapDev reactivity: mapDev reactivity: mapDev reactivityCopy the code
Let’s take a look at the example code, which implements the same effect as scenario 1.
Const {effect, track, trigger, targetMap} = VueReactivity var obj = {x: 1 } effect(() => { patch(); ActiveEffect track(obj, 'get', 'x'); console.log(targetMap, 'targetMap') }) setTimeout(() => { obj.x = 2; trigger(obj, 'set', 'x') }, 1000) function patch() { document.body.innerText = obj.x }Copy the code
Reactive source code
Reactive Is used to create reactive objects. The target object is hijacked by Proxy. Look at the source code below, specific implementation:
Export const enum ReactiveFlags {// SKIP, const enum ReactiveFlags {// const enum ReactiveFlags { SKIP = '__v_skip' IS_REACTIVE = '__v_isReactive' IS_READONLY = '__v_isReadonly', // RAW = '__v_raw', REACTIVE = '__v_reactive', READONLY = '__v_readonly'} export function Reactive (target: Object) {// If the proxy is read-only, If (target && (target as target)[reactiveFlags.is_readOnly]) {return target} // create a responsive object return CreateReactiveObject (target, false, mutableHandlers mutableCollectionHandlers)} / / read-only agent, Export function readOnly <T extends Object >(target: T): DeepReadonly<UnwrapNestedRefs<T>> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers ) }Copy the code
/* * if the object returned can be observed: * 1. Objects marked with SKIP state, used to not be converted to proxy, corresponding API is markRaw * 2. Object type belongs to one of these: Object, Array, Map, Set, WeakMap, WeakSet * 3. */ const canObserve = (value: Target): Boolean => {return (! value[ReactiveFlags.SKIP] && isObservableType(toRawType(value)) && ! Object. IsFrozen (value))} function createReactiveObject(target: target, isReadonly: Handlers: <any>; collectionHandlers: <any>) {return if (! IsObject (target)) {return target} if (target [reactiveFlags.raw] &&! (isReadonly &&target [reactiveflags.is_reactive]) {return target} Const reactiveFlag = isReadonly? ReactiveFlags.READONLY : REACTIVE if (hasOwn(target, reactiveFlag)) {return target[reactiveFlag]} ReactiveFlags.REACTIVE if (! CanObserve (target)) {return target} const observed = new Proxy(target, // If Set, Map, WeakMap, WeakSet collectionTypes // Base data type Object/Array baseHandlers collectionTypes. Has (target. Constructor)? (object, reactiveFlag, observed) def(target, reactiveFlag, observed) return observable}Copy the code
The code mentioned here is about creating reactive objects through Reactive, and the requirements for the input parameters are as follows:
- Non-objects cannot do proxy hijacking.
- Target has been hijacked, return, do not repeat hijacking.
- Read only hijacking.
- Object __v_SKIP property is not true and the data type is
Object,Array,Map,Set,WeakMap,WeakSet
And the object is not frozen, because the object cannot be modified after it is frozen. - Choose different hijacking methods according to different data types.
Set type Set, Map, WeakMap, WeakSet, collectionHandlers;
Base data type Object, Array, baseHandlers.
To pass in a correct object, let’s see how a proxy can be used for data hijacking of an object to achieve a responsive effect. BaseHandlers, for example.
- First of all,
baseHandlers
Is the interfaceProxyHandler
First look at what the interface defines.
interface ProxyHandler<T extends object> { getPrototypeOf? (target: T): object | null; setPrototypeOf? (target: T, v: any): boolean; isExtensible? (target: T): boolean; preventExtensions? (target: T): boolean; getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined; has? (target: T, p: PropertyKey): boolean; get? (target: T, p: PropertyKey, receiver: any): any; set? (target: T, p: PropertyKey, value: any, receiver: any): boolean; deleteProperty? (target: T, p: PropertyKey): boolean; defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean; ownKeys? (target: T): PropertyKey[]; apply? (target: T, thisArg: any, argArray? : any): any; construct? (target: T, argArray: any, newTarget? : any): object; }Copy the code
In fact, this contains basically all the trap functions of Proxy. Proxy intercepts low-level object operations on targets within the JavaScript engine, which, when intercepted, trigger trap functions in response to specific operations.
- Look at the mutableHandlers implementation and pick up the basic get/ and set understanding
Const set = /*#__PURE__*/ createSetter() const get = /*#__PURE__*/ createGetter() // export const mutableHandlers: ProxyHandler<object> = {get, set, deleteProperty, has, ownKeys} TargetMap function createGetter(isReadonly = false) {return function get(target: object, key: shallow) String | symbol, receiver: object) {/ / flag state related processing, not to collect the if (key = = = ReactiveFlags. IS_REACTIVE) {return! isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? (target as any)[ReactiveFlags.READONLY] : (target as any)[reactiveFlags.reactive]) {return target} const targetIsArray = isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, Const res = reflect. get(target, key, receiver) // Key is a Symbol type, If (isSymbol(key)? builtInSymbols.has(key) : Key = = = ` __proto__ ` | | key = = = ` __v_isRef `) {return res} / / tracking properties change, This will be put into targetMap, the weakMap change that stores tracking data declared in the effect file if (! IsReadonly) {track(target, trackoptypes.get, key)} // Shallow proxies (e.g., shallowReactive) do not do recursion, and ref references do not unpack because they return directly. If (shallow) {return res} // references the object, returning an array if it is an array and a value if it is an object. If (isRef(res)) {return targetIsArray? Res: res.value} if (isObject(res)) {return isReadonly? readonly(res) : Reactive (res)} return res}} To perform the corresponding effect function createSetter (shallow = false) {return function set (target: the object, the key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] if (! Shallow) {value = toRaw(value) isArray(target) && isRef(oldValue) && ! IsRef (value)) {oldValue.value = value return true}} else {// In shallow mode, whether reactive or not, } const hadKey = hasOwn(target, key) const result = reflect. set(target, key, value) Receiver) // If the target is something in the prototype chain, do not trigger 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 } }Copy the code
Saw above the get | set two blocks of code to achieve, we can come to the conclusion that, in the process of implementation reactive conclusion:
- In the get trap function, the corresponding processing of different types of attributes is performed, and the track operation of data is performed (tracking collection cache).
- In the set trap function, the effect method is triggered by the key type and whether it exists or not, and by the trigger operation
Since the track and trigger methods are in the effect.ts file, I won’t address them here and will cover them in the next article.
Other source notes
1. Responsive objects
We know that Vue3 can create many types of reactive objects. How are these reactive objects defined? As you can see below, other types of reactive objects are also defined using the createReactiveObject method, but with a different handler.
export function shallowReactive<T extends object>(target: T): T {return createReactiveObject (target, false, shallowReactiveHandlers, shallowCollectionHandlers)} / / the second argument, // shallow=true. The task attribute ref is not automatically unpacked. Const shallowGet = /*#__PURE__*/ createGetter(false, true) // Shallow mode const shallowSet = /*#__PURE__*/ createSetter(true)Copy the code
shallowReactive
Create a responsive object that tracks only its own property changes and does not respond to nested objects. Unlike Reactive, any attribute ref used is not automatically unpacked by the agent. This is because the shallow = true pattern is used.
const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) // mutating state's own properties is reactive state.foo++ // ... but does not convert nested objects isReactive(state.nested) // false state.nested.bar++ // non-reactiveCopy the code
shallowReadonly
Create a proxy that makes its own properties read-only but does not perform deep read-only conversion of nested objects. Unlike readonly, any attribute ref used is not automatically unpacked by the agent
const state = shallowReadonly({ foo: 1, nested: { bar: 2 } }) // mutating state's own properties will fail state.foo++ // ... but works on nested objects isReadonly(state.nested) // false state.nested.bar++ // worksCopy the code
2. Other apis
Take a look at the API source code implementation.
Export function isReactive(value: unknown): boolean { if (isReadonly(value)) { return isReactive((value as Target)[ReactiveFlags.RAW]) } return !! (value && (value as Target)[reactiveFlags.is_reactive])} export function isReadonly(value: unknown): boolean { return !! (value && (value as Target)[reactiveFlags.is_readonly])} export function isProxy(value: unknown): Boolean {return isReactive (value) | | isReadonly (value)} / / return reactive or readonly agent of the original object export function toRaw<T>(observed: T): T {return ((observed && toRaw ((observed as Target) [ReactiveFlags. RAW])) | | observed)} / / tag an object, Export function markRaw<T extends Object >(value: T): T {def(value, reactiveFlags.skip, true) return value}Copy the code
conclusion
So far, I have read the source code for the main core of Reactive. Here is a brief description of the core implementation process:
Const reactive = (target){return new Proxy(target, {get(target, prop) { Data into targetMap Track (Target, prop); return Reflect.get(target, prop); }, set(target, prop, newVal) { Reflect.set(target, prop, newVal); // Trigger effect trigger(target, prop); return true; }})}Copy the code
If there is any incorrect interpretation, please correct it. Next: Track, trigger and effect.