国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

vue的Virtual Dom實現(xiàn)snabbdom解密

2019-11-19 16:40:28
字體:
供稿:網(wǎng)友

vue在官方文檔中提到與react的渲染性能對比中,因為其使用了snabbdom而有更優(yōu)異的性能。

JavaScript 開銷直接與求算必要 DOM 操作的機制相關(guān)。盡管 Vue 和 React 都使用了 Virtual Dom 實現(xiàn)這一點,但 Vue 的 Virtual Dom 實現(xiàn)(復(fù)刻自 snabbdom)是更加輕量化的,因此也就比 React 的實現(xiàn)更高效。

看到火到不行的國產(chǎn)前端框架vue也在用別人的 Virtual Dom開源方案,是不是很好奇snabbdom有何強大之處呢?不過正式解密snabbdom之前,先簡單介紹下Virtual Dom。

什么是Virtual Dom

Virtual Dom可以看做一棵模擬了DOM樹的JavaScript樹,其主要是通過vnode,實現(xiàn)一個無狀態(tài)的組件,當組件狀態(tài)發(fā)生更新時,然后觸發(fā)Virtual Dom數(shù)據(jù)的變化,然后通過Virtual Dom和真實DOM的比對,再對真實DOM更新。可以簡單認為Virtual Dom是真實DOM的緩存。

為什么用Virtual Dom

我們知道,當我們希望實現(xiàn)一個具有復(fù)雜狀態(tài)的界面時,如果我們在每個可能發(fā)生變化的組件上都綁定事件,綁定字段數(shù)據(jù),那么很快由于狀態(tài)太多,我們需要維護的事件和字段將會越來越多,代碼也會越來越復(fù)雜,于是,我們想我們可不可以將視圖和狀態(tài)分開來,只要視圖發(fā)生變化,對應(yīng)狀態(tài)也發(fā)生變化,然后狀態(tài)變化,我們再重繪整個視圖就好了。

這樣的想法雖好,但是代價太高了,于是我們又想,能不能只更新狀態(tài)發(fā)生變化的視圖?于是Virtual Dom應(yīng)運而生,狀態(tài)變化先反饋到Virtual Dom上,Virtual Dom在找到最小更新視圖,最后批量更新到真實DOM上,從而達到性能的提升。

除此之外,從移植性上看,Virtual Dom還對真實dom做了一次抽象,這意味著Virtual Dom對應(yīng)的可以不是瀏覽器的DOM,而是不同設(shè)備的組件,極大的方便了多平臺的使用。如果是要實現(xiàn)前后端同構(gòu)直出方案,使用Virtual Dom的框架實現(xiàn)起來是比較簡單的,因為在服務(wù)端的Virtual Dom跟瀏覽器DOM接口并沒有綁定關(guān)系。

基于Virtual DOM 的數(shù)據(jù)更新與UI同步機制:

初始渲染時,首先將數(shù)據(jù)渲染為 Virtual DOM,然后由 Virtual DOM 生成 DOM。

數(shù)據(jù)更新時,渲染得到新的 Virtual DOM,與上一次得到的 Virtual DOM 進行 diff,得到所有需要在 DOM 上進行的變更,然后在 patch 過程中應(yīng)用到 DOM 上實現(xiàn)UI的同步更新。

Virtual DOM 作為數(shù)據(jù)結(jié)構(gòu),需要能準確地轉(zhuǎn)換為真實 DOM,并且方便進行對比。

介紹完Virtual DOM,我們應(yīng)該對snabbdom的功用有個認識了,下面具體解剖下snabbdom這只“小麻雀”。

snabbdom

vnode

DOM 通常被視為一棵樹,元素則是這棵樹上的節(jié)點(node),而 Virtual DOM 的基礎(chǔ),就是 Virtual Node 了。

Snabbdom 的 Virtual Node 則是純數(shù)據(jù)對象,通過 vnode 模塊來創(chuàng)建,對象屬性包括:

sel
data
children
text
elm
key

可以看到 Virtual Node 用于創(chuàng)建真實節(jié)點的數(shù)據(jù)包括:

元素類型
元素屬性
元素的子節(jié)點

源碼:

//VNode函數(shù),用于將輸入轉(zhuǎn)化成VNode /** * * @param sel 選擇器 * @param data 綁定的數(shù)據(jù) * @param children 子節(jié)點數(shù)組 * @param text 當前text節(jié)點內(nèi)容 * @param elm 對真實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并沒有直接暴露vnode對象給我們用,而是使用h包裝器,h的主要功能是處理參數(shù):

h(sel,[data],[children],[text]) => vnode

從snabbdom的typescript的源碼可以看出,其實就是這幾種函數(shù)重載:

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

創(chuàng)建vnode后,接下來就是調(diào)用patch方法將Virtual Dom渲染成真實DOM了。patch是snabbdom的init函數(shù)返回的。
snabbdom.init傳入modules數(shù)組,module用來擴展snabbdom創(chuàng)建復(fù)雜dom的能力。

不多說了直接上patch的源碼:

return function patch(oldVnode, vnode) { var i, elm, parent; //記錄被插入的vnode隊列,用于批觸發(fā)insert var insertedVnodeQueue = []; //調(diào)用全局pre鉤子 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //如果oldvnode是dom節(jié)點,轉(zhuǎn)化為oldvnode if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } //如果oldvnode與vnode相似,進行更新 if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { //否則,將vnode插入,并將oldvnode從其父節(jié)點上直接刪除 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); } } //插入完后,調(diào)用被插入的vnode的insert鉤子 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } //然后調(diào)用全局下的post鉤子 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); //返回vnode用作下次patch的oldvnode return vnode; };

先判斷新舊虛擬dom是否是相同層級vnode,是才執(zhí)行patchVnode,否則創(chuàng)建新dom刪除舊dom,判斷是否相同vnode比較簡單:

function sameVnode(vnode1, vnode2) { //判斷key值和選擇器 return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;}

patch方法里面實現(xiàn)了snabbdom 作為一個高效virtual dom庫的法寶―高效的diff算法,可以用一張圖示意:

diff算法的核心是比較只會在同層級進行, 不會跨層級比較。而不是逐層逐層搜索遍歷的方式,時間復(fù)雜度將會達到 O(n^3)的級別,代價非常高,而只比較同層級的方式時間復(fù)雜度可以降低到O(n)。

patchVnode函數(shù)的主要作用是以打補丁的方式去更新dom樹。

function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; //在patch之前,先調(diào)用vnode.data的prepatch鉤子 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; //如果oldvnode和vnode的引用相同,說明沒發(fā)生任何變化直接返回,避免性能浪費 if (oldVnode === vnode) return; //如果oldvnode和vnode不同,說明vnode有更新 //如果vnode和oldvnode不相似則直接用vnode引用的DOM節(jié)點去替代oldvnode引用的舊節(jié)點 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; } //如果vnode和oldvnode相似,那么我們要對oldvnode本身進行更新 if (isDef(vnode.data)) { //首先調(diào)用全局的update鉤子,對vnode.elm本身屬性進行更新 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); //然后調(diào)用vnode.data里面的update鉤子,再次對vnode.elm更新 i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } //如果vnode不是text節(jié)點 if (isUndef(vnode.text)) { //如果vnode和oldVnode都有子節(jié)點 if (isDef(oldCh) && isDef(ch)) { //當Vnode和oldvnode的子節(jié)點不同時,調(diào)用updatechilren函數(shù),diff子節(jié)點 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } //如果vnode有子節(jié)點,oldvnode沒子節(jié)點 else if (isDef(ch)) { //oldvnode是text節(jié)點,則將elm的text清除 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); //并添加vnode的children addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } //如果oldvnode有children,而vnode沒children,則移除elm的children else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } //如果vnode和oldvnode都沒chidlren,且vnode沒text,則刪除oldvnode的text else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } //如果oldvnode的text和vnode的text不同,則更新為vnode的text else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); } //patch完,觸發(fā)postpatch鉤子 if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }

patchVnode將新舊虛擬DOM分為幾種情況,執(zhí)行替換textContent還是updateChildren。

updateChildren是實現(xiàn)diff算法的主要地方:

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

updateChildren的代碼比較有難度,借助幾張圖比較好理解些:

過程可以概括為:oldCh和newCh各有兩個頭尾的變量StartIdx和EndIdx,它們的2個變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設(shè)置了key,就會用key進行比較,在比較的過程中,變量會往中間靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一個已經(jīng)遍歷完了,就會結(jié)束比較。

具體的diff分析:
對于與sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)為true的情況,不需要對dom進行移動。

有3種需要dom操作的情況:

1.當oldStartVnode,newEndVnode相同層級時,說明oldStartVnode.el跑到oldEndVnode.el的后邊了。

2.當oldEndVnode,newStartVnode相同層級時,說明oldEndVnode.el跑到了newStartVnode.el的前邊。

3.newCh中的節(jié)點oldCh里沒有,將新節(jié)點插入到oldStartVnode.el的前邊。

在結(jié)束時,分為兩種情況:

1.oldStartIdx > oldEndIdx,可以認為oldCh先遍歷完。當然也有可能newCh此時也正好完成了遍歷,統(tǒng)一都歸為此類。此時newStartIdx和newEndIdx之間的vnode是新增的,調(diào)用addVnodes,把他們?nèi)坎暹Mbefore的后邊,before很多時候是為null的。addVnodes調(diào)用的是insertBefore操作dom節(jié)點,我們看看insertBefore的文檔:parentElement.insertBefore(newElement, referenceElement)如果referenceElement為null則newElement將被插入到子節(jié)點的末尾。如果newElement已經(jīng)在DOM樹中,newElement首先會從DOM樹中移除。所以before為null,newElement將被插入到子節(jié)點的末尾。

2.newStartIdx > newEndIdx,可以認為newCh先遍歷完。此時oldStartIdx和oldEndIdx之間的vnode在新的子節(jié)點里已經(jīng)不存在了,調(diào)用removeVnodes將它們從dom里刪除。

hook

shabbdom主要流程的代碼在上面就介紹完畢了,在上面的代碼中可能看不出來如果要創(chuàng)建比較復(fù)雜的dom,比如有attribute、props、eventlistener的dom怎么辦?奧秘就在與shabbdom在各個主要的環(huán)節(jié)提供了鉤子。鉤子方法中可以執(zhí)行擴展模塊,attribute、props、eventlistener等可以通過擴展模塊實現(xiàn)。

在源碼中可以看到hook是在snabbdom初始化的時候注冊的。

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在全局下有6種類型的鉤子,觸發(fā)這些鉤子時,會調(diào)用對應(yīng)的函數(shù)對節(jié)點的狀態(tài)進行更改首先我們來看看有哪些鉤子以及它們觸發(fā)的時間:

比如在patch的代碼中可以看到調(diào)用了pre鉤子

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

我們找一個比較簡單的class模塊來看下其源碼:

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

可以看出create和update鉤子方法調(diào)用的時候,可以執(zhí)行class模塊的updateClass:從elm中刪除vnode中不存在的或者值為false的類。

將vnode中新的class添加到elm上去。

總結(jié)snabbdom

  • vnode是基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
  • patch創(chuàng)建或更新DOM樹
  • diff算法只比較同層級
  • 通過鉤子和擴展模塊創(chuàng)建有attribute、props、eventlistener的復(fù)雜dom

參考:

snabbdom

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 改则县| 共和县| 北海市| 兴国县| 邳州市| 保山市| 黎川县| 疏附县| 交城县| 安多县| 文昌市| 华安县| 信宜市| 玉门市| 寻甸| 舒兰市| 铜山县| 稷山县| 襄垣县| 宣武区| 玛多县| 溆浦县| 伊金霍洛旗| 二手房| 兴仁县| 榆中县| 松阳县| 苗栗市| 台山市| 龙江县| 阳朔县| 安国市| 神农架林区| 莱西市| 陇西县| 岚皋县| 峨山| 安多县| 马山县| 当涂县| 综艺|