久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

如何編寫自己的虛擬DOM

 侯培彬 2019-01-10

圖片描述

要構(gòu)建自己的虛擬DOM,,需要知道兩件事,。你甚至不需要深入 React 的源代碼或者深入任何其他虛擬DOM實(shí)現(xiàn)的源代碼,,因?yàn)樗鼈兪侨绱她嫶蠛蛷?fù)雜——但實(shí)際上,,虛擬DOM的主要部分只需不到50行代碼,。

有兩個(gè)概念:

  • Virtual DOM 是真實(shí)DOM的映射
  • 當(dāng)虛擬 DOM 樹中的某些節(jié)點(diǎn)改變時(shí),,會(huì)得到一個(gè)新的虛擬樹。算法對這兩棵樹(新樹和舊樹)進(jìn)行比較,,找出差異,,然后只需要在真實(shí)的 DOM 上做出相應(yīng)的改變。

用JS對象模擬DOM樹

首先,,我們需要以某種方式將 DOM 樹存儲(chǔ)在內(nèi)存中,。可以使用普通的 JS 對象來做,。假設(shè)我們有這樣一棵樹:

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

看起來很簡單,,對吧? 如何用JS對象來表示呢?

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

這里有兩件事需要注意:

  • 用如下對象表示DOM元素
{ type: ‘…’, props: { … }, children: [ … ] }

  • 用普通 JS 字符串表示 DOM 文本節(jié)點(diǎn)

但是用這種方式表示內(nèi)容很多的 Dom 樹是相當(dāng)困難的。這里來寫一個(gè)輔助函數(shù),,這樣更容易理解:

function h(type, props, …children) {
  return { type, props, children };
}

用這個(gè)方法重新整理一開始代碼:

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

這樣看起來簡潔多了,,還可以更進(jìn)一步。這里使用 JSX,如下:

<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’),
);

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

/** @jsx h */
<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

它實(shí)際上告訴 Babel ' 嘿,,小老弟幫我編譯 JSX 語法,,用 h(...) 函數(shù)代替 React.createElement(…),然后 Babel 就開始編譯,。'

綜上所述,,我們將DOM寫成這樣:

/** @jsx h */
const a = (
  <ul className=”list”>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

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

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

當(dāng)函數(shù) “h” 執(zhí)行時(shí),它將返回普通JS對象-即我們的虛擬DOM:

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

從Virtual DOM 映射到真實(shí) DOM

好了,,現(xiàn)在我們有了 DOM 樹,,用普通的 JS 對象表示,還有我們自己的結(jié)構(gòu),。這很酷,,但我們需要從它創(chuàng)建一個(gè)真正的DOM。

首先讓我們做一些假設(shè)并聲明一些術(shù)語:

  • 使用以' $ '開頭的變量表示真正的DOM節(jié)點(diǎn)(元素,,文本節(jié)點(diǎn)),,因此 $parent 將會(huì)是一個(gè)真實(shí)的DOM元素
  • 虛擬 DOM 使用名為 node 的變量表示

* 就像在 React 中一樣,只能有一個(gè)根節(jié)點(diǎn)——所有其他節(jié)點(diǎn)都在其中

那么,,來編寫一個(gè)函數(shù) createElement(…),,它將獲取一個(gè)虛擬 DOM 節(jié)點(diǎn)并返回一個(gè)真實(shí)的 DOM 節(jié)點(diǎn)。這里先不考慮 propschildren 屬性:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

上述方法我也可以創(chuàng)建有兩種節(jié)點(diǎn)分別是文本節(jié)點(diǎn)和 Dom 元素節(jié)點(diǎn),,它們是類型為的 JS 對象:

{ type: ‘…’, props: { … }, children: [ … ] }

因此,,可以在函數(shù) createElement 傳入虛擬文本節(jié)點(diǎn)和虛擬元素節(jié)點(diǎn)——這是可行的。

現(xiàn)在讓我們考慮子節(jié)點(diǎn)——它們中的每一個(gè)都是文本節(jié)點(diǎn)或元素,。所以它們也可以用 createElement(…) 函數(shù)創(chuàng)建,。是的,這就像遞歸一樣,,所以我們可以為每個(gè)元素的子元素調(diào)用 createElement(…),,然后使用 appendChild() 添加到我們的元素中:

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;
}

哇,看起來不錯(cuò),。先把節(jié)點(diǎn) props 屬性放到一邊,。待會(huì)再談。我們不需要它們來理解虛擬DOM的基本概念,,因?yàn)樗鼈儠?huì)增加復(fù)雜性,。

完整代碼如下:

/** @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));

比較兩棵虛擬DOM樹的差異

現(xiàn)在我們可以將虛擬 DOM 轉(zhuǎn)換為真實(shí)的 DOM,這就需要考慮比較兩棵 DOM 樹的差異,?;镜?,我們需要一個(gè)算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,,然后相應(yīng)的去改變真實(shí)的 DOM,。

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

  • 添加新節(jié)點(diǎn),,使用 appendChild(…) 方法添加節(jié)點(diǎn)

圖片描述

  • 移除老節(jié)點(diǎn),,使用 removeChild(…) 方法移除老的節(jié)點(diǎn)

圖片描述

  • 節(jié)點(diǎn)的替換,使用 replaceChild(…) 方法

圖片描述

如果節(jié)點(diǎn)相同的——就需要需要深度比較子節(jié)點(diǎn)

圖片描述

編寫一個(gè)名為 updateElement(…) 的函數(shù),,它接受三個(gè)參數(shù)—— $parent,、newNodeoldNode,其中 $parent 是虛擬節(jié)點(diǎn)的一個(gè)實(shí)際 DOM 元素的父元素?,F(xiàn)在來看看如何處理上面描述的所有情況,。

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

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

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

這里遇到了一個(gè)問題——如果在新虛擬樹的當(dāng)前位置沒有節(jié)點(diǎn)——我們應(yīng)該從實(shí)際的 DOM 中刪除它—— 這要如何做呢?

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

好的,,讓我們假設(shè)這個(gè)索引將被傳遞給 updateElement 函數(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ù)來比較兩個(gè)節(jié)點(diǎn)(舊節(jié)點(diǎn)和新節(jié)點(diǎn)),,并告訴節(jié)點(diǎn)是否真的發(fā)生了變化,。還有需要考慮這個(gè)節(jié)點(diǎn)可以是元素或是文本節(jié)點(diǎn):

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é)點(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)的每一個(gè)子節(jié)點(diǎn)并比較它們——實(shí)際上為每個(gè)節(jié)點(diǎn)調(diào)用updateElement(…)方法,同樣需要用到遞歸,。

  • 當(dāng)節(jié)點(diǎn)是 DOM 元素時(shí)我們才需要比較( 文本節(jié)點(diǎn)沒有子節(jié)點(diǎn) )
  • 我們需要傳遞當(dāng)前的節(jié)點(diǎn)的引用作為父節(jié)點(diǎn)
  • 我們應(yīng)該一個(gè)一個(gè)的比較所有的子節(jié)點(diǎn),,即使它是 undefined 也沒有關(guān)系,我們的函數(shù)也會(huì)正確處理它,。
  • 最后是 index,它是子數(shù)組中子節(jié)點(diǎn)的 index
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
      );
    }
  }
}

完整的代碼

Babel+JSX
/* @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;
}

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

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
      );
    }
  }
}

// ---------------------------------------------------------------------

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, b, a);
});

HTML

<button id="reload">RELOAD</button>
<div id="root"></div>

CSS

#root {
  border: 1px solid black;
  padding: 10px;
  margin: 30px 0 0 0;
}

打開開發(fā)者工具,,并觀察當(dāng)按下“Reload”按鈕時(shí)應(yīng)用的更改,。

圖片描述

總結(jié)

現(xiàn)在我們已經(jīng)編寫了虛擬 DOM 實(shí)現(xiàn)及了解它的工作原理。作者希望,,在閱讀了本文之后,,對理解虛擬 DOM 如何工作的基本概念以及在幕后如何進(jìn)行響應(yīng)有一定的了解。

然而,,這里有一些東西沒有突出顯示(將在以后的文章中介紹它們):

  • 設(shè)置元素屬性(props)并進(jìn)行 diffing/updating
  • 處理事件——向元素中添加事件監(jiān)聽
  • 讓虛擬 DOM 與組件一起工作,,比如React
  • 獲取對實(shí)際DOM節(jié)點(diǎn)的引用
  • 使用帶有庫的虛擬 DOM,,這些庫可以直接改變真實(shí)的 DOM,比如 jQuery 及其插件

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多