VNode與遞歸diff

本文同步在我的博客shymean.com上,歡迎關注html

本文將深刻研究虛擬DOMVNode相關的技術實現,瞭解前端框架的基礎。前端

本文包含大量的示例代碼,主要實現node

  • createVNode,建立vnode
  • VNode2DOM,將vnode轉換爲DOM節點
  • VNode2HTML,將vnode轉換爲HTML字符串
  • diff算法,可分爲遞歸實現(Vue)和循環實現(React Fiber),因爲篇幅和結構的問題,本文主要實現遞歸diff,關於fiber相關實現,將在下一篇博客中進行,請移步Fiber與循環diff

本系列文章列表以下git

排在後面文章內會大量採用前面文章中的一些概念和代碼實現,如createVNodediffChildrendoPatch等方法,所以建議逐篇閱讀,避免給讀者形成困惑。本文相關示例代碼均放在github上,若是發現問題,煩請指正。github

vnode

咱們知道vnode實際上就是一個用於描述UI的對象,包含一些基本屬性,咱們經過type描述須要渲染的標籤,經過prpos描述樣式、事件等屬性,經過children描述子節點,最簡單的實現以下所示面試

function createVNode(type, props = {}, children = []) {
    return {
        type,
        props,
        children
    }
}
複製代碼

若是要描述下面這個html結構的簡單視圖算法

<div>
    <h1>hello title</h1>
    <ul class="list-simple">
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
</div>
複製代碼

使用createVNode,構建一顆vnode樹,瀏覽器

let data = {
    title: 'hello vnode',
    list: [1, 2, 3]
}
createRoot(data)
function createRoot(data) {
    let listItem = data.list.map(item => {
        return createVNode('li', {
            onClick() {
                console.log(item)
            }
        }, [item])
    })
    let list = createVNode('ul', {
        class: 'list-simple',
    }, listItem)

    let title = createVNode('h1', {}, [data.title])
    let root = createVNode('div', {}, [title, list])
    return root
}
複製代碼

能夠看見VNode樹與DOM樹是一一對應的,相比而言,vnode包含的屬性要比一個真實DOM的屬性少得多。在後面的實現中,咱們會向VNode上添加一些額外的的屬性。前端框架

此處須要注意,對於children而言,其元素的類型有兩種:能夠是一個VNode,也能夠是原始字面量(視爲文本節點)。爲了統一處理文本節點和元素節點,咱們能夠在createVNode中對文本節點進行特殊處理併發

const TEXT_NODE = Symbol('__text_node')
// 暴露一個是否爲文本節點的接口
function isTextNode(type) {
    return type === TEXT_NODE
}
function createVNode(type, props = {}, children = []) {
    let vnode = {
        type,
        props,
    }
    vnode.children = children.map((child, index) => {
        // 將無type的節點處理爲文本節點,並將其值保存爲nodeValue
        if (!child.type) {
            child = {
                type: TEXT_NODE,
                props: {
                    nodeValue: child
                },
                children: []
            }
        }
        return child
    })
    return vnode
}
複製代碼

這樣每一個VNode均可以包含了統一的屬性。

經過VNode渲染視圖

當咱們將整個UI經過VNode樹描述以後,咱們還須要將其渲染爲真實的DOM節點,有兩個實現思路

  • 直接將vnode映射爲DOM節點,經過appendChild等方式渲染到頁面上
  • 將vnode樹解析成HTML字符串,結合innerHTML渲染到頁面上

VNode2DOM

根據vnode.type,咱們能夠調用DOM接口實例化真實DOM,而後根據vnode.props設置相關DOM屬性,最後根據vnode.children渲染子節點,這個過程最直觀的方法是使用遞歸。

因爲須要正確將子節點插入父節點中,所以須要提早構建父節點,方便起見此處使用先序遍歷。

function VNode2DOM(root, parentDOM) {
    let { type, props, children } = root

    // 將當前vnode渲染爲對應的DOM節點
    let dom
    if (isTextNode(type)) {
        dom = document.createTextNode(root.props.nodeValue)
    } else {
        dom = document.createElement(type)
        for (var key in props) {
            setAttribute(dom, key, props[key])
        }
    }
    // 將子節點也轉換爲dom節點
    Array.isArray(children) && children.forEach(child => {
        VNode2DOM(child, dom)
    })
    // 將當前節點插入節點
    if (parentDOM) {
        parentDOM.appendChild(dom)
    }
    root.$el = dom

    return dom
}

// 向dom元素增長屬性
function setAttribute(el, prop, val) {
    // 處理事件
    let isEvent = prop.indexOf('on') === 0
    if (isEvent) {
        let eventName = prop.slice(2).toLowerCase()
        el.addEventListener(eventName, val)
    } else {
        el.setAttribute(prop, val)
    }
}
複製代碼

測試一下,能夠看見頁面渲染了真實的DOM節點,同時正確添加了props屬性

let root = createRoot({
    title: 'hello vnode',
    list: [1, 2, 3]
})
let dom = VNode2DOM(root, null)
document.body.appendChild(dom)
複製代碼

VNode2HTML

除了渲染DOM節點,咱們也能夠直接拼接HTML字符串(甚至在某些場景下,如SSR,咱們須要的反而僅僅是HTML字符串)。一樣地,咱們可使用遞歸來實現。

因爲構建一個vnode的html片斷須要知道其所有子節點的html片斷,所以此處使用後序遍歷。

function VNode2HTML(root) {
    let { type, props, children } = root

    let sub = '' // 獲取子節點渲染的html片斷
    Array.isArray(children) && children.forEach(child => {
        sub += VNode2HTML(child)
    })

    let el = '' // 當前節點渲染的html片斷
    if (type) {
        let attrs = ''
        for (var key in props) {
            attrs += getAttr(key, props[key])
        }
        el += `<${type}${attrs}>${sub}</${type}>` // 將子節點插入當前節點
    } else {
        el += root // 純文本節點則直接返回
    }

    return el
    function getAttr(prop, val) {
        // 渲染HTML,假設咱們不須要 事件 等props
        let isEvent = prop.indexOf('on') === 0
        return isEvent ? '' : ` ${prop}="${val}"`
    }
}
複製代碼

測試一下

let html = VNode2HTML(root)
console.log(html)
// 輸出結果爲
// <div><h1>hello vnode</h1><ul class="list-simple"><li>1</li><li>2</li><li>3</li></ul></div>
app2.innerHTML = html // 也能夠渲染視圖,儘管貌似少了註冊事件等邏輯
複製代碼

能夠看見,VNode2DOMVNode2HTML均可以達到將VNode描述的UI渲染出來的目的。VNode2HTML主要用於服務端渲染的場景,而VNode2DOM能夠在瀏覽器端直接經過DOM接口渲染,更加直觀且靈活,本文主要研究瀏覽器環境中的VNode。關於SSR的相關知識,我會在後面的文章中繼續實現(本次學習框架原理的一個主要目的就是更新博客的同構渲染)。

視圖更新diff

當vnode發生變化時,咱們能夠經過從新渲染根節點來更新視圖。可是,當vnode結構比較龐大時,咱們就不得不考慮所有從新渲染所帶來的性能問題。

因爲咱們在初始化的時候構建了所有的DOM節點,在vnode發生變化時的理想狀態是:咱們只更新發生了變化的那些vnode,其他未變化的vnode,咱們不必又從新構建一次。

所以如今問題轉化爲:如何找到那些發生了變化的vnode?解決這個問題的算法就被稱爲diff:從根節點開始,依次對比並更新新舊vnode樹上的節點,並儘量地複用DOM,避免額外開銷。

diff算法

爲了性能和效率的均衡,diff算法遵循下面約定

  • 只對比同一層級的節點
  • 不一樣type的節點對應類型的DOM,須要徹底更新當前節點及其子節點樹
  • 相同type的節點則檢測props是否變化,只更新發生了變化的屬性,若是props未變化,則不進行任何更改

基於這些約定,對於vnode樹中的某個節點而言,可能發生的變化有:刪除、新增、更新節點屬性,基於此咱們來實現diff算法。整個diff算法分爲兩步,

  • 首先遍歷vnode樹,收集變化的節點,
  • 而後將收集的變化更新到視圖上

diff

與前面的思路差很少,咱們能夠經過遞歸實現diff

// 定義節點可能發生的變化
const [REMOVE, REPLACE, INSERT, UPDATE] = [0, 1, 2, 3];

// 對比新舊節點,經過patches收集變化
function diff(oldNode, newNode, patches = []) {
    if (!newNode) {
        // 舊節點及其子節點都將移除
        patches.push({ type: REMOVE, oldNode })
    } else if (!oldNode) {
        // 當前節點與其子節點都將插入
        patches.push({ type: INSERT, newNode })
        diffChildren([], newNode.children, patches);
    } else if (oldNode.type !== newNode.type) {
        // 使用新節點替換舊節點
        patches.push({ type: REPLACE, oldNode, newNode })
        // 新節點的字節點都須要插入
        diffChildren([], newNode.children, patches);
    } else {
        // 若是存在有變化的屬性,則使用新節點的屬性更新舊節點
        let attrs = diffAttr(oldNode.props, newNode.props) // 發生變化的屬性
        if (Object.keys(attrs).length > 0) {
            patches.push({ type: UPDATE, oldNode, newNode, attrs })
        }
        newNode.$el = oldNode.$el // 直接複用舊節點
        // 繼續比較子節點
        diffChildren(oldNode.children, newNode.children, patches);
    }
    // 收集變化
    return patches
}

function diffAttr(oldAttrs, newAttrs) {
    let attrs = {};
    // 判斷老的屬性中和新的屬性的關係
    for (let key in oldAttrs) {
        if (oldAttrs[key] !== newAttrs[key]) {
            attrs[key] = newAttrs[key]; // 有可能仍是undefined
        }
    }
    for (let key in newAttrs) {
        // 老節點沒有新節點的屬性
        if (!oldAttrs.hasOwnProperty(key)) {
            attrs[key] = newAttrs[key];
        }
    }
    return attrs;
}
// 按順序對比子節點,在後面咱們會實現其餘方式的新舊節點對比方式
function diffChildren(oldChildren, newChildren, patches) {
    let count = 0;
    // 比較新舊子樹的節點
    if (oldChildren && oldChildren.length) {
        oldChildren.forEach((child, index) => {
            count++;
            diff(child, (newChildren && newChildren[index]) || null, patches);
        });
    }

    // 若是還有未比較的新節點,繼續進行diff將其標記爲INSERT
    if (newChildren && newChildren.length) {
        while (count < newChildren.length) {
            diff(null, newChildren[count++], patches);
        }
    }
}
複製代碼

使用方式大體以下,對比兩個根節點

let root = createRoot({
    title: 'change title',
    list: [1,2,3]
})
let root2 = createRoot({
    title: 'hello vnode',
    list: [3, 2]
})

var patches = diff(root, root2)
console.log(patches) // 能夠看見收集到的變化的節點
複製代碼

doPatch

doPatch階段,主要是將收集的變化更新到視圖上

// 將變化更新到視圖上
function doPatch(patches) {
    // 特定類型的變化,須要從新生成DOM節點,因爲沒法徹底保證patches的順序,所以在此步驟生成vnode.$el
    const beforeCommit = {
        [REPLACE](oldNode, newNode) {
            newNode.$el = createDOM(newNode)
        },
        [UPDATE](oldNode, newNode) {
            // 複用舊的DOM節點,只須要更新必要的屬性便可
            newNode.$el = oldNode.$el
        },
        [INSERT](oldNode, newNode) {
            newNode.$el = createDOM(newNode)
        },
    };
    // 執行此步驟時全部vnode.$el都已準備就緒
    const commit = {
        [REMOVE](oldNode, newNode) {
            oldNode.$parent.$el.removeChild(oldNode.$el)
        },
        [REPLACE](oldNode, newNode) {
            let parent = oldNode.$parent.$el
            let old = oldNode.$el
            let el = newNode.$el

            // 新插入的節點上添加屬性
            setAttributes(newNode, newNode.props)
            parent.insertBefore(el, old);
            parent.removeChild(old);
        },
        [UPDATE](oldNode, newNode) {
            // 只須要更更新diff階段收集到的須要變化的屬性
            setAttributes(newNode, newNode.attrs)
            // 將newNode移動到新的位置,問題在於前面的節點移動後,會影響後面節點的順序
        },
        [INSERT](oldNode, newNode) {
            // 新插入的節點上添加屬性
            setAttributes(newNode, newNode.props)
            insertDOM(newNode)
        },
    }
    // 首先對處理須要從新建立的DOM節點
    patches.forEach(patch => {
        const { type, oldNode, newNode } = patch
        let handler = beforeCommit[type];
        handler && handler(oldNode, newNode);
    })

    // 將每一個變化更新到真實的視圖上
    patches.forEach(patch => {
        const { type, oldNode, newNode } = patch
        let handler = commit[type];
        handler && handler(oldNode, newNode);
    })
}
// 建立節點
function createDOM(node) {
    let type = node.type
    return isTextNode(type) ?
        document.createTextNode(node.props.nodeValue) :
        document.createElement(type)
}
// 將節點插入父節點,若是節點存在父節點中,則調用insertBefore執行的是移動操做而不是複製操做,所以也能夠用來進行MOVE操做
function insertDOM(newNode) {
    let parent = newNode.$parent.$el
    let children = parent.children

    let el = newNode.$el
    let after = children[newNode.index]

    after ? parent.insertBefore(el, after) : parent.appendChild(el)
}
// 設置DOM節點屬性
function setAttributes(vnode, attrs) {
    if (isTextNode(vnode.type)) {
        vnode.$el.nodeValue = vnode.props.nodeValue
    } else {
        let el = vnode.$el
        attrs && Object.keys(attrs).forEach(key => {
            setAttribute(el, key, attrs[key])
        });
    }
}

複製代碼

能夠看見在doPatch操做中,咱們須要獲取vnode的DOM實例和其父節點的引用,所以咱們爲vnode增長一個$el的屬性,引用根據該vnode實例化的真實DOM節點,初始化時爲null,在VNode2DOM時能夠更新其值

function createVNode(type, props = {}, children = []) {
    let vnode = {
        // ...
        type,props
        $el: null
    }
    vnode.children = children.map(child => {
        child.$parent = vnode // 保存對父節點的引用
        return child
    })
    return vnode
}
function VNode2DOM(root, parentDOM) {
    // ...
    root.$el = dom // 在vnode的DOM實例化後更新vnode.$el
    return dom
}
複製代碼

這樣在初始化視圖後,後續更新時,咱們會獲得新的vnode樹,先進行diff收集patches,而後將patches更新到頁面上

let root = createRoot({
    title: 'change title',
    list: [1,2,3]
})
let dom = VNode2DOM(root) // 此時舊節點的$el已保持對於DOM實例的引用
document.body.appendChild(dom) // 初始化完成
// 數據發生變化,獲取新的vnode樹
let root2 = createRoot({
    title: 'hello vnode',
    list: [3, 2]
})

var patches = diff(root, root2) // 收集變化的節點
doPatches(patches) // 更新視圖
複製代碼

代碼優化:拋棄VNode2DOM,合併初始化和更新

在上面的例子中,咱們按照下面的流程實現應用

  • 首先使用createRoot(data)初始化根節點root,並調用VNode2DOM(root)將vnode渲染爲DOM節點
  • data變化時,從新調用createRoot(data2)獲取新的根節點root2,並經過diff(root, root2)獲取新舊節點樹中的變化patchs,最後經過doPatch(patchs)將變化更新在視圖上

整個過程看起來比較簡明,但能夠發現VNode2DOMdoPatch中的初始化DOM節點的邏輯是重複的。換個思路,初始化的時候,能夠看作是新舊點與一個爲null的舊節點進行diff操做。

所以,咱們如今能夠直接跳過VNode2DOM,將初始化與diff的過程放在一塊兒。

root = createRoot({
    title: 'hello vnode',
    list: [1, 2, 3]
})
let patches = diff(null, root)
root.$parent = {
    $el: app
}
doPatch(patches)

// 視圖更新時與上面相同的例子相同
// let patches =diff(root, root2) 
// doPatch(patches)
複製代碼

就這樣,咱們只須要爲根節點手動添加一個root.$parent.$el屬性用於掛載,除此以外就再也不須要VNode2DOM這個方法(儘管這個方法是瞭解vnode映射爲真實DOM最簡單直觀的實現了)

diff算法優化:儘量對比type相同的節點

在上面的diff算法中,咱們在對比新舊節點時,是經過相同的索引值在父元素中的進行對比的,當兩個節點的類型不相同時,會標記爲REPLACE,在patch時會移除舊節點,同時在原位置插入節點。

考慮下面問題,當子節點列表從[h1, ul]變成了[h1,p,ul]時,咱們的算法會將新節點中的p標記爲REPLACE,將ul標記爲INSERT,這顯然不能達到性能上的優化,最理想的狀態是直接在第二個位置插入p標籤便可。

這個問題能夠轉換爲:在某些時刻,咱們不能簡單地經過默認的索引值來查找並對比新舊節點,反之,咱們應該儘量去對比子節點中vnode.type相同的節點。

(感謝咱們將整個diff過程分紅了diffdoPatch兩個階段,咱們如今只須要修改diffChildren方法中的一些邏輯便可~)

如下面例子來講,

// 這裏列舉的abcde都是指不一樣的type
oldChildren = [a,b,c,d] 
newChildren = [b,e,d,c]
// 爲了儘量地複用舊節點,理想狀態是複用b、d,刪除a,在指定位置插入d以前插入e,將c移動到d以後,整個操做共計3步。

// 咱們上面的具體例子中[h1, ul] -> [h1, p, ul]
// 理想狀態應該是直接將p插入ul節點以前,只須要一步操做
複製代碼

所以咱們從新實現一個diffChildren方法,並將以前的diffChildren方法從新命名爲diffChildrenByIndex

// 儘量地與相同type的節點進行比較
// 在這種邏輯下,會盡量地按順序複用子節點中類型相同的節點
// 整個算法的時間複雜度爲O(n),空間複雜度也爲O(n)
// 注意在這種策略下不會再產生REPLACE類型的patch,而是直接將REPLACE拆分紅了INSERT新節點和REMOVE舊節點的兩個patch,對於doPatch階段沒有影響
function diffChildrenByType(oldChildren, newChildren, patches) {
    let map = {}
    oldChildren.forEach(child => {
        let { type } = child
        if (!map[type]) map[type] = []
        map[type].push(child)
    })
    for (let i = 0; i < newChildren.length; ++i) {
        let cur = newChildren[i]
        // 按順序找到第一個類型相同的元素並複用,這種方式存在的問題是當兩個類型相同的節點僅僅是調換位置,他們也會進行UPDATE
        // 針對這個問題,能夠進一步判斷,找到類型相同且props和children最接近的元素,從而避免上面的問題,可是這樣作會增長時間複雜度
        // 所以,對於類型相同且順序可能發生變化的節點,咱們須要額外的手段來檢測重複的節點,一種方法是使用語義化的標籤,減小類型相同的標籤,二是使用key
        if (map[cur.type] && map[cur.type].length) {
            let old = map[cur.type].shift()
            diff(old, cur, patches)
        } else {
            // 因爲部分操做如INSERT依賴最終children的順序,所以須要保證patches的順序
            // 此處對於同一層級的節點而言,在前面的節點會先進入patches隊列,所以會先插入
            diff(null, cur, patches)
        }
    }
    // 剩餘未被使用的舊節點,將其移除
    Object.keys(map).forEach(type => {
        let arr = map[type]
        arr.forEach(old => {
            diff(old, null, patches)
        })
    })
}
複製代碼

測試一下

function diffChildren(oldChildren, newChildren, patches){
    // diffChildrenByIndex(oldChildren, newChildren, patches) // 根據索引值查找並diff節點
    diffChildrenByType(oldChildren, newChildren, patches) // 根據type查找並diff節點
}
// 變化[h1, ul] -> [h1, p, ul]
複製代碼

通過測試能夠發現,在上面的例子中

  • diffChildrenByType會只會產生2個INSERT類型的patch(一個li節點和一個文本節點),
  • diffChildrenByIndex1個REPLACE和8個INSERTpatch,(儘管這個測試用例有點極端,會從新構建整個ul子節點)。

使用key:避免原地複用元素節點

diffChildrenByType中咱們提到了相同類型元素順序調換會致使兩個元素都進行UPDATE的問題,咱們能夠在建立節點時手動爲節點添加一個惟一標識,從而保證在不一樣的順序中也能快速找到該節點,按照行規咱們將這個惟一標識命名爲key

接下來對craeteVNodediffChildrenByType稍做修改,優先根據對比key相同的節點,而後再對比類型相同的節點

// 在diffChildrenByType的基礎上增長了根據key查找舊節點的邏輯
// 根據type和key來進行判斷,避免同類型元素順序變化致使的沒必要要更新
function diffChildrenByKey(oldChildren, newChildren, patches) {
    newChildren = newChildren.slice() // 複製一份children,避免影響父節點的children屬性
    // 找到新節點列表中帶key的節點
    let keyMap = {}
    newChildren.forEach((child, index) => {
        let { key } = child
        // 只有攜帶key屬性的會參與同key節點的比較
        if (key !== undefined) {
            if (keyMap[key]) {
                console.warn(`請保證${key}的惟一`, child)
            } else {
                keyMap[key] = {
                    vnode: child,
                    index
                }
            }
        }
    })

    // 在遍歷舊列表時,先比較類型與key均相同的節點,若是新節點中不存在key相同的節點,纔會將舊節點保存起來
    let typeMap = {}
    oldChildren.forEach(child => {
        let { type, key } = child
        // 先比較類型與key均相同的節點
        let { vnode, index } = (keyMap[key] || {})
        if (vnode && vnode.type === type) {
            newChildren[index] = null // 該節點已被比較,須要彈出
            // newChildren.splice(index, 1) // 該節點已被比較,須要彈出
            delete keyMap[key]
            diff(child, vnode, patches)
        } else {
            // 將剩餘的節點保存起來,與剩餘的新節點進行比較
            if (!typeMap[type]) typeMap[type] = []
            typeMap[type].push(child)
        }
    })

    // 剩下的節點處理與diffChildrenByType相同,此時key相同的節點已被比較
    for (let i = 0; i < newChildren.length; ++i) {
        let cur = newChildren[i]
        if (!cur) continue; // 已在在前面與此時key相同的節點進行比較
        // ... 找到一個類型相同的節點進行比較
    }
    // ... 剩餘未被使用的舊節點,將其移除
}
複製代碼

同時增長一種MOVE的patch類型,在diff方法中,若是新舊節點在父節點中的位置不一致,則會提交一個patch,此外咱們須要在vnode上增長一個index屬性,用於記錄新舊節點在父節點中的位置

function createVNode(type, props = {}, children = []) {
    vnode.key = props.key  // 增長key
    vnode.children = children.map((child, index) => {
        // ...
        child.index = index // 增長index, 記錄在該節點的索引值
        return child
    })
}

const [REMOVE, REPLACE, INSERT, UPDATE, MOVE] = [0, 1, 2, 3, 5]; // 增長MOVE類型的patch
function diff(oldNode, newNode, patches = []) {
    // 新舊節點類型相同但索引值不一致,則表示節點複用,節點須要移動位置,進行MOVE
    if (oldNode.index !== newNode.index) {
        patches.push({ type: MOVE, oldNode, newNode })
    }
    return patches
}
複製代碼

最後,在doPatch階段須要爲MOVE類型的節點增長DOM更新處理方法

// 將節點插入父節點,若是節點存在父節點中,則調用insertBefore執行的是移動操做而不是複製操做,
// 所以也能夠用來進行MOVE操做
function insertDOM(newNode) {
    let parent = newNode.$parent.$el
    let children = parent.children

    let el = newNode.$el
    let after = children[newNode.index]

    after ? parent.insertBefore(el, after) : parent.appendChild(el)
}
// 須要MOVE的元素按照新的索引值排序,保證排在前面的先進行移動位置的操做
patches
    .filter(patch => patch.type === MOVE)
    .sort((a, b) => a.index - b.index)
    .forEach(patch => {
        const { oldNode, newNode } = patch
        insertDOM(newNode)
    })
複製代碼

測試一下

function testKey() {
    let list1 = createList([1, 2, 3], true) // true 使用元素值做爲key
    let patches = diff(null, list1)
    list1.$parent = {
        $el: app
    }
    doPatch(patches)
    btn.onclick = function () {
        let list2 = createList([4, 3, 2, 1], true)
        let patches = diff(list1, list2)
        console.log(patches)  // 查看收集的變化
        doPatch(patches)
    }
}

// 測試三種diff策略的影響
function diffChildren(oldChildren, newChildren, patches) {
    // diffChildrenByIndex(oldChildren, newChildren, patches)
    // diffChildrenByType(oldChildren, newChildren, patches)
    diffChildrenByKey(oldChildren, newChildren, patches)
}
複製代碼

一樣進行上面的操做

  • diffChildrenByKey包含3個MOVE操做(一、二、3節點不會建立新的文本節點,而是移動li節點),兩個INSERT操做(一個li節點和一個文本節點)
  • diffChildrenByType包含3個UPDATE操做(更新前三個文本節點),兩個INSERT操做
  • diffChildrenByIndex,因爲循環節點的類型一致,致使該方法的diff結果與diffChildrenByType相同

能夠看見,增長key以後,會盡量地複用元素節點並移動位置,而不是在原地複用元素節點並更新文本節點(移動位置在性能上並不見得優於原地更新文本節點),所以

使用key並不必定能帶來性能上的提高,而是爲了不原地複用元素節點帶來的影響。

小結

本文從構造vnode節點開始,

  • 首先了解如何經過vnode描述一段HTML結構
  • 而後經過VNode2DOMVNode2HTML瞭解瞭如何將vnode樹渲染爲真實的DOM節點
  • 接着考慮視圖更新的狀況,實現diff算法,主要實現diff(收集變化)和doPatch(將變化更新到頁面上)兩個方法,併合並了初始化和更新邏輯,移除了VNode2DOM
  • 而後優化同層新舊子節點之間的查找和對比,儘量地複用type相同的DOM節點,並發現了順序發生變化的同type節點存在的問題,
  • 最後引入了key進一步更新同層新舊子節點的查找和對比,對於key相同的節點,優先使用MOVE操做移動節點,避免原地複用元素節點

本文參考了Vue源碼中的一些實現,可是Vue中使用多個遊標進行diff的方式我的感受不是很清楚明瞭,所以按照本身的理解實現了上面的幾種diff策略

通過上面的步驟,大概能瞭解vnode與diff算法的一些核心思想。

因爲上面的diff是遞歸實現的,很難被臨時中斷,在某個時刻又恢復至原來調用的地方,所以當vnode樹過於複雜時,將長時間佔用JavaScript執行線程,致使瀏覽器卡死。在下一篇文章Fiber與循環diff,將參考React中的 fiber,按循環實現diff。

感謝閱讀,九月份經歷了一次很失敗的面試,感受過去挺長一段時間,本身的學習態度和方法都出現了一些問題,總結起來就是:學而不思則罔,學習的時候,應該多思考才行。因爲本人水平有限,文中出現的錯誤,煩請指正。

相關文章
相關標籤/搜索