preface

In order to better explain, I will adjust the source code interface, type, function declaration order, and will add some comments to facilitate reading

In the previous chapter, we introduced the ref, and if you look closely, you should know the ref at the back of your hand. If you haven’t, or have forgotten…. You can start by reviewing the previous article.

The prerequisites for reading this article are:

  1. Proxy
  2. WeakMap
  3. Reflect

Reactive

Reactive is a file that doesn’t have much code, about 100 lines, and a lot of the logic is actually in handlers and effects. Let’s start with the introduction of this file:

External references

import {
  isObject, // Determine if it is an object
  toTypeString // Get the type name of the data
} from '@vue/shared'
// The handles here are eventually passed to the second parameter of Proxy(target, handle)
import {
  mutableHandlers, // Variable data proxy processing
  readonlyHandlers // Read-only (immutable) data agent processing
} from './baseHandlers'

// Collections means Set, Map, WeakMap, WeakSet
import {
  mutableCollectionHandlers, // Variable set data broker processing
  readonlyCollectionHandlers // Read-only collection data agent processing
} from './collectionHandlers'

// The last article talked about generic types for a long time
import { UnwrapRef } from './ref'
// This is the type of listener returned by effect
import { ReactiveEffect } from './effect'
Copy the code

So don’t be afraid, a lot of them are just simple tools and methods and types, but really there are a couple of handlers that are associated with external functions.

Types and Constants

Let’s look at declarations of types and variables, starting with the heavily commented targetMap.

// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
WeakMap is used to better reduce memory overhead.
export const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code

Traget means the first entry to the Proxy(Target, handle) function, which is the raw data that we want to turn into responsive data. But this KeyToDepMap actually doesn’t make any sense. We’ll leave it there until we actually use it.

Moving on, it’s a bunch of constant declarations.

// The word raw, which we saw in ref, means raw data in this library
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any.any> ()const reactiveToRaw = new WeakMap<any.any> ()const rawToReadonly = new WeakMap<any.any> ()const readonlyToRaw = new WeakMap<any.any> ()// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any> ()const nonReactiveValues = new WeakSet<any> ()// Set type
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
Object + array + collectionTypes
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
Copy the code

If you have read the single test (reactive’s 8th, 9th and 10th single test), you may remember that two Internal WeakMaps are needed to achieve bidirectional mapping of raw data to response data. Obviously rawToReactive and reactiveToRaw are the two WeakMap. RawToReadonly and readonlyToRaw are weakMaps that map raw data to read-only response data.

ReadonlyValues and nonReactiveValues may be related to markNonReactive and markReadonly, based on comments and memories from previous playtests. Guess is used to store data built with these two apis, but more on that later.

CollectionTypes observableValueRE

Tool function

Before actually looking at Reactive, we are going through some of the tools in this document so that we don’t have to jump around the source code. This part is easy. Just take a peek.

// Whether the data can be observed
const canObserve = (value: any) :boolean= > {
  return (
    Vue3 library _isVue is a vue component. Vue3 library _isVue is a vue component! value._isVue &&// The node of the virtual DOM cannot be observed! value._isVNode &&// Is an observable data type
    observableValueRE.test(toTypeString(value)) &&
    // The data stored in this collection is not observable! nonReactiveValues.has(value) ) }// If the data exists in reactiveToRaw or readonlyToRaw, the data is responsive
export function isReactive(value: any) :boolean {
  return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}

// Check if it is read-only responsive data
export function isReadonly(value: any) :boolean {
  return readonlyToRaw.has(value)
}

// Convert reactive data to raw data, or if not, return the source data
export function toRaw<T> (observed: T) :T {
  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}

// Pass data to add it to the read-only data collection
// Note that readonlyValues is a WeakSet, and repeated addition can be avoided by using the uniqueness of set elements
export function markReadonly<T> (value: T) :T {
  readonlyValues.add(value)
  return value
}

// Pass the data to add it to the unresponsive data set
export function markNonReactive<T> (value: T) :T {
  nonReactiveValues.add(value)
  return value
}
Copy the code

The core to realize

The code mentioned above is all the ingredients, so let’s look at the core code of this document, starting with the Reactive and readonly functions

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. IsReadonly if (readonlytoraw. has(target)) {return target} // Target is marked as readonly by user If (readonlyvalues.has (target)) {return readonly(target)} // The target of this step, Non-read-only data can be guaranteed // by using this method, Return createReactiveObject(target, // raw data rawToReactive, // raw data -> reactiveToRaw, / / response type data - > original data mapping mutableHandlers, / / agent hijack method of variable data mutableCollectionHandlers / / variable data collection agent hijacked method)} / / function declaration + implementation, accept an object, Returns a read-only reactive data. export function readonly<T extends object>( target: T ): Readonly<UnwrapNestedRefs<T>> { // value is a mutable observable, Retrieve its original and return // a readonly version. If (reactiveToraw.has (target)) {target = reactiveToraw.get (target)} // Create a reactive data return createReactiveObject( target, rawToReadonly, readonlyToRaw, readonlyHandlers, readonlyCollectionHandlers ) }Copy the code

The code for both methods is actually very simple. The main logic is encapsulated in createReactiveObject. The main functions of the two methods are:

  1. Through to thecreateReactiveObjectCorresponding bidirectional mapping map of proxy data to responsive data.
  2. reactiveWould like to be doingreadonlyAnd vice versareadonlySo is the method.

Here we go:

function createReactiveObject(
  target: any,
  toProxy: WeakMap<any.any>,
  toRaw: WeakMap<any.any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // Is not an object, returns raw data directly, in the development environment will be a warning
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)}return target
  }
  // Get the response data by mapping the original data -> response data
  let observed = toProxy.get(target)
  // target already has corresponding Proxy
  // If the original data is already reactive, the response data is returned directly
  if(observed ! = =void 0) {
    return observed
  }
  // target is already a Proxy
  // If the original data itself is response data, return itself directly
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // If the object is not observable, the original object is returned directly
  if(! canObserve(target)) {return target
  }
  // Collection data is handled differently than (object/array) data.
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // Declare a proxy object, i.e., reactive data
  observed = new Proxy(target, handlers)
  // Set up bidirectional mapping between raw data and responsive data
  toProxy.set(target, observed)
  toRaw.set(observed, target)

  // We use targetMap here, but we don't know what its value is
  if(! targetMap.has(target)) { targetMap.set(target,new Map())
  }
  return observed
}
Copy the code

We can see some details:

  1. If a non-object is passed, the development environment will alert you and no exception will be caused. This is because the production environment is extremely complex, because JS is a dynamic language, if the error is reported directly, it will directly affect all kinds of online applications. Here you just return the raw data and lose responsiveness, but it doesn’t cause the real page to fail.
  2. This method basically has no TS type.

The Reactive file is very straightforward, and after looking at it, we have only two questions in mind:

  1. baseHandlers.collectionHandlersThe specific implementation of and why to distinguish?
  2. targetMapWhat is it?

And of course we know that handlers do dependency collection and response triggering. So let’s just look at two files.

baseHandles

To open this file, look again at the external reference:

// We already know that
import { reactive, readonly, toRaw } from './reactive'
import { isRef } from './ref'
// These are the utility methods. HasOwn means whether an object owns some data
import { isObject, hasOwn, isSymbol } from '@vue/shared'
// Enumerations of actions that manipulate data are defined here
import { OperationTypes } from './operations'
// LOCKED: global immutability lock
// a global switch to determine whether data is immutable
import { LOCKED } from './lock'
// Collect dependencies and trigger listener methods
import { track, trigger } from './effect'
Copy the code

Only the internal implementation of track and trigger is unknown to us, and the rest is either already understood or can be understood by clicking on it.

Then there is a set of descriptors that represent the internal language behavior of JS. If you don’t understand, you can see the corresponding MDN. How to use it can be seen later.

const builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol)
    .map(key= > Symbol[key])
    .filter(key= > typeof key === 'symbol'))Copy the code

And then we have a hundred lines of code down there, and we have mutableHandlers, readonlyHandlers that are referenced in Reactive. We’ll start with simple mutableHandlers:

export const mutableHandlers: ProxyHandler<any> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
Copy the code

This is a ProxyHandle. If you forget about Proxy, look at MDN again.

And then finally came to the reactive system the key place, the five traps: get, set, deleteProperty, has, ownKeys. Of course, these five traps are not the only ones that can be implemented by Proxy. DefineProperty and getOwnPropertyDescriptor traps do not involve responsivity and do not need to be hijacked. And enumerate is obsolete. Enumerate would have hijacked the for-in operation, so you’d think, well, what happens to our for-in now that it’s gone? It still goes to the ownKeys trap and triggers our listener function.

But back to the code, let’s look at get, which is responsible for collecting dependencies. This trap is generated by createGetter, so let’s look at it.

get

CreateGetter takes an input parameter: isReadonly. That’s naturally true in readonlyHandlers.

// Only one input parameter is read-only
function createGetter(isReadonly: boolean) {
  / / get on the proxy, please read: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
  // Receiver is a proxy object that is created
  return function get(target: any, key: string | symbol, receiver: any) {
    / / if you still don't understand Reflect, it is suggested that read the document: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
    // Get the corresponding value of the original data
    const res = Reflect.get(target, key, receiver)
    // If it is a built-in method of JS, do not do dependency collection
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    // If the data is of the Ref type, it indicates that too many dependencies have been collected.
    if (isRef(res)) {
      return res.value
    }
    // Collect dependencies
    track(target, OperationTypes.GET, key)
    // If the value obtained by get is not an object, return it directly
    // Otherwise, response data is returned according to isReadyonly
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
Copy the code

At a glance, each expression of the GET method is actually quite simple, but it seems a bit confusing.

Question 1: Why should I passReflect“Rather than directlytarget[key]?

Yes, target[key] seems to work, so why Reflect and pass a receiver? The reason is that the get of raw data is not as simple as you might think, for example:

const targetObj = {
  get a() {
    return this}}const proxyObj = reactive(targetObj)
Copy the code

Proxyobj.a: proxyObj: targetObj: proxyObj: targetObj I think the logical answer would be proxyObj. But a is not a method, can not call/apply directly. And if you want to implement that, it’s a little bit more convoluted, it’s roughly equal to implementing polyfill for Reflect. So thanks to ES6, Reflect makes it easy to Reflect the existing action exactly as it is on the target object, while maintaining the true scope (via the third parameter, receiver). The receiver is the generated proxy object, in this case proxyObj.

Question 2: Why don’t built-in methods need to collect dependencies?

If a listener function looks like this:

const origin = {
  a() {}
}
const observed = reactive(origin)
effect((a)= > {
  console.log(observed.a.toString())
})
Copy the code

Obviously, when the origin. A changes, observed. A.tostring () should also change, so why not use monitoring? It is very simple, because the trap of observed.a.tostring () has gone once, there is no need to collect dependency repeatedly. Hence the similar built-in method, return directly.

Question 3: Why does an attribute value need to be reused when it is an objectreactive|readonlyPerform?

The comments read:

need to lazy access readonly and reactive here to avoid circular dependency

Is translated into mandarin, need to use reactive delay | readonly to avoid circular dependencies. This word needs to taste, fine taste, taste after a while finally taste understand.

Because of the Proxy thing, its traps actually only hijack the first level of access and updates to the object. If it’s a nested object, you can’t hijack it. So we have two approaches:

Method one: when using reactive | readonly transformation of the original object, the recursive solution of one layer, if the object is, reoccupy reactive execution, and then walk ProxyHandle. When you access these nested properties in the future, you will naturally end up with a trap. But there’s a big problem with that. What if the object is referenced in a loop? There has to be a logical judgment, if you find that the value of the property is self then it’s not recursive. What if it’s a circular reference? Like this:

const a = {
  b: {
    c: a
  }
}

const A = {
  B: {
    C: a
  }
}
Copy the code

It’s crazy to think about it.

Method two: that is, the method in the source code, when converting the original object, no recursion. If you find that the attribute value is an object when you go to the trap of GET, you can continue to transform and hijack it. That’s lazy in the comments. With this approach, circular references are naturally avoided. Another obvious benefit is that you can optimize performance.

In addition to these three questions, there is one small detail:

if (isRef(res)) {
  return res.value
}
Copy the code

If it is of type Ref, value is returned directly. Because the dependency trace logic has already been done in the REF function. In addition, if we look at the single test and ref sections, we know that this is where the code implements the ability to pass a nested ref type to a Reacitive function and return a recursive solution to the ref-type response. Thanks to this, the reactive function returns type UnwrapNestedRefs.

But remember: passing a pure Ref to Reactive does not unpack it. It only unpacks nested Ref data. The following is an example:

reactive(ref(4)) // = ref(4);
reactive({ a: ref(4)})// = { a: 4 }
Copy the code

So far, get has been figured out, except that track is an externally introduced method for collecting dependencies (see more later).

Now let’s look at set.

set

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
) :boolean {
  // If value is reactive, the mapped source data is returned
  value = toRaw(value)
  // Get the old value
  const oldValue = target[key]
  // If the old value is Ref, but the new value is not, update the value of the old value, return the update success
  if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
  }
  // There is a key in the proxy object, there is no indication that the operation is new
  const hadKey = hasOwn(target, key)
  // Reflect the setting action to the original object
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  // If it is a data operation on the original data prototype chain, do nothing to trigger the listener function.
  if (target === toRaw(receiver)) {
    // Istanbul is a single-measure coverage tool
    /* istanbul ignore else */
    if (__DEV__) {
      // In a development environment, trigger is passed extended data containing new and old values. Obviously, it's easy to do some debugging in a development environment.
      const extraInfo = { oldValue, newValue: value }
      // If there is no key, the attribute is added, and the operation type is ADD
      If the new value is not equal to the old value, the update operation is real, which triggers the trigger
      if(! hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) }else if(value ! == oldValue) { trigger(target, OperationTypes.SET, key, extraInfo) } }else {
      // Same logic as above, only less extraInfo
      if(! hadKey) { trigger(target, OperationTypes.ADD, key) }else if(value ! == oldValue) { trigger(target, OperationTypes.SET, key) } } }return result
}
Copy the code

Set is like GET. Every expression is clear, but we still have questions.

Question 1:isRef(oldValue) && ! isRef(value)What is the logic of this paragraph?

// If the old value is Ref, but the new value is not, update the value of the old value, return the update success
if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
}
Copy the code

When can oldValue be a Ref? Reactive has the ability to unnest ref data. Reactive has the ability to unnest ref data.

const a = {
  b: ref(1)}const observed = reactive(a) // { b: 1 }
Copy the code

At this point, observed. B outputs 1, when the assignment operation observed. B = 2. OldValue = a.b; oldValue = a.b; oldValue = a.b; oldValue = a.b; So why just go back and not trigger down? Because in ref, there is already the logic to hijack set.

Question 2: When will it happentarget ! == toRaw(receiver)?

In previous cognition, receiver kind of exists like this, which refers to the Proxy object executed by Proxy. The proxy object is converted to the original object by toRaw, which is congruent with target. Here involves a partial door knowledge points, detailed introduction can see MDN. One of them said:

Receiver: The object that is called initially. This is usually the proxy itself, but the handler’s set method may also be called indirectly on the prototype chain or in other ways (so not necessarily the proxy itself)

This is the meaning behind the comments in the code:

don’t trigger if target is something up in the prototype chain of original.

Here’s an example:

const child = new Proxy(
  {},
  {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('child', receiver)
      return true}})const parent = new Proxy(
  { a: 10 },
  {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('parent', receiver)
      return true}})Object.setPrototypeOf(child, parent)

child.a = 4

// Print the result
// parent Proxy {child: true, a: 4}
// Proxy {child: true, a: 4}
Copy the code

In this case, the set of the parent object is also raised once, but the passed receiver is always a Child, and the changed data is always a Child. In this case, the parent hasn’t actually changed, and logically, it really shouldn’t fire its listener.

Question 3: Arrays may update data through methods. What is the listening logic for this process?

For an object, we can assign property values directly, but what about arrays? If const arr = [], it can either arr[0] = ‘value’ or arr. Push (‘value’), but no trap hijacks push. But when you actually debug it, push also fires set twice.

const proxy = new Proxy([], {
  set(target, key, value, receiver) {
    console.log(key, value, target[key])
    return Reflect.set(target, key, value, receiver)
  }
})
proxy.push(1)
// 0 1 undefined
// length 1 1
Copy the code

The internal logic of push is to assign a value to the subscript and then set length, triggering the set twice. However, there is another phenomenon that although the length operation brought by push will trigger two sets of sets, the old length is already a new value when it comes to the length logic, so value === oldValue will actually only trigger once. But! If it is shift or unshift, such logic do not set up again, and if the array length is N, shift | unshift will bring N times the trigger. In fact, this involves the low-level implementation and specification of Array, and I cannot simply explain it. I suggest that you refer to the standards related to Array in ECMA-262.

But here did leave a small pit, shift | unshift and splice, will bring many times triggered effect. No optimizations have been seen in the Reacivity system. Of course, while using vue@3, run-time Core will do batch updates for rendering.

The logic of the set itself is well understood, except for an externally introduced trigger. But we know that it triggers the listener function when the data changes. We’ll see later.

Now it’s a little bit easier.

Other traps

// Hijacking attribute deleted
function deleteProperty(target: any, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    /* istanbul ignore else */
    if (__DEV__) {
      trigger(target, OperationTypes.DELETE, key, { oldValue })
    } else {
      trigger(target, OperationTypes.DELETE, key)
    }
  }
  return result
}
// hijack the in operator
function has(target: any, key: string | symbol) :boolean {
  const result = Reflect.has(target, key)
  track(target, OperationTypes.HAS, key)
  return result
}
/ / hijacked Object. Keys
function ownKeys(target: any) : (string | number | symbol) []{
  track(target, OperationTypes.ITERATE)
  return Reflect.ownKeys(target)
}
Copy the code

There are basically no difficulties in these traps, you can see them at a glance.

Finally, take a look at the special logic for readonly:

readonly

export const readonlyHandlers: ProxyHandler<any> = {
  // Create a trap for get
  get: createGetter(true),
  / / set the trap
  set(target: any, key: string | symbol, value: any, receiver: any) :boolean {
    if (LOCKED) {
      // Development environment operation read-only datagram warning.
      if (__DEV__) {
        console.warn(
          `Set operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      // If the immutable switch is off, the set data is allowed to change
      return set(target, key, value, receiver)
    }
  },
  // The logic of delete trap is similar to that of set
  deleteProperty(target: any, key: string | symbol): boolean {
    if (LOCKED) {
      if (__DEV__) {
        console.warn(
          `Delete operation on key "${String( key )}" failed: target is readonly.`,
          target
        )
      }
      return true
    } else {
      return deleteProperty(target, key)
    }
  },
  has,
  ownKeys
}
Copy the code

Readonly is also very simple. The createGetter logic has been seen before. Get does not change data, so why do you need to transmit isReadonly to reactive? This is because the dependency collection via GET is delayed hijacking of nested object data, so only isReadonly is passed through to let subsequent hijacked child objects know whether they should be read-only.

Since has and ownKeys do not alter data and do not recursively collect dependencies, they do not differentiate from mutable data logic.

After reading this, we will have a basic understanding of the dependencies to collect and trigger the listener.

A small summary

A quick summary of baseHandles:

  1. For raw object data, new responsive data (Proxy data) is returned via Proxy hijacking.
  2. Any read or write operations to the proxy data are passedRefelctReflected on the original object.
  3. During this process, the logic of collecting dependencies is performed for read operations. For write operations, the logic of the listening function is triggered.

In summary, it is relatively simple. But we’re missing the handlers of the collection data, which is really the hard part.

collectionHandlers

I open up this file and I see that this file is much longer than reactive and baseHandlers. Unexpectedly, it is the processing of data types that are rarely used that is the most troublesome.

Why do we do it separately

Before the source, in fact, there will be a question, why the Set | Map | WeakMap | WeakSet these data needs to be special treatment? Is it any different from the rest of the data? We click on the file, look at this handlers, and it looks like this:

export const mutableCollectionHandlers: ProxyHandler<any> = {
  get: createInstrumentationGetter(mutableInstrumentations)
}
export const readonlyCollectionHandlers: ProxyHandler<any> = {
  get: createInstrumentationGetter(readonlyInstrumentations)
}
Copy the code

Only get, no set, no has. What about hijack set and get? Why not hijack Set? Because we can’t do that, we can simply try:

const set = new Set([1.2.3])
const proxy = new Proxy(set, {
  get(target, key, receiver) {
    console.log(target, key, receiver)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(target, key, value, receiver)
    return Reflect.set(target, key, value, receiver)
  }
})
proxy.add(4)
Copy the code

This code gives an error when running:

Uncaught TypeError: Method Set.prototype.add called on incompatible receiver [object Object]

It is found that any time you hijack set, or introduce Reflect directly to Target, you will get an error. Why is that? It is also associated with Map | Set the internal implementation, their internal storage data must pass through this visit, known as the so-called “internal slots”, and through a proxy object to operation, this is actually a proxy, is not Set, so don’t have access to its internal data, and an array? For some historical reasons, yes. A detailed explanation can be found in this introduction to Proxy limitations. The article also offers a solution:

let map = new Map(a)let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(... arguments)return typeof value == 'function' ? value.bind(target) : value
  }
})
proxy.set('test'.1)
Copy the code

General principle is that when the acquisition is a function of the this binding as the original object, also is wants to hijack the map | set. This avoids the “this” orientation problem.

Then we can sort of understand why the collection data needs special processing and only hijacks a GET. So how do we do that? Let’s look at the code.

The convention is to look at references and tool methods first:

import { toRaw, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { LOCKED } from './lock'
import {
  isObject,
  capitalize, // Change the initial to uppercase
  hasOwn
} from '@vue/shared'

// Convert data to reactive data, or return itself if it is not an object
const toReactive = (value: any) = > (isObject(value) ? reactive(value) : value)
const toReadonly = (value: any) = > (isObject(value) ? readonly(value) : value)
Copy the code

These references, we should be able to understand them pretty much without looking at the annotations, except for one tool, the method capitalize that you have to look at a little bit. Then we need to adjust the reading order to see how a trap of get can hijack write operations.

Insert the pile

// proxy handlers
export const mutableCollectionHandlers: ProxyHandler<any> = {
  // Create a peg getter
  get: createInstrumentationGetter(mutableInstrumentations)
}
Copy the code

First of all, we have to understand its function, createInstrumentationGetter. Well, students who are poor in English like me may not understand what Instrumentation means. Here is the expression “pile in” meaning. I’m not going to say much about “piling”, but the common single test coverage is often achieved through piling.

In this code, staking refers to a method being injected with a piece of code that does something else, in order to hijack those methods and add logic to them. Let’s see how it’s done here.

// Variable data staking object, and a series of corresponding staking methods
const mutableInstrumentations: any = {
  get(key: any) {
    return get(this, key, toReactive)
  },
  get size() {
    return size(this)
  },
  has,
  add,
  set.delete: deleteEntry,
  clear,
  forEach: createForEach(false)}// Iterator-dependent methods
const iteratorMethods = ['keys'.'values'.'entries', Symbol.iterator]
iteratorMethods.forEach(method= > {
  mutableInstrumentations[method] = createIterableMethod(method, false)
  readonlyInstrumentations[method] = createIterableMethod(method, true)})// Create a getter function
function createInstrumentationGetter(instrumentations: any) {
  // Return a pinned get
  return function getInstrumented(
    target: any,
    key: string | symbol,
    receiver: any
  ) {
    // If there is a pile object with this key, and the target object also has this key,
    // Then use the peg object as the reflection get object, otherwise use the original object
    target =
      hasOwn(instrumentations, key) && key in target ? instrumentations : target
    return Reflect.get(target, key, receiver)
  }
}
Copy the code

From the above, due to the native nature of Proxy and collection data, there is no way to hijack set or directly reflect it. So here, we create a new object with the same method name as set and Map. These method names correspond to methods that inject dependency collection and response triggers after staking. Then Reflect to the pile object, get the data after the pile, call the method after the pile.

For some custom attributes or methods, Reflect will Reflect the original data instead of the inserted data. For these cases, reactive logic will not be applied, such as in single test:

it('should not observe custom property mutations'.(a)= > {
  let dummy
  const map: any = reactive(new Map())
  effect((a)= > (dummy = map.customProp))

  expect(dummy).toBe(undefined)
  map.customProp = 'Hello World'
  expect(dummy).toBe(undefined)})Copy the code

Pile insertion read operation

2. The VariableInstrumentations, from the top, are instrumenting.

const mutableInstrumentations: any = { get(key: Return get(this, key, toReactive)} return get(this, key, toReactive)} return get(this, key, toReactive)} } function get(target: any, key: any, wrap: (t: any) => any): Any {// get the raw data target = toRaw(target) // Key = toRaw(key) const proto: Any = reflect.getPrototypeof (target) // Collection relies on track(target, operationtypes.get, key) // Uses prototype methods to retrieve the value of the key from raw data. Const res = proto.get.call(target, key) // wrapCopy the code

Note: In the get method, the first entry parameter target cannot be confused with the first entry parameter of the Proxy constructor. The first input parameter to the Proxy function, target, refers to the raw data. In the get method, the target is actually the proxied data. Also known as receiver in reflect. get(target, key, receiver).

Then it becomes clear that the essence is to avoid the above problems by using the prototype method of raw data + Call this and return the real data.

const mutableInstrumentations: any = {
  // ...
  get size() {
    return size(this)
  },
  has
  // ...
}
function size(target: any) {
  // Get the raw data
  target = toRaw(target)
  const proto = Reflect.getPrototypeOf(target)
  track(target, OperationTypes.ITERATE)
  return Reflect.get(proto, 'size', target)
}

function has(this: any, key: any) :boolean {
  // Get the raw data
  const target = toRaw(this)
  key = toRaw(key)
  const proto: any = Reflect.getPrototypeOf(target)
  track(target, OperationTypes.HAS, key)
  return proto.has.call(target, key)
}
Copy the code

Size and has, both are “check” logic. Size is a property, not a method, so you need to get size() to hijack it. Has is a method that does not need to bind this. The internal logic of the two methods is simple and basically the same as that of GET. But here’s a little detail about TypeScript. Has (somemap.has (key)) somemap.has (key) somemap.has (key) somemap.has (key) somemap.has (key) somemap.has (key)

In addition to these two lookup methods, there are iterator-related lookup methods.

Pile iterator

If you are not familiar with iterators, you are advised to read the relevant documentation, such as MDN.

// Iterator-dependent methods
const iteratorMethods = ['keys'.'values'.'entries', Symbol.iterator]
iteratorMethods.forEach(method= > {
  mutableInstrumentations[method] = createIterableMethod(method, false)})function createIterableMethod(method: string | symbol, isReadonly: boolean) {
  return function(this: any. args:any[]) {
    // Get the raw data
    const target = toRaw(this)
    // Get the prototype
    const proto: any = Reflect.getPrototypeOf(target)
    // isPair is true for entries, or for map iterations
    // In this case the iterator method returns a [key, value] structure
    const isPair =
      method === 'entries' ||
      (method === Symbol.iterator && target instanceof Map)
    // Call the corresponding iterator method on the prototype chain
    const innerIterator = proto[method].apply(target, args)
    // Get the corresponding method to convert the response data
    const wrap = isReadonly ? toReadonly : toReactive
    // Collect dependencies
    track(target, OperationTypes.ITERATE)
    // return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    // Pin the innerIterator returned and turn its value into responsive data
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? // When done, value is next of the last value, undefined, there is no need for reactive conversion
            { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol
      [Symbol.iterator]() {
        return this}}}}Copy the code

The core of this logic is to hijack the iterator method and convert the value returned by next to reactive. The only can let a person not clear, is for the Iterator and Map | Set are not familiar with. If you are not familiar with them, it is recommended to read their documentation first.

There is also a forEach method associated with iterators.

function createForEach(isReadonly: boolean) {
  // This, which we already know is a false argument, is the caller to forEach
  return function forEach(this: any, callback: Function, thisArg? :any) {
    const observed = this
    const target = toRaw(observed)
    const proto: any = Reflect.getPrototypeOf(target)
    const wrap = isReadonly ? toReadonly : toReactive
    track(target, OperationTypes.ITERATE)
    // important: create sure the callback is
    // 1. invoked with the reactive map as `this` and 3rd arg
    // 2. the value received should be a corresponding reactive/readonly.
    // Pin the incoming callback method to convert the incoming callback data into responsive data
    function wrappedCallback(value: any, key: any) {
      // The data used by forEach is converted into responsive data
      return callback.call(observed, wrap(value), wrap(key), observed)
    }
    return proto.forEach.call(target, wrappedCallback, thisArg)
  }
}
Copy the code

The logic of forEach is not complicated, similar to the above iterator part, but also hijacks the method, converting the original parameter data into responsive data and returning it.

Pile writing operation

Then look at the write operation.

function add(this: any, value: any) {
  // Get the raw data
  value = toRaw(value)
  const target = toRaw(this)
  // Get the prototype
  const proto: any = Reflect.getPrototypeOf(this)
  // Use the prototype method to determine whether the key exists
  const hadKey = proto.has.call(target, value)
  // Add this key via the prototype method
  const result = proto.add.call(target, value)
  // If there is no key, it is new, and the listener response logic is triggered
  if(! hadKey) {/* istanbul ignore else */
    if (__DEV__) {
      trigger(target, OperationTypes.ADD, value, { value })
    } else {
      trigger(target, OperationTypes.ADD, value)
    }
  }
  return result
}
Copy the code

We find that writing is much simpler, and the logic is similar to baseHandlers, except that for the base data, we can Reflect the convenient reflection behavior, whereas in this case we need to manually retrieve the prototype chain and bind this. Set deleteEntry = set deleteEntry = set deleteEntry = set deleteEntry

Readonly is also very simple, I also don’t post code, purely to increase the number of words in the article. It is to add | set | delete | clear these writing method to pack a layer, the development environment throw a warning.

At this point, I’ve finally seen all the logic of collcetionsHandlers.

A small summary

To summarize how it hijacks collcetion data.

  1. Due to theSet|MapThe underlying design problem of data collection,ProxyNo direct hijackingsetOr direct reflex behavior.
  2. Hijacking the original collection datagetFor its original method or property,ReflectReflection to the peg holder, otherwise reflecting the original object.
  3. The method on the inserter will pass firsttoRaw, get the raw data of the proxy data, then get the prototype method of the raw data, and then bindthisIs the original data and retrieves the corresponding method.
  4. forgetter|hasThis type of query method inserts the collection dependent logic and converts the return value into responsive data (has returns a Boolean value so conversion is not required).
  5. For iterator dependent query methods, insert collect dependency logic and turn the iterated process data into responsive data.
  6. For write related methods, insert logic that triggers listening.

In fact, the principle is easy to understand, but it is more difficult to write.

conclusion

So that’s it. We’ve finally worked out the logic of Reactive. Reading this part of the code is a bit difficult, because it involves a lot of low-level knowledge, otherwise it will be confused everywhere, but it is also a learning process, the process of exploration is also quite interesting.

In this process, we found that the array hijacking is still a little weak, directly through reflection, will trigger the listener repeatedly in some cases. Feeling can be solved by processing similar collection data. But this adds to the complexity of the program, and there’s no telling if there might be some other bugs.

In addition, we found that when we read the reactivity-related code, ts was not as much as we expected. In many cases, it was any, but this should be looked at dialectically. First of all, as Xiaorighty said, “These data are user data, which itself is any, and it is forced to be declared, so there is no significance”. And that was a lot of generics plus derivation all the way down, very expensive. I tried it on my own anyway, and there was nothing I could do. In addition, the current code is still informal stage, if it is too troublesome to maintain. It’s hard for someone like me to contribute code if I really want to.

This article is a bit complicated, if you read it slowly, thank you very much for reading ~~

The next part is the final source code analysis related to effect, which can finally solve the puzzle of targetMap at the beginning, see the internal implementation of track and trigger, and put together the last piece of the puzzle.