preface

Learning Vue3.0 source code must have an understanding of the following knowledge:

  1. proxy reflect iterator
  2. map weakmap set weakset symbol

These knowledge can look at ruan yifeng teacher’s ES6 introduction tutorial.

Read the source code, it is recommended to go through the API under the module first, to understand what functions there are. Then look again at the relevant unit tests, which typically test out all the functional details. It is better to read the details of the source code after understanding the function.

The proxy term

const p = new Proxy(target, handler)
Copy the code
  • Handler, a placeholder object that contains a trap, can be translated as a handler object.
  • Target: the object that is proxied by Proxy.

Friendship remind

As you read the source code, always ask yourself three questions:

  1. What’s this?
  2. Why? Why not?
  3. Is there a better way to do it?

As the saying goes, know why.

Read the source code to understand not only what features a library has, but also why it is designed the way it is, and whether you can implement it in a better way. If you just stay in the “what” phase, it probably won’t help you. It’s like reading a book, and then you forget it. You have to think about it to understand it better.

The body of the

The reactivity module is a responsive system for Vue3.0. It has the following files:

baseHandlers.ts
collectionHandlers.ts
computed.ts
effect.ts
index.ts
operations.ts
reactive.ts
ref.ts
Copy the code

The API usage and implementation of each file will be explained in order of importance.

Reactive. Ts file

In vue.2x, objects are listened on using Object.defineProperty(). In Vue3.0, Proxy is used to monitor. Proxy has the following advantages over object.defineProperty () :

  1. You can listen for attributes to be added or deleted.
  2. You can listen for changes in an index of an array as well as changes in its length.

reactive()

Reactive () is primarily used to transform the target into a reactive proxy instance. Such as:

const obj = {
    count: 0
}

const proxy = reactive(obj)
Copy the code

If the object is nested, the child object is recursively converted into a responsive object.

Reactive () is the API exposed to the user, and what it really does is execute the createReactiveObject() function:

// Generate a proxy instance based on target
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
       
        , collectionHandlers: ProxyHandler
        
       ) {
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${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.isReactive]) ) {return target
  }
  // target already has corresponding Proxy
  if (
    hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
  ) {
    return isReadonly
      ? target[ReactiveFlags.readonly]
      : target[ReactiveFlags.reactive]
  }
  // only a whitelist of value types can be observed.
  if(! canObserve(target)) {return target
  }
 
  const observed = new Proxy(
    target,
    WeakMap, WeakSet Determine proxy handler parameters according to whether Set, Map, WeakMap, WeakSet
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  // Define an attribute on the original object ("__v_readonly" if read-only, "__v_reactive" otherwise). The value of this attribute is the proxy instance generated from the original object.
  def(
    target,
    isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
    observed
  )
  
  return observed
}
Copy the code

The processing logic for this function is as follows:

  1. If target is not an object, return target.
  2. If target is already a proxy instance, return target.
  3. If Target is not an observable object, return Target.
  4. Generate the proxy instance and add a property (read-only) to the original target object__v_readonly, or for__v_reactive), points to the proxy instance, and returns the instance. This attribute is added for judgment purposes in step 2 to prevent repeated listening on the same object.

The third and fourth points need to be carried out separately.

What is an observable

const canObserve = (value: Target): boolean= > {
  return (
    !value[ReactiveFlags.skip] &&
    isObservableType(toRawType(value)) &&
    !Object.isFrozen(value)
  )
}
Copy the code

The canObserve() function is used to check whether a value is an observable if the following conditions are met:

  1. The value of reactiveFlags. skip cannot be__v_skip.__v_skipIs used to define whether the object can be skipped, that is, not listened on.
  2. The type of target must be one of the following valuesObject,Array,Map,Set,WeakMap,WeakSetTo be monitored.
  3. Cannot be a frozen object.

What is the processor object passed to the proxy

As you can see from the code above, when generating the proxy instance, the processor object is generated from a ternary expression:

// collectionTypes 的值为 Set, Map, WeakMap, WeakSet
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
Copy the code

This ternary expression is very simple. If it’s a normal Object or Array, the processor Object uses baseHandlers; So if it’s Set, Map, WeakMap, WeakSet, we use collectionHandlers.

CollectionHandlers and baseHandlers are introduced from collectionHandlers. Ts and baseHandlers.

How many proxy instances are there

CreateReactiveObject () Creates different proxy instances with different parameters:

  1. Fully responsive proxy instances are called recursively if they have nested objectsreactive().
  2. Read-only proxy instance.
  3. Shallow responsive proxy instances, in which only the first layer of an object’s properties are responsive.
  4. Read-only shallow responding proxy instance.

What is the shallow response proxy instance?

The reason why there are shallow proxy instances is that the proxy only proxies the first level attributes of the object, not the deeper attributes. If you really need to generate a fully responsive proxy instance, you need to recursively call Reactive (). However, this process is automatically performed internally and is not perceived by the user.

Some other functions are introduced

// Check whether value is responsive
export function isReactive(value: unknown) :boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.raw])
  }
  return!!!!! (value && (valueas Target)[ReactiveFlags.isReactive])
}
// Check whether value is read-only
export function isReadonly(value: unknown) :boolean {
  return!!!!! (value && (valueas Target)[ReactiveFlags.isReadonly])
}
// Check whether value is a proxy instance
export function isProxy(value: unknown) :boolean {
  return isReactive(value) || isReadonly(value)
}

// Convert reactive data to raw data, or if not, return the source data
export function toRaw<T> (observed: T) :T {
  return (
    (observed && toRaw((observed as Target)[ReactiveFlags.raw])) || observed
  )
}

// Set skip property for value to skip proxy and make data unproxied
export function markRaw<T extends object> (value: T) :T {
  def(value, ReactiveFlags.skip, true)
  return value
}
Copy the code

BaseHandlers. Ts file

Incorrect handlers are defined for the four proxy instances in the basehandlers.ts file. Since there is not much difference between them, we will only cover fully responsive processor objects here:

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

The processor intercepts five operations:

  1. Get property reading
  2. Set property setting
  3. DeleteProperty Deletes an attribute
  4. Has Indicates whether a property is owned
  5. ownKeys

OwnKeys can intercept the following operations:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. Reflect.ownKeys()

Dependencies are collected by the GET, HAS, and ownKeys operations, and triggered by the set and deleteProperty operations.

get

The handler for the get attribute is created with the createGetter() function:

// *#__PURE__*/
const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // target is a responsive object
    if (key === ReactiveFlags.isReactive) {
      return! isReadonly// target is a read-only object
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (
      / / if the access key is __v_raw and receiver = = target. __v_readonly | | receiver. = = target __v_reactive
      // Return target directly
      key === ReactiveFlags.raw &&
      receiver ===
        (isReadonly
          ? (target as any).__v_readonly
          : (target as any).__v_reactive)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    // If the target is an array and the key belongs to one of the three methods ['includes', 'indexOf', 'lastIndexOf'], one of the three operations is triggered
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // No matter how the Proxy changes the default behavior, you can always get the default behavior in Reflect.
    // If you don't Reflect, something might go wrong when listening on the array
    Specific see article / / the data detection in the Vue3 - https://juejin.cn/post/6844903957807169549#heading-10
    const res = Reflect.get(target, key, receiver)

    // If the key is symbol and belongs to one of Symbol's built-in methods, or if the object is a prototype, the result is returned without collecting dependencies.
    if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
      return res
    }

    // Read-only objects do not collect dependencies
    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }// Shallow responses are returned immediately without recursive calls to reactive()
    if (shallow) {
      return res
    }

    // If it is a ref object, the real value is returned, i.e. Ref. Value, except for arrays.
    if (isRef(res)) {
      // ref unwrapping, only for Objects, not for Arrays.
      return targetIsArray ? res : res.value
    }

    if (isObject(res)) {
      // If the target[key] value is an object, it will continue to be proxied
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code

The processing logic of this function should be clear from the code comments. There are a few points that need to be mentioned separately:

  1. Reflect.get()
  2. Array handling
  3. builtInSymbols.has(key)True or prototype objects do not collect dependencies

Reflect.get()

The reflect.get () method is similar to reading a property from an object (target[key]), but it operates through a function execution.

Why reflect. get(target, key, receiver) when you can get the value directly from target[key]?

Let’s start with a simple example:

const p = new Proxy([1.2.3] and {get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
    }
})

p.push(100)
Copy the code

Running this code returns an error:

Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
Copy the code

But it works fine with a few minor changes:

const p = new Proxy([1.2.3] and {get(target, key, receiver) {
        return target[key]
    },
    set(target, key, value, receiver) {
        target[key] = value
        return true // Add a line return true
    }
})

p.push(100)
Copy the code

This code works fine. Why is that?

The difference is that the new code adds a return true to the set() method. The explanation I found on MDN goes like this:

The set() method should return a Boolean value.

  • returntrueIndicates that the property is set successfully.
  • In strict mode, ifset()Method returnsfalse, then one will be thrownTypeErrorThe exception.

At this time, I tried to execute p[3] = 100 directly, and found that it works normally, only execute push method will report an error. By this point, I had the answer in mind. To verify my guess, I added console.log() to the code, printing out some properties of the code’s execution.

const p = new Proxy([1.2.3] and {get(target, key, receiver) {
        console.log('get: ', key)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        target[key] = value
        return true
    }
})

p.push(100)

// get: push
// get: length
// set: 3 100
// set: length 4
Copy the code

As you can see from the code above, the length property is also accessed when the push operation is performed. The execution process is as follows: according to the value of length, obtain the final index, then set a new set, and finally change the length.

Combined with MDN’s explanation, my guess is that the array native method should run in strict mode (if anyone knows the truth, please leave it in the comments section). Because in JS a lot of code will work in both non-strict mode and strict mode, but strict mode will give you an error. As in this case, the last attempt to set the length property failed, but the result was fine. If you don’t want an error, you have to return true every time.

Then look at the reflect.set () return statement:

Returns a Boolean value indicating whether the property was set successfully.

So the above code could look like this:

const p = new Proxy([1.2.3] and {get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})

p.push(100)
Copy the code

Also, no matter how the Proxy changes the default behavior, you can always get the default behavior at Reflect.

From the example above, it’s easy to see why reflect.set () is used instead of Proxy to do the default operation. The same goes for reflect.get ().

Array handling

// If the target is an array and the key belongs to one of the three methods ['includes', 'indexOf', 'lastIndexOf'], one of the three operations is triggered
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
  return Reflect.get(arrayInstrumentations, key, receiver)
}
Copy the code

When executing the includes, indexOf, and lastIndexOf methods, the target object is converted to arrayInstrumentations and then executed.

const arrayInstrumentations: Record<string, Function> = {}
;['includes'.'indexOf'.'lastIndexOf'].forEach(key= > {
  arrayInstrumentations[key] = function(. args: any[]) :any {
    // If the getter is specified in the target object, receiver is the this value when the getter is called.
    // So this refers to receiver, the proxy instance, toRaw to get the raw data
    const arr = toRaw(this) as any
    // Track each value of the array to collect dependencies
    for (let i = 0, l = (this as any).length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + ' ')}// we run the method using the original args first (which may be reactive)
    If the function returns -1 or false, try again with the original value of the argument
    constres = arr[key](... args)if (res === -1 || res === false) {
      // if that didn't work, run it again using raw values.
      returnarr[key](... args.map(toRaw)) }else {
      return res
    }
  }
})
Copy the code

As you can see from the above code, Vue3.0 wraps includes, indexOf, and lastIndexOf. In addition to returning the results of the original methods, Vue3.0 also collects dependent values of each array.

builtInSymbols.has(key)True or prototype objects do not collect dependencies

const p = new Proxy({}, {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})

p.toString() // get: toString
             // get: Symbol(Symbol.toStringTag)
p.__proto__  // get: __proto__
Copy the code

From the execution result of p.tostring (), it triggers two get’s, one we want and one we don’t want (Symbol (symbol.tostringTag) is not clear to me, please leave a comment if anyone knows). Builtinsymbols. has(key) is true and returns it directly to prevent repeated collection of dependencies.

The result of the p.__proto__ execution also triggers a get operation. In general, there is no scenario that requires a separate access to the stereotype, only to access methods on the stereotype, such as p.__proto__.tostring (), so skip dependencies with key __proto__.

set

const set = /*#__PURE__*/ createSetter()

/ / reference document data in Vue3 detection - https://juejin.cn/post/6844903957807169549#heading-10
function createSetter(shallow = false) {
  return function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
    const oldValue = (target as any)[key]
    if(! shallow) { value = toRaw(value)// If the original value is ref, but the new value is not, assign the new value to ref.value.
      if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey = 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) {// If the target does not have a key, it is a new operation that needs to trigger a dependency
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // Dependencies are triggered if the old and new values are not equal
        // When will old and new values be equal? For example, if you're listening to an array and you do push, you fire setters multiple times
        // The first setter is the new value and the second setter is the length change caused by the new value
        Value === oldValue
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

The logic of the set() function is not so hard to handle. Track () and trigger() are explained below along with the effect.ts file.

DeleteProperty, has, ownKeys

function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  // If the delete result is true and target owns the key, the dependency is triggered
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target: object, key: string | symbol) :boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}

function ownKeys(target: object) : (string | number | symbol) []{
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}
Copy the code

These three functions are relatively simple, just look at the code.

Effect. The ts file

By the time we get through the effect.ts file, the reactive module is almost done.

effect()

Effect () is used primarily with reactive objects.

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  // If it is already an effect function, get the original fn
  if (isEffect(fn)) {
    fn = fn.raw
  }
  
  const effect = createReactiveEffect(fn, options)
  // If lazy is false, execute it immediately
  // Calculates the attribute's lazy to true
  if(! options.lazy) { effect() }return effect
}
Copy the code

It is the createReactiveEffect() function that actually creates an effect.

let uid = 0

function createReactiveEffect<T = any> (fn: (... args: any[]) => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  // reactiveEffect() returns a new effect after execution
  // It sets itself to activeEffect and then executes fn if reactive properties are read in fn
  // The reactive property GET operation is triggered to collect the dependency, which is called activeEffect
  const effect = function reactiveEffect(. args: unknown[]) :unknown {
    if(! effect.active) {return options.scheduler ? undefined: fn(... args) }// To avoid recursive loops, check
    if(! effectStack.includes(effect)) {// Clear dependencies
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        returnfn(... args) }finally {
        // track adds the dependent function activeEffect to the corresponding DEP, and then activeEffect in finally
        // Reset to the value of the previous effect
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true // To determine whether effect is active, a stop() is used to set it to false
  effect.raw = fn
  effect.deps = []
  effect.options = options
  
  return effect
}
Copy the code

Cleanup (effect) cleans up all deP instances of an effect association.

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0}}Copy the code

As you can see from the code, the real dependent function is activeEffect. The dependency that performs track() collection is activeEffect. Now let’s look at the track() and trigger() functions.

track()

// Rely on collection
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect is null, indicating that no dependency is returned
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  // targetMap dependency manager, used to collect and trigger dependencies
  let depsMap = targetMap.get(target)
  // targetMap creates a map for each target
  // Each target key corresponds to a deP
  // Then use the deP to collect the dependent function. When the listening key changes, the dependent function in the DEP is triggered
  // Something like this
  // targetMap(weakmap) = {
  // target1(map): {
  // key1(dep): (fn1,fn2,fn3...)
  // key2(dep): (fn1,fn2,fn3...)
  / /},
  // target2(map): {
  // key1(dep): (fn1,fn2,fn3...)
  // key2(dep): (fn1,fn2,fn3...)
  / /},
  // }
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep)// The onTrack event is raised in the development environment
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

TargetMap is a WeakMap instance.

A WeakMap object is a set of key/value pairs where the keys are weakly referenced. The key must be an object, and the value can be arbitrary.

What does weak reference mean?

let obj = { a: 1 }
const map = new WeakMap()
map.set(obj, 'test')
obj = null
Copy the code

When obj is set to null, the reference to {a: 1} is zero, and the object in WeakMap will be reclaimed in the next garbage collection.

However, if weakMap is replaced with a Map data structure, {a: 1} will not be reclaimed even if OBj is null, because the Map data structure is a strong reference and it is still referenced by map.

trigger()

// Trigger dependencies
export function trigger(target: object, type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  
  const depsMap = targetMap.get(target)
  // If no dependencies have been collected, return directly
  if(! depsMap) {// never been tracked
    return
  }
  
  // The collected dependencies are classified as normal or computed attribute dependencies
  // Effects collects ordinary dependencies computedRunners collect dependencies for calculated properties
  // Both queues are set structures to avoid repeated collection of dependencies
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        // effect ! ActiveEffect Prevents repeated collection of dependencies
        if(effect ! == activeEffect || ! shouldTrack) {// Calculate attributes
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop}}}})// Add all of target's dependencies to the corresponding queue before the value is cleared
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) { // Emitted when the length property of the array changes
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // If not, and key! == undefined, add a dependency to the corresponding queue
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    constisAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && ! isArray(target))if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      Scheduler is called if the scheduler exists, and the calculation property owns the scheduler
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  // Trigger dependent functions
  effects.forEach(run)
}
Copy the code

After classifying dependent functions, you need to run the dependencies of computed properties first, because other ordinary dependency functions may contain computed properties. A dependency that executes the calculated property first ensures that the most recent calculated property value is available when the normal dependency executes.

What is the use of type in track() and trigger()?

The type range is defined in the operations.ts file:

// Type of track
export const enum TrackOpTypes {
  GET = 'get'./ / get operation
  HAS = 'has'./ / from the operation
  ITERATE = 'iterate' / / ownKeys operations
}

// The type of trigger
export const enum TriggerOpTypes {
  SET = 'set'.// Set the old value to the new value
  ADD = 'add'.// Add a new value. For example, add an array of values to an object
  DELETE = 'delete'.// Delete operations such as delete operations on objects and pop operations on arrays
  CLEAR = 'clear' // Clear for Map and Set operations.
}
Copy the code

Type indicates the type of track() and trigger().

The sequential judgment code in trigger()

if(key ! = =void 0) {
  add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
constisAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && ! isArray(target))if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}

if (isAddOrDelete && target instanceof Map) {
  add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
Copy the code

In trigger() there is a sequence of judgments. What do they do? In fact, they are used to judge the array/collection data structure of the more special operations. Here’s an example:

let dummy
const counter = reactive([])
effect(() = > (dummy = counter.join()))
counter.push(1)
Copy the code

Effect (() => (dummy = counter.join()))) generates a dependency and executes it once. When we execute counter. Join (), we access multiple properties of the array, namely join and length, and trigger track() to collect dependencies. That is, the join Length attribute of the array collects a dependency.

When you execute counter.push(1), you actually set the index 0 of the array to 1. This can be seen from the context by typing the debugger, where key is 0, the index of the array, and the value is 1.

After setting the value, execute trigger(target, triggeroptypes.add, key, value) since it is a new operation. However, as can be seen from the above, only when the key of the array is join length, there is no dependency.

As can be seen from the above two figures, only the join Length attribute has corresponding dependencies.

At this point, a string of if statements from trigger() come into play, one of which looks like this:

if (
  isAddOrDelete ||
  (type === TriggerOpTypes.SET && target instanceof Map)
) {
  add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
Copy the code

If target is an array, add the corresponding dependency to the length attribute to the queue. That is, if the key is 0, use the dependency corresponding to length.

There is another subtlety. The queue on which the execution depends is a set data structure. If the key is 0 and the length has the corresponding dependency, the dependency will be added twice. However, since the queue is set, it has the effect of automatic deduplication, avoiding repeated execution.

The sample

It’s hard to understand how responsive data and track() trigger() work together just by looking at code and text. So we’ll use the example to understand:

let dummy
const counter = reactive({ num: 0 })
effect(() = > (dummy = counter.num))

console.log(dummy == 0)
counter.num = 7
console.log(dummy == 7)
Copy the code

The above code execution process is as follows:

  1. right{ num: 0 }Listen and return a proxy instance, which is counter.
  2. effect(fn)Creates a dependency and executes it once when it is createdfn.
  3. fn()Read num and assign it to dummy.
  4. The read property operation triggers the proxy’s read property interception operation, which collects the dependency generated in Step 2.
  5. counter.num = 7This action triggers the proxy property setting interception, in which, in addition to returning the new value, the dependency that was just collected is also triggered. In this dependency, assign counter. Num to dummy(num has been changed to 7).

It looks something like this:

CollectionHandlers. Ts file

CollectionHandlers. Ts file contains Map WeakMap Set WeakSet processor object, corresponding to fully responsive proxy instance, shallow responsive proxy instance and read-only proxy instance respectively. This is only the handler object for a fully responsive proxy instance:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false.false)}Copy the code

Why only listen for get, set has, etc.? Take your time and look at an example:

const p = new Proxy(new Map(), {
    get(target, key, receiver) {
        console.log('get: ', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set: ', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})

p.set('ab'.100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]
Copy the code

Running the above code results in an error. This has to do with the internal implementation of Map sets, which must be accessed through this. But when reflected, this inside target refers to a proxy instance, so it’s not hard to see why.

So how do we solve this problem? Through the source can be found, in Vue3.0 is through the proxy way to achieve the Map Set and other data structure monitoring:

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (target: CollectionTypes, key: string | symbol, receiver: CollectionTypes) = > {
    // These three if judgments are handled the same way as baseHandlers
    if (key === ReactiveFlags.isReactive) {
      return! isReadonly }else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (key === ReactiveFlags.raw) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}
Copy the code

To simplify the last line of code:

target = hasOwn(instrumentations, key) && key in target? instrumentations : target
return Reflect.get(target, key, receiver);
Copy the code

2. Instrumentations

const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size() {
    return size((this as unknown) as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false.false)}Copy the code

The actual processor object is the mutableInstrumentations. Now look at another example:

const proxy = reactive(new Map())
proxy.set('key'.100)
Copy the code

After generating the proxy instance, execute proxy.set(‘key’, 100). The proxy.set operation triggers the proxy property read interception.

As you can see, the key is set. After intercepting the set operation, reflect. get(Target, key, Receiver) is called, and the Target is not the original target, but the mutableInstrumentations object. That is to say, the final execution is mutableInstrumentations set ().

Then look at the variableInstrumentations processor logic.

get

Reactive (value) if value is an object, return a reactive(value) object, otherwise return value.
const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value get(this: MapTypes, key: Unknown) {// this refers to proxy return get(this, key, toReactive)} function get(target: MapTypes, key: unknown, wrap: Typeof toReactive | typeof toReadonly | typeof toShallow) {target = toRaw (target) const rawKey = toRaw / / if the key (key) Is reactive, and an additional collection depends on if (key! == rawKey) { track(target, TrackOpTypes.GET, key) } track(target, TrackOpTypes.GET, Const {has, get} = getProto(target) const {has, get} = getProto(target) Return wrap(get.call(target, key))} else if (has.call(target, key)) {return wrap(get.call(target, key))} else if (has.call(target, key)) rawKey)) { return wrap(get.call(target, rawKey)) } }Copy the code

After intercepting get, call GET (this, key, toReactive).

set

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  // Get the raw data
  const target = toRaw(this)
  // Use the methods on the Target prototype
  const { has, get, set } = getProto(target)

  let hadKey = has.call(target, key)
  if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  const result = set.call(target, key, value)
  // Prevent dependencies from being triggered repeatedly if the key already exists
  if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
    // Dependencies are not triggered if the old and new values are equal
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return result
}
Copy the code

Set processing logic is also relatively simple, with annotations at a glance.

There are the rest of has add delete and other methods will not explain, the number of lines of code is relatively small, the logic is very simple, it is recommended to read by yourself.

Ref. Ts file

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val export function ref(value? : unknown) { return createRef(value) } function createRef(rawValue: Shallow = false) {// If the ref object is already shallow, If (isRef(rawValue)) {return rawValue} reactive(rawValue) let value = shallow? rawValue : convert(rawValue) const r = { __v_isRef: True, // to indicate that this is a ref object, Track (r, trackoptypes.get, 'value') return value}, set value(newVal) { if (hasChanged(toRaw(newVal), rawValue)) { rawValue = newVal value = shallow ? NewVal: convert(newVal) // Trigger dependencies when setting values trigger(r, triggeroptypes. SET, 'value', __DEV__? { newValue: newVal } : void 0 ) } } } return r }Copy the code

In ve2. X, base numeric types are not listened on. In Vue3.0, however, this effect can be achieved by ref().

const r = ref(0)
effect(() = > console.log(r.value)) / / print 0
r.value++ / / print 1
Copy the code

Ref () converts 0 to a ref object. If the value passed to ref(value) is an object, reactive(value) is called inside the function to turn it into a proxy instance.

The computed. Ts file

export function computed<T> (
  options: WritableComputedOptions<T>
) :WritableComputedRef<T>
export function computed<T> (
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T> / / ifgetterOrOptionsIs a function, is not configurable,setterLet's say it's an empty functionif (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () = > {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // If it is an object, it is readable and writable
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // dirty Is used to determine whether the reactive attributes used to calculate attribute dependencies have been changed
  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true.// If lazy is true, the resulting effect will not be executed immediately
    // mark effect as computed so that it gets priority during trigger
    computed: true.scheduler: () = > { / / scheduler
      // Trigger when the calculated property executes effect.options.scheduler(effect) instead of effect()
      if(! dirty) { dirty =true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  
  computed = {
    __v_isRef: true.// expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}
Copy the code

Here’s an example of how computed works:

const value = reactive({})
const cValue = computed(() = > value.foo)
console.log(cValue.value === undefined)
value.foo = 1
console.log(cValue.value === 1)
Copy the code
  1. Generate a proxy instance value.
  2. computed()Generate a calculated property object when cValue is evaluated (cValue.valueIf dirty is false, return the value directly.
  3. Set effect to activeEffect in the effect function and run the getter(() => value.fooValue). During the evaluation process, read the value of foo (value.foo).
  4. This triggers the get property read interception operation, which in turn triggers track to collect the dependency function that is the activeEffect generated in Step 3.
  5. When a reactive attribute is reassigned (value.foo = 1), will trigger the activeEffect function.
  6. And then callscheduler()Set dirty to true so that the next time computed is evaluated, the effect function is re-executed.

Index. Ts file

The index.ts file exports the API of the reactivity module.

Vue3 series of articles

  • In-depth understanding of Vue3 responsivity principle
  • Vue3 template compilation principle

The resources

  • Data detection in Vue3
  • Vue3 Reactive source code parsing -Reactive article
  • Vue3 responsive system source code analysis -Effect