日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

虚拟dom_从0到1实现一个虚拟DOM

發布時間:2024/7/23 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚拟dom_从0到1实现一个虚拟DOM 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
來源 |?https://segmentfault.com/a/1190000021331850要構建自己的虛擬 DOM,需要知道兩件事。你甚至不需要深入 React 的源代碼或者深入任何其他虛擬 DOM 實現的源代碼,因為它們是如此龐大和復雜——但實際上,虛擬 DOM 的主要部分只需不到 50 行代碼。有兩個概念:
  • Virtual DOM 是真實 DOM 的映射

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

用 JS 對象模擬 DOM 樹

首先,我們需要以某種方式將 DOM 樹存儲在內存中??梢允褂闷胀ǖ?JS 對象來做。假設我們有這樣一棵樹:<ul class="”list”"> <li>item 1li> <li>item 2li>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 文本節點

但是用這種方式表示內容很多的 Dom 樹是相當困難的。這里來寫一個輔助函數,這樣更容易理解:function h(type, props, …children) { return { type, props, children };}用這個方法重新整理一開始代碼:h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’),);這樣看起來簡潔多了,還可以更進一步。這里使用 JSX,如下:<ul className="”list”"> <li>item 1li> <li>item 2li>ul>編譯成:React.createElement(‘ul’, { className: ‘list’ }, React.createElement(‘li’, {}, ‘item 1’), React.createElement(‘li’, {}, ‘item 2’),);是不是看起來有點熟悉?如果能夠用我們剛定義的?h(...)函數代替?React.createElement(…),那么我們也能使用 JSX 語法。其實,只需要在源文件頭部加上這么一句注釋:/** @jsx h */<ul className="”list”"> <li>item 1li> <li>item 2li>ul>它實際上告訴 Babel ‘ 嘿,小老弟幫我編譯?JSX?語法,用?h(...)函數代替?React.createElement(…),然后?Babel?就開始編譯。’綜上所述,我們將 DOM 寫成這樣:/** @jsx h */ const a = (<ul className="”list”"> <li>item 1li> <li>item 2li>ul>);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 節點。這里先不考慮?props和?children屬性:function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type);}上述方法我也可以創建有兩種節點分別是文本節點和 Dom 元素節點,它們是類型為的 JS 對象:{ type: ‘…’, props: { … }, children: [ … ] }因此,可以在函數?createElement傳入虛擬文本節點和虛擬元素節點——這是可行的?,F在讓我們考慮子節點——它們中的每一個都是文本節點或元素。所以它們也可以用?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 = ( <ul class="list"> <li>item 1li> <li>item 2li> ul>);const $root = document.getElementById("root");$root.appendChild(createElement(a));

比較兩棵虛擬 DOM 樹的差異

現在我們可以將虛擬 DOM 轉換為真實的 DOM,這就需要考慮比較兩棵 DOM 樹的差異。基本的,我們需要一個算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應的去改變真實的 DOM。怎么比較 DOM 樹?需要處理下面的情況:
  • 添加新節點,使用?appendChild(…)?方法添加節點

  • 移除老節點,使用?removeChild(…)?方法移除老的節點

  • 節點的替換,使用?replaceChild(…)?方法

如果節點相同的——就需要需要深度比較子節點

編寫一個名為?updateElement(…)?的函數,它接受三個參數——?$parent、newNode?和?oldNode,其中?\$parent?是虛擬節點的一個實際 DOM 元素的父元素?,F在來看看如何處理上面描述的所有情況。

添加新節點

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 < 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 1li> <li>item 2li> ul>);const b = ( <ul> <li>item 1li> <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">RELOADbutton><div id="root">div>CSS#root { border: 1px solid black; padding: 10px; margin: 30px 0 0 0;}打開開發者工具,并觀察當按下“Reload”按鈕時應用的更改。

總結

現在我們已經編寫了虛擬 DOM 實現及了解它的工作原理。作者希望,在閱讀了本文之后,對理解虛擬 DOM 如何工作的基本概念以及在幕后如何進行響應有一定的了解。然而,這里有一些東西沒有突出顯示(將在以后的文章中介紹它們):
  • 設置元素屬性(props)并進行 diffing/updating

  • 處理事件——向元素中添加事件監聽

  • 讓虛擬 DOM 與組件一起工作,比如 React

  • 獲取對實際 DOM 節點的引用

  • 使用帶有庫的虛擬 DOM,這些庫可以直接改變真實的 DOM,比如 jQuery 及其插件

總結

以上是生活随笔為你收集整理的虚拟dom_从0到1实现一个虚拟DOM的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。