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
beforeCreateis called, data initialization has not completed, so properties likedataandpropscannot be accessed - By the time of
created, data initialization is complete and properties likedataandpropscan be accessed, but theDOMhas not been mounted at this point, soDOMelements cannot be accessed stateinitialization 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
vuesource code prohibits this behavior - The
mountimplementation in vue2 replaces the mount target; if thebodynode is replaced directly, thebodychildren still contain JS scripts to be executed, which can easily cause unpredictable problems - The
vDOMdiff algorithm is based on the correspondence between a blankDOMand a blankvDOM; whenbodycannot be replaced, theDOMandvDOMdo 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
scopedstyles are based on thehashattribute of HTML elements; if mounted tobody,scopedmay fail- Losing
bodymay 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 Vueis called, the_initmethod is invoked- Defines methods such as
$set,$get,$delete,$watch - Defines events such as
$on,$off,$emit,$once - Defines the
_update,$forceUpdate,$destroylifecycle
- Defines methods such as
Calls
$mountto mount the pageMounting mainly goes through the
mountComponentmethodDefines the
updateComponentupdate functionExecutes
renderto generate the virtualDOM_updateturns the virtualDOMinto the realDOMstructure 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
datacan be either an object or a function (the root instance is a singleton), so no data pollution occurs - A component instance's
datamust be a function, in order to prevent multiple component instances from sharing onedataand causing data pollution. In function form,initDatatreats it as a factory function that returns a brand-newdataobject
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]
Observeclass: recursively traverses all properties of the object, callingObject.definePropertyto define newgetter/setter; each reactive object has only oneObserveinstance, mounted onvm.__ob__.Depclass (dependency collector): each property corresponds to oneDep, saved via closure; for arrays it is placed onvm.__ob__.dep.depend(): collects the currently activeWatcher.notify(): notifies all subscribedWatchers to execute.
Watcherclass- Render
Watcher: watchesstatechanges and triggers the componentupdate computed: caches theWatcherresult- Custom
Watch: user-definedstatewatching behavior
- Render
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
$setto be made reactive, or a manual$forceUpdatere-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 thelengthproperty. - Array function coverage is incomplete; only a limited set of methods can be covered, with poor extensibility.
- Changes to array members cannot be detected, such as assigning to a member accessed via
- 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:
- No new node: directly trigger the old
VNode'sdestroy - No old node: create a new
VNodeviacreateElm - Old and new nodes are the same node: update the node via
patchVnode - 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:
- Same
key - Both are async components or both are not
- Same component
tag datais either defined on both or on neither- If it is an
input, thetypeof theinputmust be compared for equality
patch of the Same Node's Properties and Children
The main workflow of patchVnode is as follows:
- Whether the new node is a text node; if so, just update the DOM's text content directly.
- If both old and new nodes have children,
patchthe children first; if the children differ, update them viaupdateChildren - If only the new node has children, directly create new children and append them to the vnode.
- 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:
oldHeadandnewHeadoldTailandnewTailoldHeadandnewTailoldTailandnewHead
Specifically, it handles the following scenarios:
oldHead===newHead: update directly viapatchVnode, then move thenewHeadandoldHeadpointers backwardoldTail===newTail: update directly viapatchVnode, then move thenewTailandoldTailpointers forwardoldHead===newTail: update directly viapatchVnode, then move the node to thenewHeadposition, then movenewHeadbackward andoldTailforwardoldTail===newHead: update directly viapatchVnode, then move the node to thenewTailposition, then moveoldHeadbackward andnewTailforward- When nothing matches, it still tries its best to reuse old nodes; the detailed flow is as follows:
- Traverse the old nodes, looking up by key whether a node with the same key as
newHeadexists; if it exists, update viapatchVnode, then move it to thenewHeadposition, and movenewHeadbackward - If no node with the same key is found among the old nodes, create a new node directly, and move
newHeadbackward
- Traverse the old nodes, looking up by key whether a node with the same key as
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
watchercallspatchto patch the realDOM - It judges via
isSameVnode; if they are the same, thepatchVnodemethod is called patchVnodedoes the following:- Finds the corresponding real
dom, calledel - If both have text nodes and they are not equal, sets the
eltext node to theVnode's text node - If
oldVnodehas children butVNodedoes not, removes theelchildren - If
oldVnodehas no children butVNodedoes, materializes theVNode's children and adds them toel - If both have children, executes the
updateChildrenfunction to compare the children
- Finds the corresponding real
updateChildrenmainly 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
patchVnodeaccording to the situation to repeat thepatchflow, callingcreateElemto create a new node, or looking upVNodenodes with the samekeyfrom the hash table and then handling them case by case
- Sets the head and tail pointers of the old and new
ref
Principle
Essentially it is implemented via Vue.observable.
Vue.observablemakes 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
beforeRouteEnterbeforeCreatecreatedbeforeMountmountedactivatedbeforeRouteLeavedeactivated
- Re-entry
beforeRouteEnteractivatedbeforeRouteLeavedeactivated
During SSR,
activatedanddeactivatedare 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:
- Destroy the component
- Remove the cached data from
cache - 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-aliveinternals, 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
excludescenario already exists, there may be logical conflicts ( manually removed and then appended back ). - After
excludechanges, 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/excludesupported 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 thenameparameterarg: the directive argument, passed to the directive as theargparametermodify1/modify2: directive modifiers, combined into{ modify1: true, modify2: true }and passed to the directive as themodifiersparameter1 + 1: the directive expression, passed to the directive as theexpressionparameter2: the execution result of the directive expression, passed to the directive as thevalueparameter
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 operationsbinding: an object for operating on the directive usagename: the directive namevalue: the value bound to the directiveoldValue: the previous value bound to the directive, only available inupdateandcomponentUpdated; passed regardless of whether the value changedexpression: the directive expression.arg: the directive argumentmodifiers: which modifiers the directive includes
vnode: the virtual node generated by VueoldVnode: the previous virtual node, only available inupdateandcomponentUpdated
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 asSignals) to directly locate the DOM nodes affected by data changes, without full comparison - Compile-time optimization replacing runtime overhead: frameworks represented by
Solid.jsandSveltecompile component templates into efficient native DOM operation code at build time, eliminating the virtual DOM's runtime computation and memory usage. For example,Solid.jscompiles 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.textContentreduces 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.jstrack 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);