目錄
用JS物件模擬DOM樹
添加新节点
移除老节点
节点的替换
比较子节点
完整的代码
总结
首頁 web前端 js教程 如何寫出自己的虛擬DOM?方法介紹

如何寫出自己的虛擬DOM?方法介紹

Oct 29, 2020 pm 05:30 PM
javascript node.js 前端

如何寫出自己的虛擬DOM?方法介紹

要建立自己的虛擬DOM,需要知道兩件事。你甚至不需要深入 React 的源代碼或深入任何其他虛擬DOM實現的源代碼,因為它們是如此龐大和複雜——但實際上,虛擬DOM的主要部分只需不到50行代碼。

有兩個概念:

  • Virtual DOM 是真實DOM的映射
  • 當虛擬DOM 樹中的某些節點改變時,會得到一個新的虛擬樹。演算法將這兩棵樹(新樹和舊樹)進行比較,找出差異,然後只需要在真實的 DOM 上做出相應的改變。

用JS物件模擬DOM樹

首先,我們需要以某種方式將 DOM 樹儲存在記憶體中。可以使用普通的 JS 物件來做。假設我們有這樣一棵樹:

登入後複製
登入後複製
      
  • item 1
  •   
  • item 2

看起來很簡單,對吧? 如何用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 文字節點
##但用這種方式表示內容很多的Dom 樹是相當困難的。這裡來寫一個輔助函數,這樣比較容易理解:

function h(type, props, …children) {
  return { type, props, children };
}
登入後複製
用這個方法重新整理一開始程式碼:

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);
登入後複製
這樣看起來簡潔多了,還可以更進一步。這裡使用  JSX,如下:

登入後複製
登入後複製
      
  • item 1
  •   
  • item 2
編譯成:

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);
登入後複製
是不是看起來有點熟悉?如果能夠用我們剛定義的

h(...) 函數來取代 React.createElement(…),那麼我們也能使用JSX 語法。其實,只需要在原始檔頭加上這麼一句註解:

/** @jsx h */
登入後複製
      
  • item 1
  •   
  • item 2
它實際上告訴Babel ' 嘿,小老弟幫我編譯

JSX 語法,用h( ...) 函數取代React.createElement(…),然後Babel 就開始編譯。 '

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

/** @jsx h */
const a = (
  
登入後複製
        
  • item 1
  •     
  • item 2
  •   
);Babel 會幫我們編譯成這樣的程式碼:

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);
登入後複製
當函數

“h” 執行時,它將返回普通JS物件-即我們的虛擬DOM:

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);
登入後複製
從Virtual DOM 映射到真實DOM

#好了,現在我們有了DOM 樹,用普通的JS 物件表示,還有我們自己的結構。這很酷,但我們需要從它創建一個真正的DOM。

首先讓我們做一些假設並聲明一些術語:

    使用以'
  • $ '開頭的變數表示真正的DOM節點(元素,文字節點),因此$parent 將會是一個真實的DOM元素
  • 虛擬DOM 使用名為
  • node 的變數表示
* 就像在React 中一樣,只能有一個根節點-所有其他節點都在其中

那麼,來寫一個函數

createElement(…),它將取得一個虛擬DOM 節點並傳回一個真實的DOM 節點。這裡先不考慮propschildren 屬性:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}
登入後複製
上述方法我也可以建立有兩種節點分別是文字節點和Dom 元素節點,它們是類型為的JS 物件:

{ type: ‘…’, props: { … }, children: [ … ] }
登入後複製
登入後複製
因此,可以在函數

createElement 傳入虛擬文字節點和虛擬元素節點-這是可行的。

現在讓我們考慮子節點——它們中的每一個都是文字節點或元素。所以它們也可以用

createElement(…) 函數來建立。是的,這就像遞歸一樣,所以我們可以為每個元素的子元素呼叫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;
}
登入後複製
哇,看起來不錯。先把節點

props 屬性放到一邊。待會再談。我們不需要它們來理解虛擬DOM的基本概念,因為它們會增加複雜性。

完整程式碼如下:

/** @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 = (
  
登入後複製
        
  • item 1
  •     
  • item 2
  •   
); const $root = document.getElementById('root'); $root.appendChild(createElement(a));比較兩棵虛擬DOM樹的差異

現在我們可以將虛擬DOM 轉換為真實的DOM,這就需要考慮比較兩棵DOM 樹的差異。基本的,我們需要一個演算法來比較新的樹和舊的樹,它能夠讓我們知道什麼地方改變了,然後相應的去改變真實的 DOM。

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

    新增節點,使用
  • appendChild(…) 方法新增節點

如何寫出自己的虛擬DOM?方法介紹

##移除舊節點,使用
    removeChild(…)
  • 方法移除舊的節點

如何寫出自己的虛擬DOM?方法介紹##節點的替換,使用

replaceChild(…)
    方法

#如果節點相同的-就需要有深度比較子節點如何寫出自己的虛擬DOM?方法介紹

编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parentnewNodeoldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。

添加新节点

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}
登入後複製

移除老节点

这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢?

如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…) 方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。

好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:

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

节点的替换

首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}
登入後複製

现在,当前的节点有了 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]
    );
  }
}
登入後複製

比较子节点

最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。

  • 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )
  • 我们需要传递当前的节点的引用作为父节点
  • 我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。
  • 最后是 index,它是子数组中子节点的 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 <h2 id="完整的代码">完整的代码</h2><p><strong>Babel+JSX</strong><br>/<em>* @jsx h </em>/</p><pre class="brush:php;toolbar:false">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 
    
登入後複製
  • item 1
  •     
  • item 2
  •    ); const b = (   
          
    • item 1
    •     
    • hello!
    •   
    ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => {   updateElement($root, b, a); });

    HTML

    <button>RELOAD</button>
    <p></p>
    登入後複製

    CSS

    #root {
      border: 1px solid black;
      padding: 10px;
      margin: 30px 0 0 0;
    }
    登入後複製

    打开开发者工具,并观察当按下“Reload”按钮时应用的更改。

    如何寫出自己的虛擬DOM?方法介紹

    总结

    现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。

    然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):

    • 设置元素属性(props)并进行 diffing/updating
    • 处理事件——向元素中添加事件监听
    • 让虚拟 DOM 与组件一起工作,比如React
    • 获取对实际DOM节点的引用
    • 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件

    原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

    作者:deathmood

    为了保证的可读性,本文采用意译而非直译。

    更多编程相关知识,请访问:编程入门!!

    以上是如何寫出自己的虛擬DOM?方法介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    Video Face Swap

    Video Face Swap

    使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    熱門話題

    Java教學
    1664
    14
    CakePHP 教程
    1423
    52
    Laravel 教程
    1317
    25
    PHP教程
    1268
    29
    C# 教程
    1243
    24
    PHP與Vue:完美搭檔的前端開發利器 PHP與Vue:完美搭檔的前端開發利器 Mar 16, 2024 pm 12:09 PM

    PHP與Vue:完美搭檔的前端開發利器在當今網路快速發展的時代,前端開發變得愈發重要。隨著使用者對網站和應用的體驗要求越來越高,前端開發人員需要使用更有效率和靈活的工具來創建響應式和互動式的介面。 PHP和Vue.js作為前端開發領域的兩個重要技術,搭配起來可以稱得上是完美的利器。本文將探討PHP和Vue的結合,以及詳細的程式碼範例,幫助讀者更好地理解和應用這兩

    Django是前端還是後端?一探究竟! Django是前端還是後端?一探究竟! Jan 19, 2024 am 08:37 AM

    Django是一個由Python編寫的web應用框架,它強調快速開發和乾淨方法。儘管Django是web框架,但要回答Django是前端還是後端這個問題,需要深入理解前後端的概念。前端是指使用者直接和互動的介面,後端是指伺服器端的程序,他們透過HTTP協定進行資料的互動。在前端和後端分離的情況下,前後端程式可以獨立開發,分別實現業務邏輯和互動效果,資料的交

    簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

    JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

    Go語言前端技術探秘:前端開發新視野 Go語言前端技術探秘:前端開發新視野 Mar 28, 2024 pm 01:06 PM

    Go語言作為一種快速、高效的程式語言,在後端開發領域廣受歡迎。然而,很少有人將Go語言與前端開發聯繫起來。事實上,使用Go語言進行前端開發不僅可以提高效率,還能為開發者帶來全新的視野。本文將探討使用Go語言進行前端開發的可能性,並提供具體的程式碼範例,幫助讀者更了解這一領域。在傳統的前端開發中,通常會使用JavaScript、HTML和CSS來建立使用者介面

    Django:前端和後端開發都能搞定的神奇框架! Django:前端和後端開發都能搞定的神奇框架! Jan 19, 2024 am 08:52 AM

    Django:前端和後端開發都能搞定的神奇框架! Django是一個高效、可擴展的網路應用程式框架。它能夠支援多種Web開發模式,包括MVC和MTV,可以輕鬆地開發出高品質的Web應用程式。 Django不僅支援後端開發,還能夠快速建構出前端的介面,透過模板語言,實現靈活的視圖展示。 Django把前端開發和後端開發融合成了一種無縫的整合,讓開發人員不必專門學習

    前端面試官常問的問題 前端面試官常問的問題 Mar 19, 2024 pm 02:24 PM

    在前端開發面試中,常見問題涵蓋廣泛,包括HTML/CSS基礎、JavaScript基礎、框架和函式庫、專案經驗、演算法和資料結構、效能最佳化、跨域請求、前端工程化、設計模式以及新技術和趨勢。面試官的問題旨在評估候選人的技術技能、專案經驗以及對行業趨勢的理解。因此,應試者應充分準備這些方面,以展現自己的能力和專業知識。

    如何在JavaScript中取得HTTP狀態碼的簡單方法 如何在JavaScript中取得HTTP狀態碼的簡單方法 Jan 05, 2024 pm 01:37 PM

    JavaScript中的HTTP狀態碼取得方法簡介:在進行前端開發中,我們常常需要處理與後端介面的交互,而HTTP狀態碼就是其中非常重要的一部分。了解並取得HTTP狀態碼有助於我們更好地處理介面傳回的資料。本文將介紹使用JavaScript取得HTTP狀態碼的方法,並提供具體程式碼範例。一、什麼是HTTP狀態碼HTTP狀態碼是指當瀏覽器向伺服器發起請求時,服務

    Golang與前端技術結合:探討Golang如何在前端領域發揮作用 Golang與前端技術結合:探討Golang如何在前端領域發揮作用 Mar 19, 2024 pm 06:15 PM

    Golang與前端技術結合:探討Golang如何在前端領域發揮作用,需要具體程式碼範例隨著互聯網和行動應用的快速發展,前端技術也愈發重要。而在這個領域中,Golang作為一門強大的後端程式語言,也可以發揮重要作用。本文將探討Golang如何與前端技術結合,以及透過具體的程式碼範例來展示其在前端領域的潛力。 Golang在前端領域的角色作為一門高效、簡潔且易於學習的

    See all articles