Talking about Vue3 Responsive Principle and Source Code Interpretation

1. Understand a few concepts

What is responsive

Before starting the responsive principle and source code analysis, you need to understand what is responsive? First, clarify a concept: responsiveness is a process , which has two participants:

  • Triggered by: data

  • Responder: Function that references data

When the data changes, the function that refers to the data will be automatically re-executed. For example, if the data is used in view rendering, the view will be automatically updated after the data changes, which completes a response process.

side effect function

Both in Vueand Reactin have the concept of side effect function, what is a side effect function? If a function refers to external data, this function will be affected by the change of external data, we say that this function has side effects , which is what we call a side effect function . It is not easy to understand the name at first, but in fact, a side effect function is a function that references data or a function associated with data . for example:

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
    <div id="app"></div>
    <script>
        const obj = {
            name: 'John',
        }
        // 副作用函数 effect
        function effect() {
            app.innerHTML = obj.name
            console.log('effect', obj.name)
        }

        effect()

        setTimeout(() => {
            obj.name = 'ming'
            // 手动执行 effect 函数
            effect()
        }, 1000);
    </script>
</body>
</html>

In the above example, effectexternal data is referenced in the function obj.name. If the data changes, it will affect this function. Similar effectfunctions are side effect functions.

Basic steps to implement responsiveness

In the above example, when obj.namea change occurs, effectwe execute it manually. If we can monitor obj.namethe change and let it automatically execute the side effect function effect, then the responsive process is realized. In fact, whether it is Vue2or Vue3, the core of responsiveness is the same 数据劫持/代理、依赖收集、依赖更新, but the difference in the specific implementation is due to the difference in the way of realizing data hijacking.

  • Vue2Responsive: 基于Object.defineProperty()hijacking of data achieved

  • Vue3Responsive: based on Proxyimplementing a proxy for the entire object

There is no Vue2focus on the responsiveness here. This article focuses on Vue3the realization of the responsiveness principle.

2. Proxy and Reflect

Before parsing Vue3the responsive principle, you first need to understand two new ES6 APIs: Porxyand Reflect.

Proxy

Proxy: Proxy, as the name suggests, is mainly used to create a proxy for the object, so as to realize the interception and customization of the basic operations of the object . It can be understood that a layer of "interception" is set up before the target object, and external access to the object must first pass through this layer of interception, so a mechanism is provided to filter and rewrite external access. Basic syntax:

let proxy = new Proxy(target, handler);
  • target: The target object that needs to be intercepted

  • handler: is also an object, used to customize the interception behavior. For example:

const obj = {
    name: 'John',
    age: 16
}

const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
// 输出
[Log] obj.age – 20
[Log] objProxy.age – 20 
[Log] obj与objProxy是否相等 – false

objProxyIf it is empty here handler, it directly points to the proxy object, and the proxy object is not . If you need more flexible interception of object operations, you need to handleradd corresponding attributes in . For example:

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        target[key] = value
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return delete target[key]
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

// 输出
[Log] 获取对象属性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 设置对象属性age值 (example01.html, line 26)
[Log] 删除对象属性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)

In the above example, we defined set()the , get(), and attributes in the capturer, and implemented the right operation interception deleteProperty()through the right operation . The trigger method for these properties has the following parameters:proxyobj

  • target-- is the target object, which is passed as the first parameter tonew Proxy

  • key- target attribute name

  • value- the value of the target property

  • receiver—— Points to the correct context for the current operation. If the target property is an getteraccessor property, then receiveris thisthe object . Usually, receiverthis is proxythe object itself, but if we proxyinherit from , we receivermean the object that proxyinherits

  • Of course, in addition to the above three, there are some common attribute operation methods:

    • has(), intercept: in operator.

    • ownKeys(), to intercept:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)

    • construct(), Intercept: newOperation etc.

Reflect

Reflect: Reflection is to reflect the content of the agent. ReflectAs Proxywith , new is also provided ES6for manipulating objects API. It provides methods for intercepting JavaScriptoperations, and these methods Proxy handlersare in one-to-one correspondence with the methods provided. As long as it is Proxythe method of the object, Reflectthe corresponding method can be found on the object. And Reflectis not a function object, that is, it cannot be instantiated, and all its properties and methods are static. Or the above example

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return Reflect.deleteProperty(target, key)
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

In the example above

  • Reflect.get()instead of target[key]operation

  • Reflect.set()instead of target[key] = valueoperation

  • Reflect.deleteProperty()delete target[key]Of course, in addition to the above methods, there are some commonly used methods for replacing operations Reflect:

Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

3. Reactive, ref source code analysis

After understanding Proxyand Reflect, let's see Vue3how to porxyimplement responsiveness. Its core is the two methods to be introduced below: reactive, ref. Here it is analyzed according to the source code of Vue3.2 version.

The source code implementation of reactive

Open the source file, find the file packages/reactivity/src/reactive.tsand view the source code.

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
  • At the beginning, targetthe responsive read-only judgment is made, and if it is true, it will be returned directly target. reactiveThe core method of implementation is createReactiveObject():

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`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[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
  • createReactiveObject()The method has five parameters:

    • target: the original target object passed in

    • isReadonly: Is it a read-only flag

    • baseHandlers: proxyThe second parameter when creating an ordinary objecthandler

    • collectionHandlers: The second parameter when collectioncreating an object of typeproxyhandler

    • proxyMap: WeakMaptype map, mainly used to targetstore proxythe correspondence between and his

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
  }
}
  • As you can see from the source code, he divides objects into COMMONobjects ( Objectand Array) and COLLECTIONtype objects ( Map, Set, WeakMap, WeakSet). The main purpose of this distinction is to customize different object types according to different object types.handler

  • In createReactiveObject()the first few lines, a series of judgments are made:

    • First judge targetwhether it is an object, if it is false, directlyreturn

    • Determine targetwhether it is a responsive object, if so true, directlyreturn

    • Determine whether it has been targetcreated proxy, if so true, directlyreturn

    • Determine targetwhether it is the 6 object types mentioned above, if yes false, directlyreturn

    • If the above conditions are met, it is targetcreated proxy, and returnthisproxy

handlerThe next step is to pass in different logic processing according to different object types. The main focus is baseHandlersthat there are five attribute operation methods, which focus on analysis getand setmethods.

Source location:packages/reactivity/src/baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
getwith dependency collection
  • You can see mutableHandlersthat there are various hook functions we are familiar with. When we proxyaccess or modify the object, we call the corresponding function for processing. First look at gethow to targetcollect the side effect functions of the access:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    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)

    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
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    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
  }
}
  • If keythe value is __v_isReactive, __v_isReadonlyreturn accordingly, if key==='__v_raw'and the value WeakMapin is not empty, then returnkeytargettarget

  • If targetit is an array, override /enhance the method corresponding to the array

    Call in these methods track()for dependency collection

    • How to find array elements :includes、indexOf、lastIndexOf

    • How to modify the original array :push、pop、unshift、shift、splice

  • Judging Reflect.get()the return value of the method, that is, the attribute value of the current data object res, if resit is an ordinary object and not read-only, call track()for dependency collection

  • If resit is a shallow response, return it directly, if resit is refan object, return its valuevalue

  • If resit is an object type and is read-only , call it readonly(res), otherwise call reactive(res)the method recursively

  • If none of the above is satisfied, directly return the corresponding attribute value

Then the core method is if you use ** track()** to process the dependency collection, the source code is in ``packages/reactivity/src/effect.ts`

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}
  • First, judge whether the dependency collection is in progress

  • const targetMap = new WeakMap<any, KeyToDepMap>()Create a targetMapcontainer to save the dependent content related to the current responsive object, which itself is a WeakMaptype

  • The corresponding responsivetargetMap as the key , targetMapand the Value is one depsMap(belonging to Mapthe instance ), depsMapwhich stores the specific dependencies keycorresponding

  • depsMapThe key is the key of the responsive data object, and the Value is an depsinstance (belonging to Setthe instance ). The reason why it is used here Setis to avoid repeated addition of side-effect functions and repeated calls

The above is the core process of the entire **get()** capturer and dependency collection.

setupdate with dependencies

Let's go back baseHandlersto see Sethow the dependency update is performed in the capturer

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    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
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : 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
    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
  }
}
  • First save the old valueoldValue

  • If it is not a shallow response targetbut an ordinary object, and the old value is a responsive object, perform the assignment operation: oldValue.value = value, returns true, indicating that the assignment is successful

  • Determine whether there is a corresponding key valuehadKey

  • Execution Reflect.setsetting the corresponding property value

  • Judging that the object is the content on the original prototype chain (not added by custom), then the dependency update will not be triggered

  • According to the target object does not have the corresponding key, call triggerand update the dependency

The above is the entire baseHandlerscore process of dependency collection and dependency update .

The source code implementation of ref

We know that we refcan define the responsiveness of basic data types and reference data types. Let's take a look at its source code implementation:packages/reactivity/src/ref.ts

export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}
  • As can be seen from the above function call process, refthe core of the implementation is to instantiate an RefImplobject. Why is an object instantiated here RefImpl? The purpose is that the target of Proxythe proxy is also an object type, and proxydata proxy cannot be performed by creating a basic data type. Only basic data types can be packaged as an object, and dependency collection and dependency update can be performed through custom get、setmethods

  • Look at RefImplthe meaning of the object properties:

    • _ value : used 保存ref当前值, if the passed parameter is an object , it is used to save the value converted by the reactive function , otherwise _valueit is _rawValuethe same as

    • _ rawValue : It is used to save the original value corresponding to the current ref value . If the passed parameter is an object , it is used to save the original value before conversion, otherwise _valueit is _rawValuethe same as . The function of this toRaw()function is to convert the responsive object into a normal object

    • dep : It is a Settype of data used to store the dependencies of the current refvalue collection. As for why Setwe use the explanation above, here is the same reason

    • _v_isRef : Mark bit, as long as it is refdefined, it will identify the current data as one Ref, that is, its value is marked astrue

    • In addition, it can be clearly seen that the method RefImpl类exposed to the instance object get、setis value , so refwe must bring **.value** for the operation of the defined responsive data

  • If the value passed in is an objectconvert() type, the method will be called , and reactive()the method will be called in this method to perform responsive processing on it

  • RefImplThe key to the example lies in the processing of the two functions of trackRefValue(this)and triggerRefValue(this, newVal). We probably also know that they are dependent collection and dependent update . The principle is basically reactivesimilar to the processing method, so I won’t elaborate here

V. Summary

  • For the basic data type , it can only refrealize its responsiveness through . The core still packs it into an object, and implements dependency collection and dependency update RefImplinternally through custom and . get value()set value(newVal)

  • For the object type , refboth reactivecan convert it into responsive data , but refinternally, it will eventually call reactivea function to realize the conversion. reactiveFunction, mainly through 创建了Proxy实例对象, through Reflectthe acquisition and modification of data.

Some references:

https://github.com/vuejs/vue

https://zh.javascript.info/proxy#reflect

- END -

About Qi Wu Troupe

Qi Wu Troupe is the largest front-end team of 360 Group, and participates in the work of W3C and ECMA members (TC39) on behalf of the group. Qi Wu Troupe attaches great importance to talent training, and has various development directions such as engineers, lecturers, translators, business interface people, and team leaders for employees to choose from, and provides corresponding technical, professional, general, and leadership training course. Qi Dance Troupe welcomes all kinds of outstanding talents to pay attention to and join Qi Dance Troupe with an open and talent-seeking attitude.

4e6f39033c9c9821f060ffa32caf7fda.png

Guess you like

Origin blog.csdn.net/qiwoo_weekly/article/details/131078699