第一篇地址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的实现