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

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

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

這里有2個(gè)概念:

  • Virtual DOM是真實(shí)DOM的映射。
  • 當(dāng)我們?cè)赩irtual DOM樹改變一些東西的時(shí)候无虚,我們得到了一個(gè)新的Virtual DOM樹缔赠,通過(guò)算法比較新樹和舊樹,找到不同的地方友题,然后只需要在真實(shí)的DOM上做出相應(yīng)的改變嗤堰。

僅此而已,讓我們來(lái)深入這兩個(gè)概念。

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

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

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

看起來(lái)非常簡(jiǎn)單离唬,那我們?cè)趺从肑S對(duì)象來(lái)構(gòu)造它呢后专?

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

這里有兩個(gè)點(diǎn)需要注意下:

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

但是用這樣的方式寫一個(gè)更大的樹的結(jié)構(gòu)是非常復(fù)雜的输莺,所以讓我們先寫一個(gè)幫助函數(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'),
)

這樣看起來(lái)清晰多了吧嫂用?但是我們還可以讓它變得更好型凳,你應(yīng)該聽(tīng)過(guò)JSX,對(duì)吧嘱函?它是怎么工作的呢甘畅?

如果你看過(guò)Babel JSX的官方文檔,你就會(huì)知道往弓,Babel會(huì)把下面的代碼:

<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'),
)

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

/** @jsx h */

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

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

Babel會(huì)幫我們編譯成這樣的代碼:

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

當(dāng)h(...)執(zhí)行的之后,它將會(huì)返回純的JS對(duì)象祖灰,即我們的虛擬DOM钟沛。

運(yùn)用Virtual DOM構(gòu)建真實(shí)的DOM

現(xiàn)在我們使用JS對(duì)象來(lái)表示DOM的結(jié)構(gòu),這非尘址觯酷恨统,但是我們需要用它創(chuàng)建一個(gè)真實(shí)的DOM。

首先三妈,讓我們做一些假設(shè)并設(shè)置一些術(shù)語(yǔ)畜埋。

  • 我會(huì)用帶$的變量名來(lái)表示真實(shí)的DOM樹,?—?因此$parent將會(huì)是一個(gè)真實(shí)的DOM節(jié)點(diǎn)畴蒲。
  • Virtual DOM在變量中使用node命名悠鞍。
  • 就像在React中,你僅僅只有一個(gè)root節(jié)點(diǎn)模燥,其他所有的節(jié)點(diǎn)都將會(huì)在它里面咖祭。

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

因?yàn)槲覀冇袃煞N節(jié)點(diǎn)么翰,text和element牺汤。因此我們的createElement函數(shù)需要處理這兩種情況。

讓我們想一下浩嫌,其實(shí)子節(jié)點(diǎn)要么是一個(gè)element檐迟,要么是一個(gè)text節(jié)點(diǎn),是text節(jié)點(diǎn)的話固该,我們直接渲染:

document.createTextNode(node)

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

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锅减,是不是看起來(lái)很不錯(cuò),讓我們暫時(shí)先拋開(kāi)props伐坏,我們稍后會(huì)談到它怔匣。

比較兩棵虛擬DOM樹的差異

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

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

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

讓我們開(kāi)始寫updateElement方法贷掖,它需要傳遞3個(gè)參數(shù):$parent, newNodeoldNode$parent是我們虛擬節(jié)點(diǎn)的真實(shí)的父級(jí)DOM元素】视铮現(xiàn)在我們來(lái)看看怎么處理上面描述的所有的情況苹威。

添加新節(jié)點(diǎn)

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

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

移除老節(jié)點(diǎn)

這里我們遇到一個(gè)問(wèn)題?—?如果在新的Virtual DOM樹里面沒(méi)有某個(gè)節(jié)點(diǎn)牙甫,那我們應(yīng)該在真實(shí)的DOM樹移除它。但我們應(yīng)該怎么做呢调违?

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

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

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

節(jié)點(diǎn)變化

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

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

現(xiàn)在黄绩,當(dāng)前的節(jié)點(diǎn)有了index屬性羡洁,我們能夠很簡(jiǎn)單的用新節(jié)點(diǎn)替換它:

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é)點(diǎn)

最后,我們應(yīng)該遍歷每一個(gè)子節(jié)點(diǎn)然后比較它們爽丹。實(shí)際上是對(duì)每一個(gè)節(jié)點(diǎn)調(diào)用updateElement方法筑煮,同樣需要用到遞歸。

但是在寫代碼之前我們需要先考慮幾點(diǎn):

  • 只有當(dāng)節(jié)點(diǎn)是元素的時(shí)候粤蝎,我們才需要比較子節(jié)點(diǎn)(文本節(jié)點(diǎn)沒(méi)有子元素)
  • 我們需要傳遞當(dāng)前的節(jié)點(diǎn)的引用作為父節(jié)點(diǎn)
  • 我們應(yīng)該一個(gè)一個(gè)的比較所有的子節(jié)點(diǎn)真仲,即使它是undefined也沒(méi)有關(guān)系,我們的函數(shù)會(huì)處理它初澎。
  • index?— 它只是子節(jié)點(diǎn)數(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
      )
    }
  }
}

到此就基本完成了,當(dāng)你點(diǎn)擊Reload按鈕的時(shí)候碑宴,你可以打開(kāi)開(kāi)發(fā)者工具觀察元素的變化软啼。

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

原文地址 How to write your own Virtual DOM

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祸挪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贞间,更是在濱河造成了極大的恐慌贿条,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件增热,死亡現(xiàn)場(chǎng)離奇詭異闪唆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)钓葫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)票顾,“玉大人础浮,你說(shuō)我怎么就攤上這事〉旖荆” “怎么了豆同?”我有些...
    開(kāi)封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)含鳞。 經(jīng)常有香客問(wèn)我影锈,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任鸭廷,我火速辦了婚禮枣抱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辆床。我一直安慰自己佳晶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布讼载。 她就那樣靜靜地躺著轿秧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咨堤。 梳的紋絲不亂的頭發(fā)上菇篡,一...
    開(kāi)封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音一喘,去河邊找鬼驱还。 笑死,一個(gè)胖子當(dāng)著我的面吹牛津滞,可吹牛的內(nèi)容都是我干的铝侵。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼触徐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼咪鲜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起撞鹉,我...
    開(kāi)封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤疟丙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鸟雏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體享郊,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年孝鹊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炊琉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡又活,死狀恐怖苔咪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柳骄,我是刑警寧澤团赏,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站耐薯,受9級(jí)特大地震影響舔清,放射性物質(zhì)發(fā)生泄漏丝里。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一体谒、第九天 我趴在偏房一處隱蔽的房頂上張望杯聚。 院中可真熱鬧,春花似錦营密、人聲如沸械媒。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)纷捞。三九已至,卻和暖如春被去,著一層夾襖步出監(jiān)牢的瞬間主儡,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工惨缆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糜值,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓坯墨,卻偏偏與公主長(zhǎng)得像寂汇,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捣染,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容