【Vue原理】Diff - 原始碼版 之 相關輔助函式

神仙朱發表於2019-09-18

寫文章不容易,點個讚唄兄弟

專注 Vue 原始碼分享,文章分為白話版和 原始碼版,白話版助於理解工作原理,原始碼版助於瞭解內部詳情,讓我們一起學習吧 研究基於 Vue版本 【2.5.17】

如果你覺得排版難看,請點選 下面連結 或者 拉到 下面關注公眾號也可以吧

【Vue原理】Diff - 原始碼版 之 相關輔助函式

在開始我們的 Diff 主要內容之前,我們先來了解下其中會用的的一些輔助函式

本來可以放到 Diff 那裡寫,但是全部合起來內容又太多

而且這些函式比較具有公用性,就是抽出來用

所以打算獨立一篇文章,先預熱一下,內容也不多,也挺簡單,光看下也會對我們的思維有所幫助


節點操作函式

下面是 Diff 在比較節點時,更新DOM 會使用到的一些函式

本來會有更多,為了看得方便,我把一些合併了

下面會介紹三個函式

insert,createElm,createChildren

簡單介紹

insert 作用是把節點插入到某個位置

createElm 作用是建立DOM 節點

createChildren 作用也是建立DOM 節點,但是處理的是一個陣列,並且會建立 DOM 節點 和 文字節點

下面就來仔細說說這三個方法

1 insert

這個函式的作用就是 插入節點

但是插入也會分兩種情況

1、沒有參考兄弟節點,直接插入父節點的子節點末尾

2、有參考兄弟節點,則插在兄弟節點前面

可以說這個函式是 Diff 的基石哈哈

function insert(parent, elm, ref) {    

    if (parent) {        

        if (ref) {            

            if (ref.parentNode === parent) {

                parent.insertBefore(elm, ref);
            }
        } else {
            parent.appendChild(elm);
        }
    }
}
複製程式碼

2 createElm

這個函式的作用跟它的名字一樣,就是建立節點的意思

建立完節點之後,會呼叫 insert 去插入節點

你可以看一下,不難

function createElm(vnode, parentElm, refElm) {  



    var children = vnode.children;    

    var tag = vnode.tag;    

    if (tag) {

        vnode.elm = document.createElement(tag)        



        // 先把 子節點插入 vnode.elm,然後再把 vnode.elm 插入parent

        createChildren(vnode, children);

       

        //  插入DOM 節點

        insert(parentElm, vnode.elm, refElm);
    }    



    else {

        vnode.elm = document.createTextNode(vnode.text);
        
        insert(parentElm, vnode.elm, refElm);
    }
}
複製程式碼

createElm 需要根據 Vnode 來判斷需要建立什麼節點

1、文字節點

2、普通節點

1 文字節點

當 vnode.tag 為 undefined 的時候,建立文字節點,看下 真實文字vnode

公眾號

並且,文字節點是沒有子節點的

2 普通節點

vnode.tag 有值,那就建立相應的 DOM

但是 該 DOM 可能存在子節點,所以子節點甚至子孫節點,也都是要建立的

所以會呼叫一個 createChildren 去完成所有子孫節點的建立

3 createChildren

這個方法處理子節點,必然是用遍歷遞迴的方法逐個處理的

1如果子節點是陣列,則遍歷執行 createElm 逐個處理

2如果子節點的 text 屬性有資料,則表示這個 vnode 是個文字節點,直接建立文字節點,然後插入到父節點中

function createChildren(vnode, children) {    



    if (Array.isArray(children)) {      



        for (var i = 0; i < children.length; ++i) {

            createElm(children[i], vnode.elm, null);
        }



    }



    else if (        

        typeof vnode.text=== 'string' ||

        typeof vnode.text=== 'number' ||
        typeof vnode.text=== 'boolean'
    ) {
        vnode.elm.appendChild(

            document.createTextNode(vnode.text)

        )

    }
}
複製程式碼

服務Diff工具函式

下面的函式是 Vue 專門用來服務 Diff 的,介紹兩個

createKeyToOldIdx,sameVnode

1createKeyToOldIdx

接收一個 children 陣列,生成 key 與 index 索引對應的一個 map 表

function createKeyToOldIdx(

    children, beginIdx, endIdx

) {    



    var i, key;    

    var map = {};    



    for (i = beginIdx; i <= endIdx; ++i) {

        key = children[i].key;        

        if (key) {

            map[key] = i;
        }
    }    

    return map

}
複製程式碼

比如你的舊子節點陣列是

[{    
    tag:"div",  key: "key_1"

},{  

    tag:"strong", key:"key_2"

},{  

    tag:"span",  key:"key_4"

}]
複製程式碼

經過 createKeyToOldIdx 生成一個 map 表 oldKeyToIdx,是下面這樣

{
    "key_1":0,
    "key_2":1,
    "key_4":2
}
複製程式碼

把 vnode 的 key 作為屬性名,而該 vnode 在children 的位置 作為 屬性值

這個函式在 Diff 中的作用是

判斷某個新 vnode 是否在 這個舊的 Vnode 陣列中,並且拿到它的位置。就是拿到 新 Vnode 的 key,然後去這個 map 表中去匹配,是否有相應的節點,有的話,就返回這個節點的位置

比如

現在我有一個 新子節點陣列,一個 舊子節點陣列

我拿到 新子節點陣列 中的某一個 newVnode,我要判斷他是否 和 舊子節點陣列 中某個vnode相同

要怎麼判斷???難道是雙重遍歷陣列,逐個判斷 newVnode.key==vnode.key ??

Vue 用了更聰明的辦法,使用 舊 Vnode 陣列生成一個 map 物件 obj

當 obj[ newVnode.key ] 存在的時候,說明 新舊子節點陣列都存在這個節點

並且我能拿到該節點在 舊子節點陣列 中的位置(屬性值)

反之,則不存在

這個方法也給我們提供了在專案中相似場景的一個解決思路,以物件索引查詢替代陣列遍歷

希望大家記住哦

2 sameVnode

這個函式在 Diff 中也起到非常大的作用,大家務必要記住啊

它的作用是判斷兩個節點是否相同

這裡說的相同,並不是完全一毛一樣,而是關鍵屬性一樣,可以先看下原始碼

function sameVnode(a, b) {    



    return (

        a.key === b.key &&
        a.tag === b.tag &&
        !!a.data === !!b.data &&
        sameInputType(a, b)
    )
}



function sameInputType(a, b) {    



    if (a.tag !== 'input') return true

    var i;    

    var types = [

        'text','number','password',

        'search','email','tel','url'

    ]    



    var typeA = (i = a.data) && (i = i.attrs) && i.type;    

    var typeB = (i = b.data) && (i = i.attrs) && i.type;    

    

    // input 的型別一樣,或者都屬於基本input型別

    return (
        typeA === typeB ||
        types.indexOf(typeA)>-1 &&

        types.indexOf(typeB)>-1

    )
}
複製程式碼

判斷的依據主要是 三點,key,tag,是否存在 data

這裡判斷的節點是隻是相對於 節點本身,並不包括 children 在內

也就是說,就算data不一樣,children 不一樣,兩個節點還是可能一樣

比如下面這兩個

公眾號

公眾號

有一種特殊情況,就是 input 節點

input 需要額外判斷, 兩個節點的 type 是否相同

或者

兩個節點的型別可以不同,但是必須屬於那些 input 型別

sameVnode 的內容就到這裡了,但是我不禁又開始思考一個問題

為什麼 sameVnode 會這麼判斷??

下面純屬個人意淫想法,僅供參考

sameVnode 應用在 Diff ,作用是為了判斷節點是否需要新建

當兩個 新舊vnode 進行 sameVnode 得到 false 的時候,說明兩個vnode 不一樣,會新建DOM插入

也就是兩個節點從根本上不一樣時才會建立

其中會比較 唯一識別符號 key 和 標籤名 tag,從而得到 vnode 是否一樣 ,這些是毫無疑問的了

但是這裡不需要判斷 data 是否一樣,我開始不太明白

後面想到 data 是包含有一些 dom 上的屬性的,所以 data 不一樣沒有關係

因為就算不一樣,他們還是基於同一個 DOM

因為DOM屬性的值是可能是動態繫結動態更新變化的,所以變化前後的 兩個 vnode,相應的 data 肯定不一樣,但是其實他們是同一個 Vnode,所以 data 不在判斷範疇

但是 data 在新舊節點中,必須都定義,或者都不定義

不存在 一個定義,而一個沒定義, 但是會相同的 Vnode

比如,下面這個就會存在data

公眾號

這個就不會存在data

公眾號

他們在模板中,肯定是不屬於同一個節點


總結

涉及的函式主要分為兩類

一類是專門負責操作 DOM 的,insert,createElm,createChildren

這類函式比較通用,就算在我們自己的專案中也可以用得上

一類是專門特殊服務 Diff 的,createKeyToOldIdx,sameVnode

其中會包含一些專案的解決思路

大家務必先記住一下這幾個函式,在下篇內容的原始碼中會頻繁出現

到時不會仔細介紹


最後

鑑於本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵,如果有任何描述不當的地方,歡迎後臺聯絡本人,有重謝

公眾號

相關文章