Vue Virtual Dom implements snabbdom decryption and vuesnabbdom
Vue mentioned in the official document that compared with react's rendering performance, it has better performance because it uses snabbdom.
The JavaScript overhead is directly related to the mechanism for calculating necessary DOM operations. Although both Vue and React use Virtual Dom to achieve this, Vue's Virtual Dom implementation (from snabbdom) is more lightweight and therefore more efficient than React implementation.
I can see that vue, a domestically developed front-end framework, is also using other people's Virtual Dom open-source solutions. Are you curious about the power of snabbdom? However, before officially decrypting snabbdom, we should first briefly introduce Virtual Dom.
What is Virtual Dom?
Virtual Dom can be seen as a JavaScript tree simulating the DOM tree. It mainly uses vnode to implement a stateless component. When the component status changes, then, changes the Virtual Dom data are triggered, and the real Dom is updated by comparing the Virtual DOM with the real DOM. We can simply think that Virtual Dom is a real DOM cache.
Why use Virtual Dom?
We know that when we want to implement a complex interface, If we bind events and field data to each component that may change, the status will soon be too large, we need to maintain more and more events and fields, and the code will become more and more complex. Therefore, we think we can split the view and status as long as the view changes, the corresponding status also changes, and then the status changes. We can re-paint the entire view.
This is a good idea, but the cost is too high. So we thought, could we just update the view with the status changed? As a result, Virtual Dom came into being. State changes were first reported to Virtual Dom. Virtual Dom found the minimum update view and finally updated to the real DOM in batches to improve the performance.
In addition, from the perspective of portability, Virtual Dom also abstracts the real dom, which means that the Virtual Dom corresponds to a component of different devices instead of a browser DOM, this greatly facilitates the use of multiple platforms. To implement the frontend and backend homogeneous direct output scheme, it is relatively simple to use the Virtual Dom framework, because the Virtual Dom on the server is not bound to the browser DOM interface.
Data Update and UI Synchronization Based on Virtual DOM:
During initial rendering, the data is first rendered as Virtual DOM, and then the DOM is generated by Virtual DOM.
When the data is updated, the new Virtual DOM is rendered and diff is performed with the last Virtual DOM to get all the changes that need to be made on the DOM, then, the UI is synchronously updated by applying it to the DOM during the patch process.
As a data structure, Virtual DOM needs to be accurately converted to real DOM for convenient comparison.
After introducing the Virtual DOM, we should have an understanding of the functions of the snabbdom. The specific anatomy of the snabbdom is as follows ".
Snabbdom
Vnode
DOM is usually regarded as a tree, and the element is the node of the tree. The basis of Virtual DOM is Virtual Node.
The Virtual Node of Snabbdom is a pure data object created through the vnode module. The object attributes include:
Sel
Data
Children
Text
Elm
Key
You can see that the data used by Virtual nodes to create real nodes includes:
Element type
Element attributes
Child node of the element
Source code:
// VNode function, convert the input to VNode/***** @ param sel selector * @ param data-bound data * @ param children subnode array * @ param text content of the current text node * @ param elm references the real dom element * @ returns {sel: *, data: *, children: *, text: *, elm: *, key: undefined} */function vnode (sel, data, children, text, elm) {var key = data === undefined? Undefined: data. key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key };}
Snabbdom does not directly expose the vnode object for use, but uses the h package. The main function of h is to process parameters:
H (sel, [data], [children], [text]) => vnode
From the source code of snabbdom typescript, we can see that these functions are actually overloaded:
export function h(sel: string): VNode; export function h(sel: string, data: VNodeData): VNode; export function h(sel: string, text: string): VNode; export function h(sel: string, children: Array<VNode | undefined | null>): VNode; export function h(sel: string, data: VNodeData, text: string): VNode; export function h(sel: string, data: VNodeData, children: Array<VNode | undefined | null>): VNode;
Patch
After creating a vnode, call the patch method to render the Virtual Dom into a real DOM. Patch is returned by the init function of snabbdom.
Snabbdom. init is passed into the modules array, and the module is used to expand the ability of snabbdom to create complex dom.
Let's not talk about the source code of patch directly:
Return function patch (oldVnode, vnode) {var I, elm, parent; // record the inserted vnode queue for batch triggering insert var insertedVnodeQueue = []; // call the global pre hook for (I = 0; I <cbs. pre. length; ++ I) cbs. pre [I] (); // if oldvnode is a dom node, convert it to oldvnode if (isUndef (oldVnode. sel) {oldVnode = emptyNodeAt (oldVnode);} // if the oldvnode is similar to the vnode, update if (sameVnode (oldVnode, vnode) {patchVnode (oldVnode, vnode, insertedVnodeQueue);} else {// otherwise, Set Insert vnode and delete oldvnode from its parent node. elm = oldVnode. elm; parent = api. parentNode (elm); createElm (vnode, insertedVnodeQueue); if (parent! = Null) {api. insertBefore (parent, vnode. elm, api. nextSibling (elm); removeVnodes (parent, [oldVnode], 0, 0) ;}// after insertion, call the insert hook of the inserted vnode for (I = 0; I <insertedVnodeQueue. length; ++ I) {insertedVnodeQueue [I]. data. hook. insert (insertedVnodeQueue [I]);} // then call the global post hook for (I = 0; I <cbs. post. length; ++ I) cbs. post [I] (); // return vnode used as the oldvnode return vnode OF THE NEXT patch ;};
Run the patchVnode command to check whether the new and old virtual dom are vnodes of the same level. Otherwise, it is easier to create a new dom and delete the old dom:
Function sameVnode (vnode1, vnode2) {// judge the key value and the selector return vnode1.key === vnode2.key & vnode1.sel === vnode2.sel ;}
The patch method implements the snabbdom as a magic weapon for an efficient virtual dom library-the efficient diff algorithm, which can be illustrated as follows:
The core of the diff algorithm is that the comparison will only be performed at the same level and will not be compared across layers. Instead of layer-by-layer search and traversal, the time complexity will reach the O (n ^ 3) level, and the cost is very high, the time complexity can be reduced to O (n) by comparing only the same level of mode ).
The main function of the patchVnode function is to update the dom tree by patching.
Function patchVnode (oldVnode, vnode, insertedVnodeQueue) {var I, hook; // call vnode before the patch. data prepatch hook if (isDef (I = vnode. data) & isDef (hook = I. hook) & isDef (I = hook. prepatch) {I (oldVnode, vnode);} var elm = vnode. elm = oldVnode. elm, oldCh = oldVnode. children, ch = vnode. children; // if oldvnode and vnode are referenced in the same way, it indicates that no changes are made and the return is returned directly, avoiding performance waste if (oldVnode = vnode) return; // If the oldvnode and vnode are different, the vnode has Update // if vnode and oldvnode are not similar, replace the old node referenced by oldvnode with the DOM node referenced by vnode if (! SameVnode (oldVnode, vnode) {var parentElm = api. parentNode (oldVnode. elm); elm = createElm (vnode, insertedVnodeQueue); api. insertBefore (parentElm, elm, oldVnode. elm); removeVnodes (parentElm, [oldVnode], 0, 0); return;} // If the vnode is similar to the oldvnode, then we need to update the oldvnode itself if (isDef (vnode. data) {// first, call the Global update hook for vnode. elm attributes are updated for (I = 0; I <cbs. update. length; ++ I) cbs. update [I] (oldVnode, vnode); // then Call vnode. the update hook in data. elm update I = vnode. data. hook; if (isDef (I) & isDef (I = I. update) I (oldVnode, vnode);} // if the vnode is not a text node if (isUndef (vnode. text) {// if both vnode and oldVnode have sub-nodes if (isDef (oldCh) & isDef (ch) {// when the sub-nodes of Vnode and oldvnode are different, call the updatechilren function. if (oldCh! = Ch) updateChildren (elm, oldCh, ch, insertedVnodeQueue);} // if the vnode has a subnode, The oldvnode does not have a subnode else if (isDef (ch )) {// if the oldvnode is a text node, the elm text is cleared if (isDef (oldVnode. text) api. setTextContent (elm, ''); // Add the children addVnodes (elm, null, ch, 0, ch. length-1, insertedVnodeQueue);} // if oldvnode has children but vnode does not have children, remove the children else if (isDef (oldCh) {removeVnodes (elm, oldCh, 0, oldCh. length -1);} // if neither vnode nor oldvnode has chidlren, and vnode does not have text, delete the text else if (isDef (oldvnode. text) {api. setTextContent (elm, '') ;}// if the oldvnode text is different from the vnode text, update it to the vnode text else if (oldVnode. text! = Vnode. text) {api. setTextContent (elm, vnode. text);} // trigger the postpatch hook if (isDef (hook) & isDef (I = hook. postpatch) {I (oldVnode, vnode );}}
PatchVnode divides the new and old virtual DOM into several situations, and performs replacement of textContent or updateChildren.
UpdateChildren is the main implementation of the diff algorithm:
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx; var idxInOld; var elmToMove; var before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx]; } 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)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
The updateChildren code is more difficult, and it is easier to understand with the help of several images:
The process can be summarized as follows: The oldCh and newCh have two variables, StartIdx and EndIdx. Their two variables are compared with each other, and there are four comparison methods. If the four comparisons do not match, if the key is set, the key is used for comparison. During the comparison, the variable is in the middle, once StartIdx> EndIdx indicates that at least one of oldCh and newCh has been traversed, the comparison will end.
Specific diff analysis:
When sameVnode (oldStartVnode, newStartVnode) and sameVnode (oldEndVnode, newEndVnode) are true, dom does not need to be moved.
There are three cases where dom operations are required:
1. When oldStartVnode and newEndVnode have the same level, it indicates that oldStartVnode. el is behind oldEndVnode. el.
2. When oldEndVnode and newStartVnode have the same level, it indicates that oldEndVnode. el has reached the front of newStartVnode. el.
3. If the node oldCh in newCh does not exist, insert the new node to the front edge of oldStartVnode. el.
There are two cases at the end:
1. oldStartIdx> oldEndIdx, you can think that oldCh is first traversed. Of course, it is also possible that newCh completes the traversal at this time. In this case, the vnode between newStartIdx and newEndIdx is added. Call addVnodes to insert all of them to the back of before. before is usually null. AddVnodes calls the insertBefore operation dom node. Let's take a look at the insertBefore document: parentElement. insertBefore (newElement, referenceElement) If referenceElement is null, newElement will be inserted to the end of. If newElement is already in the DOM tree, newElement will first be removed from the DOM tree. Therefore, if before is null, newElement is inserted to the end of the subnode.
2. newStartIdx> newEndIdx. It can be considered that newCh is traversed first. At this time, the vnode between oldStartIdx and oldEndIdx does not exist in the new subnode. Call removeVnodes to delete them from the dom.
Hook
The code of the main shabbdom process is described above. The code above may not show how to create complex dom, such as dom with attribute, props, and eventlistener? The mysteries are related to shabbdom in various major links. The extension module can be executed in the Hook method. attribute, props, eventlistener, and so on can be implemented through the extension module.
In the source code, we can see that the hook was registered during snabbdom initialization.
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];var h_1 = require("./h");exports.h = h_1.h;var thunk_1 = require("./thunk");exports.thunk = thunk_1.thunk;function init(modules, domApi) { var i, j, cbs = {}; var api = domApi !== undefined ? domApi : htmldomapi_1.default; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { var hook = modules[j][hooks[i]]; if (hook !== undefined) { cbs[hooks[i]].push(hook); } } }
Snabbdom has six types of hooks globally. When these hooks are triggered, the corresponding functions are called to change the node status. First, let's look at the hooks and Their triggering time:
For example, you can see in the patch code that the pre hook is called.
return function patch(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); }
Let's look at the source code of a simple class module:
function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; if (!oldClass && !klass) return; if (oldClass === klass) return; oldClass = oldClass || {}; klass = klass || {}; for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } }}exports.classModule = { create: updateClass, update: updateClass };Object.defineProperty(exports, "__esModule", { value: true });exports.default = exports.classModule;},{}]},{},[1])(1)});
We can see that when the create and update hook methods are called, you can execute the updateClass of the class module: delete classes that do not exist in the vnode from elm or whose value is false.
Add the new class in vnode to elm.
Summary snabbdom
- Vnode is the basic data structure
- Create or update a DOM tree by using a patch
- The diff algorithm is only at the same level.
- Create complex dom with attribute, props, and eventlistener through hooks and extension modules
Refer:
Snabbdom
The above is all the content of this article. I hope it will be helpful for your learning and support for helping customers.