[suggested collection] here are all the Vue3 core source codes you want to know

Posted by maxpup979 on Wed, 26 Jan 2022 02:04:57 +0100

Writing is not easy. Reprinting in any form is prohibited without the permission of the author!
If you think the article is good, you are welcome to pay attention, praise and share!
Continue to share technical blog, pay attention to WeChat official account. 👉🏻 Front end LeBron

Effect and Reactive

As the core of Vue's responsive principle, effect appears in Computed, Watch and Reactive

It mainly works with Reactive(Proxy), track, trigger and other functions to collect dependencies and trigger dependency updates

  • Effect
    • Side effect dependent function
  • Track
    • Dependency collection
  • Trigger
    • Dependency trigger

Effect

Effect can be understood as a side effect function, which is collected as a dependency and triggered after responsive data update.

Vue's responsive API s, such as Computed and Watch, are implemented with effect

  • Let's look at the entry function first
    • The entry function is mainly used for some logic processing, and the core logic is located in createReactiveEffect
function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // If it is already an effect, reset it
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // Create effect
  const effect = createReactiveEffect(fn, options)
  // If it is not lazy execution, execute it once first
  if (!options.lazy) {
    effect()
  }
  return effect
}
  • createReactiveEffect
const effectStack: ReactiveEffect[] = []

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    // If it is not activated, the effect stop function is called
    if (!effect.active) {
      // If there is no dispatcher, return directly; otherwise, execute fn
      return options.scheduler ? undefined : fn()
    }
    // Judge whether there is an effect in the EffectStack. If there is, it will not be processed
    if (!effectStack.includes(effect)) {
      // Clear effect
      cleanup(effect)
      try {
        /*
        * Start collecting dependencies again
        * Press in stack
        * Set effect to activeEffect
        * */
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        /*
        * When finished, the effect will pop up
        * Reset dependency
        * Reset activeEffect
        * */
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++ // Self incrementing id, unique id of effect
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true // Is it effect
  effect.active = true  // Activate
  effect.raw = fn   // Mount original object
  effect.deps = []  // dep array of current effect
  effect.options = options  // Incoming options
  return effect
}

// Every time the effect runs, the dependencies will be collected again. deps is the dependency array of the effect, which needs to be emptied
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
  }
}

Track

The Track function often appears in the getter function of reactive and is used for dependency collection

See notes for detailed source code

function track(target: object, type: TrackOpTypes, key: unknown) {
  // An empty activeEffect indicates that there is no dependency
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  // targetMap dependency management Map is used to collect dependencies
  // Check whether there is a target in the targetMap. If not, create a new one
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // Dep is used to collect dependent functions. When the monitored key value changes, the dependent function update in dep is triggered
  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 development environment triggers onTrack for debugging only
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

Trigger

Trigger often appears in the setter function in reactive to trigger dependency updates

See notes for detailed source code

function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // Get the dependency Map. If it doesn't exist, it doesn't need to be triggered
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  // Use Set to save the effect to be triggered to avoid repetition
  const effects = new Set<ReactiveEffect>()
  // Define dependency add function
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  // Add dependencies from depsMap to effects
  // Just for understanding and principle, you don't have to look at each branch
  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
    }
  }

  // Encapsulating the effects execution function
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // Called if a scheduler exists
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Trigger all dependent functions in effects
  effects.forEach(run)
}

Reactive

After knowing that Track is used for dependency collection and Trigger is used for dependency Trigger, when will they be called? Let's take a look at the source code of Reactive. See the notes for a detailed explanation of the source code.

Note: the structure of the source code is complex (encapsulation). In order to understand the principle, the following is the simplified source code.

  • In conclusion
    • Dependency collection at getter
    • Trigger dependency update on setter
function reactive(target:object){
    return new Proxy(target,{
        get(target: Target, key: string | symbol, receiver: object){
            const res = Reflect.get(target, key, receiver)
            track(target, TrackOpTypes.GET, key)
            return res
        }
        set(target: object, key: string | symbol, value: unknown, receiver: object){
            let oldValue = (target as any)[key]
            const result = Reflect.set(target, key, value, receiver)
            // trigger(target, TriggerOpTypes.ADD, key, value)
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
            return result
        }
    })
}

Computed

Computed is a commonly used and easy-to-use attribute in Vue. The value of this attribute changes synchronously after the dependency changes, and the cached value is used when the dependency does not change.

  • Vue2
    • In Vue2, the implementation of Computed realizes the dependency collection of responsive data through nested watcher s, and indirectly triggers dependency updates in a chain.
  • effect appears in Vue3 and the Computed attribute is re implemented
    • Effect can be understood as a side effect function, which is collected as a dependency and triggered after responsive data update.

Show me the Code

  • After reading the computed function, you will find that here is only a brief assignment of getter s and setter s
    • computed supports two writing methods
      • function
      • getter,setter
function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  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 new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}
  • The core logic is in ComputedRefImpl. Let's move on
    • Mark whether the data is old through the dirty variable
    • Assign dirty to true after responsive data update
    • In the next get, recalculate when dirty is true and assign dirty to false
class ComputedRefImpl<T> {
  private _value!: T
  private _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,
      // After the responsive data is updated, assign dirty to true
      // The next time you execute getter, judge whether dirty is true, that is, recalculate the computed value
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          // Distribute all side-effect functions that reference the current calculated attribute 
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    // dirty is true when the responsive data is updated
    // After recalculating the data, assign dirty to false
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    // Dependency collection
    track(self, TrackOpTypes.GET, 'value')
    
    // Returns the calculated value
    return self._value
  }

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

Watch

Watch is mainly used to monitor a variable and handle it accordingly

Vue3 not only reconstructs the watch, but also adds a WatchEffect API

  • Watch

It is used to listen to a variable. At the same time, you can get the new value and the old value through callBack

watch(state, (state, prevState)=>{})
  • WatchEffect

Each update is executed, and the dependencies used are automatically collected

Unable to get the new and old values. You can stop listening manually

onInvalidate(fn) the callback passed in will be executed when watchEffect runs again or watchEffect stops

const stop = watchEffect((onInvalidate)=>{
    // ...
    onInvalidate(()=>{
        // ...
    })
})    
// Manually stop listening
stop()

Differences between watch and watchEffect

  • watch is executed lazily, and watchEffect is executed every time the code is loaded
  • watch can specify the listening variable, and watchEffect automatically depends on the collection
  • watch can get old and new values, but watchEffect cannot
  • watchEffect has the function of onInvalidate, but watch does not
  • watch can only listen to objects such as ref and reactive, and watchEffect can only listen to specific attributes

Source Code

Show me the Code

  • Here you can see that the core logic of watch and watcheffect are encapsulated in doWatch
// watch
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}
  • doWatch

The following is the deleted version of the source code, you can understand the core principles

See note for details

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {

  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  // Make getter assignment for different situations
  if (isRef(source)) {
    // ref passed value acquisition
    getter = () => (source as Ref).value
    forceTrigger = !!(source as Ref)._shallow
  } else if (isReactive(source)) {
    // reactive direct acquisition
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    // If it is an array, do traversal
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER, [
            instance && (instance.proxy as any)
          ])
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    // If it's a function
    // If there is cb, it is watch. If there is no cb, it is watchEffect
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
          instance && (instance.proxy as any)
        ])
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  } else {
    // Abnormal condition
    getter = NOOP
    // Throw exception
    __DEV__ && warnInvalidSource(source)
  }

  // Deep listening logic processing
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  let cleanup: () => void
  let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }

  // Record the oldValue and get the newValue through the runner
  // The encapsulation of callback is job
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = runner()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      runner()
    }
  }

  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb


  // Handle the trigger timing of the job by reading the configuration
  // And encapsulate the execution of the job into the scheduler again
  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') { // Synchronous execution
    scheduler = job
  } else if (flush === 'post') { // Execute after update
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    // Execute before update
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // with 'pre' option, the first call must happen before
        // the component is mounted so it is called synchronously.
        job()
      }
    }
  }

  // Use effect side effects to handle dependency collection, and call scheduler after updating, which encapsulates the execution of callback.
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })

  // Collection dependency
  recordInstanceBoundEffect(runner, instance)

  // Read the configuration and initialize the watch
  // Is there a cb
  if (cb) {
    // Execute now
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    // Execute after update
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }

  // Return to manual stop function
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

Mixin

Mixin means mixed. It is a sharp tool for public logic encapsulation.

The principle is relatively simple, that is, merging.

  • Merging is divided into object merging and lifecycle merging
    • Object, mergeOption
      • Type object The merge of assign will overwrite
    • Lifecycle, mergeHook
      • Merging puts the two lifecycles into a queue and calls them in turn
  • mergeOptions
function mergeOptions(
  to: any,
  from: any,
  instance?: ComponentInternalInstance | null,
  strats = instance && instance.appContext.config.optionMergeStrategies
) {
  if (__COMPAT__ && isFunction(from)) {
    from = from.options
  }

  const { mixins, extends: extendsOptions } = from

  extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
  mixins &&
    mixins.forEach((m: ComponentOptionsMixin) =>
      mergeOptions(to, m, instance, strats)
    )
    
   // Traversal of objects in mixin
  for (const key in from) {
    // If it exists, overwrite it
    if (strats && hasOwn(strats, key)) {
      to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
    } else {
    // If it does not exist, it is assigned directly
      to[key] = from[key]
    }
  }
  return to
}
  • mergeHook

Simply put it into the Set and call it in turn

function mergeHook(
  to: Function[] | Function | undefined,
  from: Function | Function[]
) {
  return Array.from(new Set([...toArray(to), ...toArray(from)]))
}

Vuex4

Vuex is a commonly used state management library in Vue. After Vue3 is published, the state management library also sends Vuex4 adapted to Vue3

Fast pass vuex3 X principle

  • Why can every component pass

    this.$store access to store data?

    • During beforeCreate, the store is injected through mixin
  • Why is the data in Vuex responsive

    • When creating a store, new Vue is called and a Vue instance is created, which is equivalent to borrowing Vue's response.
  • How does mapXxxx get the data and methods in the store

    • mapXxxx is just a syntax sugar, and the underlying implementation also obtains it from $store and returns it to calculated / methods.

In general, vue3 X is understood as a mixin injected into each component?

Research on Vuex4 principle

Remove redundant code and see the essence

createStore

  • Starting with createStore
    • It can be found that the state in Vuex4 is the responsive data created through the reactive API, and Vuex3 is the new Vue instance
    • The implementation of dispatch and commit basically encapsulates a layer of execution without too much concern
export function createStore (options) {
    return new Store(options)
}
class Store{
    constructor (options = {}){
        // Omit some code
        this._modules = new ModuleCollection(options)
        const state = this._modules.root.state
        resetStoreState(this, state)
        
        // bind commit and dispatch to self
        const store = this
        const { dispatch, commit } = this
        this.dispatch = function boundDispatch (type, payload) {
          return dispatch.call(store, type, payload)
        }    
        this.commit = function boundCommit (type, payload, options) {
          return commit.call(store, type, payload, options)
        }
        // Omit some code
    }
}
function resetStoreState (store, state, hot) {
    // Omit some code
    store._state = reactive({
        data: state
    })
    // Omit some code
}

install

  • Vuex is used in Vue as a plug-in. When creating app, install is called
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
  
    // Omit part of the code
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
      
      // Omit part of the code

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`
          )
        }
        return app
      },
      // Omit part of the code
   }
}
  • install of Store class
    • Get through inject
    • Implement this$ Store get

Next, let's look at the implementation of provide

install (app, injectKey) {
  // Get through inject
  app.provide(injectKey || storeKey, this)
  // Implement this$ Store get
  app.config.globalProperties.$store = this
}

app.provide implementation

provide(key, value) {
  // Warning if already exists
  if (__DEV__ && (key as string | symbol) in context.provides) {
    warn(
      `App already provides property with key "${String(key)}". ` +
        `It will be overwritten with the new value.`
    )
  }

  // Put the store into the provide of the context
  context.provides[key as string] = value
  return app
}

// Context context is a context object
const context = createAppContext()
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

Vue.useStore

  • Using Vuex in Vue3 Composition API
import { useStore } from 'vuex'

export default{
    setup(){
        const store = useStore();
    }
}
  • Implementation of useStore
function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

Vue.inject

  • Take out the store through the key stored during provide
  • If there is a parent instance, the provider of the parent instance is taken; if there is no parent instance, the provider of the root instance is taken
function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // If there is a parent instance, the provider of the parent instance is taken. If there is no parent instance, the provider of the root instance is taken
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    // Take out the store through the key stored during provide
    if (provides && (key as string | symbol) in provides) {
      return provides[key as string]
    // Omit part of the code
  } 
}

Vue.provide

  • Vue's provide API is also relatively simple, which is equivalent to assigning values directly through key/value
  • When the current instance provides the same as the parent instance, the connection is established through the prototype chain
function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}

injection

  • Why does every component instance have a Store object?
    • Providers are injected when creating component instances
function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        parent,
        appContext,
        // ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // ...
    }
    // ...
    return instance;
}

API s such as provide, inject and getCurrentInstance can be introduced from vue for library development / high-level usage, which will not be repeated here.

Diff algorithm optimization

Before understanding the Diff algorithm optimization of Vue3, you can first understand it Diff algorithm of Vue2

This part focuses on clarifying the algorithm and will not conduct line by line source code analysis

  • The main optimization points in Vue3 are
    • In updateChildren, double ended comparison - > longest increment subsequence
    • Full Diff - > static tag + partial Diff
    • Static lift

updateChildren

  • Vue2
    • Head to head comparison
    • Tail tail comparison
    • Head tail comparison
    • Tail head comparison
  • Vue3
    • Head to head comparison
    • Tail tail comparison
    • Move / delete / add based on the longest increment subsequence

For example 🌰

  • oldChild [a,b,c,d,e,f,g]
  • newChild [a,b,f,c,d,e,h,g]
  1. First, perform head to head comparison, and jump out of the loop when comparing different nodes
    • Get [a,b]
  2. Then conduct tail to tail comparison, and jump out of the loop when different nodes are compared
    • Get [g]
  3. Remaining [f,c,d,e,h]
    • Generate array [5, 2, 3, 4, - 1] through newIndexToOldIndexMap
    • The corresponding node of the longest increasing subsequence [2, 3, 4] is [c, d, e]
    • The remaining nodes are moved / added / deleted based on [c, d, e]

The longest increasing subsequence reduces the movement of dom elements and achieves the least dom operations to reduce the overhead.

The longest increasing subsequence algorithm can be seen Longest increasing subsequence

Static tag

In Vue2, vdom is fully diffed, and in Vue3, a static tag is added for partial Diff

vnode is marked statically as in the following enumeration

  • patchFlag
export const enum PatchFlags{
  TEXT = 1 ,  //Dynamic text node
  CLASS = 1 << 1, //2 dynamic class
  STYLE = 1 << 2, //4 dynamic style
  PROPS = 1 << 3, //8 dynamic attributes, but not class names and styles
  FULL_PROPS = 1 << 4, //16 has a dynamic key attribute. When the key is changed, a complete diff comparison is required
  HYDRATE_EVENTS = 1 << 5,//32 nodes with listening events
  STABLE_FRAGMENT = 1 << 6, //64 a fragment that does not change the order of child nodes
  KEYED_FRAGMENT = 1 << 7,  //128 fragment s with key attribute or some child nodes have keys
  UNKEYEN_FRAGMENT = 1 << 8,   //256 child nodes do not have fragment s of key s
  NEED_PATCH = 1 << 9,   //512 only non props comparison will be performed for one node
  DYNAMIC_SLOTS = 1 << 10,//1024 dynamic slot
  HOISTED = -1,   //Static node 
  //Indicates that you want to exit optimization mode during diff
  BAIL = -2
}

For example 🌰

  • The template looks like this
<div>
  <p>Hello World</p>
  <p>{{msg}}</p>
</div>
  • Generate vdom source code

The msg variable is marked

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "Hello World"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

summary

  • Mark vnode s to classify nodes that need dynamic update and nodes that do not need dynamic update
  • Static nodes only need to be created once, rendering is directly reused, and does not participate in the diff algorithm process.

Static lift

  • In Vue2, whether the element participates in the update or not, it will be recreated every time

  • In Vue3, elements that do not participate in the update will only be created once, and then they will be reused every time they are rendered

  • In the future, each time you render, you will not create these static contents repeatedly, but directly take them from the constants created at the beginning.

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

/*
* Before static lifting
*/
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

/*
* After static lifting
*/
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _hoisted_3,
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

cacheHandlers event listener cache

  • By default, onClick is regarded as a dynamic binding, so it will track its changes every time

  • However, because it is the same function, it does not track changes. It can be directly cached and reused.

// Template
<div>
  <button @click="onClick">btn</button>
</div>


// Before using cache
// Here, we haven't started the event listening cache. The familiar static tag 8 /* PROPS * / appears,
// It marks the Props (properties) of the label as dynamic properties.
// If we don't want this property to be marked as dynamic, we need the appearance of cacheHandler.
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "btn", 8 /* PROPS */, ["onClick"])
  ]))
}

// Check the console for the AST


// After using cache
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
    }, "btn")
  ]))
}

// Check the console for the AST

Its meaning is obvious. The onClick method is stored in the cache.

When using, if this method can be found in the cache, it will be used directly.

If not, inject this method into the cache.

In short, the method is cached.

Topics: Java Front-end Vue source code