[Front-end learning Vue - (11) Principle of virtual DOM implementation]

1. Virtual DOM implementation principle

The realization principle of virtual DOM mainly includes the following three parts:

  • Use JavaScript objects to simulate the real DOM tree and abstract the real DOM;
  • diff algorithm — compare the differences between two virtual DOM trees;
  • The pach algorithm — applies the difference of two virtual DOM objects to the real DOM tree.

The following details are reproduced

1. Truth DOMand its analysis process

​ In this section, we mainly introduce the DOMreal parsing process, and by introducing its parsing process and existing problems, we will draw out why virtualization is needed DOM. A picture is worth a thousand words, the following figure is webkitthe rendering engine workflow

 

​ All browser rendering engine workflows are roughly divided into 5 steps: create DOMtree —> create Style Rules—> build Rendertree —> layout Layout—> draw Painting.

  • The first step is to build a DOM tree: use an HTML analyzer to analyze HTML elements and build a DOM tree;
  • The second step is to generate a style sheet: use a CSS analyzer to analyze the inline style on CSS files and elements, and generate a style sheet for the page;
  • The third step is to build the Render tree: associate the DOM tree with the style sheet to build a Render tree (Attachment). Each DOM node has an attach method that accepts style information and returns a render object (also known as renderer), and these render objects will eventually be built into a Render tree;
  • The fourth step is to determine the node coordinates: according to the Render tree structure, determine a precise coordinate that appears on the display screen for each node on the Render tree;
  • The fifth step is to draw the page: display the coordinates according to the Render tree and nodes, and then call the paint method of each node to draw them.

important point:

1. DOMDoes the construction of the tree start when the document is loaded? Building DOMthe tree is a gradual process. In order to achieve a better user experience, the rendering engine will display the content on the screen as soon as possible. It does not have to wait until the entire HTMLdocument is parsed before starting to build renderthe tree and layout.

2. RenderThe tree DOMis CSSbuilt after the tree and the style sheet are built? These three processes are not completely independent when they are actually carried out, but there will be crossover, and they will be loaded, parsed, and rendered at the same time.

3. CSSWhat are the points for attention in the analysis? CSSThe parsing is reversed from right to left, the more nested tags, the slower the parsing.

4. What is the realJS price of the operation ? DOMUsing our traditional development mode, when native JSor operatingJQ , the browser will execute the process from beginning to end starting from building the DOM tree. DOMIn one operation, I need to update 10 DOMnodes . DOMAfter the browser receives the first request, it does not know that there are 9 more update operations, so it will execute the process immediately, and finally execute 10 times. For example, after the first calculation, immediately after the next DOMupdate request, the coordinate value of this node will change, and the previous calculation is useless. Calculating DOMnode coordinate values, etc. are all wasted performance. Even if the computer hardware has been iteratively updated, the cost DOMof is still expensive, and frequent operations will still cause page freezes, affecting user experience

2. Virtual-DOMBasic

2.1, the benefits DOMof

​ Virtual DOMis designed to solve browser performance problems. As before, if there are 10 DOMupdate , the virtual DOMwill not operate immediately DOM, but save diffthe content to a local JSobject, and finally put this JSobject attchon DOMthe tree at one time, and then perform subsequent operations , to avoid a large amount of unnecessary calculations. Therefore, the advantage of using JSobjects to simulate DOMnodes is that all page updates can be reflected on JSobjects (virtual DOM) first, and the speed of operating JSobjects is obviously faster. After the update is completed, the final JSobjects are mapped to real DOM, to be drawn by the browser.

2.2. Algorithm implementation

2.2.1 JS. Simulating DOMtrees with objects

(1) How JSto simulate DOMa tree with an object

For example a real DOMnode is as follows:

<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> 

We use JavaScriptobjects to represent DOMnodes, and use the properties of the object to record the type, attributes, child nodes, etc. of the node.

element.jsIndicates that the node object code is as follows:

/**
 * Element virdual-dom 对象定义
 * @param {String} tagName - dom 元素名称
 * @param {Object} props - dom 属性
 * @param {Array<Element|String>} - 子节点
 */
function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
    // dom 元素的 key 值,用作唯一标识符
    if(props.key){
       this.key = props.key
    }
    var count = 0
    children.forEach(function (child, 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;

According to the setting of elementthe object , the above DOMstructure can be simply expressed as:

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'])
]) 

Now ulis the structureJavaScript we represent with the object , and we output and view the corresponding data structure as follows:DOMul

 

(2) JSRender DOMthe object represented by

But there is no such structure on the page. In the next step, we will introduce how to ulrender into the real DOMstructure on the page. The relevant rendering functions are as follows:

/**
 * render 将virdual-dom 对象渲染为实际 DOM 元素
 */
Element.prototype.render = function () {
    var el = document.createElement(this.tagName)
    var props = this.props
    // 设置节点的DOM属性
    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() // 如果子节点也是虚拟DOM,递归构建DOM节点
            : document.createTextNode(child) // 如果字符串,只构建文本节点
        el.appendChild(childEl)
    })
    return el
} 

By looking at the above rendermethod will tagNamebuild a real DOMnode according to , then set the properties of this node, and finally recursively build our own child nodes.

We will add the built DOMstructure to the page body, as follows:

ulRoot = ul.render();
document.body.appendChild(ulRoot); 

In this way body, there is a real DOMstructure in the page, and the effect is shown in the following figure:

 

2.2.2. Comparing the difference between two virtual DOMtrees — diffalgorithm

diffThe algorithm is used to compare the differences between two Virtual DOMtrees . If the complete comparison of two trees is required, diffthe time complexity of the algorithm is O(n^3). But in the front end, you rarely move DOMelements , so Virtual DOMwill only compare elements of the same level, as shown in the figure below, divwill only divcompare , and the second level will only compare with the second level In contrast, the complexity of the algorithm can be achieved O(n).

 

(1) Depth-first traversal, recording differences

In the actual code, a depth-first traversal will be performed on the old and new trees, so that each node will have a unique tag:

 

During depth-first traversal, every time a node is traversed, the node is compared with the new tree. If there is a difference, it is recorded in an object.

// diff 函数,对比两棵树
function diff(oldTree, newTree) {
  var index = 0 // 当前节点的标志
  var patches = {} // 用来记录每个节点差异的对象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, index, patches) {
  var currentPatch = []
  if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
    // 文本内容改变
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 节点相同,比较属性
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // 比较子节点,如果子节点有'ignore'属性,则不需要比较
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else if(newNode !== null){
    // 新节点和旧节点不同,用 replace 替换
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
} 

From the above it can be concluded that, patches[1]said p, patches[3]said ul, and so on.

(2) Difference type

DOMThe types of discrepancies caused by actions include the following:

  • Node replacement: the node has changed, for example, divreplace the above with h1;
  • Sequence exchange: move, delete, and add child nodes, such as the child nodes divabove , exchange the order ofp and ;ul
  • Attribute change: modify the attributes of the node, such as deleting the liabove classstyle class;
  • Text change: change the text content of the text node, for example, change the text content of the above pnode to " Real Dom";

Several difference types described above are defined in the code as follows:

var REPLACE = 0 // 替换原先的节点
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了节点的属性
var TEXT = 3 // 文本内容改变 

(3) List comparison algorithm

​ The comparison algorithm of child nodes, for example p, ul, div, the order of is changed div, p, ul. How should this be compared? If they are compared sequentially at the same level, they will all be replaced. If pthe divis tagNamedifferent from , pit will be replaced divby . Eventually, all three nodes will be replaced, DOMwhich can be very expensive. In fact, there is no need to replace the node, but only need to move through the node. We only need to know how to move.

​ Abstracting this problem is actually the minimum edit distance problem of strings ( Edition Distance), the most common solution is Levenshtein Distancea Levenshtein Distancestring metric that measures the difference between two character sequences, between two words Levenshtein Distanceis a The minimum number of single-character edits (insertions, deletions, or substitutions) required to convert a word into another word. Levenshtein DistanceIt was invented by Soviet mathematician Vladimir Levenshtein in 1965. Levenshtein DistanceAlso known as the edit distance ( Edit Distance), solved by dynamic programming , the time complexity is O(M*N).

Definition: For two strings a、b, their Levenshtein Distanceis :

 

Example: strings aand b, a=“abcde” ,b=“cabef”according to the calculation formula given above, Levenshtein Distancetheir calculation process is as follows:

 

In this demopaper , the plug-in list-diff2algorithm is used for comparison. The time complexity of the algorithm is great O(n*m). Although the algorithm is not the optimal algorithm, it is sufficient for routine operations on domelements . The specific implementation process of the algorithm will not be introduced in detail here. For the specific introduction of the algorithm, please refer to: github.com/livoras/lis…

(4) Example output

The two virtual DOMobjects are shown in the figure below, where ul1represents the original virtual DOMtree and ul2represents the changed virtual DOMtree

var ul1 = 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'])
]) 
var ul2 = el('div',{id:'virtual-dom'},[  el('p',{},['Virtual DOM']),
  el('ul', { id: 'list' }, [	el('li', { class: 'item' }, ['Item 21']),
	el('li', { class: 'item' }, ['Item 23'])
  ]),
  el('p',{},['Hello World'])
]) 
var patches = diff(ul1,ul2);
console.log('patches:',patches);

We look at the difference object between the two output virtual DOMobjects as shown in the figure below, we can get DOMwhat changes have been made between the two virtual objects through the difference object, and then patcheschange the original real DOMstructure , thus Change DOMthe structure of the page .

 

2.2.3. Apply the difference of two virtual DOMobjects to the real DOMtree

(1) Depth-first traversal of DOMthe tree

​Because the JavaScriptobject renderhas the same information and structure as the real DOMtree . Therefore, we can also perform depth-first traversal on that DOMtree . When traversing patches, find out the difference between the currently traversed nodes from the object generated in step 2, as shown in the following related code:

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  // 从patches拿出当前节点的差异
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  // 深度遍历子节点
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }
  // 对当前节点进行DOM操作
  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
} 

(2 DOM) DOMOperate the original tree

We perform different DOMoperations , for example, if the node is replaced, the node DOMreplacement operation is performed; if the node text is changed, the text replacement DOMoperation ; and sub-node rearrangement, attribute changes, etc. OperationDOM , the related code is applyPatchesshown :

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)
    }
  })
} 

(3) DOM structure changes

By applying the difference between the two DOMobjects to the first (previous) DOMstructure, we can see that DOMthe structure undergoes the expected change, as shown in the following figure:

 

2.3. Conclusion

The relevant code implementation has been put on github. Interested students can clone to run the experiment. The github address is: github.com/fengshi123/…

Virtual DOMThe algorithm mainly implements the above three steps to achieve:

  • Simulate a tree with JSan object—DOMelement.js

    <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> 
    复制代码
  • Compare the differences of two virtual DOMtrees—diff.js

 

  • Apply the difference of two dummy DOMobjects to a real DOMtree—patch.js

    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)
        }
      })
    } 
    

3. Brief analysis ofVue source codeVirtual-DOM

From the second chapter ( Virtual-DOMBasic), we have Virtual DOMmastered the definition, , and other processes ofDOM rendering it into a real one , so the analysis of the source code in this chapter is also briefly analyzed according to these processes.VNodediffpatchVue

3.1. VNodeSimulation DOMtree

3.1.1, VNodeclass brief analysis

Vue.jsIn , Virtual DOMuse VNodethis Classto describe, and it is defined in src/core/vdom/vnode.js , as you can see from the following code block Vue.js, Virtual DOMthe definition of in is more complicated, because it contains Vue.jsmany features of . Vue.jsIn fact, the middle Virtual DOMis borrowed from the implementation of an open source library   snabbdom , and then added some features Vue.jsof the middle.

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 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
  }
}

Don't be scared by VNodesuch , or grit your teeth to find out the meaning of each attribute. In fact, we only need to understand a few core key attributes, for example:

  • tagThe attribute is vnodethe label attribute of this
  • dataThe attribute contains the , , and bound events domon the node after the final rendering into a real nodeclassattributestyle
  • childrenattribute is vnodea child node of
  • textattribute is a text attribute
  • elmThe attribute is the vnodecorresponding real domnode
  • keyAttributes are vnodemarkers that diffcan improve diffefficiency in the process

3.1.2. Source code creation VNodeprocess

(1) Initialize vue

We are instantiating an vueinstance , new Vue( )that is, actually src/core/instance/index.jsexecuting Functionthe function defined in .

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)
}

By looking at Vuethe function, we know Vuethat can only newbe initialized by the keyword, and then call this._initthe method , which is src/core/instance/init.js defined in .

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
      
    // 省略一系列其它初始化的代码
      
    if (vm.$options.el) {
      console.log('vm.$options.el:',vm.$options.el);
      vm.$mount(vm.$options.el)
    }
  }

(2) Vueinstance mount

Vueis mounted through $mountthe instance method dom, below we compileranalyze mountthe implementation of the version, and the relevant source code is defined in the directory src/platforms/web/entry-runtime-with-compiler.js file:.

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  
   // 省略一系列初始化以及逻辑判断代码  
 
  return mount.call(this, el, hydrating)
}

We found that in the end, the method on the original prototype is called and mounted  , and  the method  $mount on the original prototype   is defined in .$mountsrc/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)
}

We found that $mount the method actually calls  mountComponent the method, which is defined in  src/core/instance/lifecycle.js the file

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 = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

As can be seen from the above code, mountComponent the core is to instantiate a rendering first Watcher, and the method will be called in its callback function  updateComponent . In this method, the calling  vm._render method first generates a virtual Node, and finally calls  vm._update the update DOM.

(3) Create a virtual Node

VueThe  _render method is a private method of the instance, which is used to render the instance as a virtual Node. Its definition is in  src/core/instance/render.js the file:

 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
  }

Vue.jsCreated using _createElementthe method VNode, which is defined in  src/core/vdom/create-elemenet.js :

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    
  // 省略一系列非主线代码
  
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 场景是 render 函数不是编译生成的
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 场景是 render 函数是编译生成的
    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)) {
      // 创建虚拟 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, 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()
  }
}

_createElement The method has 5 parameters, context which represent the context of the VNode, which is  Component a type; tagrepresent the label, which can be a string or one  Component; represent  the data data of the VNode, which is a  VNodeData type, and  flow/vnode.js its definition can be found in childrenThe child node of the current VNode, which is of any type, needs to be standardized as a standard VNodearray ;

3.1.3. Instance view

In order to see more intuitively how Vuethe code VNodeis represented by a class, we have a deeper understanding through the conversion of an instance.

For example, to instantiate a Vueinstance :

  var app = new Vue({
    el: '#app',
    render: function (createElement) {
      return createElement('div', {
        attrs: {
          id: 'app',
          class: "class_box"
        },
      }, this.message)
    },
    data: {
      message: 'Hello Vue!'
    }
  })

We print out its corresponding VNoderepresentation :

 

3.2. diffProcess

3.2.1. CallVue.js logic of source codediff

Vue.jsThe source code instantiates one watcher, and this ~ is added to the dependencies of the variables bound in the template. modelOnce responsive data in changes, depthe array maintained by these responsive data will call dep.notify()the method to complete all dependency traversal The work performed, which includes the update of the view, that is, the invocation of updateComponentthe method . watcherand updateComponentmethods   are defined in src/core/instance/lifecycle.js the file.

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 = () => {
      // 生成虚拟 vnode   
      const vnode = vm._render()
      // 更新 DOM
      vm._update(vnode, hydrating)
     
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  return vm
}

Completing the update of the view is actually calling vm._updatethe method. The first parameter received by this method is just generated , and the method  Vnodeto call is defined in .vm._updatesrc/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) {
      // 第一个参数为真实的node节点,则为初始化
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
      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
    }
  }

The most critical method in this method is vm.__patch__the method , which is also virtual-domthe most core method in the whole. It mainly completes the process ofprevVnode and and opens according to the nodes . Finally, a new real node and the update of the view is completed.vnodediffvdompatchdom

Next, let's look at the logical process vm.__patch__of , vm.__patch__the method is defined src/core/vdom/patch.jsin .

function patch (oldVnode, vnode, hydrating, removeOnly) {
    ......
    if (isUndef(oldVnode)) {
      // 当oldVnode不存在时,创建新的节点
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 对oldVnode和vnode进行diff,并对oldVnode打patch  
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } 
	......
  }
}
复制代码

In patchthe method , we see that there are two situations, one is that when oldVnodedoes not exist, a new node will be created; the other is that it already exists oldVnode, oldVnodethen the process of and will be vnodeperformed diffon and . Thepatch method will be called in the process to compare the basic attributes of the two incoming , only when the basic attributes are the same, it is considered that the two are only partially updated, and then the two will be updated , if 2 If there is an inconsistency in the basic attributes of a , then the process of is skipped directly, and a real node is created , and the old node .patchsameVnodevnodevnodevnodediffvnodediffvnodedomdom

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)
  )
}
复制代码

diffThe process is mainly carried out by calling patchVnodethe method :

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    ...... 
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 如果vnode没有文本节点
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children属性存在且vnode的children属性也存在  
      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)
        }
        // 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去  
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 删除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有子节点,而vnode没有,那么就清空这个节点  
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }
    ......
  }

From the above code we know that,

diffThere are several situations in the process, which are child nodes ofoldCh and child nodes of :oldVnodechVnode

  • First, judge the text node, if so oldVnode.text !== vnode.text, then directly replace the text node;
  • In vnodethe case of no text node, enter the child node diff;
  • When oldChboth chexist and are not the same, call is performed on the updateChildrenchild node diff;
  • If oldChdoes not exist, chexists, first clear the text node oldVnodeof , and call addVnodesthe method to chadd it to elmthe real domnode ;
  • If oldChexists , chdoes not exist, then elmdelete oldChthe child node under the real node;
  • If oldVnodethere is a text node but vnodenot, then the text node is cleared.

3.2.2. Sub-node diffprocess analysis

(1) Vue.jsSource code

​ Here we focus on analyzing updateChildrenthe method, which is also the most important link in the whole diffprocess . The following is Vue.jsthe source code process of . In order to understand diffthe process , we give relevant schematic diagrams to explain it.

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 为oldCh和newCh分别建立索引,为之后遍历的依据
    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

    // 直到oldCh或者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, 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)
    }
  }

diffBefore starting to traverse , first assign a andoldCh to each of and as indexes for traversal, and when or is traversed (the condition for traversing is or ), stop the process of and . Next, let's take a look at the whole ( the case without in the node attribute).newChstartIndexendIndexoldChnewCholdChnewChstartIndex >= endIndexoldChnewChdiffdiffkey

(2) keyNo diffprocess

We explain the above code process through the following diagram:

(2.1) First start the comparison from the first node, no matter whether it is oldChor newChthe start or end node does not exist sameVnode, and there is no keymark , so after the first round of diffis completed, newChthe startVnodeis added oldStartVnodein front of , Simultaneously newStartIndexmove forward one bit;

 

(2.2) diffIn , it is satisfied sameVnode(oldStartVnode, newStartVnode), so the two vnodeare carried out diff, and finally patchhit oldStartVnodeon , and both oldStartVnodeand newStartIndexmove forward one bit;

 

(2.3) diffIn , if it is satisfied, then first perform sameVnode(oldEndVnode, newStartVnode)on oldEndVnodeand , and perform on , and complete the shift operation, and finally move one bit forward and one bit backward;newStartVnodediffoldEndVnodepatcholdEndVnodenewStartIndexoldStartVnode

 

(2.4) diffIn , the process is the same as step 3;

 

(2.5) diffIn , the same as process 1;

 

(2.6) After the traversal process ends, newStartIdx > newEndIdx, indicating that oldChthere are , then these redundant nodes need to be deleted in the end.

 

(3) keyExisting diffprocess

vnodeIn keythe case of without , each diffround is compared 起始with the node until or is traversed. And when the attribute is introduced for , in each round of the process , when the and nodes are not found , then judge whether there is in the attribute of and whether the corresponding node is found in :结束oldChnewChvnodekeydiff起始结束sameVnodenewStartVnodekeyoldKeyToIndx

  • If this does not exist key, then create this newStartVnodeas a new node and insert it into rootthe child node of the original ;
  • If there is this key, then take out what exists oldChin keythis vnode, and then diffproceed ;

Through the above analysis, after vdomadding keythe attribute to , diffin the process of traversing , when the search for the starting point , the end point , and are still unable to match, it will be used as the unique identifier to proceed , so that the efficiency .diffkeydiffdiff

The process with Keythe attribute can be seen in the following figure:vnodediff

(3.1) First start the comparison from the first node, whether it is oldChor newChthe start or end node does not exist sameVnode, but the node attribute is keymarked , and then oldKeyToIndxfind the corresponding node in , so that diffafter oldChround B节点of Removed, but newChthe on propertyB节点 on the keeps a reference to the on .elmoldChB节点elm

 

(3.2) diffIn , it is satisfied sameVnode(oldStartVnode, newStartVnode), so the two vnodeare carried out diff, and finally patchhit oldStartVnodeon , and both oldStartVnodeand newStartIndexmove forward one bit;

 

(3.3) diffIn , if it is satisfied, then first perform sameVnode(oldEndVnode, newStartVnode)on oldEndVnodeand , and perform on , and complete the shift operation, and finally move forward by one bit and move back by one bit;newStartVnodediffoldEndVnodepatcholdEndVnodenewStartIndexoldStartVnode

 

(3.4) In the fourth round diff, the process is the same as step 2;

 

(3.5) In the fifth round diff, because oldStartIndexis already oldEndIndex, the remaining Vnodequeue inserted into the queue at the end.

 

3.3. patchProcess

Through diffthe process , we will see that nodeOpsthe related methods operate on the real DOMstructure , nodeOpswhich is defined in src/platforms/web/runtime/node-ops.js , which is a basic DOMoperation , and will not be introduced in detail here.

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)
}
复制代码

3.4. Summary

Through the brief analysis of the first three sections, we have completed the analysis of how to render the template and data into the DOMfinal . We can see Vuethe whole process from initialization to final rendering more intuitively through the figure below.

 

Guess you like

Origin blog.csdn.net/weixin_60364883/article/details/126715522