一步一步帶你實現virtual dom(一)
一步一步帶你實現virtual dom(二)--Props和事件html
要寫你本身的虛擬DOM,有兩件事你必須知道。你甚至都不用翻看React的源代碼,或者其餘的基於虛擬DOM的代碼。他們代碼量都太大,太複雜。然而要實現一個虛擬DOM的主要部分只須要大約50行的代碼。50行代碼!!node
下面就是那兩個你要知道的事情:react
下面咱們就來看看這兩條是如何實現的。web
首先咱們須要在內存裏存儲咱們的DOM樹。只要使用js就能夠達到這個目的。假設咱們有這樣的一個樹:算法
<ul class="list"> <li>item 1</li> <li>item 2</li> </ur>
看起來很是簡單對吧。咱們怎麼用js的對象來對應到這個樹呢?babel
{ type: 'ul', props: {'class': 'list}, children: [ {type: 'li', props: {}, children: ['item 1']}, {type: 'li', props: {}, children: ['item 2']} ]}
這裏咱們會注意到兩件事:app
{type: '...', props: {...}, children: [...]}
。function h(type, props, ...children) { return {type, props, children}; }
如今咱們能夠這樣生成一個虛擬DOM樹:dom
h('ul', {'class': 'list'}, h('li', {}, 'item 1'), h('li', {}, 'item 2'), )
這樣看起來就清晰了不少。可是咱們還能夠作的更好。你應該據說過JSX對吧。是的,咱們也要用那種方式。可是,這個應該如何下手呢?spa
若是你讀過Babel的JSX文檔的話,你就會知道這些都是Babel的功勞。Babel會把下面的代碼轉碼:.net
<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') );
你注意到多類似了嗎?若是把React.createElement(...)
體換成咱們本身的h
方法的話,那咱們也已使用相似於JSX的語法。咱們只須要在咱們的文件最頂端加這麼一句話:
/** @jsx h */ <ul className="list"> <li>item 1</li> <li>item 2</li> </ul>
這一行/** @jsx h */
就是在告訴Babel「大兄弟,按照jsx的方式轉碼,可是不要用React.createElement
, 使用h
。你可使用任意的東西來代替h。
那麼把上面咱們說的總結一下,咱們會這樣寫咱們的虛擬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。咱們來寫一個方法:createElement()
,這個方法能夠接收一個虛擬節點以後返回一個真實的DOM節點。先不考慮props
和children
,這個以後會有介紹。
function createElement(node) { if(typeof node === 'string') { return document.createTextNode(node); } return document.createElement(node.type); }
由於咱們不只須要處理文本節點(js的字符串),還要處理各類元素(element)。這些元素都是想js的對象同樣的:
{ type: '-', props: {...}, children: [...]}
咱們能夠用這個結構來處理文本節點和各類element了。
那麼子節點如何處理呢,他們也基本是文本節點或者各類元素。這些子節點也能夠用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了。如今該考慮比較咱們的虛擬樹了。基本上咱們須要寫一點算法了。虛擬樹的比較須要用到這個算法,比較以後只作必要的修改。
如何比較樹的不一樣?
appendChild
方法來添加。//new <ul> <li>item 1</li> <li>item 2</li> </ul> //old <ul> <li>item 1</li> </ul>
removeChild
方法來刪除掉多餘的子節點。//new <ul> <li>item 1</li> </ul> //old <ul> <li>item 1</li> <li>item 2</li> // 這個要被刪掉 </ul>
replaceChild
方法。//new <div> <p>hi there!</p> <p>hello</p> </div> //old <div> <p>hi there!</p> <button>click it</button> //發生了修改,變成了new裏的<p />節點 </div>
//new <ul> <li>item 1</li> <li> //* <span>hello</span> <span>hi!</span> </li> </ul> //old <ul> <li>item 1</li> <li> //* <span>hello</span> <div>hi!</div> </li> </ul>
加醒的兩個節點能夠看到都是<li>
,是相等的。可是它的子節點裏面卻有不一樣的節點。
咱們來寫一個方法updateElement
,它接收三個參數:$parent
、newNode
和oldNode
。$parent
是真的DOM元素。它是咱們虛擬節點的父節點。如今咱們來看看如何處理上面提到的所有問題。
這個問題很簡單:
function updateElement($parent, newNode, oldNode) { if(!oldNode) { $parent.appendChild( createElement(newNode) ); } }
若是當前沒有新的虛擬節點,咱們就應該把它從真的DOM裏刪除掉。可是,如何作到呢?咱們知道父節點(做爲參數傳入了方法),那麼咱們就能夠調用$parent.removeChild
方法,並傳入真DOM的引用。可是咱們沒法獲得它,若是咱們知道的節點在父節點的位置,就能夠用$parent.childNodes[index]
來獲取它的引用。index
就是節點的位置。
假設index
也做爲參數傳入了咱們的方法,咱們的方法就能夠這麼寫:
function updateElement($parent, newNode, oldNode, index = 0) { if(!oldNode) { $parent.appendChild( createElement(newNode); ); } else if(!newNode) { $parent.removeChild( $parent.childNodes[index]; ); } }
首先寫一個方法來比較兩個節點(新的和舊的)來區分節點是否發生了改變。要記住,節點能夠是文本節點,也能夠是元素(element):
function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type; }
如今有了當前節點的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(chianged(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } }
最後,須要遍歷新舊節點的子節點,並比較他們。能夠在每一個節點上都使用updateElement
方法。是的,遞歸。
可是在開始代碼以前須要考慮一些問題:
undefined
的狀況。沒有關係,咱們的方法能夠處理。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 ); } } }
在JSFiddle裏看看代碼把!
祝賀你!咱們搞定了。咱們寫出了虛擬節點的實現。從上面的例子中你已經能夠理解虛擬節點的概念了,也大致能夠知道React是如何運做的了。
當時還有不少須要講述的內容,其中包括:
原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060