First published at: github.com/USTB-musion…
Writing in the front
We are familiar with the concept of Virtual DOM and Vritual DOM is relative to DOM(document Object Model). The definition of DOM in MDN is as follows: “The DOM model represents a document with a logical tree. Each branch of the tree ends in a node, and each node contains objects. The DOM’s methods let you manipulate the tree in ways that change the structure, style, or content of the document. Compared to the performance problems caused by frequent manipulation of DOM, Vritual DOM makes a good mapping of DOM, mapping a series of operations that originally needed to be performed on DOM to Virtual DOM.
The “expensive” DOM
To get a better feel for the “expensive” DOM, now print out all the attribute values for a simple DIV element:
let div = document.createElement('div')
let str = ' '
for (let key in div) {
str += key + ' '
}
Copy the code
The printed STR value is:
As you can see, the actual DOM elements are very large, and because browsers have designed the DOM to be very complex, there are performance issues when we update the DOM frequently. As you can imagine, rewriting the entire DOM structure onto the page with innerHTML in a very simple and crude way can be very performance consuming to redraw the entire view layer. So when we update the DOM, can we just update where we changed it?
VNode
As we know, after going through the Render function we get the VNode node. If you don’t understand this picture, take a look at the two articles I wroteVue.js source perspective: analysis of the template and data rendering into the final DOM processandThe responsive system principle of vue.jsA Vritual DOM is actually a layer of encapsulation of the real DOM, which is based on VNode nodes (JavaScript objects) and describes the nodes with object attributes. A Vritual DOM defines some of the key information about the real DOM, and Vritual DOM is implemented entirely in JS, with no connection to the host browser, and thanks to the speed of JS execution, A series of complex DOM operations, such as creating nodes, deleting nodes, and adding nodes, which need to be carried out in the real DOM, are all carried out in the Vritual DOM. This greatly improves performance compared to roughly redrawing the entire view with innerHTML. Using the diff algorithm to update only the modified Virtual DOM, this can avoid many unnecessary DOM changes and thus improve performance.
SRC /core/vdom/vnode.js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodesfnOptions: ? ComponentOptions;// for SSR cachingfnScopeId: ? string;// functional scope id support
constructor (tag? : string, data? : VNodeData, children? :?Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
Copy the code
Among them:
Tag: indicates the tag name of the current node
Data: indicates the object corresponding to the current node, which contains specific data information. It is of the VNodeData type. For details, see the VNodeData type
Children: An array of children of the current node
Text: indicates the text of the current node
Elm: real DOM node corresponding to the current virtual node
Ns: indicates the namespace of the current node
Context: compilation scope of the current node
FunctionalContext: Functional component scope
Key: The key attribute of a node, which is used as a node identifier for optimization
ComponentOptions: Indicates the option of a component
ComponentInstance: componentInstance corresponding to the current node
Parent: indicates the parent of the current node
Raw: Simply means whether it is native HTML or plain text. True for innerHTML and false for textContent
IsStatic: indicates whether the node isStatic
IsRootInsert: Indicates whether to be inserted as the heel node
IsComment: Indicates whether it is a comment node
Isempirical: Whether there is a clone node
IsOnce: indicates whether the V-once command exists
For example, we now have a Vritual DOM like this:
{
tag: 'div'
data: {
class: 'outer'
},
children: [{tag: 'div'.data: {
class: 'inner'
}
text: 'Virtual DOM'}}]Copy the code
The real DOM after rendering is:
<div class="outer">
<span class="inner">Virtual DOM</span>
</div>
Copy the code
Create an empty VNode
export const createEmptyVNode = (text: string = ' ') = > {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
Copy the code
Create a text node
export function createTextVNode (val: string | number) {
return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code
Clone a VNode
export function cloneVNode (vnode: VNode) :VNode {
const cloned = newVNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isComment = vnode.isComment cloned.fnContext = vnode.fnContext cloned.fnOptions = vnode.fnOptions cloned.fnScopeId = vnode.fnScopeId cloned.asyncMeta = vnode.asyncMeta cloned.isCloned =true
return cloned
}
Copy the code
In general, VNode is a JavaScript object, which uses JavaScript object attributes to describe some states of the current node, and uses the form of VNode nodes to simulate a Virtual DOM tree.
Update the view
As we know, vue.js updates views through data binding, which calls the updateComponent method. If you’re not sure about this process, take a look at the two articles mentioned above. The updateComponent method is defined as follows:
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
Copy the code
. This method will be called the vm _update method, this method takes the first parameter is the newly generated VNode, defined in SRC/core/instance/lifecycle. In js:
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
Copy the code
Add a note in key places:
/ / new vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// If the prevVnode that requires diff does not exist, create a real DOM node with the new vNode
if(! prevVnode) {// initial render
// The first parameter is the actual node
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
// If the prevVnode for diff exists, first diff the prevVnode and vNode, patch the dom operations to the prevVnode, and complete the update of the real DOM
vm.$el = vm.__patch__(prevVnode, vnode)
}
Copy the code
It can be seen that this method calls a core method __patch__, which can be said to be the most core method of the entire Virtual DOM. It mainly completes the diff process of the new Virtual DOM node and the old Virtual DOM node. After the patch process, the real DOM node is generated and the view is updated.
patch
Let’s take a look at what happens to the vm.__patch__ method, defined in SRC /core/vdom/patch.js:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if(process.env.NODE_ENV ! = ='production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.')}}// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
/ / # 6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
Copy the code
Through the source code, we can find that patchVnode is only carried out when oldVnode(the old node) and Vnode(the new node) are in sameVnode, and the method sameVnode determines whether to diff and patch the oldVnode and Vnode. In other words, patchVnode will be performed only when the old and new VNodes are judged to be the same node; otherwise, new DOM will be created and old DOM will be removed. Here are the sameVnode methods:
sameVnode
SameVnode is defined in SRC /core/vdom/patch.js:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
function sameInputType (a, b) {
if(a.tag ! = ='input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
Copy the code
If the tag, key, and isComment of the old and new vNodes are the same, and data is defined or not, the type must be the same if the tag is input. At this point, the old and new vNodes are considered as Samevnodes, and then the patchVnode operation is performed.
The diff algorithm
Vue in 2.x vDOM algorithm is based on the snabbDOM algorithm modified implementation.
As shown in the figure, DIff algorithm compares tree nodes of the same layer instead of searching and traversing the tree layer by layer, so the time complexity is only O(n), which is a very efficient algorithm. Next, take a look at the implementation of updateChildren source code, the most important part of the DIff algorithm.
updateChildren
The updateChildren source code is defined in SRC /core/vdom/patch.js:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
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
constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Copy the code
This piece of source code analysis can refer to the SNabbDOM source code to learn.
You can pay attention to my public account “Muchen classmates”, goose factory code farmers, usually record some trivial bits, technology, life, perception, grow together.