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

  1. Building the DOM tree, building the CSSOM stylesheet, building the render tree is not done step by step, but all at once

  2. When BUILDING CSSOM, the more nested CSS styles, the slower parsing.

  3. 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…