一步一步帶你實現virtual dom(一)
一步一步帶你實現virtual dom(二)--Props和事件html
很高興咱們能夠繼續分享編寫虛擬DOM的知識。此次咱們要講解的是產品級的內容,其中包括:設置和DOM一致性、以及事件的處理。node
在繼續以前,咱們須要彌補前一篇文章中沒有詳細講解的內容。假設有一個沒有任何屬性(props)的節點:web
<div></div>
Babel,在處理這個節點的時候會把節點的props屬性設置爲「null」,由於它沒有任何的屬性。所以咱們會獲得這樣的結果:面試
function h(type, props, ...children) { return {type, props: props || {}, children}; }
設置props很是簡單,記得DOM顯示嗎?咱們把props做爲簡單的js對象來存儲,因此這樣的標籤:babel
<ul className="list", style="list-style: none;"></ul>
內存裏就會有這樣的對象:app
{ type: 'ul', props: {className: 'list', style: 'list-style:none;'} }
所以每個props的字段就是一個屬性名,這個字段的值就是屬性值。因此,咱們只要把這些值給真正的DOM節點設置了就能夠了。咱們寫一個方法包裝一個setAttribute()方法:dom
function setProp($target, name, value) { $target.setAttribute(name, value); }
那麼如今咱們知道如何設置屬性了(prop)--咱們以後能夠所有都設置上,只要遍歷prop對象的屬性就能夠:性能
function setProps($target, props) { Object.keys(props).forEach(name => { setProp($target, name, props[name]); }) }
還記得createElement()方法麼?咱們只須要在真正的DOM節點建立以後調用setProp方法給它設置便可:測試
function createElement(node) { if(typeof node === 'string) { return document.createTextNode(node); } const $el = document.createElement(node.type); setProps($el, node.props); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; }
可是,這尚未完。咱們忘記了一些小細節。首先,‘class’是js的保留字。因此不能把它用做屬性名稱。咱們會使用‘className’:.net
<nav className="navbar light"> <ul></ul> </nav>
可是在真正的DOM裏並無‘className’,因此咱們應該在setProp方法裏處理這個問題。
另一個事情是,設置布爾型的屬性的時候最好使用布爾值:
<input type="checkbox" checked={false} />
在這個例子裏,我並不但願這個'checked'屬性值設置在真正的DOM節點上。可是事實上這個值足夠設置DOM節點了,固然這同時還須要給對應的虛擬DOM節點也設置這個值:
function setBooleanProp($target, name, value) { if(value) { $target.setAttribute(name, value); $target.[name] = true; } else { $target[name] = false; } }
如今咱們就來看看如何自定義屬性。此次徹底是咱們本身的實現,所以後面咱們會有不一樣做用的屬性,而且不是全都要在DOM節點上顯示的。因此要寫一個方法來檢查這個屬性是否是自定義的。如今它是空的,因此咱們尚未任何的自定義屬性:
function isCustomProp(name) { return false; }
下面就是咱們完整的setProp()
方法,把全部的問題都處理了:
function setProp($target, name, value) { if(isCustomProp(name)) { return; } else if(name === 'className') { $target.setAttribute('class', value); } else if(typeof value === 'boolean') { setBooleanProp($target, name, value); } else { $target.setAttribute(name, value); } }
如今在JSFiddle裏面試試吧.
如今咱們已經可使用prop來建立元素了,如今要處理的就是如何區分元素的props了。最終要麼是設置屬性,要麼是刪除它。咱們已經有方法能夠設置屬性了,如今來寫一個方法來刪除它們吧。事實上這很是簡單:
function removeBooleanProp($target, name) { $target.removeAttribute(name); $target[name] = false; } function removeProp($target, name, value) { if(isCustomProp(name)) { return; } else if(name === 'className') { $target.removeAttribute('class'); } else if(typeof === 'boolean') { removeBooleanProp($target, name); } else { $target.removeAttribute(name); } }
咱們再來寫一個updateProp()
方法來比較兩個屬性--就的和新的,並根據比較的結果來更新DOM元素的屬性:
new old <nav></nav> <nav className='navbar'></nav>
new old <nav style='background: blue'></nav> <nav></nav>
new old <nav className='navbar default'></nav> <nav className='navbar'></nav>
下面這個方法就是專門處理prop的:
function updateProp($target, naem, newVal, oldVal) { if(!newVal) { removeProp($target, name, oldVal); } else if(!oldVal || newVal != oldVal) { setProp($target, name, newVal); } }
是否是很簡單?可是一個節點會有不止一個屬性--因此咱們要寫一個方法能夠遍歷所有的屬性,而後調用updateProp()
方法來一對一對的處理:
function updateProps($target, newProps, oldProps = {}) { const props = Object.assign({}, newProps, oldProps); Object.leys(props).forEach(name => { updateProp($target, name, newProps[name], oldProps[name]); }); }
這裏須要注意咱們建立的組合對象。它包含了新、舊節點的屬性。所以,在遍歷的時候咱們會遇到undefined
,不過這沒有關係,咱們的方法能夠處理這個問題。
最後一件事就是把這個方法放到咱們的updateElement()
方法裏。咱們應該放在哪裏呢?若是節點自己沒有改變,那麼它的子節點呢?這個問題咱們也須要處理。因此咱們把那個方法放在最後一個if
語句塊裏。
function updateElement($parent, newNode, oldNode, index=0) { if() { ... } else if(newNode.type) { updateProps( $parent.childNodes[index], newNode.props, oldNode.props, ); ... } }
接着在這裏測試一下吧。
固然一個動態的應用是免不了會有事件的。咱們可使用querySelector()
來處理節點,而後用addEventListener()
來給節點添加事件的listener。可是,這樣沒啥意思。咱們要像React同樣來處理事件。
<button onClick={() => alert('hi')}></button>
這樣看起來就像那麼回事兒了。你看到了,咱們是用了props
來聲明一個事件監聽器的。咱們的屬性名都是on
開頭的。
function isEventProp(name) { return /^on/.test(name); }
咱們來寫一個方法,從屬性裏獲取事件名稱。記住事件的名稱都是以on
爲前綴的。
function extractEventName(name) { return name.slice(2).toLowerCase(); }
看起來,若是咱們在屬性裏聲明瞭事件,那麼咱們就須要在setProps()
或者updateProps()
方法裏處理。可是如何處理方法的不一樣呢?
你不能用相等操做符來比較兩個方法。固然你能夠用toString()
方法,而後比較兩個方法。可是有個問題,方法裏可能會包含native code,這就給比較帶來了問題。
"function () { [native code] }"
固然咱們可使用時間冒泡的方式來處理。咱們能夠寫咱們本身的事件處理管理器,這個管理器會附加到body
或者繪製咱們節點的容器節點上。所以,咱們能夠在每次更新的時候添加一次事件處理器,這樣也不會形成多大的資源浪費。
可是,咱們不會這麼作。由於這樣會增長不少的問題,並且事實上咱們的時間處理器不會頻繁的改變。因此,咱們只要在建立咱們的節點的時候添加一次事件監聽器就能夠。那麼不會在setProps
方法裏設置事件屬性。咱們本身處理添加事件的問題。怎麼實現呢?記得咱們的方法能夠檢測自定義的屬性嗎?如今它不會是空的了:
function isCustomProp(name) { return isEventProp(name); }
當咱們知道了一個真的DOM節點的時候添加事件監聽器,這時屬性對象也很是清晰的。
function addEventListeners($target, props) { Object.keys(props).forEach(name => { if(isEventProp(name)){ $target.addEventListener( exteactEventName(name), props[name] ); } }); }
把上面的代碼加入到createElement
方法裏:
function createElement(node) { if(typeof node === 'string') { return document.createTextNode('node'); } const $el = document.createElement(node.type); setProps($el, node.props); addEventListeners($el, node.props); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; }
若是你必需要再次添加事件監聽器呢?咱們來簡單理解處理一下這個問題。只是這樣的話性能會受到印象。咱們會引入一個自定義屬性:forceUpdate
。記住,咱們怎麼檢查節點的更改的:
function changed(node1, node2) { return typeof node1 ~== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type || node.props.forceUpdate; }
若是forceUpdate
爲true的話,節點就會整個的從新建立而且新的事件監聽器也會被添加進去。整個屬性也不是不該該加到實際的DOM節點的,因此須要處理一下:
function isCustomProp(name) { return isEventProp(name) || name === 'forceUpdate'; }
這基本就是所有了。是的,整個解決的方法會影響性能,可是很簡單。
這就基本是所有了。但願你以爲有趣。若是你知道更簡單的解決方法處理事件處理器的不一樣的方法的話,能分享到評論裏就太感謝了。
原文地址:https://medium.com/@deathmood/write-your-virtual-dom-2-props-events-a957608f5c76