第一篇地址html
第二部分原文:write-your-virtual-dom-2-props-events前端
完整的示例代碼地址node
首先咱們要回顧下前文講的一個有些誤差的小點,假設咱們在JSX中只寫一個最簡單的Div:git
<div></div>
Babel會自動將該JSX轉化爲以下的DOM表達式:github
{ type: ‘’, props: null, children: [] }
注意,這裏的props默認是null,咱們在以前的文章中並無關注到這個屬性,而本部分則是要講解Virtual DOM中Props的用法。通常來講,不管在哪一種編程環境下都要儘可能避免Null的出現,所以咱們首先來改造下h
函數,使得其可以默認返回一個空的Object,而不是Null:web
function h(type, props, …children) { return { type, props: props || {}, children }; }
接觸過React的同窗對於Props確定不會陌生,而設置Props也就跟使用普通的HTML標籤屬性很相似:算法
<ul className=」list」 style=」list-style: none;」></ul>
而最終會轉化爲以下的表達式:編程
{ type: ‘ul’, props: { className: ‘list’, style: ’list-style: none;’ } children: [] }
props對象中的每一個鍵即爲屬性名,而值爲屬性值,通常來講咱們只須要簡單的調用一個setAttribute
方法來說這個Props中的鍵值對設置到DOM元素上便可:segmentfault
function setProp($target, name, value) { $target.setAttribute(name, value); }
這個函數用於將單個的Prop值設置到DOM元素上,而對於props對象,咱們要作的就是依次遍歷:app
function setProps($target, props) { Object.keys(props).forEach(name => { setProp($target, name, props[name]); }); }
你應該還記得那個用於建立元素的createElement
方法吧,咱們須要將setProps
方法放置到元素成功建立以後:
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; }
不要急,這還遠遠不夠。React的初學教程中一直強調className與class的區別,在咱們的setProps中也須要對於這些JS的保留字作一個替換,譬如:
<nav className=」navbar light」> <ul></ul> </nav>
另外,還有比較常見的就是對於DOM的布爾屬性,譬如checked、disabled等等的處理:
<input type=」checkbox」 checked={false} />
在真實的DOM節點上,若是是出現了false的狀況,咱們並不但願checked屬性會出現,那麼咱們的Props函數就要能智能地進行判斷:
function setBooleanProp($target, name, value) { if (value) { $target.setAttribute(name, value); $target[name] = true; } else { $target[name] = false; } }
最後呢,要作的就是對於自定義的,即非標準的HTML屬性進行一個過濾,這些屬性只應該出如今JS對象上,而不該該出如今真實的DOM對象上:
function isCustomProp(name) { return false; }
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); } }
總結一下,本部分完整的JSX代碼爲:
/** @jsx h */ function h(type, props, ...children) { return { type, props: props || {}, children }; } function setBooleanProp($target, name, value) { if (value) { $target.setAttribute(name, value); $target[name] = true; } else { $target[name] = false; } } function isCustomProp(name) { return false; } 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); } } function setProps($target, props) { Object.keys(props).forEach(name => { setProp($target, name, props[name]); }); } 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; } //-------------------------------------------------- const f = ( <ul style="list-style: none;"> <li className="item">item 1</li> <li className="item"> <input type="checkbox" checked={true} /> <input type="text" disabled={false} /> </li> </ul> ); const $root = document.getElementById('root'); $root.appendChild(createElement(f));
如今咱們已經建立了帶有Props屬性的元素,下一個須要考慮的就是應該如何應用到咱們上文提到的Diff算法中。首先咱們要來看下如何從真實的DOM中移除某些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 value === ‘boolean’) { removeBooleanProp($target, name); } else { $target.removeAttribute(name); } }
而後咱們須要寫一個updateProp函數,來根據新舊節點的Props的變化進行恰當的真實DOM節點的修改,共有如下幾種狀況:
新節點移除了某個舊節點的Prop
新節點添加了某個舊節點沒有的Prop
新舊節點的某個Prop的值發生了變化
根據以上規則,咱們可知更新Prop的函數爲:
function updateProp($target, name, newVal, oldVal) { if (!newVal) { removeProp($target, name, oldVal); } else if (!oldVal || newVal !== oldVal) { setProp($target, name, newVal); } }
能夠看出,更新單個Prop的函數仍是很是簡單的,就是將移除與設置結合起來使用,那麼咱們擴展到Props,就獲得以下的函數:
function updateProps($target, newProps, oldProps = {}) { const props = Object.assign({}, newProps, oldProps); Object.keys(props).forEach(name => { updateProp($target, name, newProps[name], oldProps[name]); }); }
一樣地,咱們須要將該更新函數添加到updateElement
函數中:
function updateElement($parent, newNode, oldNode, index = 0) { ... } else if (newNode.type) { updateProps( $parent.childNodes[index], newNode.props, oldNode.props ); ... } }
用戶交互是任何一個應用不可或缺的部分,而在這裏咱們討論下如何爲Virtual DOM添加事件處理的能力,React大概會這麼作:
<button onClick={() => alert(‘hi!’)}></button>
能夠看出,設置一個事件處理器就是添加一個Prop,只不過名稱會以on
開始,那麼咱們能夠用以下函數來判斷某個Prop是否與事件相關:
function isEventProp(name) { return /^on/.test(name); }
判斷是事件類型以後,咱們能夠提取出事件名:
function extractEventName(name) { return name.slice(2).toLowerCase(); }
看到這裏,估計你會考慮直接將事件處理也放到setProps與updateProps函數中,不過這邊就會存在一個問題,在diffProps的時候,你很難去比較兩個function:
所以咱們將全部的事件類型的Props認爲是自定義的Props,這樣咱們上面提到的isCustomProp就起做用了:
function isCustomProp(name) { return isEventProp(name); }
而把事件響應函數綁定到真實的DOM節點也很簡單:
function addEventListeners($target, props) { Object.keys(props).forEach(name => { if (isEventProp(name)) { $target.addEventListener( extractEventName(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; }
在這裏咱們暫時不考慮地很複雜,即不深刻地比較那些事件類型的Prop發生變化的狀況,做爲替代的,咱們引入一個forceUpdate屬性,即強制整個DOM進行更新:
function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type || node.props.forceUpdate; }
function isCustomProp(name) { return isEventProp(name) || name === ‘forceUpdate’; }
最後,本文完整的JSX爲:
/** @jsx h */ function h(type, props, ...children) { return { type, props: props || {}, children }; } function setBooleanProp($target, name, value) { if (value) { $target.setAttribute(name, value); $target[name] = true; } else { $target[name] = false; } } function removeBooleanProp($target, name) { $target.removeAttribute(name); $target[name] = false; } function isEventProp(name) { return /^on/.test(name); } function extractEventName(name) { return name.slice(2).toLowerCase(); } function isCustomProp(name) { return isEventProp(name) || name === 'forceUpdate'; } 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); } } function removeProp($target, name, value) { if (isCustomProp(name)) { return; } else if (name === 'className') { $target.removeAttribute('class'); } else if (typeof value === 'boolean') { removeBooleanProp($target, name); } else { $target.removeAttribute(name); } } function setProps($target, props) { Object.keys(props).forEach(name => { setProp($target, name, props[name]); }); } function updateProp($target, name, newVal, oldVal) { if (!newVal) { removeProp($target, name, oldVal); } else if (!oldVal || newVal !== oldVal) { setProp($target, name, newVal); } } function updateProps($target, newProps, oldProps = {}) { const props = Object.assign({}, newProps, oldProps); Object.keys(props).forEach(name => { updateProp($target, name, newProps[name], oldProps[name]); }); } function addEventListeners($target, props) { Object.keys(props).forEach(name => { if (isEventProp(name)) { $target.addEventListener( extractEventName(name), props[name] ); } }); } 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; } function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type || node1.props && node1.props.forceUpdate; } 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) { updateProps( $parent.childNodes[index], newNode.props, oldNode.props ); 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 ); } } } //--------------------------------------------------------- function log(e) { console.log(e.target.value); } const f = ( <ul style="list-style: none;"> <li className="item" onClick={() => alert('hi!')}>item 1</li> <li className="item"> <input type="checkbox" checked={true} /> <input type="text" onInput={log} /> </li> {/* this node will always be updated */} <li forceUpdate={true}>text</li> </ul> ); const g = ( <ul style="list-style: none;"> <li className="item item2" onClick={() => alert('hi!')}>item 1</li> <li style="background: red;"> <input type="checkbox" checked={false} /> <input type="text" onInput={log} /> </li> {/* this node will always be updated */} <li forceUpdate={true}>text</li> </ul> ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, f); $reload.addEventListener('click', () => { updateElement($root, g, f); });
到這裏咱們就完成了一個最簡單的Virtual DOM算法,不過其與真正可以投入實戰的Virtual DOM算法仍是有很大距離,進一步閱讀推薦:
A Virtual DOM and diffing algorithm:一個比較複雜的Virtual DOM算法的實現
simple-virtual-dom:一個簡單的Virtual DOM的實現