Study with these questions in mind
- What is the virtual DOM?
- Why use the virtual DOM?
- How does it work?
- How is it implemented in the VUE framework?
What is the virtual DOM
The virtual DOM is a JS object that simulates the structure of the real DOM. It has properties such as tag, props, and chidren that hold the node name, properties, and child nodes, respectively.
Why use the virtual DOM? What are its advantages? Solve what problem?
2.1 real DOM
The actual DOM rendering process: Build the DOM tree -> Build the CSSOM stylesheet -> Build the render tree -> Layout -> Draw
Pay attention to the point
-
Building the DOM tree, building the CSSOM stylesheet, building the render tree is not done step by step, but all at once
-
When BUILDING CSSOM, the more nested CSS styles, the slower parsing.
-
How much does it cost for JS to manipulate the real DOM? If there are ten DOM operations in one operation, the browser needs to perform the same process ten times. Obviously, this is a huge and wasteful consumption of performance. A large number of operations on the real DOM will lead to page lag.
2.2 Why use the virtual DOM?
The virtual DOM is designed to solve browser performance problems. Manipulating the real DOM in large numbers consumes browser performance because every DOM change rerenders the page. The virtual DOM does not manipulate the DOM immediately, but instead stores these changes in a JS object using diff algorithms. Finally, attch this JS object to the DOM tree once, and then perform subsequent operations to avoid a lot of unnecessary calculation. That is, all page updates are saved in JS objects, and then rendered into a real DOM once the update is completed, which is finally handed over to the browser to draw
The realization of virtual DOM
3.1 Creating the Virtual DOM
The DOM tree is simulated with a JS object through the Element function
Suppose the nodes of the real DOM are:
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
Copy the code
Use properties of js objects to represent node types, properties, and child nodes
/** * Element virdual-dom Object definition * @param {String} tagName -dom Element name * @param {Object} props -dom attribute * @param {Array Element < | String >} - child node * / function Element (tagName, props, Children) {this.tagName = tagName this.props = props this.children = children If (props. Key){this.key = props. Key} var count = 0 children. I) {if (child instanceof Element) {count+ = child.count} else {children[I] = "+ child} count++} this.count = count } function createElement(tagName, props, children){ return new Element(tagName, props, children); } module.exports = createElement;Copy the code
use
var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
Copy the code
3.2 Render the virtual DOM as a real DOM
The render function converts the virtual DOM to the real DOM
/** */ element.prototype. render = function () {var el = "/** *" Document.createelement (this.tagname) var props = this.props // Set the DOM attribute of the object for (var propName in props) {var propValue = props[propName] el.setAttribute(propName, propValue) } var children = this.children || [] children.forEach(function (child) { var childEl = (child instanceof Element) ? Child.render () // If the child node is also a virtual DOM, recursively build the DOM node: Document.createtextnode (child) // If a string, build only the text node el.appendChild(childEl)}) return el}Copy the code
We add the constructed DOM structure to the page body as follows:
ulRoot = ul.render();
document.body.appendChild(ulRoot);
Copy the code
3.3 Updating the real DOM
Basic process: deeply traverse two virtual trees, compare the two trees with diff algorithm, and record the differences
The diff algorithm
If the difference between the two virtual trees above is to be completely compared, the time complexity of diff algorithm is O(n^3). However, the front-end usually does not operate DOM across levels, so it only compares with the same level, and the time complexity is O(n).
(1) Depth-first traverses the two virtual trees and records the differences
// diff function, Function diff(oldTree, newTree) {var patches = 0 var patches = {} dfsWalk(oldTree, newTree); NewTree, index, patches) return patches} function dfsWalk(oldNode, newNode, index, Patches) {var currentPatch = [] if (typeof (oldNode) === "string" && typeof (newNode) === "string") {// The text content changes if (newNode ! == oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (newNode! TagName === newNode.tagName && oldNode.key === newNode.key) {// Same node, Var propsPatches = diffProps(oldNode, newNode) if (propsPatches) {currentPatch.push({type: patch.PROPS, PROPS: PropsPatches})} // Compares child nodes. If the child node has the 'ignore' attribute, there is no need to compare if (! isIgnoreChildren(newNode)) { diffChildren( oldNode.children, newNode.children, index, patches, currentPatch ) } } else if(newNode ! Replace currentPatch.push({type: patch.replace, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } }Copy the code
In traversal, each node is compared with the old node, and if there is a difference, the difference type is recorded and recorded in an object. Patches [1] denote P, patches[3] denote ul, and so on.
The type of difference in the code above:
Var REPLACE = 0 // Replacing the original node var REORDER = 1 // reordering var PROPS = 2 // Modifying the properties of the node var TEXT = 3 // The TEXT content is changedCopy the code
(2) Apply the recorded difference object to the real DOM
The patch method is used for depth-first traversal of the real DOM tree. During traversal, the difference changes of the current traversal nodes can be found according to patches returned above
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, Var currentPatches = patches[walker.index] var len = node.childNodes? Node.childnodes. Length: 0 // Depth traverses the child node for (var I = 0; i < len; I ++) {var child = node.childnodes [I] walker. Index++ dfsWalk(child, walker, patches)} If (currentPatches) {applyPatches(node, currentPatches)}}Copy the code
Perform DOM operations on the original DOM tree using applyPatches
function applyPatches (node, currentPatches) {
currentPatches.forEach(currentPatch => {
switch (currentPatch.type) {
case REPLACE:
var newNode = (typeof currentPatch.node === 'string')
? document.createTextNode(currentPatch.node)
: currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}
Copy the code
How is the virtual DOM implemented in VUE?
1. Define the VNode
In vue. Js, the virtual DOM is used to define and initialize the properties and methods of the virtual DOM. The modified class is defined in SRC /core/vdom/vnode.js
export default class VNode { tag: string | void; / / the node name data: VNodeData | void; // Node attributes and methods children:? Array<VNode>; / / child node text: string | void; / / is a text attribute elm: Node | void; / / the corresponding real dom ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; / / vnode tag, in the process of diff can improve the efficiency of the diff 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 nodes fnOptions: ? ComponentOptions; // for SSR caching devtoolsMeta: ? Object; // used to store functional render context for devtools fnScopeId: ? 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 } }Copy the code
2. Create a VNode
(1) Initialize vUE
We at the time of instantiation vue instance, namely new vue (), is actually perform defined in SRC/core/instance/index. The vue function in js
function Vue (options) { if (process.env.NODE_ENV ! == 'production' && ! (this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }Copy the code
Vue function invoked in the enclosing _init method, this method in the SRC/core/instance/init. Js is defined
Vue.prototype._init = function (options? : Object) { const vm: $options.el) {console.log('vm.$options.el:',vm.$options.el); vm.$mount(vm.$options.el) } }Copy the code
(2) Mount Vue instance
Vue by $mount instance methods to mount the dom, the method is defined in SRC/platforms/web/runtime/index. Js
Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }Copy the code
In $mount method calls the mountComponent method, this method is defined in SRC/core/instance/lifecycle. Js
export function mountComponent ( vm: Component, el: ? Element, hydrating? : boolean ): Component {vm.$el = el // let updateComponent /* Istanbul ignore if */ if (process.env.node_env! == 'production' && config.performance && mark) {updateComponent = () => {const vnode = vm._render() // DOM vm._update(vnode, hydrating)}} else {updateComponent = () => {vm._render(), Hydrating)}} // Instantiate a render Watcher, New Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted &&! vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false return vm }Copy the code
(3) Create VNode and update DOM
The mountComponent instantiates a Watcher and defines the updateComponent method as a callback. UpdateComponent calls and _render () method to generate virtual dom, the method is defined in SRC/core/instance/render. Js, and then through _update method to update the dom.
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
// 省略一系列代码
currentRenderingInstance = vm
// 调用 createElement 方法来返回 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`){}
}
// set parent
vnode.parent = _parentVnode
console.log("vnode...:",vnode);
return vnode
}
Copy the code
The $createElement method is defined in the SRC /core/vdom/create-elemenet.js method
export function _createElement ( context: Component, tag? : string | Class<Component> | Function | Object, data? : VNodeData, children? : any, normalizationType? : number ): VNode | Array < VNode > {/ / omitted a number of lines of code if (normalizationType = = = ALWAYS_NORMALIZE) {/ / scene is not compiled children = render function NormalizeChildren (children)} else if (normalizationType === SIMPLE_NORMALIZE) {// Scenario is the render function is compiled to generate children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config. IsReservedTag (tag)) {/ / create the virtual vnode vnode = new vnode (config. ParsePlatformTagName (tag), the data, the children, undefined, undefined, context ) } else if ((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }Copy the code
3. How do I update the VNode
By calling the vm. _update method to complete the view update work, the method is defined in SRC/core/instance/lifecycle. Js
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode if (! PrevVnode) {// The first argument is the real node, $el = vm. __Patch__ (vm.$el, vnode, hydrating, false /* removeOnly */)} else {// If prevVnode exists, So to diff prevVnode and vnode vm. $el = vm. __patch__ (prevVnode, vnode) } restoreActiveInstance() // 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 } }Copy the code
In the vm._update method, the key is to use the vm.__patch__ core method to generate new real DOM nodes and update the view. This is also the core method in the VUE virtual DOM, which is defined in SRC /core/vdom/patch.js
function patch (oldVnode, vnode, hydrating, removeOnly) { ...... If (isUndef(oldVnode)) {// If oldVnode does not exist, IsInitialPatch = true createElm(vnode, insertedVnodeQueue)} else {// Diff oldVnode and vnode, Const isRealElement = isDef(oldvNode.nodeType) if (! isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } ...... }}Copy the code
The process of diff is mainly through calling patchVnode method:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { ...... Elm const oldCh = oldvNode. children const ch = vnode.children // if vnode has no text node if (isUndef(vnode.text)) {// If oldVnode children exists and vnode children exists if (isDef(oldCh) &&isdef (ch)) {// UpdateChildren diff if (oldCh! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV ! == 'production') {checkDuplicateKeys(ch)} If (isDef(oldvNode.text)) nodeops. setTextContent(elm, "") addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) { Oldch.length-1)} else if (isDef(oldvNode.text)) {// oldVnode has child nodes, vnode does not, Nodeops.settextcontent (elm, ")}} else if (oldvNode.text! == vnode.text) {// If oldVnode and vnode text attributes are different, then update the text element of the real DOM node nodeops.settextContent (elm, vnode.text)}...... }Copy the code
The updateChildren method is used to diff child nodes, which is the key method in the diff algorithm.
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, While (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (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, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) 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, newCh, newStartIdx) 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, newCh, newStartIdx) 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
PatchVnode and updateChildren method according to the diff algorithm, by the method of nodeOps to the real DOM, the method is defined in SRC/platforms/web/runtime/node – ops. Js
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
Copy the code
To summarize the virtual DOM of Vue
Vue virtual DOM process: Initialize vue instance, $mount mount instance, render, CreateElement Create VNode,vm._update -> vm.__patch__ -> patchVnode -> updateChildren -> nodeOps to compare the virtual DOM, update the real DOM, and update the view.
To tell the truth, the principle of the virtual DOM is still quite well understood, and vue virtual DOM is still very complex, especially the diff algorithm, look at the clouds in the fog, is not…
Finally thanks
Learn from the big guy juejin.cn/post/684490…