<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手寫一個(gè)虛擬DOM庫,徹底讓你理解diff算法

          共 16585字,需瀏覽 34分鐘

           ·

          2021-08-05 19:22

          來源 | https://www.cnblogs.com/wanglinmantan/archive/2021/08/02/15089513.html

          所謂虛擬DOM就是用js對(duì)象來描述真實(shí)DOM,它相對(duì)于原生DOM更加輕量,因?yàn)檎嬲腄OM對(duì)象附帶有非常多的屬性。
          另外配合虛擬DOM的diff算法,能以最少的操作來更新DOM,除此之外,也能讓vue和react之類的框架支持除瀏覽器之外的其他平臺(tái)。
          本文會(huì)參考知名的snabbdom庫來手寫一個(gè)簡易版的,配合圖片示例一步步完成代碼,一定讓你徹底理解虛擬DOM的patch及diff算法。

          創(chuàng)建虛擬DOM對(duì)象

          虛擬DOM(下文稱VNode)就是使用js的普通對(duì)象來描述DOM的類型、屬性、子元素等信息,一般通過名為h的函數(shù)來創(chuàng)建,為了純粹的理解VNode的patch過程,我們先不考慮元素的屬性、樣式、事件等,只考慮節(jié)點(diǎn)類型及節(jié)點(diǎn)內(nèi)容,看一下此時(shí)的VNode結(jié)構(gòu):
          {    tag: '',// 元素標(biāo)簽    children: [],// 子元素    text: '',// 子元素是文本節(jié)點(diǎn)的話,保存文本    el: null// 對(duì)應(yīng)的真實(shí)dom}

          h函數(shù)根據(jù)接收的參數(shù)返回該對(duì)象即可:

          export const h = (tag, children) => {    let text = ''    let el    // 子元素是文本節(jié)點(diǎn)    if (typeof children === 'string' || typeof children === 'number') {        text = children        children = undefined    } else if (!Array.isArray(children)) {        children = undefined    }    return {        tag, // 元素標(biāo)簽        children, // 子元素        text, // 文本子節(jié)點(diǎn)的文本        el// 真實(shí)dom    }}

          比如我們要?jiǎng)?chuàng)建一個(gè)div的VNode可以這樣使用:

          h('div', '我是文本')h('div', [h('span')])

          詳解patch過程

          patch函數(shù)是我們的主函數(shù),主要用來進(jìn)行新舊VNode的對(duì)比,找到差異來更新實(shí)際DOM,它接收兩個(gè)參數(shù)。

          第一個(gè)參數(shù)可以是DOM元素或者是VNode,表示舊的VNode。

          第二參數(shù)表示新的VNode,一般只有第一次調(diào)用時(shí)才會(huì)傳DOM元素。

          如果第一個(gè)參數(shù)為DOM元素的話我們直接忽略它的子元素把它轉(zhuǎn)為一個(gè)VNode:

          export const patch = (oldVNode, newVNode) => {    // dom元素    if (!oldVNode.tag) {        let el = oldVNode        el.innerhtml = ''        oldVNode = h(oldVNode.tagName.toLowerCase())        oldVNode.el = el    }}

          接下來新舊兩個(gè)VNode就可以進(jìn)行比較了:

          export const patch = (oldNode, newNode) => {    // ...    patchVNode(oldVNode, newVNode)    // 返回新的vnode    return newVNode}

          在patchVNode方法里我們對(duì)新舊VNode進(jìn)行比較及更新DOM。

          首先如果兩個(gè)VNode的類型不同,那么不用比較,直接使用新的VNode替換舊的:

          const patchVNode = (oldNode, newNode) => {    if (oldVNode === newVNode) {        return    }    // 元素標(biāo)簽相同,進(jìn)行patch    if (oldVNode.tag === newVNode.tag) {        // ...    } else { // 類型不同那么根據(jù)新的VNode創(chuàng)建新的dom節(jié)點(diǎn),然后插入新節(jié)點(diǎn),移除舊節(jié)點(diǎn)        let newEl = createEl(newVNode)        let parent = oldVNode.el.parentNode        parent.insertBefore(newEl, oldVNode.el)        parent.removeChild(oldVNode.el)    }}

          createEl方法用來遞歸的把VNode轉(zhuǎn)換成真實(shí)的DOM節(jié)點(diǎn):

          const createEl = (vnode) => {    let el = document.createElement(vnode.tag)    vnode.el = el    // 創(chuàng)建子節(jié)點(diǎn)    if (vnode.children && vnode.children.length > 0) {        vnode.children.forEach((item) => {            el.appendChild(createEl(item))        })    }    // 創(chuàng)建文本節(jié)點(diǎn)    if (vnode.text) {        el.appendChild(document.createTextNode(vnode.text))    }    return el}

          如果類型相同,那么就要根據(jù)其子節(jié)點(diǎn)的情況來判斷進(jìn)行哪種操作。

          如果新節(jié)點(diǎn)只有一個(gè)文本子節(jié)點(diǎn),那么移除舊節(jié)點(diǎn)的所有子節(jié)點(diǎn)(如果有的話),創(chuàng)建一個(gè)文本子節(jié)點(diǎn):

          const patchVNode = (oldVNode, newVNode) => {    // 元素標(biāo)簽相同,進(jìn)行patch    if (oldVNode.tag === newVNode.tag) {        // 元素類型相同,那么舊元素肯定是進(jìn)行復(fù)用的        let el = newVNode.el = oldVNode.el        // 新節(jié)點(diǎn)的子節(jié)點(diǎn)是文本節(jié)點(diǎn)        if (newVNode.text) {            // 移除舊節(jié)點(diǎn)的子節(jié)點(diǎn)            if (oldVNode.children) {                oldVNode.children.forEach((item) => {                    el.removeChild(item.el)                })            }            // 文本內(nèi)容不相同則更新文本            if (oldVNode.text !== newVNode.text) {                el.textContent = newVNode.text            }        } else {            // ...        }    } else { // 不同使用newNode替換oldNode        // ...    }}

          如果新節(jié)點(diǎn)的子節(jié)點(diǎn)非文本節(jié)點(diǎn),那也有幾種情況:

          1.新節(jié)點(diǎn)不存在子節(jié)點(diǎn),而舊節(jié)點(diǎn)存在,那么移除舊節(jié)點(diǎn)的子節(jié)點(diǎn);

          2.新節(jié)點(diǎn)不存在子節(jié)點(diǎn),舊節(jié)點(diǎn)存在文本節(jié)點(diǎn),那么移除該文本節(jié)點(diǎn);

          3.新節(jié)點(diǎn)存在子節(jié)點(diǎn),舊節(jié)點(diǎn)存在文本節(jié)點(diǎn),那么移除該文本節(jié)點(diǎn),然后插入新節(jié)點(diǎn);

          4.新舊節(jié)點(diǎn)都有子節(jié)點(diǎn)的話那么就需要進(jìn)入到diff階段;

          const patchVNode = (oldVNode, newVNode) => {    // 元素標(biāo)簽相同,進(jìn)行patch    if (oldVNode.tag === newVNode.tag) {        // ...        // 新節(jié)點(diǎn)的子節(jié)點(diǎn)是文本節(jié)點(diǎn)        if (newVNode.text) {            // ...        } else {// 新節(jié)點(diǎn)不存在文本節(jié)點(diǎn)            // 新舊節(jié)點(diǎn)都存在子節(jié)點(diǎn),那么就要進(jìn)行diff            if (oldVNode.children && newVNode.children) {                diff(el, oldVNode.children, newVNode.children)            } else if (oldVNode.children) {// 新節(jié)點(diǎn)不存在子節(jié)點(diǎn),那么移除舊節(jié)點(diǎn)的所有子節(jié)點(diǎn)                oldVNode.children.forEach((item) => {                    el.removeChild(item.el)                })            } else if (newVNode.children) {// 新節(jié)點(diǎn)存在子節(jié)點(diǎn)                // 舊節(jié)點(diǎn)存在文本節(jié)點(diǎn)則移除                if (oldVNode.text) {                    el.textContent = ''                }                // 添加新節(jié)點(diǎn)的子節(jié)點(diǎn)                newVNode.children.forEach((item) => {                    el.appendChild(createEl(item))                })            } else if (oldVNode.text) {// 新節(jié)點(diǎn)啥也沒有,舊節(jié)點(diǎn)存在文本節(jié)點(diǎn)                el.textContent = ''            }        }    } else { // 不同使用newNode替換oldNode        // ...    }}

          如果當(dāng)新舊節(jié)點(diǎn)都存在非文本的子節(jié)點(diǎn)的話,那么就要進(jìn)入到著名的diff階段了,diff算法的目的主要是用來盡可能復(fù)用舊的節(jié)點(diǎn),以減小DOM操作的開銷。

          圖解diff算法

          首先最簡單的diff顯然是同位置的新舊節(jié)點(diǎn)兩兩比較,但是在WEB場景下,倒序、排序、換位都是經(jīng)常有可能發(fā)生的。

          所以同位置比較很多時(shí)候都很低效,無法滿足這種常見場景,各種所謂的diff算法就是用來盡量能檢查出這些情況。

          然后進(jìn)行復(fù)用,snabbdom里的diff算法是一種雙端比較的策略,同時(shí)從新舊節(jié)點(diǎn)的兩端向中間開始比較,每一輪都會(huì)進(jìn)行四次比較,所以需要四個(gè)指針,如下圖:


          即上述四個(gè)位置的排列組合:oldStartIdx與newStartIdx、oldStartIdx與newEndIdx、oldEndIdx與newStartIdx、oldEndIdx與newEndIdx,每當(dāng)發(fā)現(xiàn)所比較的兩個(gè)節(jié)點(diǎn)可能可以復(fù)用的話,那么就對(duì)這兩個(gè)節(jié)點(diǎn)進(jìn)行patch和相應(yīng)操作,并更新指針進(jìn)入下一輪比較,那怎么判斷兩個(gè)節(jié)點(diǎn)是否能復(fù)用呢?

          這就需要使用到key了,因?yàn)楣饪词欠袷峭愋偷墓?jié)點(diǎn)是遠(yuǎn)遠(yuǎn)不夠的,因?yàn)橥粋€(gè)列表基本上類型都是一樣的,那就跟從頭開始的兩兩比較沒有區(qū)別了,先修改一下我們的h函數(shù):

          export const h = (tag, data = {}, children) => {    // ...    let key    // 文本節(jié)點(diǎn)    // ...    if (data && data.key) {        key = data.key    }    return {        // ...        key    }}

          現(xiàn)在創(chuàng)建VNode的時(shí)候可以傳入key:

          h('div', {key: 1}, '我是文本')

          比較的終止條件也很明顯,其中一個(gè)列表已經(jīng)比較完了,也就是oldStartIdx>oldEndIdx或newStartIdx>newEndIdx,先把算法基本框架寫一下:

          // 判斷兩個(gè)節(jié)點(diǎn)是否可進(jìn)行復(fù)用const isSameNode = (a, b) => {    return a.key === b.key && a.tag === b.tag}
          // 進(jìn)行diffconst diff = (el, oldChildren, newChildren) => { // 位置指針 let oldStartIdx = 0 let oldEndIdx = oldChildren.length - 1 let newStartIdx = 0 let newEndIdx = newChildren.length - 1 // 節(jié)點(diǎn)指針 let oldStartVNode = oldChildren[oldStartIdx] let oldEndVNode = oldChildren[oldEndIdx] let newStartVNode = newChildren[newStartIdx] let newEndVNode = newChildren[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isSameNode(oldStartVNode, newStartVNode)) {
          } else if (isSameNode(oldStartVNode, newEndVNode)) {
          } else if (isSameNode(oldEndVNode, newStartVNode)) {
          } else if (isSameNode(oldEndVNode, newEndVNode)) {
          } }}

          新增了四個(gè)變量用來保存四個(gè)位置的節(jié)點(diǎn),接下來以上圖為例來完善代碼。

          第一輪會(huì)發(fā)現(xiàn)oldEndVNode與newEndVNode是可復(fù)用節(jié)點(diǎn),那么對(duì)它們進(jìn)行patch,因?yàn)槎荚谧詈蟮奈恢茫圆恍枰苿?dòng)DOM節(jié)點(diǎn),更新指針即可:

          const diff = (el, oldChildren, newChildren) => {    // ...    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (isSameNode(oldStartVNode, newStartVNode)) {}         else if (isSameNode(oldStartVNode, newEndVNode)) {}         else if (isSameNode(oldEndVNode, newStartVNode)) {}         else if (isSameNode(oldEndVNode, newEndVNode)) {            patchVNode(oldEndVNode, newEndVNode)            // 更新指針            oldEndVNode = oldChildren[--oldEndIdx]            newEndVNode = newChildren[--newEndIdx]        }    }}

          此時(shí)的位置信息如下:


          下一輪會(huì)發(fā)現(xiàn)oldStartIdx與newEndIdx是可復(fù)用節(jié)點(diǎn),那么對(duì)oldStartVNode和newEndVNode兩個(gè)節(jié)點(diǎn)進(jìn)行patch,同時(shí)該節(jié)點(diǎn)在新列表里的位置是當(dāng)前比較區(qū)間的最后一個(gè),所以需要把oldStartIdx的真實(shí)DOM移動(dòng)到舊列表當(dāng)前比較區(qū)間的最后,也就是oldEndVNode之后:


          const diff = (el, oldChildren, newChildren) => {    // ...    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (isSameNode(oldStartVNode, newStartVNode)) {}         else if (isSameNode(oldStartVNode, newEndVNode)) {            patchVNode(oldStartVNode, newEndVNode)            // 把節(jié)點(diǎn)移動(dòng)到oldEndVNode之后            el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)            // 更新指針            oldStartVNode = oldChildren[++oldStartIdx]            newEndVNode = newChildren[--newEndIdx]        }         else if (isSameNode(oldEndVNode, newStartVNode)) {}         else if (isSameNode(oldEndVNode, newEndVNode)) {}    }}

          這輪以后位置如下:


          下一輪比較很明顯oldStartVNode與newStartVNode是可復(fù)用節(jié)點(diǎn),那么對(duì)它們進(jìn)行patch,因?yàn)槎荚诘谝粋€(gè)位置,所以也不需要移動(dòng)節(jié)點(diǎn),更新指針即可:

          const diff = (el, oldChildren, newChildren) => {    // ...    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (isSameNode(oldStartVNode, newStartVNode)) {            patchVNode(oldStartVNode, newStartVNode)            // 更新指針            oldStartVNode = oldChildren[++oldStartIdx]            newStartVNode = newChildren[++newStartIdx]        }         else if (isSameNode(oldStartVNode, newEndVNode)) {}         else if (isSameNode(oldEndVNode, newStartVNode)) {}         else if (isSameNode(oldEndVNode, newEndVNode)) {}    }}

          這輪過后位置如下:


          再下一輪會(huì)發(fā)現(xiàn)oldEndVNode與newStartVNode是可復(fù)用節(jié)點(diǎn),在新的列表里位置變成了當(dāng)前比較區(qū)間的第一個(gè),所以patch完后需要把節(jié)點(diǎn)移動(dòng)到oldStartVNode的前面:

          const diff = (el, oldChildren, newChildren) => {    // ...    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (isSameNode(oldStartVNode, newStartVNode)) {}         else if (isSameNode(oldStartVNode, newEndVNode)) {}         else if (isSameNode(oldEndVNode, newStartVNode)) {            patchVNode(oldEndVNode, newStartVNode)            // 把oldEndVNode節(jié)點(diǎn)移動(dòng)到oldStartVNode前            el.insertBefore(oldEndVNode.el, oldStartVNode.el)            // 更新指針            oldEndVNode = oldChildren[--oldEndIdx]            newStartVNode = newChildren[++newStartIdx]        }         else if (isSameNode(oldEndVNode, newEndVNode)) {}    }}

          這輪后位置如下:


          再下一輪會(huì)發(fā)現(xiàn)四次比較都沒有發(fā)現(xiàn)可以復(fù)用的節(jié)點(diǎn),這咋辦呢,因?yàn)樽罱K我們需要讓舊列表變成新列表,所以當(dāng)前的newStartVNode如果在舊列表里沒找到可復(fù)用的,需要直接創(chuàng)建一個(gè)新節(jié)點(diǎn)插進(jìn)去,但是我們一眼就看到了舊節(jié)點(diǎn)里有c節(jié)點(diǎn)。

          只是不在此輪比較的四個(gè)位置上,那么我們可以直接在舊的列表里搜索,找到了就進(jìn)行patch,并且把該節(jié)點(diǎn)移動(dòng)到當(dāng)前比較區(qū)間的第一個(gè),也就是oldStartIdx之前,這個(gè)位置空下來了就置為null,后續(xù)遍歷到就跳過,如果沒找到,那么說明這丫節(jié)點(diǎn)真的是新增的,直接創(chuàng)建該節(jié)點(diǎn)插入到oldStartIdx之前即可:

          // 在列表里找到可以復(fù)用的節(jié)點(diǎn)const findSameNode = (list, node) => {    return list.findIndex((item) => {        return item && isSameNode(item, node)    })}
          const diff = (el, oldChildren, newChildren) => { // ... while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 某個(gè)位置的節(jié)點(diǎn)為null跳過此輪比較,只更新指針 if (oldStartVNode === null) { oldStartVNode = oldChildren[++oldStartIdx] } else if (oldEndVNode === null) { oldEndVNode = oldChildren[--oldEndIdx] } else if (newStartVNode === null) { newStartVNode = oldChildren[++newStartIdx] } else if (newEndVNode === null) { newEndVNode = oldChildren[--newEndIdx] } else if (isSameNode(oldStartVNode, newStartVNode)) {} else if (isSameNode(oldStartVNode, newEndVNode)) {} else if (isSameNode(oldEndVNode, newStartVNode)) {} else if (isSameNode(oldEndVNode, newEndVNode)) {} else { let findIndex = findSameNode(oldChildren, newStartVNode) // newStartVNode在舊列表里不存在,那么是新節(jié)點(diǎn),創(chuàng)建并插入之 if (findIndex === -1) { el.insertBefore(createEl(newStartVNode), oldStartVNode.el) } else {// 在舊列表里存在,那么進(jìn)行patch,并且移動(dòng)到oldStartVNode前 let oldVNode = oldChildren[findIndex] patchVNode(oldVNode, newStartVNode) el.insertBefore(oldVNode.el, oldStartVNode.el) // 原位置空了置為null oldChildren[findIndex] = null } // 更新指針 newStartVNode = newChildren[++newStartIdx] } }}

          具體到我們的示例上,在舊的列表里找到了,所以這輪過后位置信息如下:


          再下一輪比較和上輪一樣,會(huì)進(jìn)入搜索的分支,并且找到了d,所以也是path加移動(dòng)節(jié)點(diǎn),本輪過后如下:


          因?yàn)閚ewStartIdx大于newEndIdx,所以while循環(huán)就結(jié)束了,但是我們發(fā)現(xiàn)舊的列表里多了g和h節(jié)點(diǎn),這兩個(gè)在新列表里沒有,所以需要把它們移除,反過來,如果新的列表里多了舊列表里沒有的節(jié)點(diǎn),那么就創(chuàng)建和插入之:

          const diff = (el, oldChildren, newChildren) => {    // ...    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {        if (isSameNode(oldStartVNode, newStartVNode)) {}         else if (isSameNode(oldStartVNode, newEndVNode)) {}         else if (isSameNode(oldEndVNode, newStartVNode)) {}         else if (isSameNode(oldEndVNode, newEndVNode)) {}        else {}    }    // 舊列表里存在新列表里沒有的節(jié)點(diǎn),需要?jiǎng)h除    if (oldStartIdx <= oldEndIdx) {        for(let i = oldStartIdx; i <= oldEndIdx; i++) {            oldChildren[i] && el.removeChild(oldChildren[i].el)        }    } else if (newStartIdx <= newEndIdx) {// 新列表里存在舊列表沒有的節(jié)點(diǎn),創(chuàng)建和插入        // 在newEndVNode的下一個(gè)節(jié)點(diǎn)前插入,如果下一個(gè)節(jié)點(diǎn)不存在,那么insertBefore方法會(huì)執(zhí)行appendChild的操作        let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null        for(let i = newStartIdx; i <= newEndIdx; i++) {            el.insertBefore(createEl(newChildren[i]), before)        }    }}

          以上就是雙端diff的全過程,是不是還挺簡單,畫個(gè)圖就十分容易理解了。

          屬性的更新

          其他屬性都通過data參數(shù)傳入,先修改一下h函數(shù):

          export const h = (tag, data = {}, children) => {  // ...  return {    // ...    data  }}

          類名

          類名通過data選項(xiàng)的class字段傳遞,比如:

          h('div',{    class: {        btn: true    }}, '文本')

          類名的更新在patchVNode方法里進(jìn)行,當(dāng)兩個(gè)節(jié)點(diǎn)的類型一樣,那么更新類名,替換的話就相當(dāng)于設(shè)置類名:

          // 更新節(jié)點(diǎn)類名const updateClass = (el, newVNode) => {    el.className = ''    if (newVNode.data && newVNode.data.class) {        let className = ''        Object.keys(newVNode.data.class).forEach((cla) => {            if (newVNode.data.class[cla]) {                className += cla + ' '            }        })        el.className = className    }}const patchVNode = (oldVNode, newVNode) => {    // ...    // 元素標(biāo)簽相同,進(jìn)行patch    if (oldVNode.tag === newVNode.tag) {        let el = newVNode.el = oldVNode.el        // 更新類名        updateClass(el, newVNode)        // ...    } else { // 不同使用newNode替換oldNode        let newEl = createEl(newVNode)        // 更新類名        updateClass(newEl, newVNode)        // ...    }}

          邏輯很簡單,直接把舊節(jié)點(diǎn)的類名替換成newVNode的類名。

          樣式

          樣式屬性使用data的style字段傳入:

          h('div',{    style: {        fontSize: '30px'    }}, '文本')

          更新的時(shí)機(jī)和類名的位置一致:

          // 更新節(jié)點(diǎn)樣式const updateStyle = (el, oldVNode, newVNode) => {  let oldStyle = oldVNode.data.style || {}  let newStyle = newVNode.data.style || {}  // 移除舊節(jié)點(diǎn)里存在新節(jié)點(diǎn)里不存在的樣式  Object.keys(oldStyle).forEach((item) => {    if (newStyle[item] === undefined || newStyle[item] === '') {      el.style[item] = ''    }  })  // 添加舊節(jié)點(diǎn)不存在的新樣式  Object.keys(newStyle).forEach((item) => {    if (oldStyle[item] !== newStyle[item]) {      el.style[item] = newStyle[item]    }  })}
          const patchVNode = (oldVNode, newVNode) => { // ... // 元素標(biāo)簽相同,進(jìn)行patch if (oldVNode.tag === newVNode.tag) { let el = newVNode.el = oldVNode.el // 更新樣式 updateStyle(el, oldVNode, newVNode) // ... } else { let newEl = createEl(newVNode) // 更新樣式 updateStyle(el, null, newVNode) // ... }}

          其他屬性

          其他屬性保存在data的attr字段上,更新方式及位置和樣式的完全一致:

          // 更新節(jié)點(diǎn)屬性const updateAttr = (el, oldVNode, newVNode) => {    let oldAttr = oldVNode && oldVNode.data.attr ? oldVNode.data.attr : {}    let newAttr = newVNode.data.attr || {}    // 移除舊節(jié)點(diǎn)里存在新節(jié)點(diǎn)里不存在的屬性    Object.keys(oldAttr).forEach((item) => {        if (newAttr[item] === undefined || newAttr[item] === '') {            el.removeAttribute(item)        }    })    // 添加舊節(jié)點(diǎn)不存在的新屬性    Object.keys(newAttr).forEach((item) => {        if (oldAttr[item] !== newAttr[item]) {            el.setAttribute(item, newAttr[item])        }    })}
          const patchVNode = (oldVNode, newVNode) => { // ... // 元素標(biāo)簽相同,進(jìn)行patch if (oldVNode.tag === newVNode.tag) { let el = newVNode.el = oldVNode.el // 更新屬性 updateAttr(el, oldVNode, newVNode) // ... } else { let newEl = createEl(newVNode) // 更新屬性 updateAttr(el, null, newVNode) // ... }}

          事件

          最后來看一下事件的更新,事件與其他屬性不同的是如果刪除一個(gè)節(jié)點(diǎn)的話需要把它的事件先全部解綁,否則可能會(huì)存在內(nèi)存泄漏的問題,那么就需要在各個(gè)移除節(jié)點(diǎn)的時(shí)機(jī)都先解綁事件:

          // 移除某個(gè)VNode對(duì)應(yīng)的dom的所有事件const removeEvent = (oldVNode) => {  if (oldVNode && oldVNode.data && oldVNode.data.event) {    Object.keys(oldVNode.data.event).forEach((item) => {      oldVNode.el.removeEventListener(item, oldVNode.data.event[item])    })  }}
          // 更新節(jié)點(diǎn)事件const updateEvent = (el, oldVNode, newVNode) => { let oldEvent = oldVNode && oldVNode.data.event ? oldVNode.data.event : {} let newEvent = newVNode.data.event || {} // 解綁不再需要的事件 Object.keys(oldEvent).forEach((item) => { if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) { el.removeEventListener(item, oldEvent[item]) } }) // 綁定舊節(jié)點(diǎn)不存在的新事件 Object.keys(newEvent).forEach((item) => { if (oldEvent[item] !== newEvent[item]) { el.addEventListener(item, newEvent[item]) } })}
          const patchVNode = (oldVNode, newVNode) => { // ... // 元素標(biāo)簽相同,進(jìn)行patch if (oldVNode.tag === newVNode.tag) { // 元素類型相同,那么舊元素肯定是進(jìn)行復(fù)用的 let el = newVNode.el = oldVNode.el // 更新事件 updateEvent(el, oldVNode, newVNode) // ... } else { let newEl = createEl(newVNode) // 移除舊節(jié)點(diǎn)的所有事件 removeEvent(oldNode) // 更新事件 updateEvent(newEl, null, newVNode) // ... }}// 其他還有幾處需要添加removeEvent(),有興趣請(qǐng)看源碼

          以上屬性的更新邏輯都比較粗糙,僅用于參考,可以參考snabbdom的源碼自行完善。

          總結(jié)

          以上代碼實(shí)現(xiàn)了一個(gè)簡單的虛擬DOM庫,詳細(xì)分解了patch過程和diff的過程,如果需要用在非瀏覽器平臺(tái)上,只要把DOM相關(guān)的操作抽象成接口,不同平臺(tái)上使用不同的接口即可,完整代碼在https://github.com/wanglin2/VNode-Demo

          感謝你的閱讀。


          學(xué)習(xí)更多技能

          請(qǐng)點(diǎn)擊下方公眾號(hào)

          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲61P | www.操B在线播放 | 成人理论片| 影音先锋AV啪啪资源 | 一区二区三区无码中文 |