做者:deathmood
譯者:前端小智
來源:medium
1024程序員節,160就能買到400的書,紅寶書 5 折前端
爲了保證的可讀性,本文采用意譯而非直譯。node
要構建本身的虛擬DOM,須要知道兩件事。你甚至不須要深刻 React 的源代碼或者深刻任何其餘虛擬DOM實現的源代碼,由於它們是如此龐大和複雜——但實際上,虛擬DOM的主要部分只需不到50行代碼。react
有兩個概念:git
首先,咱們須要以某種方式將 DOM 樹存儲在內存中。可使用普通的 JS 對象來作。假設咱們有這樣一棵樹:程序員
<ul class=」list」> <li>item 1</li> <li>item 2</li> </ul>
看起來很簡單,對吧? 如何用JS對象來表示呢?github
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] }
這裏有兩件事須要注意:面試
{ type: ‘…’, props: { … }, children: [ … ] }
可是用這種方式表示內容不少的 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 1</li> <li>item 2</li> </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 1</li> <li>item 2</li> </ul>
它實際上告訴 Babel ' 嘿,小老弟幫我編譯 JSX 語法,用 h(...)
函數代替 React.createElement(…)
,而後 Babel 就開始編譯。'
綜上所述,咱們將DOM寫成這樣:
/** @jsx h */ const a = ( <ul className=」list」> <li>item 1</li> <li>item 2</li> </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’] } ] } );
好了,如今咱們有了 DOM 樹,用普通的 JS 對象表示,還有咱們本身的結構。這很酷,但咱們須要從它建立一個真正的DOM。
首先讓咱們作一些假設並聲明一些術語:
$
'開頭的變量表示真正的DOM節點(元素,文本節點),所以 $parent 將會是一個真實的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 = ( <ul class="list"> <li>item 1</li> <li>item 2</li> </ul> ); const $root = document.getElementById('root'); $root.appendChild(createElement(a));
如今咱們能夠將虛擬 DOM 轉換爲真實的 DOM,這就須要考慮比較兩棵 DOM 樹的差別。基本的,咱們須要一個算法來比較新的樹和舊的樹,它可以讓咱們知道什麼地方改變了,而後相應的去改變真實的 DOM。
怎麼比較 DOM 樹?須要處理下面的狀況:
若是節點相同的——就須要須要深度比較子節點
編寫一個名爲 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(…)方法,一樣須要用到遞歸。
undefined
也沒有關係,咱們的函數也會正確處理它。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; }
打開開發者工具,並觀察當按下「Reload」按鈕時應用的更改。
如今咱們已經編寫了虛擬 DOM 實現及瞭解它的工做原理。做者但願,在閱讀了本文以後,對理解虛擬 DOM 如何工做的基本概念以及在幕後如何進行響應有必定的瞭解。
然而,這裏有一些東西沒有突出顯示(將在之後的文章中介紹它們):
原文:
https://medium.com/@deathmood...
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
文章每週持續更新,能夠微信搜索「 大遷世界 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,另外關注公衆號,後臺回覆福利,便可看到福利,你懂的。