“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:

  1. ProxyIs a proxy for the entire object, andObject.definePropertyOnly one attribute can be proxy.
  2. New property on object,ProxyYou can listen in,Object.definePropertyCan’t.
  3. Array new modification,ProxyYou can listen in,Object.definePropertyCan’t.
  4. If the internal properties of an object are to be recursively brokered,ProxyYou can recurse only when called, andObject.defineProperyYou need to do all the recursion at once, performance ratioProxyPoor.
  5. ProxyNot compatible with IE,Object.definePropertyIncompatible with IE8 and below
  6. ProxyUse more thanObject.definePropertyMore 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:

  1. Tests: Describes test cases related to code packages.
  2. Index. ts: Used to export package-specific implementation methods.
  3. 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.

  1. 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.

  1. Computed. Ts: Describes the implementation of computed properties. Actually withlazyProperties of theeffect.
  2. Effect. ts: describes how to track property changes and execute callback functions.
  3. BaseHandlers. Ts: Custom interception behavior specified by the agent for Object and Array data types.
  4. 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:

  1. Non-objects cannot do proxy hijacking.
  2. Target has been hijacked, return, do not repeat hijacking.
  3. Read only hijacking.
  4. Object __v_SKIP property is not true and the data type isObject,Array,Map,Set,WeakMap,WeakSetAnd the object is not frozen, because the object cannot be modified after it is frozen.
  5. 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.

  1. First of all,baseHandlersIs the interfaceProxyHandlerFirst 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.

  1. 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:

  1. 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).
  2. 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
  1. 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
  1. 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.