Basically, the picture is a little clown, you can also see others compare the whole picture. @mxin

preface

For a long time no contact with Vue, in a few days ago to watch the big live talk about some views on the source code, is to better use Vue? Or do you want to learn the inner frame of mind?

Domestic front end: Interview, interview will ask.

In the environment seems to have been rolled to as long as you are a developer, then it is necessary to learn the source code, whether you are an intern, or fresh students, or years of experience of the old front end.

If you stop and don’t follow the roll, the sudden stress will overwhelm you, and you may find it difficult to survive the roll, even if you are right.

If you’re interested, read @Goldmudslide’s self-anxiety chart for programmers.

After reading this article, you will have a deeper understanding of vUE 3 data response.

The reason why I choose Reactivity module is that its coupling degree is low, and it is one of the core modules of VUe3.0, and its cost performance is very high.

Based on article

Before the beginning, if you do not understand some high-level API ES6, such as Proxy, Reflect, WeakMap, WeakSet,Map, Set and so on, you can browse to the resource chapter, first understand the knowledge in front of the re-watch as the best.

Proxy

In @vue/ reActivity, the Proxy is the cornerstone of the entire schedule.

Only through Proxy objects can the subsequent things, such as dependency collection, effect, track, trigger and other operations, be completed in get and set methods. I will not expand this in detail here, but will expand it in detail later.

If you are eager, talented, and have a foundation for ES6, you can jump directly to the principle section to watch and think.

Let’s start with a simple Proxy. In which handleCallback write set, get two methods, and to intercept the current property value change data listening. First the code:

const user = {
  name: 'wangly19'.age: 22.description: 'A little guy with a little hair loss. '
}

const userProxy = new Proxy(user, {
  get(target, key) {
    console.log('userProxy: The current obtained key is${key}`)
    if (target.hasOwnProperty(key)) return target[key]
    return{}},set(target, key, value) {
    console.log('userProxy: The current value key is${key}The value for${value}`)
    let isWriteSuccess = false
    if (target.hasOwnProperty(key)) {
      target[key] = value
      isWriteSuccess = true
    }
    return isWriteSuccess
  }
})

console.log('myNaame', userProxy.name)

userProxy.age = 23
Copy the code

The current set and GET methods are triggered when we modify and print the value, respectively.

This is so important that some of the other properties and usage methods won’t go into much detail here,

Reflect

Reflect is not a class, it’s a built-in object. You should not instantiate (new) handles directly. It is similar to Proxy handles, and adds many Object methods to it.

We won’t go into Reflect here, if you want to learn more about the function, you can find the corresponding address in the follow-up resources to learn. In this chapter, we focus on practicing objects safely through Reflect.

Here are some examples of modifications to the User object that you can refer to for future use.

const user = {
  name: 'wangly19'.age: 22.description: 'A little guy with a little hair loss. '
}

console.log('change age before' , Reflect.get(user, 'age'))

const hasChange = Reflect.set(user, 'age'.23)
console.log('set user age is done? ', hasChange ? 'yes' : 'no')

console.log('change age after' , Reflect.get(user, 'age'))

const hasDelete = Reflect.deleteProperty(user, 'age')

console.log('delete user age is done? ', hasDelete ? 'yes' : 'none')

console.log('delete age after' , Reflect.get(user, 'age'))
Copy the code

The principle of article

When you have some knowledge of the preconditions, it’s time to start the @vue/ Reactivity source parsing chapter. The following starts with a simple idea of implementing a basic reactivity. Once you understand the basics, you’ll be able to rely on @vue/reactivity to track and trigger updates, and what exactly the side effects are.

reactive

Reactive is the API used in VUe3 to generate reference types.

const user = reactive({
  name: 'wangly19'.age: 22.description: 'A little guy with a little hair loss. '
})
Copy the code

So if you look inside the function, what does the reactive method do?

Internally, the incoming object is judged to be a read-only target. If the target you pass is a read-only proxy, it will be returned. Reactive normally returns the value of the createReactiveObject.

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
Copy the code

createReactiveObject

In createReactiveObject, all you do is add a proxy for the target. Reactive gets a proxy. See the simple example in the Proxy section to see how reactive works. Let’s take a look at what createReactiveObject does.

The current target type is determined first, and if not, a warning is raised and the original value is returned.

if (! isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target }Copy the code

Second, determine whether the current object has been represented and is not read-only, then itself is a proxy object, so there is no need to proxy, directly return it as a return value, to avoid repeated proxy.

if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
Copy the code

It’s not too difficult to read this judgment code, just pay attention to the condition of the judgment in if () and see what it does. The most important thing createReactiveObject does is create a proxy for the target and record it in a Map.

What is interesting is that it calls a different proxy handle to the target that is passed in. So let’s take a look at what handles does.

const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
Copy the code

The type of handles

In the Object type, Object and Array are distinguished from Map,Set, WeakMap,WeakSet. They call a different Proxy Handle.

  • baseHandlers.ts:Object & ArrayWill be called under this filemutableHandlersObject as aProxy Handle.
  • collectionHandlers.ts:Map.Set.WeakMap.WeakSetWill be called under this filemutableCollectionHandlersObject as aProxy Handle.
/** * Object type judgment *@lineNumber 41 * /
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}
Copy the code

Will be determined in the new Proxy based on the returned targetType.

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
Copy the code

Because space is limited, only mutableHandlers are used as a reference for analysis. It’s only a matter of time before you understand the mutableHandlers for collectionHandlers.

Proxy Handle

Since we talked about calling different handles depending on the Type, let’s take a look at what the mutableHandlers do.

In the base section, it is known that a Proxy can receive a configuration object, where we demonstrated the property methods get and set. And mutableHandlers are the same thing. Internally define get, set, deleteProperty, has, oneKeys, etc. If you don’t know what that means, look at Proxy Mdn. Here you need to understand that as long as the monitored data is added, deleted or modified, the vast majority of the data will go to the corresponding receipt channel.

Here, we use a simple get, set for a simple simulation example.

function createGetter () {
    return (target, key, receiver) = > {
      const result = Reflect.get(target, key, receiver)
      track(target, key)
      return result
    }
}

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

function createSetter () {
  
  return (target, key, value, receiver) = > {
    const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  if(result && oldValue ! = value) { trigger(target, key) }return result
  }
}
Copy the code

In get, a track is collected, while in set, the trigger mechanism triggers the trigger. In VUE3, the words of trigger and track are declared in our effect.ts, so let’s take a look at what dependency collection and response triggers do.

Effect

For the entire Effect module, divide it into three parts to read:

  • effect: Side effect function
  • teack: Dependent collection inproxyThe proxy datagetCalled when the
  • trigger: Triggers the response inproxyCalled when the proxy data changes.

effect

Take a look at effect using an example and see that its main argument is a function. Inside the function will help you perform side effect recording and feature judgment.

effect(() => {
    proxy.user = 1
})
Copy the code

What did Vue’s Effect do?

In this case, we first determine whether the current parameter fn is an effect. If so, we replace the fn stored in RAW. Then redo the createReactiveEffect generation.

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}

Copy the code

At createReactiveEffect will push the effect into the effectStack, and then use the activeEffect to access the currently executing effect, and then it will be removed from the stack after the execution. Also replace activeEffect as the new stack top.

During the execution of the effect, proxy Handle, track and trigger are triggered.

function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (! effect.active) { return options.scheduler ? undefined : fn() } if (! effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }Copy the code

Let’s look at a simplified version of Effect and see if the code below is much cleaner, without most of the code baggage.

function effect(eff) {
  try {
    effectStack.push(eff)
    activeEffect = eff
    returneff(... argsument) }finally {
    effectStack.pop()
    activeEffect = effectStack[effectStack.length  - 1]}}Copy the code

Track (dependency collection)

During track, we will do the familiar dependency collection, and add the current activeEffect to dep, and talk about this kind of relationship. It’s going to have a one-to-many-to-many relationship.

The value is a Map called depsMap. It is used to manage the dePs (side effect dependencies) of each key in the current target. The value is a WeakMap. That used to be depend. In VUE3, this is done through a Set.

Targetmap.set (target, (depsMap = new Map())); Depsmap.set (key, (dep = new set()))) is also used to initialize the deps. Finally, the activeEffect is added to the deps to collect dependencies.

    1. intargetMapIn looking fortarget
    1. indepsMapIn looking forkey
    1. willactiveEffectSave todepThe inside.

This creates a one-to-many-to-many structure pattern that holds all the dependencies that have been hijacked by the proxy.

function track(target: object.type: TrackOpTypes, key: unknown) {
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep)if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

Trigger (response)

In a trigger, all you do is trigger the execution of the current response dependency.

First, you need to get the deps of all channels under the current key, so you’ll see that there’s an Effects and an add function that does a very simple job of determining whether the depsMap property that’s passed in now needs to be added to the Effects. The condition here is that effect cannot be the current activeEffect and effect.allowRECURse to ensure that all dependencies on the current set key are executed.

const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect) } }) } }Copy the code

The next familiar scenario is to determine what changes are being passed in. The most common is the TriggerOpTypes behavior that is passed in the trigger, and then the add method is executed to add the qualified effect to the effects. Here @vue/reactivity does a lot of data on variation behavior, such as length variation.

Then pull depsMap data for different TriggerOpTypes and put it into effects. The current effect is then executed through the run method, and then through the effect.foreach (run).

if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) = > {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if(key ! = =void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))}break
      case TriggerOpTypes.DELETE:
        if(! isArray(target)) { add(depsMap.get(ITERATE_KEY))if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break}}Copy the code

And what does Run do?

If there is a scheduler under Options, use schedule to process the execution. Otherwise, execute effect() directly.

if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
Copy the code

To shorten it a little bit, the processing logic is to take the dependency of the corresponding key from the targetMap.

const depsMap = targetMap.get(target)
  if(! depsMap) {return
  }
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach((effect) = > {
      effect()
    })
  }
Copy the code

Ref

It is well known that a REF is a responsive data declaration of vue3 for a common type. And to get the value of a ref.value, a lot of people think that a ref is a simple reactive, but it’s not reactive.

In the source code, the ref ends up calling a createRef method that internally returns an instance of RefImpl. Unlike a Proxy, the dependency collection and response triggering of a ref is in a getter/setter, as shown in the demo, gettter/setter.

export function ref<T extends object> (value: T) :ToRef<T>
export function ref<T> (value: T) :Ref<UnwrapRef<T>>
export function ref<T = any> () :Ref<T | undefined>
export function ref(value? : unknown) {
  return createRef(value)}function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)}Copy the code

As shown in the figure, Vue calls track in getter and GET in proxy to collect dependencies, and trigger is called after _value changes in setter.

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

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

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
Copy the code

So you should know by now:

  • proxy handleisreactiveThe principle ofrefThe principle isgetter/setter.
  • ingetIs called by thetrack.setIs called by thetrigger
  • effectIs the core of data response.

Computed

There are two common uses for computed. One is to pass in an object with set and GET methods, which are ComputedOptions.

export function computed<T> (getter: ComputedGetter<T>) :ComputedRef<T>
export function computed<T> (
  options: WritableComputedOptions<T>
) :WritableComputedRef<T>
export function computed<T> (
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
)
Copy the code

And inside there are two variables, the getter and the setter.

When getterOrOptions is a function, it is assigned to the getter.

When getterOrOptions is an object, set and get are assigned to setters and getters, respectively.

We then instantiate the ComputedRefImpl class as a parameter and return it as a return value.

let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () = > {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  
  return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
Copy the code

So what does ComputedRefImpl do?

Calculate the attribute of the source code, in fact, the vast majority of the dependence on the front of some understanding of effect.

First, as we all know, effect can pass a function and an object, Options.

Here the getter is passed as a function parameter, a side effect, and lazy and Scheduler are configured in options.

Lazy means that the effect is not executed immediately, while the scheduler is in the trigger that determines if the scheduler is passed in and executes the scheduler method.

On the computed Scheduler, the system determines whether the current _dirty is false. If yes, the system sets _dirty to true and performs the trigger to trigger the response.

class ComputedRefImpl<T> {
  private_value! : Tprivate _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true.scheduler: () = > {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})this[ReactiveFlags.IS_READONLY] = isReadonly
  }
Copy the code

In getters and setters, we do different things with _value.

First, in get value, determine whether the current._dirty is true, if so, perform a cached effect and store its return results in _value, and perform track for dependency collection.

Second, in set value, you call the _setter method to get the new value.

get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
Copy the code

The resource reference

Below are some reference resources, interested partners can have a look

  • ES6 series WeakMap
  • The Proxy and Reflect
  • Vue Mastery
  • Vue Docs
  • React introduces Vue3 @vue/ ReActivity to implement responsive state management

conclusion

If you are using Vue, it is highly recommended that you debug this section to see through, it will definitely help you to write code. Vue3 is in full swing, and now there are teams working on the production environment for project development, and the ecology of the community is slowly developing.

@vue/ Reactivity reading difficulty is not high, there are a lot of quality tutorials, there is a certain basis for work and code knowledge can step by step to understand down. I personally don’t need to know it by heart, understand what every line of code means, but to understand the core idea, learn the framework ideas and some ideas about how framework developers write code. It’s all about being able to take it and absorb it into your own knowledge.

For a front end that has moved into the React ecosystem, reading Vue’s source code is actually more to enrich their intellectual knowledge than to read it for interviews. Just as you recite not for a test, but to learn knowledge. In the current environment, it is difficult to do these things, calm down to concentrate on understanding a knowledge is better than reciting several scriptures.