3

Today's article is that the author will take you to deeply analyze the realization of the reactive principle of Vue3 and how the reactive in the reactive basic API is realized. For the Vue framework, its non-intrusive responsive system is one of the most unique features, so regardless of any version of Vue, after familiarizing with its basic usage, the responsive principle is the part that I want to learn first. It is also the part that must be studied carefully when reading the source code. After all, knowing yourself knows the enemy, and when you use Vue, mastering the principle of responsiveness will definitely make your coding process easier.

Responsive principle of Vue2

Before we start to introduce the responsive principle of Vue3, let's review the responsive principle of Vue2 together.

When we pass a common option to the data option of the Vue instance, Vue will traverse all the properties of this object and use Object.defineProperty to convert all these properties to getter/setter. When Vue2 processes arrays, it also hijacks the method of changing the elements in the array through the prototype chain, observes the new elements in the prototype chain, and distributes update notifications.

vue2-observer

Here is a picture of responsiveness introduced in the Vue2 document. For some descriptions in the document, the author will not repeat them, but compare the pictures from the perspective of Vue2's source code. There is an observer module under the src/core path in the source code of Vue2, which is the place where the response is handled in Vue2. Under this module, the observer is responsible for converting objects and arrays into responsive ones, that is, the purple part in the figure, and processing Data getters and setters. When the option in data is accessed, the getter will be triggered. At this time, the wather.js module under the observer directory will start to work. Its task is to collect dependencies. The dependencies we collect are instantiated objects of the Dep class. When the options in the data are changed, the setter will be called. In the process of the setter, the notify function of dep is triggered to dispatch update events, thereby realizing the response monitoring of the data.

Responsive changes in Vue3

After briefly reviewing the reactive principle of Vue2, we will have a doubt. What is the difference between the reactive principle of Vue3 and Vue2?

The biggest difference in the responsive system in Vue3 is that the data model is a JavaScript object being proxied. Whether we return a normal JavaScript object in the component's data option or use the composition api to create a reactive object, Vue3 will wrap the object in a Proxy with get and set handlers.

The Proxy object is used to create a proxy for an object, so as to realize the interception and customization of basic operations (such as attribute lookup, assignment, etc.).

The basic syntax is similar to:

const p = new Proxy(target, handler)

What are the advantages of Proxy over Object.defineProperty? Let's start with this question from the drawbacks of Object.defineProperty.

From the perspective of Object, since Object.defineProperty generates getter/setter for the specified key for change tracking, if the key does not exist on the object we defined at the beginning, the responsive system will be powerless, so in Vue2 The addition or removal of the object's property cannot be detected. For this defect, Vue2 provides vm.$set and the global Vue.set API so that we can add responsive properties to objects.

From the perspective of the array, when we directly use the index to set an array item, or when we modify the length of the array, Vue2's responsive system cannot monitor the change. The solution is the same as above, using the two mentioned above api.

These problems are all non-existent in the face of ES6's new feature Proxy. Proxy objects can use handler traps to capture any changes during get and set, and can also monitor changes to array indexes and changes to array length.

The way of dependency collection and distribution of updates has also become different in Vue3. Here I will quickly describe it as a whole: In Vue3, dependencies are collected through the processor function of track, and updates are dispatched through the processor function of trigger. The use of each dependency will be wrapped in an effect function, and the side effect function will be executed after the update is dispatched, so that the value of the dependency is updated.

Responsive implementation based on reactive

Since this is a source code analysis article, let's analyze how responsive is implemented from the perspective of source code. Therefore, the author will first analyze the reactive-based API-reactive. I believe that by explaining the implementation of reactive, everyone will have a deeper understanding of Proxy.

reactive

Without further ado, just look at the source code. The following is a function of the reactive API. The parameter of the function accepts an object. After createReactiveObject function, it directly returns a proxy object.

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 如果试图去观察一个只读的代理对象,会直接返回只读版本
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  // 创建一个代理对象并返回
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

In the third line, you can see whether the object is a read-only object by judging whether there is an IS_READONLY key in ReactiveFlags in the target. The ReactiveFlags enumeration will continue to meet with us in the source code, so it is necessary to introduce ReactiveFlags in advance:

export const enum ReactiveFlags {
  SKIP = '__v_skip', // 是否跳过响应式 返回原始对象
  IS_REACTIVE = '__v_isReactive', // 标记一个响应式对象
  IS_READONLY = '__v_isReadonly', // 标记一个只读对象
  RAW = '__v_raw' // 标记获取原始值
}

There are 4 enumeration values in the ReactiveFlags enumeration, and the meanings of these four enumeration values are in the comments. The use of ReactiveFlags is a very good application for proxy objects to trap traps in handlers. These keys do not exist in the object, and when these keys are accessed through get, the return value is processed in the function of the get trap. After introducing ReactiveFlags, we continue to look down.

createReactiveObject

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
)

First look at the signature of the createReactiveObject function, which accepts 5 parameters:

  • target: The target object, you want to generate a responsive original object.
  • isReadonly: Whether the generated proxy object is read-only.
  • baseHandlers: Handler parameters for generating proxy objects. This handler is used when the target type is Array or Object.
  • collectionHandlers: Use this handler when the target type is Map, Set, WeakMap, or WeakSet.
  • proxyMap: Stores the Map object after generating the proxy object.

What needs to be noted here is the difference between baseHandlers and collectionHandlers. These two parameters will be judged according to the type of target, and finally choose which parameter to pass into the Proxy constructor and use it as the handler parameter.

Then we start to look at the logical part of createReactiveObject:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 如果目标不是对象,直接返回原始值
  if (!isObject(target)) {
    return target
  }
  // 如果目标已经是一个代理,直接返回
  // 除非对一个响应式对象执行 readonly
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 目标已经存在对应的代理对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 只有白名单里的类型才能被创建响应式对象
  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
}

In the logic part of the function, you can see that the basic data type will not be converted into a proxy object, but will directly return the original value.

And the generated proxy object will be cached into the incoming proxyMap. When the proxy object already exists, it will not be generated repeatedly, and the existing object will be returned directly.

The target type will also be judged by TargetType. Vue3 will only generate proxies for Array, Object, Map, Set, WeakMap, WeakSet, and other objects will be marked as INVALID and return the original value.

When the target object passes the type verification, a proxy object proxy will be generated through new Proxy(). The incoming handler parameter is also related to the targetType, and finally the generated proxy object will be returned.

So reviewing the reactive api, we may get a proxy object, or it may just get the original value of the incoming target object.

The composition of Handlers

There are two modules, baseHandlers and collectionHandlers, in the @vue/reactive library, which generate trap traps in the handlers of the Proxy proxy respectively.

For example, in the reactive api generated above, the parameter of baseHandlers is passed in a mutableHandlers object, which looks like this:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

We can know that there are 5 traps in mutableHandlers through the variable name. In baseHandlers, both get and set are generated through factory functions to facilitate adaptation to other APIs besides reactive, such as readonly, shallowReactive, shallowReadonly, etc.

baseHandlers deal with the data types of Array and Object, which are also the types we use most of the time when using Vue3, so the author will focus on the get and set traps in baseHandlers.

get trap

As mentioned in the previous paragraph, get is generated by a factory function. Let’s first look at the types of get traps.

const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

There are 4 types of get traps, which correspond to different reactive APIs. You can know the corresponding API name from the name, which is very clear. And all get is generated by the createGetter function. So next we focus on the logic of createGetter.

Still the old rules, let's start with the function signature.

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {}
}

createGetter has two parameters, isReadonly and shallow, which allow apis that use get traps to use them on demand. The inside of the function returns a get function, which uses a higher-order function to return the function that will be passed to the get parameter in handlers.

Then look at the logic of createGetter:

// 如果 get 访问的 key 是 '__v_isReactive',返回 createGetter 的 isReadonly 参数取反结果
if (key === ReactiveFlags.IS_REACTIVE) {
  return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
  // 如果 get 访问的 key 是 '__v_isReadonly',返回 createGetter 的 isReadonly 参数
  return isReadonly
} else if (
  // 如果 get 访问的 key 是 '__v_raw',并且 receiver 与原始标识相等,则返回原始值
  key === ReactiveFlags.RAW &&
  receiver ===
    (isReadonly
      ? shallow
        ? shallowReadonlyMap
        : readonlyMap
      : shallow
        ? shallowReactiveMap
        : reactiveMap
    ).get(target)
) {
  return target
}

From this createGetter logic, the ReactiveFlags enumeration that the author specifically introduced has achieved a magical effect here. In fact, these keys are not in the target object, but Vue3 treats these keys specially in get. When we access these special enumeration values on the object, it will return a result with a specific meaning. And you can pay attention to the judgment method of the ReactiveFlags.IS_REACTIVE key. Why is the read-only flag reversed? Because when an object's access can trigger this get trap, it means that the object must already be a Proxy object, so as long as it is not read-only, it can be considered a reactive object.

Then look at the follow-up logic of get.

Continue to determine whether the target is an array. If the proxy object is not read-only, and the target is an array, and the accessed key is in a method where the array needs special processing, the result of the special processing array function will be directly called and returned.

ArrayInstrumentations is an object in which several specially processed array methods are stored and stored in the form of key-value pairs.

We said before that Vue2 hijacked arrays in the form of prototype chains, and it has a similar effect here, and we plan to introduce the array part in a follow-up article. The following is an array that needs special processing.

  • Index-sensitive array methods

    • includes、indexOf、lastIndexOf
  • Array methods that will change their length, need to avoid length being collected by dependency, because this may cause circular references

    • push、pop、shift、unshift、splice
// 判断 taeget 是否是数组
const targetIsArray = isArray(target)
// 如果不是只读对象,并且目标对象是个数组,访问的 key 又在数组需要劫持的方法里,直接调用修改后的数组方法执行
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}

// 获取 Reflect 执行的 get 默认结果
const res = Reflect.get(target, key, receiver)

// 如果是 key 是 Symbol,并且 key 是 Symbol 对象中的 Symbol 类型的 key
// 或者 key 是不需要追踪的 key: __proto__,__v_isRef,__isVue
// 直接返回 get 结果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
  return res
}

// 不是只读对象,执行 track 收集依赖
if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

// 如果是 shallow 浅层响应式,直接返回 get 结果
if (shallow) {
  return res
}

if (isRef(res)) {
  // 如果是 ref ,则返回解包后的值 - 当 target 是数组,key 是 int 类型时,不需要解包
  const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  return shouldUnwrap ? res.value : res
}

if (isObject(res)) {
  // 将返回的值也转换成代理,我们在这里做 isObject 的检查以避免无效值警告。
  // 也需要在这里惰性访问只读和星影视对象,以避免循环依赖。
  return isReadonly ? readonly(res) : reactive(res)
}

// 不是 object 类型则直接返回 get 结果
return res

After processing the array, we execute the Reflect.get method on the target to obtain the get return value of the default behavior.

Then determine whether the current key is a Symbol, or whether it is a key that does not need to be tracked, and if it is, directly return the result of get res.

The following 👇 several keys do not need to be collected by dependencies or return responsive results.

  • __proto__
  • _v_isRef
  • __isVue

Then determine whether the current proxy object is a read-only object. If it is not read-only, run the tarck processor function mentioned above to collect dependencies.

If it is shallow, there is no need to convert the internal attributes into a proxy, and directly return res.

If res is an object of type Ref, it will be automatically unpacked and returned. Here we can explain the feature that ref mentioned in the official document will automatically unpack in reactive. It should be noted that when the target is an array type and the key is an int type, that is, when an array element is accessed using an index, it will not be automatically unpacked.

If res is an object, the object will be converted into a responsive Proxy proxy object and returned. Combined with the proxy object generated by the cache we analyzed before, we can know that the logic here will not generate the same res repeatedly, or Understand that when we access the key in the reactive object as an object mentioned in the document, it will also be automatically converted into a reactive object, and since generating a reactive or readonly object here is a delayed behavior, it does not need to be the first It takes time to traverse all the keys in the object passed in by reactive, which is also a help to the performance improvement.

When res does not meet the above conditions, the res result is returned directly. For example, the basic data type will directly return the result without special processing.

At this point, the logic of get traps is all over.

set trap

Corresponding to createGetter, set also has a createSetter factory function, which also returns a set function by currying.

The function signatures are all the same, so the author will directly show you the logic.

The set function is relatively short, so this time put the commented code up at once, look at the code first, and then talk about the logic.

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)
      // 当不是 shallow 模式时,判断旧值是否是 Ref,如果是则直接更新旧值的 value
      // 因为 ref 有自己的 setter
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // shallow 模式不需要特殊处理,对象按原样 set
    }
        
    // 判断 target 中是否存在 key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // Reflect.set 获取默认行为的返回值
    const result = Reflect.set(target, key, value, receiver)
    // 如果目标是原始对象原型链上的属性,则不会触发 trigger 派发更新
    if (target === toRaw(receiver)) {
      // 使用 trigger 派发更新,根据 hadKey 区别调用事件
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

In the process of set, the old and new values are first obtained. When the current proxy object is not a shallow comparison, it will judge whether the old value is a Ref. If the old value is not an array and is a ref type object, and the new value is not When ref object, it will directly modify the value of the old value.

You may have questions when you see this. Why do you want to update the value of the old value? If you have used the api of ref, you will know that the value of each ref object is placed in value, and the implementation of ref and reactive is different. Ref is actually a class instance, and its value has its own set. , So the set will not continue here. The ref part will be explained in detail in subsequent articles.

After processing the ref type value, a variable hadKey will be declared to determine whether the key currently to be set is an existing property in the object.

Next, call Reflect.set to get the set return value result of the default behavior.

Then it will start the process of distributing the update. Before distributing the update, you need to ensure that the target is equal to the original receiver, and the target cannot be an attribute on the prototype chain.

After that, the trigger processor function is used to distribute updates. If the hadKey does not exist, it is a new attribute, which is marked by the TriggerOpTypes.ADD enumeration. Here you can see where the opening analysis of Proxy is stronger than Object.defineProperty, any new key will be monitored to make the responsive system more powerful.

If the key is an attribute that already exists on the current target, compare the old and new values. If the old and new values are not the same, it means that the attribute has been updated. Use TriggerOpTypes.SET to mark the distribution update.

After the update is distributed, the result of the set is returned, and the set ends.

to sum up

In today's article, the author first reviewed the responsive principle of Vue2, and then began to introduce the responsive principle of Vue3. By comparing the difference between the responsive system of Vue2 and Vue3, the improvement of the responsive system of Vue3 was revealed, especially The main adjustment is to replace Object.defineProperty with Proxy proxy object.

In order to let everyone attribute the influence of Proxy on the reactive system, the author emphatically introduces the basic reactive API: reactive. The implementation of reactive and the handlers traps used by the proxy object returned by the reactive api are analyzed. And analyze the source code of our most commonly used get and set in the trap. I believe that after reading this article, everyone will have a new understanding of the use of proxy, a new feature of ES2015.

This article only introduces the first article of the Vue3 responsive system, so the process of track collection dependencies and trigger distribution updates is not expanded in detail. In subsequent articles, I plan to explain the side-effect function effect and the process of track and trigger in detail. To understand the source code of the responsive system, please pay attention to avoid getting lost.

Finally, if this article can help you understand the reactive principle and the implementation of reactive in Vue3, I hope I can give this article a little like ❤️. If you want to continue to follow up the follow-up articles, you can also follow my account or follow my github , thank you again for the lovely masters.


Originalix
165 声望63 粉丝

前端工程师,欢迎来 github 互相 follow, originlaix