您好,欢迎访问一九零五行业门户网

从零开始一步一步写一个简单的 Virtual DOM 实现 2 :Props&Event 处理_html/css_WEB-ITnose

props 首先我们要回顾下前文讲的一个有些偏差的小点,假设我们在jsx中只写一个最简单的div:

babel会自动将该jsx转化为如下的dom表达式:
{ type: ‘’, props: null, children: [] }
注意,这里的props默认是null,我们在之前的文章中并没有关注到这个属性,而本部分则是要讲解virtual dom中props的用法。一般来说,无论在哪种编程环境下都要尽量避免null的出现,因此我们首先来改造下 h 函数,使得其能够默认返回一个空的object,而不是null:
function h(type, props, …children) { return { type, props: props || {}, children };}
setting props:设置props 接触过react的同学对于props肯定不会陌生,而设置props也就跟使用普通的html标签属性很类似:

而最终会转化为如下的表达式:
{ type: ‘ul’, props: { classname: ‘list’, style: ’list-style: none;’ } children: []}
props对象中的每个键即为属性名,而值为属性值,一般来说我们只需要简单的调用一个 setattribute 方法来讲这个props中的键值对设置到dom元素上即可:
function setprop($target, name, value) { $target.setattribute(name, value);}
这个函数用于将单个的prop值设置到dom元素上,而对于props对象,我们要做的就是依次遍历:
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的保留字做一个替换,譬如:

另外,还有比较常见的就是对于dom的布尔属性,譬如checked、disabled等等的处理:

在真实的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 = ( item 1 );const $root = document.getelementbyid('root');$root.appendchild(createelement(f));
diffing props:props变化比较 现在我们已经创建了带有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 ); ... }}
events 用户交互是任何一个应用不可或缺的部分,而在这里我们讨论下如何为virtual dom添加事件处理的能力,react大概会这么做:
alert(‘hi!’)}>
可以看出,设置一个事件处理器就是添加一个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;}
re-adding events:重新设置了事件响应 在这里我们暂时不考虑地很复杂,即不深入地比较那些事件类型的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 item 1 {/* this node will always be updated */} text );const $root = document.getelementbyid('root');const $reload = document.getelementbyid('reload');updateelement($root, f);$reload.addeventlistener('click', () => { updateelement($root, g, f);});
到这里我们就完成了一个最简单的virtual dom算法,不过其与真正能够投入实战的virtual dom算法还是有很大距离,进一步阅读推荐:
深度剖析:如何实现一个 virtual dom 算法
我的前端故事----react算法又是个什么鬼?!
a virtual dom and diffing algorithm :一个比较复杂的virtual dom算法的实现
simple-virtual-dom :一个简单的virtual dom的实现
how-to-write-your-own-virtual-dom
virtual dom benchmark
其它类似信息

推荐信息