vdom的原理

結論

vue中vdom渲染頁面的過程:將<template>模板,通過render渲染函數(createElement())得到虛擬的DOM樹,通過diff算法進行新舊虛擬節點的比較,再通過patch更新到真實的dom上實現視圖的更新。

1.什麼是Virtal DOM?

vdom指的是用JS模擬的DOM結構,將DOM的變化對比放在JS層。

 <ul id="list">
 <li class="item">Item1</li>
</ul>

變成vdom就是:

{
tag:'ul',
attrs:{
 id:'list'
},
children:[{
    tag:'li',
    attrs:{
      className:'item',
  },
  children:['Item1']
}]
}

2.爲什麼要使用vdom?

問題

在《高性能javascript》中提到,操作dom的代價很‘昂貴’,會導致頁面的重排和重繪等問題,影響js的性能。所以應該減少dom的訪問次數,將操作放在js中。

解決

使用vdom,只需要改變更新的DOM,不需要改動的地方不動,對dom的一些頻繁操作都在vdom樹上,減少重排重繪帶來的性能消耗,提高渲染的效率。

  • 將真實的DOM編譯成vnode
  • diff,比較oldVnode和newVnode之間的變化
  • patch,將這些變化用打補丁的方式更新到真實的dom上去。

3.借用snabbdom的思想實現vdom

3.1介紹snabbdom

snabbdom是一個簡易的實現vdom功能的庫,vdom裏面有兩個核心的API,一個是h函數,一個是patch函數。前者是用來生成vdom對象(vue中使用render函數,將真實的節點轉換成vnode),後者是用做vdom之間的對比以及將vdom掛載到真實的dom上。vue就是因爲其使用了snabbdom而有更優異的性能。

var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);


var h = require('snabbdom/h').default; // helper function for creating vnodes
//h:創建一個虛擬的節點,定義id=container
var container = document.getElementById('container');
//h:container有兩個類名class和two 綁定一個click事件,跟着一個數組
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
//h: <h style='font-weight:bold'>This is bold </h>
  h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
  ' and this is just normal text',
  h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
//pacth:首次渲染時將vnode放入到空的container中
patch(container, vnode);
//一個新的vnode
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
  h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
//patch: dom變化時,新舊vnode比較,只更新需要改動的內容
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

3.2用h方法實現創建vdom

<ul id="list">
    <li class="item">Item1</li>
</ul>
var vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),,
])

3.3patch源碼

return function patch(oldVnode, vnode) {
 var i, elm, parent;
 //記錄被插入的vnode隊列,用於批觸發insert
 var insertedVnodeQueue = [];
 //調用全局pre鉤子
 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
 //如果oldvnode是dom節點,轉化爲oldvnode
 if (isUndef(oldVnode.sel)) {
 oldVnode = emptyNodeAt(oldVnode);
 }
 //如果oldvnode與vnode相似,進行更新
 if (sameVnode(oldVnode, vnode)) {
 patchVnode(oldVnode, vnode, insertedVnodeQueue);//patchnode裏有diff算法的核心updatechildren
 } else {
 //否則,將vnode插入,並將oldvnode從其父節點上直接刪除
 elm = oldVnode.elm;
 parent = api.parentNode(elm);
 //將vnode生成真實的dom
 createElm(vnode, insertedVnodeQueue);
 
 if (parent !== null) {
 api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
 removeVnodes(parent, [oldVnode], 0, 0);
 }
 }
 //插入完後,調用被插入的vnode的insert鉤子
 for (i = 0; i < insertedVnodeQueue.length; ++i) {
 insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
 }
 //然後調用全局下的post鉤子
 for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
 //返回vnode用作下次patch的oldvnode
 return vnode;
 };

patch方法中實現了snabbdom作爲高效vdom庫的法寶——diff算法,diff爲了找出需要更新的節點,核心邏輯在updateChildren函數中。
patch中的重要函數sameVnode實現了只能同級訪問。
並且對於vdom中的children比較,因爲同層和有可能移動,順序比較無法最大化的複用已有的DOM,所以通過給每個vnode加上key來跟蹤這種順序的變動,形成爲一標識,高效更新虛擬DOM。

3.4diff算法

爲什麼要是有diff算法

找出新舊虛擬dom的差別,來更新一些節點。怎麼找出,就是通過diff算法。
如果要比較vdom樹的差異理論上的時間複雜度高達O(n^3),但是由於我們在實際開發中很少出現跨級的DOM變更,所以在vdom庫中,之比較同級的,此時的複雜度爲O(n).

diff的算法實現的流程
  • 實現的過程patch(vnode,container)初始化加載,將vnode打包渲染到空的容器中和patch(vnode,newVnode)。新舊vnode的比較
  • 核心就是createElement和updateChildren

vnode通過updateChildren找出區別:

//簡單的實現更新的操作
function updateChildren(vnode, newVnode) {
  let children = vnode.children || [];
  let newChildren = newVnode.children || [];

  children.forEach((childVnode, index) => {
    let newChildVNode = newChildren[index];
    //新舊的標籤名是否相等
    if(childVnode.tag === newChildVNode.tag) {
      //深層次對比, 遞歸過程
      updateChildren(childVnode, newChildVNode);
    } else {
      //替換
      replaceNode(childVnode, newChildVNode);
    }
  })
}

通過createElement將模擬的JS轉化成真實的DOM

const createElement = (vnode) => {
  let tag = vnode.tag;
  let attrs = vnode.attrs || {};
  let children = vnode.children || [];
  if(!tag) {
    return null;
  }
  //創建元素
  let elem = document.createElement(tag);
  //屬性
  let attrName;
  for (attrName in attrs) {
    if(attrs.hasOwnProperty(attrName)) {
      elem.setAttribute(attrName, attrs[attrName]);
    }
  }
  //子元素
  children.forEach(childVnode => {
    //給elem添加子元素
    elem.appendChild(createElement(childVnode));
  })

  //返回真實的dom元素
  return elem;
}

https://www.cnblogs.com/chrislinlin/p/12585851.html
https://www.cnblogs.com/tomatoto/p/10002343.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章