50行代碼實現(xiàn)Virtual DOM

50行代碼實現(xiàn)Virtual DOM

在你創(chuàng)造出自己的Virtual DOM之前,你只需要知道兩件事情。你甚至不需要深入了解React的源代碼,或者其他Virtual DOM的實現(xiàn)。它們都太龐大和復雜了,但實際上Virtual DOM的部分只需要不超過50行的代碼!(當然,你千萬不要把它放在生產(chǎn)環(huán)境)

這里有2個概念:

  • Virtual DOM是真實DOM的映射。
  • 當我們在Virtual DOM樹改變一些東西的時候,我們得到了一個新的Virtual DOM樹,通過算法比較新樹和舊樹,找到不同的地方,然后只需要在真實的DOM上做出相應的改變。

僅此而已,讓我們來深入這兩個概念。

構(gòu)建我們的Virtual DOM樹

首先,我們要在內(nèi)存中存儲我們的DOM樹,我們能夠用純JS對象來表示它,假設我們有這樣的一個結(jié)構(gòu):

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

看起來非常簡單,那我們怎么用JS對象來構(gòu)造它呢?

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

這里有兩個點需要注意下:

  • 我們用JS對象表示DOM的元素:
{ type: '...', props: {...}, children: [...] }
  • 我們用JS字符串表示DOM的文本節(jié)點。

但是用這樣的方式寫一個更大的樹的結(jié)構(gòu)是非常復雜的,所以讓我們先寫一個幫助函數(shù),它能讓我們更容易的理解結(jié)構(gòu)。

function h(type, props, ...children) {
  return {
    type,
    props,
    children
  }
}

現(xiàn)在我們能這樣去寫我們的DOM樹:

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

這樣看起來清晰多了吧?但是我們還可以讓它變得更好,你應該聽過JSX,對吧?它是怎么工作的呢?

如果你看過Babel JSX的官方文檔,你就會知道,Babel會把下面的代碼:

<ul className="list">
  <li>item 1</li>
  <li>item 2</li>
</ul>

編譯成:

React.createElement('ul', { className: 'list' },
  React.createElement('li', {}, 'item 1'),
  React.createElement('li', {}, 'item 2'),
)

是不是看起來有點熟悉?如果我們能夠用我們的h(...)函數(shù)代替React.createElement(…),那么我們也能使用JSX語法。其實,我們只需要在源文件頭部加上這么一句注釋:

/** @jsx h */

它實際上是告訴Babel:'哥們, 幫我編譯JSX語法,用h(...)函數(shù)代替React.createElement(…),然后Babel就開始編譯。
因此,總結(jié)我之前說的,我們將用這樣的方式去寫我們的DOM樹:

/** @jsx h */
const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
)

Babel會幫我們編譯成這樣的代碼:

const a = h( 'ul',{ 'class': 'list' },
  h( 'li', null, 'item 1' ),
  h( 'li', null, 'item 2' )
);

h(...)執(zhí)行的之后,它將會返回純的JS對象,即我們的虛擬DOM。

運用Virtual DOM構(gòu)建真實的DOM

現(xiàn)在我們使用JS對象來表示DOM的結(jié)構(gòu),這非??幔俏覀冃枰盟鼊?chuàng)建一個真實的DOM。

首先,讓我們做一些假設并設置一些術語。

  • 我會用帶$的變量名來表示真實的DOM樹,?—?因此$parent將會是一個真實的DOM節(jié)點。
  • Virtual DOM在變量中使用node命名。
  • 就像在React中,你僅僅只有一個root節(jié)點,其他所有的節(jié)點都將會在它里面。

如上所述,讓我們來寫一個createElement(…)函數(shù)把Virtual DOM轉(zhuǎn)換成真實的DOM。

因為我們有兩種節(jié)點,text和element。因此我們的createElement函數(shù)需要處理這兩種情況。

讓我們想一下,其實子節(jié)點要么是一個element,要么是一個text節(jié)點,是text節(jié)點的話,我們直接渲染:

document.createTextNode(node)

是element節(jié)點的話 需要遞歸地把它的子節(jié)點也構(gòu)建起來:

const $el = document.createElement(node.type)
node
  .children
  .map(createElement)
  .forEach($el.appendChild.bind($el))

createElement代碼如下:

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node)
  }

  const $el = document.createElement(node.type)
  node
    .children
    .map(createElement)
    .forEach($el.appendChild.bind($el))

  return $el
}

現(xiàn)在的完整代碼如下:

<div id="root"></div>
/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children }
}

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

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
)

const $root = document.getElementById('root')
$root.appendChild(createElement(a))

WOW,是不是看起來很不錯,讓我們暫時先拋開props,我們稍后會談到它。

比較兩棵虛擬DOM樹的差異

現(xiàn)在我們已經(jīng)把virtual DOM轉(zhuǎn)換成一棵真實的DOM樹,是時候考慮下怎么比較兩棵虛擬DOM樹的差異了。最基本的,我們需要一個算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應的去改變真實的DOM。

怎么比較DOM樹呢?我們需要處理下面的情況:

  • 添加新節(jié)點,我們需要用appendChild方法添加節(jié)點
c1
  • 移除老節(jié)點,我們需要用removeChild方法移除老的節(jié)點
c2
  • 節(jié)點的替換,我們需要用replaceChild方法
c3
  • 節(jié)點相同,因此我們需要深度比較子節(jié)點
c4

讓我們開始寫updateElement方法,它需要傳遞3個參數(shù):$parent, newNodeoldNode。$parent是我們虛擬節(jié)點的真實的父級DOM元素?,F(xiàn)在我們來看看怎么處理上面描述的所有的情況。

添加新節(jié)點

非常直接,我甚至都不需要寫注釋。

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    )
  }
}

移除老節(jié)點

這里我們遇到一個問題?—?如果在新的Virtual DOM樹里面沒有某個節(jié)點,那我們應該在真實的DOM樹移除它。但我們應該怎么做呢?

如果我們已知父元素(通過參數(shù)傳遞),我們就能調(diào)用$parent.removeChild(…)方法把變化映射到真實的DOM上。但前提是我們得知道我們的節(jié)點在父元素上的索引,我們才能通過$parent.childNodes[index]得到該節(jié)點的引用。

OK,讓我們假設index將會通過參數(shù)傳遞(確實如此,稍后會看到),我們的代碼如下:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    )
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    )
  }
}

節(jié)點變化

首先我們需要寫一個函數(shù)比較舊樹和新樹的不同,告訴我們node真的改變了。我們需要考慮文本和元素這兩種情況:

function changed(node1, node2) {
  return (
    typeof node1 !== typeof node2 ||
    typeof node1 === 'string' && node1 !== node2 ||
    node1.type !== node2.type
  )
}

現(xiàn)在,當前的節(jié)點有了index屬性,我們能夠很簡單的用新節(jié)點替換它:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    )
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    )
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    )
  }
}

比較子節(jié)點

最后,我們應該遍歷每一個子節(jié)點然后比較它們。實際上是對每一個節(jié)點調(diào)用updateElement方法,同樣需要用到遞歸。

但是在寫代碼之前我們需要先考慮幾點:

  • 只有當節(jié)點是元素的時候,我們才需要比較子節(jié)點(文本節(jié)點沒有子元素)
  • 我們需要傳遞當前的節(jié)點的引用作為父節(jié)點
  • 我們應該一個一個的比較所有的子節(jié)點,即使它是undefined也沒有關系,我們的函數(shù)會處理它。
  • index?— 它只是子節(jié)點數(shù)組的索引。

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(createElement(newNode))
  } 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 newLength = newNode.children.length
    const oldLength = oldNode.children.length

    for (let i = 0; i < newLength || i < oldLength; i++) {
      updateElement(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      )
    }
  }
}

到此就基本完成了,當你點擊Reload按鈕的時候,你可以打開開發(fā)者工具觀察元素的變化。

你可以在這里找到所有的代碼,github

原文地址 How to write your own Virtual DOM

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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