Skip to content
Back to blog

Vue2 Knowledge Points

Vue2 lifecycle, reactivity, two-ended diff, component caching, custom directives, and virtual DOM organized from the source code.

Updated

Vue Lifecycle

Reference source code

Lifecycle Description
beforeCreate At the very beginning when the component instance is created, data, props, etc. have not been initialized yet and cannot be accessed
created state ( props / methods / data / computed / watch ) initialization is complete, but vm.$el has not been created yet
beforeMount Triggered before the VNode is mounted to the DOM.
At this point vm.$el is already accessible.
At this point the content on the DOM is still the old, unreplaced content, or the DOM may not have been mounted to the DOM yet
mounted Triggered after the VNode is mounted to the DOM.
beforeUpdate Triggered when the component's state ( props / methods / data / computed / watch ) changes, before the component updates; updating state at this point does not trigger beforeUpdate
updated Triggered after the component VNode is updated; updating state ( props / methods / data / computed / watch ) at this point will re-trigger the beforeUpdate, updated flow
beforeDestroy Triggered before the component instance is destroyed; state ( props / methods / data / computed / watch ) is still accessible at this point
destroyed Triggered after the component instance is destroyed; state ( props / methods / data / computed / watch ) is no longer accessible at this point
activated Triggered after a keep-alive cached component is mounted from the cache into the vDOM, before updated
deactivated Triggered when a keep-alive cached component is removed from the vDOM
errorCaptured Called when an error occurs in a descendant component
setup Added in the composition API.
The @vue/composition-api package intercepts data and calls this function before data is initialized.
Since version 2.7, it is triggered after props initialization completes and before methods initialization.
Therefore, reactive variables defined in data cannot be accessed in setup
beforeUnmount Alias of beforeDestroy; officially renamed to beforeUnmount in the composition API
unmounted Alias of destroyed; officially renamed to unmounted in the composition API

Initialization

  • Before beforeCreate is called, data initialization has not completed, so properties like data and props cannot be accessed
  • By the time of created, data initialization is complete and properties like data and props can be accessed, but the DOM has not been mounted at this point, so DOM elements cannot be accessed
  • state initialization order: props > methods > data > computed > watch
---
title: Vue2 Initialization Flow
---
graph TD
  A[Generate uid by incrementing]
  B[init scope]
  C[init lifecycle]
  D[init events]
  E[init render]
  F[trigger beforeCreate]
  G[init injections]
  H[init state]
  HA[init state: init props]
  HB[init state: init methods]
  HC[init state: init data]
  HD[init state: init computed]
  HE[init state: init watch]
  I[init provide]
  J[trigger created]

  A --> B
  B --> C
  C --> D

  subgraph Before beforeCreate is called
    D --> E
  end

  E --> F
  F --> G

  subgraph Before created is called
    G --> H
    H --> HA
    HA --> HB
    HB --> HC
    HC --> HD
    HD --> HE
    HE --> I
  end

  I --> J

Mounting

The mounting flow is triggered via vm.$mount + the shared mount. The detailed flow is as follows:

---
title: Vue2 Component Mounting Flow
---
graph TD
  A[Get the mount target DOM]
  B[Normalize the render input to render<br>Priority: render > template > outerHTML<br>template is parsed into an AST and then generates render]
  C[Validate mount arguments]
  D[trigger beforeMount]
  E[Initialize the updateComponent function]
  F[Use a watcher to watch the vDOM and update the DOM after vDOM changes]
  G[Run immediate watch]
  H[Mounting complete]
  I[trigger mounted]

  subgraph Before beforeMount is triggered
    subgraph "vm.$mount function"
      A --> B
    end
    B --> C
  end

  C --> D
  D --> E

  subgraph Before mounted is triggered
    E --> F
    F --> G
    G --> H
  end

  H --> I

Why You Cannot mount to body / html

  • The vue source code prohibits this behavior
  • The mount implementation in vue2 replaces the mount target; if the body node is replaced directly, the body children still contain JS scripts to be executed, which can easily cause unpredictable problems
  • The vDOM diff algorithm is based on the correspondence between a blank DOM and a blank vDOM; when body cannot be replaced, the DOM and vDOM do not match, which may cause other problems
  • It affects the work of other third-party libraries and may replace their own global features, such as global popups in component libraries
  • scoped styles are based on the hash attribute of HTML elements; if mounted to body, scoped may fail
  • Losing body may cause SEO problems or a11y problems

Updating

During the mounting flow, a watcher is created to watch vDOM changes in order to update the DOM.

When the watcher detects a vDOM change, beforeUpdate is triggered. The full update flow is as follows:

---
title: Vue2 Update Component Flow
---
graph TD
  A[watcher detects a VNode change]
  B[trigger beforeUpdate]
  C[patch vnode]
  D[Flush the task scheduling queue]
  E[Trigger the component's activated and update hooks in the scheduling queue]

  subgraph Before beforeUpdate is triggered
    A
  end

  A --> B

  subgraph Before update is triggered
    B --> C
    C --> D
  end

  D --> E

Summary

  • When new Vue is called, the _init method is invoked

    • Defines methods such as $set, $get, $delete, $watch
    • Defines events such as $on, $off, $emit, $once
    • Defines the _update, $forceUpdate, $destroy lifecycle
  • Calls $mount to mount the page

  • Mounting mainly goes through the mountComponent method

  • Defines the updateComponent update function

  • Executes render to generate the virtual DOM

  • _update turns the virtual DOM into the real DOM structure and renders it into the page

v-if and v-for Priority

From the source code, you can see that v-for runs before v-if.

So when v-if and v-for are applied to the same element, N elements are generated first, and then the v-if condition is applied to each element.

data as a Function vs. an Object

  • The root instance's data can be either an object or a function (the root instance is a singleton), so no data pollution occurs
  • A component instance's data must be a function, in order to prevent multiple component instances from sharing one data and causing data pollution. In function form, initData treats it as a factory function that returns a brand-new data object

Why data Cannot Be an Object

Reference the vue source code

When initializing data, if data is an object it is used directly; if it is a function, the function is called.

The essential problem is not the form of data: if data returns a shared object, all instances of that component will also share this data

vue also validates this when merging option

Why data Needs to Be a Function

Defining a component is essentially defining a constructor.

When rendering, the defined component is instantiated into an object, and data would exist in the shared instance, shared by all component instances.

To avoid pollution, the scope needs to be isolated — that is, a fresh copy is made each time the component is created, instead of using the shared data.

Two-Way Binding

Principle

Object.defineProperty is used to recursively traverse each property of the object, creating a Dep (dependency collector) for each property, collecting dependencies in the getter and triggering updates in the setter

The whole dependency collection is implemented using the Observer pattern + Publish/Subscribe pattern

---
title: Two-Way Binding Architecture Diagram
---
flowchart TD
    A[Original object] --> B[Observer]
    B -->|Recursive traversal| C[defineReactive]
    C --> D[Create Dep instance]
    D -->|getter| E[Collect Watcher dependencies]
    D -->|setter| F["Trigger Dep.notify()"]
    F --> G[Watcher updates the view]
  • Observe class: recursively traverses all properties of the object, calling Object.defineProperty to define new getter / setter; each reactive object has only one Observe instance, mounted on vm.__ob__.
  • Dep class (dependency collector): each property corresponds to one Dep, saved via closure; for arrays it is placed on vm.__ob__.dep.
    • depend(): collects the currently active Watcher.
    • notify(): notifies all subscribed Watchers to execute.
  • Watcher class
    • Render Watcher: watches state changes and triggers the component update
    • computed: caches the Watcher result
    • Custom Watch: user-defined state watching behavior

Arrays

Array watching uses the approach of inserting an intermediate prototype to intercept, so that data changes can be detected and reactive callbacks triggered. Example code:

// Get the array prototype
const arrayProto = Array.prototype
// Create a new object based on the array prototype
const middleProto = Object.create(arrayProto)

// A list of methods to be overridden
const overrideMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

for (const method of overrideMethods) {
  // Cache the original method
  const original = arrayProto[method]
  // Define the overridden method
  Object.defineProperty(middleProto, method, {
    value: function mutator(...args) {
      // Call the original method
      const result = original.apply(this, args)
      // Get the array object's ob object, which is an Observer instance
      const ob = this.__ob__
      // Notify the change
      ob.dep.notify()
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true,
  })
}

// Assume arr is an array managed by Vue
// Point arr's prototype to the object with overridden methods
Reflect.setPrototypeOf(arr, middleProto)

Limitations

  • Static watching: only properties defined initially can be watched; newly added properties are hard to watch and need $set to be made reactive, or a manual $forceUpdate re-render after each assignment.
  • Incomplete array implementation
    • Changes to array members cannot be detected, such as assigning to a member accessed via index, or changing array members by assigning to the length property.
    • Array function coverage is incomplete; only a limited set of methods can be covered, with poor extensibility.
  • Performance issues: high performance overhead, especially with nested objects, where initializing deps incurs significant memory overhead.

Diff Algorithm

The overall strategy is: depth-first, same-level comparison, two-ended comparison

Same-Level Comparison

---
title: Vue2 Two-Way Diff Algorithm Flow
---
graph TD
  oldA[A]
  oldB[B]
  oldC[C]
  oldB1[B1]
  oldB2[B2]
  oldC1[C1]
  oldC2[C2]

  newA[A]
  newB[B]
  newC[C]
  newB1[B]
  newB2[B]
  newC1[C1]
  newC2[C2]

  subgraph old[Old VDOM]
    oldA --> oldB
    oldA --> oldC
    oldB --> oldB1
    oldB --> oldB2
    oldC --> oldC1
    oldC --> oldC2
  end

  subgraph new[New VDOM]
    newA --> newB
    newA --> newC
    subgraph "Fully replaced, not reused"
      newB --> newB1
      newB --> newB2
    end
    newC --> newC1
    newC --> newC2
  end

  old --> new

vnode Tree patch

When data changes, the setter calls Dep.notify to notify all subscribed Watchers, which then call patch to update the DOM nodes

patch mainly has the following four checks:

  1. No new node: directly trigger the old VNode's destroy
  2. No old node: create a new VNode via createElm
  3. Old and new nodes are the same node: update the node via patchVnode
  4. Old and new nodes are not the same node: remove the old node and create the new node

For the function implementation see the source code. Below is part of the core code:

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // 1. No new node
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) {
      invokeDestroyHook(oldVnode)
    }
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue: any[] = []
  if (isUndef(oldVnode)) {
    // 2. No old node, create the new node directly
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 3. Old and new nodes are the same component
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 4. Old and new nodes are not the same component
      if (isRealElement) {
        // If it is a DOM node, check whether it is the result of SSR rendering
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        // If it is a node after SSR hydration, update via hydration and end patch directly
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          }
        }
        // If hydration fails / is not SSR, change the old node to an empty node for updating
        oldVnode = emptyNodeAt(oldVnode)
      }

      // Replace the old node
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // Create the new node
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // Recursively update the parent node
      if (isDef(vnode.parent)) {
      }

      // Remove the old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

The conditions for judging the same node can be seen in the source code, mainly including the following:

  1. Same key
  2. Both are async components or both are not
  3. Same component tag
  4. data is either defined on both or on neither
  5. If it is an input, the type of the input must be compared for equality

patch of the Same Node's Properties and Children

The main workflow of patchVnode is as follows:

  1. Whether the new node is a text node; if so, just update the DOM's text content directly.
  2. If both old and new nodes have children, patch the children first; if the children differ, update them via updateChildren
  3. If only the new node has children, directly create new children and append them to the vnode.
  4. If only the old node has children, directly remove the old node's children.

Below is an excerpt of the code with compatibility code omitted:

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // If the old and new nodes are identical, do nothing
  if (oldVnode === vnode) {
    return
  }

  // Let vnode.el reference the current real dom; when el changes, vnode.el changes in sync
  const elm = vnode.elm = oldVnode.elm

  // Async component placeholder
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // If both old and new are static nodes with the same key
  // when vnode is a cloned node or a node controlled by the v-once directive
  // you only need to copy oldVnode.elm and oldVnode.child onto vnode
  // with no further operations needed
  if (isTrue(vnode.isStatic)
    && isTrue(oldVnode.isStatic)
    && vnode.key === oldVnode.key
    && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) {
      cbs.update[i](oldVnode, vnode)
    }
    if (isDef(i = data.hook) && isDef(i = i.update)) {
      i(oldVnode, vnode)
    }
  }
  // If vnode is not a text node or comment node
  if (isUndef(vnode.text)) {
    // And both have children
    if (isDef(oldCh) && isDef(ch)) {
      // And the children are not identical, call updateChildren
      if (oldCh !== ch) {
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      }

      // If only the new vnode has children
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
      // elm already references the old dom node; add children onto the old dom node
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

      // If the new vnode has no children while vnode has children, remove the old oldCh directly
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)

      // If the old node is a text node
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }

    // If the new vnode and the old vnode are text nodes or comment nodes
    // but vnode.text != oldVnode.text, you only need to update the text content of vnode.elm
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) {
      i(oldVnode, vnode)
    }
  }
}

Child Node Updates (Two-Ended Comparison)

Main Principle

Looking at the source code, you can see that it mainly maintains these four pairs of relationships:

  • oldHead and newHead
  • oldTail and newTail
  • oldHead and newTail
  • oldTail and newHead

Specifically, it handles the following scenarios:

  1. oldHead === newHead: update directly via patchVnode, then move the newHead and oldHead pointers backward
  2. oldTail === newTail: update directly via patchVnode, then move the newTail and oldTail pointers forward
  3. oldHead === newTail: update directly via patchVnode, then move the node to the newHead position, then move newHead backward and oldTail forward
  4. oldTail === newHead: update directly via patchVnode, then move the node to the newTail position, then move oldHead backward and newTail forward
  5. When nothing matches, it still tries its best to reuse old nodes; the detailed flow is as follows:
    1. Traverse the old nodes, looking up by key whether a node with the same key as newHead exists; if it exists, update via patchVnode, then move it to the newHead position, and move newHead backward
    2. If no node with the same key is found among the old nodes, create a new node directly, and move newHead backward
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0 // Old head index, i.e. oldHead
  let newStartIdx = 0 // New head index, i.e. newHead
  let oldEndIdx = oldCh.length - 1 // Old tail index, i.e. oldTail
  let newEndIdx = newCh.length - 1 // New tail index, i.e. newTail
  let oldStartVnode = oldCh[0] // oldVnode's first child
  let oldEndVnode = oldCh[oldEndIdx] // oldVnode's last child
  let newStartVnode = newCh[0] // newVnode's first child
  let newEndVnode = newCh[newEndIdx] // newVnode's last child
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  // If oldStartVnode and oldEndVnode coincide, and the new ones also coincide, the diff is done and the loop ends
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // If oldVnode's first child does not exist
    if (isUndef(oldStartVnode)) {
      // Move the oldStart index right
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // If oldVnode's last child does not exist
    } else if (isUndef(oldEndVnode)) {
      // Move the oldEnd index left
      oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode and newStartVnode are the same node
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // Patch oldStartVnode and newStartVnode, move indexes, continue the loop
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]

      // oldEndVnode and newEndVnode are the same node
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // Patch oldEndVnode and newEndVnode, move indexes, continue the loop
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]

      // oldStartVnode and newEndVnode are the same node
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // Patch oldStartVnode and newEndVnode
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      // If removeOnly is false, move oldStartVnode.elm after oldEndVnode.elm
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      // Move the oldStart index right, and the newEnd index left
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]

      // If oldEndVnode and newStartVnode are the same node
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // Patch oldEndVnode and newStartVnode
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      // If removeOnly is false, move oldEndVnode.elm before oldStartVnode.elm
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      // Move the oldEnd index left, and the newStart index right
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]

      // If nothing matches
    } else {
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }

      // Try to find a Vnode in oldChildren with the same key as newStartVnode
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

      // If not found, newStartVnode is a new node
      if (isUndef(idxInOld)) { // New element
        // Create a new Vnode
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // If a Vnode with the same key as newStartVnode is found, call it vnodeToMove
      } else {
        vnodeToMove = oldCh[idxInOld]
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
          warn(
            'It seems there are duplicate keys that is causing an update error. '
            + 'Make sure each v-for item has a unique key.'
          )
        }

        // Compare whether the two new nodes with the same key are the same node
        // Without key, newCh and oldCh are only compared at the two ends; after setting key, besides the two-end comparison, matching nodes are also looked up from the oldKeyToIdx object generated from keys, so setting key on nodes can use the dom more efficiently.
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // Patch vnodeToMove and newStartVnode
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
          // Clean up
          oldCh[idxInOld] = undefined
          // If removeOnly is false, take the found Vnode with the same key as newStartVnode, called vnodeToMove.elm,
          // and move it before oldStartVnode.elm
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // If the key is the same but the node is different, create a new node
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
        }
      }

      // Move right
      newStartVnode = newCh[++newStartIdx]
    }
  }
}

Summary

  • When data changes, the render watcher calls patch to patch the real DOM
  • It judges via isSameVnode; if they are the same, the patchVnode method is called
  • patchVnode does the following:
    • Finds the corresponding real dom, called el
    • If both have text nodes and they are not equal, sets the el text node to the Vnode's text node
    • If oldVnode has children but VNode does not, removes the el children
    • If oldVnode has no children but VNode does, materializes the VNode's children and adds them to el
    • If both have children, executes the updateChildren function to compare the children
  • updateChildren mainly does the following:
    • Sets the head and tail pointers of the old and new VNodes
    • Compares the old and new head/tail pointers, looping toward the middle, calling patchVnode according to the situation to repeat the patch flow, calling createElem to create a new node, or looking up VNode nodes with the same key from the hash table and then handling them case by case

ref

Principle

Essentially it is implemented via Vue.observable.

Vue.observable makes an object into reactive data. Vue internally uses it to process the object returned by the data function

const obj = { foo: 123 }
const objRef = Vue.observable(obj)

console.log(obj === objRef) // true
// `Vue.observable` sets each property's `getter` / `setter` via `Object.defineProperty`, and does not directly return a new object.

Source Code Analysis

Reference the full source code; a simplified version is as follows:

export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  // If it is already a reactive variable, return it directly
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }

  if (
    // Global switch determining whether to perform the reactive operation
    shouldObserve
    // Use SSR to mock reactivity, and rendering is not complete
    && (ssrMockReactivity || !isServerRendering())
    // Non-primitive type
    && (isArray(value) || isPlainObject(value))
    // The variable is extensible
    && Object.isExtensible(value)
    // The variable itself is not marked as non-reactive
    && !value.__v_skip /* ReactiveFlags.SKIP */ &&
    // Does not have the composition API reactive mark
    !isRef(value)
    // Is not a VNode
    && !(value instanceof VNode)
  ) {
    // Return the new reactive observer
    return new Observer(value, shallow, ssrMockReactivity)
  }
}
export class Observer {
  /** Collect dependencies */
  dep: Dep
  /** How many Vue instances use this object as root $data */
  vmCount: number

  constructor(public value: any, public shallow = false, public mock = false) {
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          // Add one more layer to the prototype chain to modify array mutation methods; this is the basic path
          /* eslint-disable no-proto */
          ;(value as any).__proto__ = arrayMethods
          /* eslint-enable no-proto */
        } else {
          // In scenarios where `__proto__` is not supported, define mutation methods manually
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }

      // If a deep ref is needed, continue processing recursively
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      // For objects, you only need to enumerate the keys and define them one by one
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

keep-alive

Lifecycle

Vue's built-in cache component keeps a hash table caching the VNodes of created components. The lifecycle of a cached component is as follows:

  • First entry
    1. beforeRouteEnter
    2. beforeCreate
    3. created
    4. beforeMount
    5. mounted
    6. activated
    7. beforeRouteLeave
    8. deactivated
  • Re-entry
    1. beforeRouteEnter
    2. activated
    3. beforeRouteLeave
    4. deactivated

During SSR, activated and deactivated are not triggered

Implementation Principle

See the source code

The cache structure is

interface CacheEntry {
  /** Component name */
  name?: string
  /** Component tag */
  tag?: string
  /** Component instance */
  componentInstance?: Component
}

// The key is not the component name, but a unique ID generated according to rules
// This solves the conflict problem of caching multiple different instances of the same component at the same time
type CacheEntryMap = Record<string, CacheEntry | null>

When the cache becomes invalid or expires, pruneCacheEntry is called to clean up the cache:

  1. Destroy the component
  2. Remove the cached data from cache
  3. Remove the component's unique ID from the cached keys

The keep-alive workflow is as follows:

function render() {
  // Get the content of the default slot
  const slot = this.$slots.default
  // Take the component to be displayed from the slot
  const vnode = getFirstComponentChild(slot)
  // Get the component options
  const componentOptions = vnode && vnode.componentOptions
  // If options are not found, return directly; if found, start checking whether there is a cache
  if (componentOptions) {
    // Get the component name and check whether it matches the include / exclude rules
    const name = _getComponentName(componentOptions)
    const { include, exclude } = this
    if (
      // Not in included
      (include && (!name || !matches(include, name)))
      // In excluded
      || (exclude && name && matches(exclude, name))
    ) {
      // Does not meet the caching condition; return directly
      return vnode
    }

    const { cache, keys } = this
    // The cache key is not the component name, otherwise it would collide; a unique ID needs to be generated
    const key
      = vnode.key == null
        ? // Using cid + tag as the cache key can basically guarantee uniqueness, with a low chance of collision
        componentOptions.Ctor.cid
        + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        // If there is a key, use it directly; by convention the key itself is unique within its scope
        : vnode.key
    // Look up whether the cache exists
    if (cache[key]) {
      // Cache exists; reuse the component instance
      vnode.componentInstance = cache[key].componentInstance
      // LRU: move it to the end of the queue
      remove(keys, key)
      keys.push(key)
    } else {
      // Mark as a component that needs caching; keep-alive caches this component when updated is triggered
      // The reason for handling it in updated: child component updates trigger the parent component's updated.
      this.vnodeToCache = vnode
      this.keyToCache = key
    }

    // @ts-expect-error can vnode.data can be undefined
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}

Removing Cached Components

Mainstream Approach

Dynamically modify exclude, leveraging keep-alive's watching of properties like include and exclude to dynamically modify the bound component name, then update exclude back after nextTick.

Advantages:

  • No need to intrude into keep-alive internals, making future upgrades relatively simple.

Disadvantages:

  • The data passing logic is fairly complex; it needs to be passed from the triggering component all the way up to keep-alive's parent component.
  • If a dynamic exclude scenario already exists, there may be logical conflicts ( manually removed and then appended back ).
  • After exclude changes, the entire cache table needs to be fully updated; if a lot of content is cached, there may be some performance risk.

Intrusive Component-Internal Approach

Traverse the VDOM to find the keep-alive component VNode, generate the component key via the corresponding key generation strategy in the source code, then delete cache[key].

Advantages

  • Compatible with older versions ( include / exclude supported since 2.1.0 ).
  • No need to change exclude.
  • Low overall performance overhead: only traversing up the VNodes is needed, without re-updating all cached data.

Disadvantages

  • Intrudes into the component internals, which may cause unexpected errors.
  • There is legacy risk when upgrading Vue, which may cause malfunction after the upgrade.

Custom Directives

Basic Form

Take v-name:arg.modify1.modify2="1 + 1" as an example:

  • name: the directive name, passed to the directive as the name parameter
  • arg: the directive argument, passed to the directive as the arg parameter
  • modify1/modify2: directive modifiers, combined into { modify1: true, modify2: true } and passed to the directive as the modifiers parameter
  • 1 + 1: the directive expression, passed to the directive as the expression parameter
  • 2: the execution result of the directive expression, passed to the directive as the value parameter

Definition

Vue.directive('demo', {
  bind(el, binding, vnode) {
    const s = JSON.stringify
    el.innerHTML
      = `name: ${s(binding.name)}<br>`
        + `value: ${s(binding.value)}<br>`
        + `expression: ${s(binding.expression)}<br>`
        + `argument: ${s(binding.arg)}<br>`
        + `modifiers: ${s(binding.modifiers)}<br>`
        + `vnode keys: ${Object.keys(vnode).join(', ')}`
  }
})

// Directives used within a component
Vue.extends({
  directives: {
    focus(el) {
      // Using a function directly instead of an object is also fine
      // By default this function is applied to `bind` and `update`
      el.focus()
    }
  }
})

Supported Hook Functions and Their Trigger Timing

Hook function name Trigger timing
bind Called only once, when the directive is first bound to the element. One-time initialization setup can be done here.
inserted Called when the bound element is inserted into its parent node ( only guarantees the parent node exists, but it may not have been inserted into the document yet ).
update Called when the containing component's VNode updates, but may occur before its child VNodes update. The directive's value may or may not have changed. You can ignore unnecessary template updates by comparing the values before and after the update
componentUpdated Called after the VNode of the directive's containing component and all its child VNodes have been updated.
unbind Called only once, when the directive is unbound from the element.

All hooks support the following parameters:

  • el: the element the directive is bound to, used for DOM operations
  • binding: an object for operating on the directive usage
    • name: the directive name
    • value: the value bound to the directive
    • oldValue: the previous value bound to the directive, only available in update and componentUpdated; passed regardless of whether the value changed
    • expression: the directive expression.
    • arg: the directive argument
    • modifiers: which modifiers the directive includes
  • vnode: the virtual node generated by Vue
  • oldVnode: the previous virtual node, only available in update and componentUpdated

Virtual DOM

Problems Solved

  • The virtual DOM was born from the need for Diff: when diffing nodes, native DOM operations are inefficient, and every creation or deletion requires significant resource overhead, far less efficient than the virtual DOM which only records key information.
  • The virtual DOM itself is a set of abstract concepts that can also render normally in non-DOM scenarios, such as cross-platform scenarios: you only need to map out the corresponding view structure according to the schema.
  • The programming paradigm shifts from imperative to declarative; developers only need to describe the UI corresponding to the state, without manually manipulating the DOM. And since developers' skill levels vary, the DOM operations encapsulated at the framework's bottom layer are often more efficient than most hand-written partial refresh code.
  • Each component independently maintains its VDOM and communicates via props, events, etc., enabling components to achieve a high-cohesion, low-coupling structure and improving maintainability.

The No-Virtual-DOM Route

Reasons for the renewed popularity of the no-virtual-DOM approach:

  • Bypassing the performance bottleneck of the Diff algorithm: the virtual DOM's Diff algorithm has high complexity (such as the traditional O(n³) time complexity) and may become a performance bottleneck during large-scale data updates. No-virtual-DOM frameworks use fine-grained reactive systems (such as Signals) to directly locate the DOM nodes affected by data changes, without full comparison
  • Compile-time optimization replacing runtime overhead: frameworks represented by Solid.js and Svelte compile component templates into efficient native DOM operation code at build time, eliminating the virtual DOM's runtime computation and memory usage. For example, Solid.js compiles down to only the precise update logic bound to the data.
  • The efficiency of native DOM operations: directly calling browser-native APIs such as element.textContent reduces the performance loss brought by the intermediate abstraction layer (virtual DOM). Measurements show that no-virtual-DOM frameworks can improve rendering speed by 20 - 30% on low-end devices.
  • Memory usage and startup performance optimization: the virtual DOM needs to maintain a copy of the complete tree structure, while frameworks like Solid.js track only the necessary data through fine-grained reactivity, significantly reducing memory usage and first-render time.
Metric Virtual DOM No Virtual DOM
Initial load size Larger (includes a runtime library) Smaller (only compiled code)
Memory usage High (extra storage of the virtual DOM) Low
Rendering speed Depends on Diff algorithm efficiency Directly updates the corresponding nodes, 20% - 30% faster
CPU usage Larger (computing Diff) Lower (skips the Diff step)

Component API-ification

// Import the component
import Modal from './Modal.vue'
// Use extends to create a new component, isolating side effects
const ComponentClass = Vue.extend(Modal);
// Instantiate the component and mount it to an empty div
const instance = new ComponentClass({ el: document.createElement("div") });
// Mount the new div onto body
document.body.appendChild(instance.$el);