如何自己寫(xiě)簡(jiǎn)單的virtual dom(讀博客筆記)

原文

正常的dom

<ul class=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

用js的object來(lái)代表dom

{
    type: 'ul', props: {'class': 'list'}, children: [
        {type: 'li', props: {}, children: ['item 1']},
        {type: 'li', props: {}, children: ['item 2']}
    ]
}

寫(xiě)個(gè)幫助方法創(chuàng)建js的dom

function helper(type, props, ...children) {
    return {type, props, children};
}

現(xiàn)在就可以這樣寫(xiě):

helper('ul', {'class': 'list'}, 
    helper('li', {}, 'item 1'),
    helper('li', {}, 'item 2')
)

可以通過(guò)babel 來(lái)轉(zhuǎn)換jsx

實(shí)現(xiàn)從我們的js的object到真實(shí)dom

function createElement(node) {
    if (typeof node == 'string') {
        return document.createTextNode(node);
    }
    $el = document.createElement(node.type);
    node.children
        .map(createElement)
        .forEach($el.appendChild.bind($el));
    return $el; 
}

接下來(lái)處理diff

有四種情況

  • 新增
// old
<ul>
    <li>item 1</li>
</ul>
// new 
<ul>
    <li>item 1</li>
    <li>item 2</li>
</ul>
  • 刪除
// old
<ul>
    <li>item 1</li>
    <li>item 2</li>
</ul>
// new 
<ul>
    <li>item 1</li>
</ul>
  • 替換
// old
<div>
    <p>item 1</p>
    <button>cpck it</button>
</div>
// new 
<div>
    <p>item 1</p>
    <p>hello</p>
</div>
  • 節(jié)點(diǎn)一致,子節(jié)點(diǎn)不一致
// old
<ul>
    <li>item 1</li>
    <li>
        <span>hello</span>
        <div>hi!</div>
    </li>
</ul>
// new 
<ul>
    <li>item 1</li>
    <li>
        <span>hello</span>
        <span>hi!</span>
    </li>
</ul>

所以我們可以寫(xiě)一個(gè)更新函數(shù),接收三個(gè)參數(shù),$parent、newNode、oldNode, 其中$parent是真實(shí)dom元素,并且是虛擬節(jié)點(diǎn)的父節(jié)點(diǎn)。(暫時(shí)不考慮props)

當(dāng)無(wú)新節(jié)點(diǎn)或者舊節(jié)點(diǎn)時(shí)

function updateElement($parent, newNode, oldNode,  index = 0) {
    // 無(wú)舊節(jié)點(diǎn)
    if (!oldNode) {
        $parent.appendChild(newNode);
    // 無(wú)新節(jié)點(diǎn)
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index];
        );
    }
}

有新節(jié)點(diǎn)和舊節(jié)點(diǎn)時(shí),需要判斷節(jié)點(diǎn)是否改變,所以我們可以先寫(xiě)一個(gè)判斷節(jié)點(diǎn)是否改變的函數(shù)。

function changed(node1, node2) {
            // 基礎(chǔ)數(shù)據(jù)類(lèi)型判斷
    return typeof node1 !== typeof node2 ||
            // 文本節(jié)點(diǎn)時(shí)是否一致
           typeof node1 == 'string' && node1 !== node2 ||
           // 元素節(jié)點(diǎn)時(shí)類(lèi)型是否一致
           node1.type !== node2.type;
}

那么現(xiàn)在我們就可以完善一下 updateElement 函數(shù):

function updateElement($parent, newNode, oldNode,  index = 0) {
    // 無(wú)舊節(jié)點(diǎn)
    if (!oldNode) {
        $parent.appendChild(newNode);
    // 無(wú)新節(jié)點(diǎn)
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index];
        );
        // 新舊節(jié)點(diǎn)發(fā)生變化時(shí)
    } else if (changed(newNode, oldNode)) {
        $parent.replaceChild(
            createElement(newNode), 
            $parent.childNodes[index];
        )
    }
}

最后不過(guò)也非常重要的事情

我們?cè)趯?duì)比節(jié)點(diǎn)時(shí),需要保證它們的子節(jié)點(diǎn)也需要對(duì)比,才能判斷他們的差異。在寫(xiě)代碼之前我們需要考慮以下幾個(gè)問(wèn)題:

  1. 我們只需要對(duì)比元素節(jié)點(diǎn)而不用對(duì)比文本節(jié)點(diǎn)(文本節(jié)點(diǎn)無(wú)子節(jié)點(diǎn));
  2. 我們把現(xiàn)在這個(gè)節(jié)點(diǎn)當(dāng)做父節(jié)點(diǎn);
  3. 我們需要一個(gè)一個(gè)節(jié)點(diǎn)對(duì)比,甚至是undefined,我們函數(shù)中需要有能應(yīng)對(duì)undefined這種情況的能力;
  4. index只是子節(jié)點(diǎn)的索引。

考慮到以上,我們可以繼續(xù)完善 updateElement 函數(shù):

function updateElement($parent, newNode, oldNode,  index = 0) {
    // 無(wú)舊節(jié)點(diǎn)
    if (!oldNode) {
        $parent.appendChild(newNode);
    // 無(wú)新節(jié)點(diǎn)
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index];
        );
    } else if (changed(newNode, oldNode)) {
        $parent.replaceChild(createElement(newNode), 
            $parent.childNodes[index];
        )
    } else if (newNode.type) {
        const len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
        for (var i = 0; i<len; i++) {
            updateElement(
                $parent.childNodes[index],
                newNode.childNodes[i],
                oldNode.childNodes[i],
                i
            );
        }
    }
}

現(xiàn)在我們從整體來(lái)看

// index.html
<button id="reload">RELOAD</button>
<div id="root"></div>

js(babel+jsx)

function createElement(node) {
    if (typeof node == 'string') {
        return document.createTextNode(node);
    }
    $el = document.createElement(node.type);
    node.children
        .map(createElement)
        .forEach($el.appendChild.bind($el));
    return $el; 
}

function changed(node1, node2) {
            // 基礎(chǔ)數(shù)據(jù)類(lèi)型判斷
    return typeof node1 !== typeof node2 ||
            // 文本節(jié)點(diǎn)時(shí)是否一致
           typeof node1 == 'string' && node1 !== node2 ||
           // 元素節(jié)點(diǎn)時(shí)類(lèi)型是否一致
           node1.type !== node2.type;
}

function updateElement($parent, newNode, oldNode,  index = 0) {
    // 無(wú)舊節(jié)點(diǎn)
    if (!oldNode) {
        $parent.appendChild(newNode);
    // 無(wú)新節(jié)點(diǎn)
    } else if (!newNode) {
        $parent.removeChild(
            $parent.childNodes[index];
        );
    } else if (changed(newNode, oldNode)) {
        $parent.replaceChild(createElement(newNode), 
            $parent.childNodes[index];
        )
    } else if (newNode.type) {
        const len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
        for (var i = 0; i<len; i++) {
            updateElement(
                $parent.childNodes[index],
                newNode.childNodes[i],
                oldNode.childNodes[i],
                i
            );
        }
    }
}

const a = (
  <ul>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const b = (
  <ul>
    <li>item 1</li>
    <li>hello!</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);

$reload.addEventListener('click', () => {
    updateElement($root, a, b);
})

總結(jié)

我們到現(xiàn)在已經(jīng)基本完成了 Virtual Dom 的簡(jiǎn)單實(shí)現(xiàn),通過(guò)這我們應(yīng)該能夠了解 Virtual Dom 的基本原理,和了解 React 內(nèi)部基本原理。

在這篇文章中我們還有一些我們沒(méi)完成的東西,如下:

  • 設(shè)置節(jié)點(diǎn)的屬性,并且計(jì)算差別和更新它們;
  • 節(jié)點(diǎn)的事件監(jiān)聽(tīng);
  • 讓我們的 Virtual Dom 和組件工作,比如 React;
  • 拿到真實(shí)的Dom的引用;
  • 支持其它庫(kù)直接操作真實(shí)DOM;
  • 其它...
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容