摘要:要構建自己的虛擬,需要知道兩件事。現在來看看如何處理上面描述的所有情況。代碼如下節點的替換首先,需要編寫一個函數來比較兩個節點舊節點和新節點,并告訴節點是否真的發生了變化。總結現在我們已經編寫了虛擬實現及了解它的工作原理。
要構建自己的虛擬DOM,需要知道兩件事。你甚至不需要深入 React 的源代碼或者深入任何其他虛擬DOM實現的源代碼,因為它們是如此龐大和復雜——但實際上,虛擬DOM的主要部分只需不到50行代碼。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
有兩個概念:
Virtual DOM 是真實DOM的映射
當虛擬 DOM 樹中的某些節點改變時,會得到一個新的虛擬樹。算法對這兩棵樹(新樹和舊樹)進行比較,找出差異,然后只需要在真實的 DOM 上做出相應的改變。
用JS對象模擬DOM樹首先,我們需要以某種方式將 DOM 樹存儲在內存中。可以使用普通的 JS 對象來做。假設我們有這樣一棵樹:
看起來很簡單,對吧? 如何用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,如下:
編譯成:
React.createElement(‘ul’, { className: ‘list’ }, React.createElement(‘li’, {}, ‘item 1’), React.createElement(‘li’, {}, ‘item 2’), );
是不是看起來有點熟悉?如果能夠用我們剛定義的 h(...) 函數代替 React.createElement(…),那么我們也能使用JSX 語法。其實,只需要在源文件頭部加上這么一句注釋:
/** @jsx h */
它實際上告訴 Babel " 嘿,小老弟幫我編譯 JSX 語法,用 h(...) 函數代替 React.createElement(…),然后 Babel 就開始編譯。"
綜上所述,我們將DOM寫成這樣:
/** @jsx h */ const a = (
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 傳入虛擬文本節點和虛擬元素節點——這是可行的。
現在讓我們考慮子節點——它們中的每一個都是文本節點或元素。所以它們也可以用 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 = (
現在我們可以將虛擬 DOM 轉換為真實的 DOM,這就需要考慮比較兩棵 DOM 樹的差異。基本的,我們需要一個算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應的去改變真實的 DOM。
怎么比較 DOM 樹?需要處理下面的情況:
添加新節點,使用 appendChild(…) 方法添加節點
移除老節點,使用 removeChild(…) 方法移除老的節點
節點的替換,使用 replaceChild(…) 方法
如果節點相同的——就需要需要深度比較子節點
編寫一個名為 updateElement(…) 的函數,它接受三個參數—— $parent、newNode 和 oldNode,其中 $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 < 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 = (
HTML
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 及其插件
原文:
https://medium.com/@deathmood...
你的點贊是我持續分享好東西的動力,歡迎點贊!
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100839.html
摘要:與大多數全局對象不同,沒有構造函數。為什么要設計更加有用的返回值早期寫法寫法函數式操作早期寫法寫法可變參數形式的構造函數一般寫法寫法當然還有很多,大家可以自行到上查看什么是代理設計模式代理模式,為其他對象提供一種代理以控制對這個對象的訪問。 這是專門探索 JavaScript 及其所構建的組件的系列文章的第 19 篇。 如果你錯過了前面的章節,可以在這里找到它們: 想閱讀更多優質文章請...
摘要:關于前端框架大家都有了解,或多或少的使用過,比如,,等等。那么你是否也想自己手寫一個的前端框架呢,我們從入手,手把手教你寫基于的前端框架,在整個編寫的過程中,希望大家學習更多,理解更多。本節我們以打包工具結合轉換插件實現數據的抽象。 關于MVVM前端框架大家都有了解,或多或少的使用過,比如Angular,React,VUE等等。那么你是否也想自己手寫一個MVVM的前端框架呢,我們從Vi...
摘要:一個字符串或者虛擬的數組用于表示該節點的。傳遞給函數的參數有兩個首先是當前狀態,其次是事件處理的回調函數,對生成的視圖中觸發的事件進行處理。回調函數主要負責為應用程序構建一個新的狀態,并使用新的狀態重啟循環。 原文鏈接原文寫于 2015-07-31,雖然時間比較久遠,但是對于我們理解虛擬 DOM 和 view 層之間的關系還是有很積極的作用的。 React 是 JavaScript...
摘要:本文轉載自眾成翻譯譯者鏈接原文今天,我們從一開始就開始。讓我們看看是什么,是什么讓運轉起來。什么是是一個用于構建用戶界面的庫。它是應用程序的視圖層。所有應用程序的核心是組件。組件是可組合的。虛擬完全存在于內存中,并且是網絡瀏覽器的的表示。 本文轉載自:眾成翻譯譯者:iOSDevLog鏈接:http://www.zcfy.cc/article/3765原文:https://www.ful...
摘要:我們將使用,這是一個現代,簡單,漂亮的框架,在內部使用并將響應式編程概念應用于前端編程。驅動程序采用從我們的應用程序發出數據的,它們返回另一個導致副作用的。我們將使用來呈現我們的應用程序。僅采用長度超過兩個字符的文本。 Rxjs 響應式編程-第一章:響應式Rxjs 響應式編程-第二章:序列的深入研究Rxjs 響應式編程-第三章: 構建并發程序Rxjs 響應式編程-第四章 構建完整的We...
閱讀 2609·2021-09-28 09:35
閱讀 3262·2021-09-03 10:28
閱讀 2905·2019-08-30 15:43
閱讀 1477·2019-08-30 14:04
閱讀 1801·2019-08-29 17:02
閱讀 1812·2019-08-26 13:59
閱讀 691·2019-08-26 11:51
閱讀 3251·2019-08-23 17:16