Writing is not easy, without the permission of the author forbid to reprint in any form! If you think the article is good, welcome to follow, like and share! Continue to share technical blog posts, follow the wechat public account 👉🏻 front-end LeBron

Effect and Reactive

Effect, as the core of Vue’s responsive principle, appears in Computed, Watch and Reactive

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

  • Effect
    • Side effect dependent function
  • Track
    • Depend on the collection
  • Trigger
    • Depend on the trigger

Effect

Effect can be understood as a side effect function that is collected as a dependency and triggered after a reactive data update.

Vue’s responsive apis, such as Computed and Watch, are implemented by Effect

  • Let’s start with the entry function
    • Entry functions are mainly logical processing, and the core logic is located in create Active Effect
function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  // If it is already effect, reset it
  if (isEffect(fn)) {
    fn = fn.raw
  }
  / / create the effect
  const effect = createReactiveEffect(fn, options)
  // If the execution is not lazy, execute it first
  if(! options.lazy) { effect() }return effect
}
Copy the code
  • createReactiveEffect
const effectStack: ReactiveEffect[] = []

function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    // Effect stop is called if the function is not activated
    if(! effect.active) {// if there is no scheduler, return directly, otherwise execute fn
      return options.scheduler ? undefined : fn()
    }
    // Check whether EffectStack has effect
    if(! effectStack.includes(effect)) {/ / remove effect
      cleanup(effect)
      try {
        /* * Start recollecting dependencies * press into the stack * Set Effect to activeEffect * */
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        /* * Eject effect after completion * reset dependencies * reset activeEffect * */
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  effect.id = uid++ // Add id, effect unique identifiereffect.allowRecurse = !! options.allowRecurse effect._isEffect =true // whether is effect
  effect.active = true  // Whether to activate
  effect.raw = fn   // Mount the original object
  effect.deps = []  // The current effect DEP array
  effect.options = options  // Options passed in
  return effect
}

// Every time effect runs, dependencies are recollected. Deps is an effect dependency array that 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}}Copy the code

Track

The Track function is often found in the getter of Reactive and is used for dependency collection

Source code details see notes

function track(target: object.type: TrackOpTypes, key: unknown) {
  // activeEffect null indicates no dependency
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }

  // targetMap dependency management Map, used to collect dependencies
  // Check if there is target in targetMap
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// DeP is used to collect dependent functions. When the listening key changes, the dependency function in the DEP is updated
  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 will trigger onTrack for debugging only
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
Copy the code

Trigger

Trigger is often found in setter functions in Reactive that Trigger dependency updates

Source code details see notes

function trigger(
  target: object.type: TriggerOpTypes, key? : unknown, newValue? : unknown, oldValue? : unknown, oldTarget? :Map<unknown, unknown> | Set<unknown>
) {
  // Get dependency Map, if not, no need to trigger
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  // Use Set to save effects that need to be triggered to avoid duplication
  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 the dependency from depsMap to effects
  // Don't look at the branches just to understand the principles
  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}}// Encapsulate effects to execute functions
  const run = (effect: ReactiveEffect) = > {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // call if there is a scheduler
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Triggers all dependent functions in Effects
  effects.forEach(run)
}
Copy the code

Reactive

Knowing that Track is used for dependency collection and Trigger is used for dependency triggering, when are they called? Check out the source code of Reactive (see the comments for details).

Note: the source code structure is more complex (encapsulation), to facilitate the understanding of the principle, the following is a simplified source code.

  • In summary
    • Do dependency collection in the getter
    • Trigger dependency updates when you 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
        }
    })
}
Copy the code

Computed

Computed is a common and useful attribute in Vue that changes its value synchronously after a dependency changes and uses cached values if the dependency does not change.

  • Vue2
    • The implementation of Computed in Vue2 implements dependency collection of responsive data and dependency update triggered by indirect chain through nested watcher.
  • Effect appears in Vue3, reimplementing the Computed property
    • Effects can be understood as side effects functions that are collected as dependencies and fired after reactive data updates.

Show me the Code

  • If you read through this computed function, you’ll see that you’re just doing a brief assignment of getters and setters
    • Computed supports two ways of writing
      • function
      • Getter and 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 newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
}
Copy the code
  • The core logic is all in ComputedRefImpl, so let’s move on
    • Data is marked as old by the dirty variable
    • Assign dirty to true after reactive data updates
    • On the next get, it recalculates when dirty is true and assigns dirty to false
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.// Assign dirty to true after reactive data updates
      // The next time you run the getter to determine that dirty is true, that is, to recalculate the computed value
      scheduler: () = > {
        if (!this._dirty) {
          this._dirty = true
          // Dispatches all side effects that refer to the currently calculated property
          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)
    // When responsive data is updated, dirty is true
    // After recalculating the data, assign dirty to false
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    // Rely on collection
    track(self, TrackOpTypes.GET, 'value')
    
    // Returns the calculated value
    return self._value
  }

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

Watch

Watch is mainly used to monitor a variable and perform corresponding processing

Vue3 not only reconstructs Watch, but also adds a WatchEffect API

  • Watch

CallBack is used to listen on a variable and retrieve both old and new values via callBack

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

Every update is executed, automatically collecting used dependencies

Unable to obtain new and old values, you can manually stop listening

The callback passed in by onInvalidate(fn) is executed when watchEffect is restarted or stopped

const stop = watchEffect((onInvalidate) = >{
    // ...
    onInvalidate(() = >{
        // ...})})// Stop listening manually
stop()
Copy the code

Differences between Watch and watchEffect

  • Watch executes lazily, and watchEffect executes every time code is loaded
  • Watch can specify listening variables, and watchEffect automatically relies on collection
  • Watch gets old and new values, but watchEffect does not
  • The watchEffect has onInvalidate, while the Watch does not
  • Watch can only listen on objects such as REF and Reactive, and watchEffect can only listen on specific attributes

Source Code

Show me the Code

  • Here you can see that the core logic of Watch and watchEffet is 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)
}
Copy the code
  • doWatch

The following is the truncated version of the source code, to understand the core principle

See notes 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

  // Do getter assignments for different cases
  if (isRef(source)) {
    // ref is obtained by.value
    getter = () = > (source asRef).value forceTrigger = !! (sourceas Ref)._shallow
  } else if (isReactive(source)) {
    // reactive directly
    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)) {
    // In the case of a function
    // Watch if cb exists and watchEffect if cb does not exist
    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 an exception
    __DEV__ && warnInvalidSource(source)
  }

  // Deep listen 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 oldValue and get newValue through runner
  // Callback is encapsulated as 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 time of the job by reading the configuration
  // Again encapsulates the execution of the job into the Scheduler
  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') { // Synchronize 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()
      }
    }
  }

  // Handle dependency collection with effect side effects, calling scheduler (which encapsulates callback execution) after dependency update
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })

  // Collect dependencies
  recordInstanceBoundEffect(runner, instance)

  // Read the configuration and initialize the watch
  // Whether there is cb
  if (cb) {
    // Whether to execute immediately
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    // Whether to execute after update
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }

  // Returns the manual stop function
  return () = > {
    stop(runner)
    if(instance) { remove(instance.effects! , runner) } } }Copy the code

Mixin

Mixin means mix and is a common logic encapsulation tool.

The principle is simple: merge.

  • Merge is divided into object merge and life cycle merge
    • Object, mergeOption
      • Overwrite the merge of type Object.assign
    • Life cycle, mergeHook
      • The merge puts the two life cycles into a queue and calls them one by one
  • 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)
    )
    
   // Iterate over the mixin object
  for (const key in from) {
    // If so, override
    if (strats && hasOwn(strats, key)) {
      to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
    } else {
    // If it does not exist, it will be assigned directly
      to[key] = from[key]
    }
  }
  return to
}
Copy the code
  • mergeHook

Simply put it into a Set and call it as it’s called

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

Vuex4

Vuex is a state management library commonly used in Vue. After the release of Vue3, this state management library also issued Vuex4 adapted to Vue3

Faster than vex3. X principle

  • Why does every component pass

    This.$store accesses store data?

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

    • When we create a store, we callnew Vue, creates an instance of Vue, which borrows Vue’s responsivity.
  • How does mapXxxx get the data and methods in store

    • MapXxxx is just a syntax sugar, and the underlying implementation is also taken from $Store and returned to computed/Methods.

In general, you can think of Vue3. X as a mixin injected by each component?

Explore the principle of Vuex4

Removing redundant code is essential

createStore

  • Start with createStore
    • It can be found that state in VEX4 is created through reactive apis, and New Vue instance in Vex3
    • The implementation of dispatch and COMMIT basically encapsulates a layer of execution, so don’t worry too much about it
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...
}
Copy the code

install

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

      version,
      
      // omit some 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 some code....}}Copy the code
  • Store of install
    • It is obtained by Inject
    • Implement this.$store fetch

Let’s move on to the provide implementation

install (app, injectKey) {
  // The implementation is obtained by inject
  app.provide(injectKey || storeKey, this)
  // Implement this.$store fetch
  app.config.globalProperties.$store = this
}
Copy the code

App. Dojo.provide implementation

provide(key, value) {
  // If it already exists, a warning is displayed
  if (__DEV__ && (key as string | symbol) in context.provides) {
    warn(
      `App already provides property with key "The ${String(key)}". ` +
        `It will be overwritten with the new value.`)}// Place store in context's provide
  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)}}Copy the code

Vue.useStore

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

export default{
    setup(){
        conststore = useStore(); }}Copy the code
  • The realization of the useStore
function useStore (key = null) {
  returninject(key ! = =null ? key : storeKey)
}
Copy the code

Vue.inject

  • Retrieve the store from the key saved while providing
  • If there is a parent instance, the parent instance provides. If there is no parent instance, the root instance provides
function inject(
  key: InjectionKey<any> | string, defaultValue? : unknown, treatDefaultAsFactory =false
) {
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    If there is a parent instance, the parent instance provides. If there is no parent instance, the root instance provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    // Retrieve the store from the key saved while providing
    if (provides && (key as string | symbol) in provides) {
      return provides[key as string]
    // omit some code......}}Copy the code

Vue.provide

  • Vue’s provide API is also relatively simple, equivalent to assigning values directly through key/value
  • The current instance provides and the parent instance provides are connected by a stereotype 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
  }
}
Copy the code

injection

  • Why does every component instance have a Store object?
    • Provides is injected when the component instance is created
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;
}
Copy the code

Provide, Inject, getCurrentInstance and other apis can be introduced from vue for library development/higher-order usage, which will not be described here.

Diff algorithm optimization

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

This section focuses on clarifying the algorithm and will not do line-by-line source analysis

  • The main optimization points in Vue3 are
    • Double-end comparison -> longest increasing subsequence when updateChildren
    • Full Diff -> Static tag + partial Diff
    • Static ascension

updateChildren

  • Vue2
    • Head to head comparison
    • Tail-to-tail comparison
    • Head to tail comparison
    • Tail-to-head comparison
  • Vue3
    • Head to head comparison
    • Tail-to-tail comparison
    • Move/remove/add based on the longest increasing subsequence

An 🌰

  • oldChild [a,b,c,d,e,f,g]
  • newChild [a,b,f,c,d,e,h,g]
  1. A head-to-head comparison is performed first, breaking out of the loop when different nodes are compared
    • [a, b]
  2. A tail-to-tail comparison is then performed, breaking out of the loop when different nodes are compared
    • [g]
  3. The remaining [f, c, d, e, h]
    • Generate arrays with newIndexToOldIndexMap [5, 2, 3, 4, -1]
    • 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 Dom element movement and minimizes Dom operations to reduce overhead.

The longest increasing subsequence algorithm can look at longest increasing subsequence

Static markup

Full Diff is performed for VDOM in Vue2, while static tags are added for incomplete Diff in Vue3

Vnodes are statically marked as in the following enumeration

  • patchFlag
export enum PatchFlags{
  TEXT = 1 ,  // Dynamic text node
  CLASS = 1 << 1./ / 2 dynamic class
  STYLE = 1 << 2./ / 4 dynamic style
  PROPS = 1 << 3.//8 dynamic property, but does not contain class name and style
  FULL_PROPS = 1 << 4.//16 has the dynamic key property. When the key changes, 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 with key attribute or partial element node with key
  UNKEYEN_FRAGMENT = 1 << 8.//256 Fragment without key on the child node
  NEED_PATCH = 1 << 9.//512 a node will only compare non-props
  DYNAMIC_SLOTS = 1 << 10.Dynamic slot / / 1024
  HOISTED = -1.// Static node
  // Indicates to exit optimization mode during diff
  BAIL = -2
}
Copy the code

An 🌰

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

The MSG variable is tagged

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
Copy the code

conclusion

  • Vnodes are tagged to classify nodes that need to be dynamically updated and those that do not
  • Static nodes only need to be created once, rendering is directly reused, and do not participate in the diff algorithm process.

Static ascension

  • Vue2 is recreated each time whether or not an element participates in an update

  • Vue3 elements that do not participate in updates are created only once and then reused repeatedly with each rendering

  • Instead of repeating the static content every time we render, we’ll just take the constants we created from the beginning.

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

/* * Static promotion before */
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 */)))}/* * Static promotion after */
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
Copy the code

CacheHandlers Event listener cache

  • OnClick is considered dynamic binding by default, so it is tracked every time it changes

  • But because it is the same function, there is no trace of the change, and can be directly cached and reused.

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


// Before using cache
// The familiar static flag 8 /* PROPS */ is displayed,
// It marks the Props (properties) of the tag as a dynamic property.
// If we have an attribute that doesn't change and we don't want it to be marked as dynamic, then we need a 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
Copy the code

The onClick method is cached.

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

If not, inject the method into the cache.

Anyway, it’s caching the method.


  • Nuggets: Front-end LeBron

  • Zhihu: Front-end LeBron

  • Continue to share technical blog posts, follow the wechat public account 👉🏻 front-end LeBron