摘要:前言在上一章我們學習了,等模塊,在這一篇我們將會學習到的核心功能和功能。如果父節點沒變化,我們就比較所有同層的子節點,對這些子節點進行刪除創建移位操作。只需要對兩個進行判斷是否相似,如果相似,則對他們進行操作,否則直接用替換。
前言
在上一章我們學習了,modules,vnode,h,htmldomapi,is等模塊,在這一篇我們將會學習到
snabbdom的核心功能——patchVnode和updateChildren功能。
首先我們先從簡單的部分開始,比如一些工具函數,我將逐個來講解他們的用處
sameNode這個函數主要用于比較oldvnode與vnode同層次節點的比較,如果同層次節點的key和sel都相同
我們就可以保留這個節點,否則直接替換節點
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }createKeyToOldIdx
這個函數的功能十分簡單,就是將oldvnode數組中位置對oldvnode.key的映射轉換為oldvnode.key
對位置的映射
function createKeyToOldIdx(children, beginIdx, endIdx) { var i, map = {}, key; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (isDef(key)) map[key] = i; } return map; }hook
snabbdom在全局下有6種類型的鉤子,觸發這些鉤子時,會調用對應的函數對節點的狀態進行更改
首先我們來看看有哪些鉤子:
Name | Triggered when | Arguments to callback |
---|---|---|
pre | the patch process begins (patch開始時觸發) | none |
init | a vnode has been added (vnode被創建時觸發) | vnode |
create | a DOM element has been created based on a vnode (vnode轉換為真實DOM節點時觸發 | emptyVnode, vnode |
insert | an element has been inserted into the DOM (插入到DOM樹時觸發) | vnode |
prepatch | an element is about to be patched (元素準備patch前觸發) | oldVnode, vnode |
update | an element is being updated (元素更新時觸發) | oldVnode, vnode |
postpatch | an element has been patched (元素patch完觸發) | oldVnode, vnode |
destroy | an element is directly or indirectly being removed (元素被刪除時觸發) | vnode |
remove | an element is directly being removed from the DOM (元素從父節點刪除時觸發,和destory略有不同,remove只影響到被移除節點中最頂層的節點) | vnode, removeCallback |
post | the patch process is done (patch完成后觸發) | none |
然后,下面列出鉤子對應的狀態更新函數:
create => style,class,dataset,eventlistener,props,hero
update => style,class,dataset,eventlistener,props,hero
remove => style
destory => eventlistener,style,hero
pre => hero
post => hero
好了,簡單的都看完了,接下來我們開始打大boss了,第一關就是init函數了
initinit函數有兩個參數modules和api,其中modules是init依賴的模塊,如attribute、props
、eventlistener這些模塊,api則是對封裝真實DOM操作的工具函數庫,如果我們沒有傳入,則默認
使用snabbdom提供的htmldomapi。init還包含了許多vnode和真實DOM之間的操作和注冊全局鉤子,
還有patchVnode和updateChildren這兩個重要功能,然后返回一個patch函數
//注冊鉤子的回調,在發生狀態變更時,觸發對應屬性變更 for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } }emptyNodeAt
這個函數主要的功能是將一個真實DOM節點轉化成vnode形式, 我們知道當我們需要remove一個vnode時,會觸發remove鉤子作攔截器,只有在所有remove鉤子 這個函數用于手動觸發destory鉤子回調,主要步驟如下: 先調用vnode上的destory 再調用全局下的destory
遞歸調用子vnode的destory 這個函數主要功能是批量刪除DOM節點,需要配合invokeDestoryHook和createRmCb服用,效果更佳 調用invokeDestoryHook以觸發destory回調 調用createRmCb來開始對remove回調進行計數
刪除DOM節點 就如太極有陰就有陽一樣,既然我們有remove操作,肯定也有createelm的操作,這個函數主要功能 初始化vnode,調用init鉤子 創建對應tagname的DOM element節點,并將vnode.sel中的id名和class名掛載上去
如果有子vnode,遞歸創建DOM element節點,并添加到父vnode對應的element節點上去, vnode轉換成dom節點操作完成后,調用create鉤子
如果vnode上有insert鉤子,那么就將這個vnode放入insertedVnodeQueue中作記錄,到時 這個函數十分簡單,就是將vnode轉換后的dom節點插入到dom樹的指定位置中去 說完上面的節點工具函數之后,我們就開始看如何進行patch操作了,首先我們從patch,也就是init 首先我們需要明確的一個是,如果按照傳統的diff算法,那么為了找到最小變化,需要逐層逐層的去 真正對vnode內部patch的還是得靠patchVnode。讓我們看看他到底做了什么? 對于同層的子節點,snabbdom主要有刪除、創建的操作,同時通過移位的方法,達到最大復用存在 oldStartIdx => 舊頭索引 oldEndIdx => 舊尾索引 newStartIdx => 新頭索引 newEndIdx => 新尾索引 然后開始將舊子節點組和新子節點組進行逐一比對,直到遍歷完任一子節點組,比對策略有5種: oldStartVnode和newStartVnode進行比對,如果相似,則進行patch,然后新舊頭索引都后移 oldEndVnode和newEndVnode進行比對,如果相似,則進行patch,然后新舊尾索引前移
oldStartVnode和newEndVnode進行比對,如果相似,則進行patch,將舊節點移位到最后 oldEndVnode和newStartVnode進行比對,處理和上面類似,只不過改為左移
如果以上情況都失敗了,我們就只能復用key相同的節點了。首先我們要通過createKeyToOldIdx 遍歷完之后,將剩余的新Vnode添加到最后一個新節點的位置后或者刪除多余的舊節點 至此,snabbdom的主要功能就分析完了 文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。 轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/88233.html
如將轉換為{sel:"div#a.b.c",data:{},children:[],text:undefined,elm: function emptyNodeAt(elm) {
var id = elm.id ? "#" + elm.id : "";
var c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
createRmCb
回調函數都觸發完才會將節點從父節點刪除,而這個函數提供的就是對remove鉤子回調操作的計數功能function createRmCb(childElm, listeners) {
return function() {
if (--listeners === 0) {
var parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
invokeDestoryHook
function invokeDestroyHook(vnode) {
var i, j, data = vnode.data;
if (isDef(data)) {
//先觸發該節點上的destory回調
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
//在觸發全局下的destory回調
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
//遞歸觸發子節點的destory回調
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
}
removeVnodes
主要步驟如下: /**
*
* @param parentElm 父節點
* @param vnodes 刪除節點數組
* @param startIdx 刪除起始坐標
* @param endIdx 刪除結束坐標
*/
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var i, listeners, rm, ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.sel)) {
//調用destroy鉤子
invokeDestroyHook(ch);
//對全局remove鉤子進行計數
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm, listeners);
//調用全局remove回調函數,并每次減少一個remove鉤子計數
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
//調用內部vnode.data.hook中的remove鉤子(只有一個)
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
i(ch, rm);
} else {
//如果沒有內部remove鉤子,需要調用rm,確保能夠remove節點
rm();
}
} else { // Text node
api.removeChild(parentElm, ch.elm);
}
}
}
}
createElm
如下:否則如果有text屬性,則創建text節點,并添加到父vnode對應的element節點上去
再在全局批量調用insert鉤子回調
function createElm(vnode, insertedVnodeQueue) {
var i, data = vnode.data;
if (isDef(data)) {
//當節點上存在hook而且hook中有init鉤子時,先調用init回調,對剛創建的vnode進行處理
if (isDef(i = data.hook) && isDef(i = i.init)) {
i(vnode);
//獲取init鉤子修改后的數據
data = vnode.data;
}
}
var elm, children = vnode.children, sel = vnode.sel;
if (isDef(sel)) {
// Parse selector
var hashIdx = sel.indexOf("#");
//先id后class
var dotIdx = sel.indexOf(".", hashIdx);
var hash = hashIdx > 0 ? hashIdx : sel.length;
var dot = dotIdx > 0 ? dotIdx : sel.length;
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
//創建一個DOM節點引用,并對其屬性實例化
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag): api.createElement(tag);
//獲取id名 #a --> a
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
//獲取類名,并格式化 .a.b --> a b
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/./g, " ");
//如果存在子元素Vnode節點,則遞歸將子元素節點插入到當前Vnode節點中,并將已插入的子元素節點在insertedVnodeQueue中作記錄
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
api.appendChild(elm, createElm(children[i], insertedVnodeQueue));
}
//如果存在子文本節點,則直接將其插入到當前Vnode節點
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
//當創建完畢后,觸發全局create鉤子回調
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (i.create) i.create(emptyNode, vnode);
//如果有insert鉤子,則推進insertedVnodeQueue中作記錄,從而實現批量插入觸發insert回調
if (i.insert) insertedVnodeQueue.push(vnode);
}
}
//如果沒聲明選擇器,則說明這個是一個text節點
else {
elm = vnode.elm = api.createTextNode(vnode.text);
}
return vnode.elm;
}
addVnodes
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
}
}
返回的函數開始
搜索比較,這樣時間復雜度將會達到 O(n^3)的級別,代價十分高,考慮到節點變化很少是跨層次的,
vdom采取的是一種簡化的思路,只比較同層節點,如果不同,那么即使該節點的子節點沒變化,我們
也不復用,直接將從父節點開始的子樹全部刪除,然后再重新創建節點添加到新的位置。如果父節點
沒變化,我們就比較所有同層的子節點,對這些子節點進行刪除、創建、移位操作。有了這個思想,
理解patch也十分簡單了。patch只需要對兩個vnode進行判斷是否相似,如果相似,則對他們進行
patchVnode操作,否則直接用vnode替換oldvnode。return function(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);
} else {
//否則,將vnode插入,并將oldvnode從其父節點上直接刪除
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);
}
}
//插入完后,調用被插入的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;
};
patchVnode
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
var i, hook;
//在patch之前,先調用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的引用相同,說明沒發生任何變化直接返回,避免性能浪費
if (oldVnode === vnode) return;
//如果oldvnode和vnode不同,說明vnode有更新
//如果vnode和oldvnode不相似則直接用vnode引用的DOM節點去替代oldvnode引用的舊節點
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)) {
//首先調用全局的update鉤子,對vnode.elm本身屬性進行更新
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
//然后調用vnode.data里面的update鉤子,再次對vnode.elm更新
i = vnode.data.hook;
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
}
//如果vnode不是text節點
if (isUndef(vnode.text)) {
//如果vnode和oldVnode都有子節點
if (isDef(oldCh) && isDef(ch)) {
//當Vnode和oldvnode的子節點不同時,調用updatechilren函數,diff子節點
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
//如果vnode有子節點,oldvnode沒子節點
else if (isDef(ch)) {
//oldvnode是text節點,則將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完,觸發postpatch鉤子
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
updateChildren
節點的目的,其中需要維護四個索引,分別是:然后舊頭索引后移,尾索引前移,為什么要這樣做呢?我們思考一種情況,如舊節點為【5,1,2,3,4】
,新節點為【1,2,3,4,5】,如果缺乏這種判斷,意味著需要先將5->1,1->2,2->3,3->4,4->5五
次刪除插入操作,即使是有了key-index來復用,也會出現也會出現【5,1,2,3,4】->
【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】共4次操作,如果
有了這種判斷,我們只需要將5插入到舊尾索引后面即可,從而實現右移
創建key-index的映射,如果新節點在舊節點中不存在,我們將它插入到舊頭索引節點前,
然后新頭索引向后;如果新節點在就舊節點組中存在,先找到對應的舊節點,然后patch,并將
舊節點組中對應節點設置為undefined,代表已經遍歷過了,不再遍歷,否則可能存在重復
插入的問題,最后將節點移位到舊頭索引節點之前,新頭索引向后
/**
*
* @param parentElm 父節點
* @param oldCh 舊節點數組
* @param newCh 新節點數組
* @param insertedVnodeQueue
*/
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, idxInOld, elmToMove, before;
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)) {
//對舊頭索引節點和新頭索引節點進行diff更新, 從而達到復用節點效果
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];
}
//如果舊頭索引節點和新頭索引節點相似,可以通過移動來復用
//如舊節點為【5,1,2,3,4】,新節點為【1,2,3,4,5】,如果缺乏這種判斷,意味著
//那樣需要先將5->1,1->2,2->3,3->4,4->5五次刪除插入操作,即使是有了key-index來復用,
// 也會出現【5,1,2,3,4】->【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】
// 共4次操作,如果有了這種判斷,我們只需要將5插入到最后一次操作即可
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
//原理與上面相同
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
//如果上面的判斷都不通過,我們就需要key-index表來達到最大程度復用了
else {
//如果不存在舊節點的key-index表,則創建
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
//找到新節點在舊節點組中對應節點的位置
idxInOld = oldKeyToIdx[newStartVnode.key];
//如果新節點在舊節點中不存在,我們將它插入到舊頭索引節點前,然后新頭索引向后
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
} else {
//如果新節點在就舊節點組中存在,先找到對應的舊節點
elmToMove = oldCh[idxInOld];
//先將新節點和對應舊節點作更新
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
//然后將舊節點組中對應節點設置為undefined,代表已經遍歷過了,不在遍歷,否則可能存在重復插入的問題
oldCh[idxInOld] = undefined;
//插入到舊頭索引節點之前
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
//新頭索引向后
newStartVnode = newCh[++newStartIdx];
}
}
}
//當舊頭索引大于舊尾索引時,代表舊節點組已經遍歷完,將剩余的新Vnode添加到最后一個新節點的位置后
if (oldStartIdx > oldEndIdx) {
before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
}
//如果新節點組先遍歷完,那么代表舊節點組中剩余節點都不需要,所以直接刪除
else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
摘要:閑聊在學的過程中,虛擬應該是聽的最多的概念之一,得知其是借鑒進行開發,故習之。以我的觀點來看,多個相同元素渲染時,則需要為每個元素添加值。 閑聊:在學vue的過程中,虛擬dom應該是聽的最多的概念之一,得知其是借鑒snabbdom.js進行開發,故習之。由于我工作處于IE8的環境,對ES6,TS這些知識的練習也只是淺嘗輒止,而snabbdom.js從v.0.5.4這個版本后開始使用TS...
摘要:前言最近在學習的源碼,剛開始看其源碼,著實找不到方向,因為其在的實現上還加入了很多本身的鉤子,加大了閱讀難度。 前言 最近在學習vue2.0的源碼,剛開始看其vdom源碼,著實找不到方向,因為其在vdom的實現上還加入了很多vue2.0本身的鉤子,加大了閱讀難度。于是看到第一行尤大說vue2.0的vdom是在snabbdom的基礎上改過來的,而snabbdom只有不到300sloc,那...
摘要:司徒正美的一款了不起的化方案,支持到。行代碼內實現一個胡子大哈實現的作品其實就是的了源碼學習個人文章源碼學習個人文章源碼學習個人文章源碼學習個人文章這幾片文章的作者都是司徒正美,全面的解析和官方的對比。 前言 在過去的一個多月中,為了能夠更深入的學習,使用React,了解React內部算法,數據結構,我自己,從零開始寫了一個玩具框架。 截止今日,終于可以發布第一個版本,因為就在昨天,我...
摘要:那個率先改變的實例的返回值,就會傳遞給的回調函數。函數對函數的改進,體現在以下四點內置執行器。進一步說,函數完全可以看作多個異步操作,包裝成的一個對象,而命令就是內部命令的語法糖。中的本質就是沒有的隱藏的組件。 1、原型 - jquery使用showImg(https://segmentfault.com/img/bVbwNcY?w=692&h=442);注釋 : 實例雖然不同,但是構...
閱讀 542·2023-04-26 01:39
閱讀 4518·2021-11-16 11:45
閱讀 2619·2021-09-27 13:37
閱讀 892·2021-09-01 10:50
閱讀 3598·2021-08-16 10:50
閱讀 2225·2019-08-30 15:55
閱讀 2992·2019-08-30 15:55
閱讀 2263·2019-08-30 14:07