一步一步带你实现virtual dom(二) -- Props和事件
一步一步带你实现virtual dom(一)
一步一步带你实现virtual dom(二)--Props和事件
很高兴我们可以继续分享编写虚拟DOM的知识。这次我们要讲解的是产品级的内容,其中包括:设置和DOM一致性、以及事件的处理。
使用Babel
在继续之前,我们需要弥补前一篇文章中没有详细讲解的内容。假设有一个没有任何属性(props)的节点:
<div></div>
Babel,在处理这个节点的时候会把节点的props属性设置为“null”,因为它没有任何的属性。因此我们会得到这样的结果:
function h(type, props, ...children) {
return {type, props: props || {}, children};
}
设置props
设置props非常简单,记得DOM显示吗?我们把props作为简单的js对象来存储,所以这样的标签:
<ul className="list", style="list-style: none;"></ul>
内存里就会有这样的对象:
{
type: 'ul',
props: {className: 'list', style: 'list-style:none;'}
}
因此每一个props的字段就是一个属性名,这个字段的值就是属性值。所以,我们只要把这些值给真正的DOM节点设置了就可以了。我们写一个方法包装一个setAttribute()方法:
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’:
<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里面试试吧.
属性区分(Diff Props)
现在我们已经可以使用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元素的属性:
- 在DOM里没有这个属性的话,就删除掉
new old
<nav></nav> <nav className='navbar'></nav>
- 在新的节点里包含了某个属性,那么就需要在DOM上设置这个属性
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
一步一步带你实现virtual dom(二) -- Props和事件的更多相关文章
- 一步一步带你实现virtual dom(一)
一步一步带你实现virtual dom(一) 一步一步带你实现virtual dom(二)--Props和事件 要写你自己的虚拟DOM,有两件事你必须知道.你甚至都不用翻看React的源代码,或者其他 ...
- 抛开react,如何理解virtual dom和immutability
去年以来,React的出现为前端框架设计和编程模式吹来了一阵春风.很多概念,无论是原本已有的.还是由React首先提出的,都因为React的流行而倍受关注,成为大家研究和学习的热点.本篇分享主要就聚焦 ...
- 如何实现一个 Virtual DOM 及源码分析
如何实现一个 Virtual DOM 及源码分析 Virtual DOM算法 web页面有一个对应的DOM树,在传统开发页面时,每次页面需要被更新时,都需要手动操作DOM来进行更新,但是我们知道DOM ...
- Virtual DOM的简单实现
了解React的同学都知道,React提供了一个高效的视图更新机制:Virtual DOM,因为DOM天生就慢,所以操作DOM的时候要小心翼翼,稍微改动就会触发重绘重排,大量消耗性能. 1.Virtu ...
- [翻译]Review——The Inner Workings Of Virtual DOM
The Inner Workings Of Virtual DOM 虚拟DOM的内部工作机制 原文地址:https://medium.com/@rajaraodv/the-inner-workings ...
- 【转】Virtual DOM
前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常多的介绍 React.Virtual DOM 的文章.但是直到前不久我专门花时间去学习 ...
- why updating the Real DOM is slow, what is Virtaul DOM, and how updating Virtual DOM increase the performance?
个人翻译: Updating a DOM is not slow, it is just like updating any JavaScript object; then what exactly ...
- 如何理解Virtual DOM
什么是虚拟DOM 接下来用vdom(Virtual DOM)来简称为虚拟DOM. 指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做.换而言之,虚拟DOM就是JS对象.如下DOM结构: & ...
- Virtual DOM 简直就是挥霍
彻底澄清"Virtual DOM 飞快"的神话. 注意:原文发表于2018-12-27,随着框架不断演进,部分内容可能已不适用. 近年来,如果你有使用过 JavaScript 框架 ...
随机推荐
- iOS代码处理横屏问题
借助通知来控制界面的横竖屏切换.还是整个App中大部分界面都是竖屏,某个界面可以横竖屏切换的情况. 首先,在[General]-->[Device Orientation]设置仅支持竖屏,lik ...
- 对着java并发包写.net并发包之原子类型实现
众所周知,java1.5并发包通过volatile+CAS原理提供了优雅的并发支持.今天仔细想想.net也有volatile关键字保证内存的可见性,同时也有Interlocked提供了CAS的API, ...
- sql server 2008 18456错误
来自:http://blog.csdn.net/qishuangquan/article/details/6024767 百度搜18456错误几乎只能搜到一篇文章,并不是说结果条数,而是所有的文章都是 ...
- Windows下安装RabbitMQ
今天正好给自己机器安装rabbitmq,总结下安装经验. 现在国内访问erlang,和 RabbitMQ 官网好像都很难连上.我已下载好了资源,需要的朋友可以下载. 链接: https://pan.b ...
- CountDownLatch、CyclicBarrier和 Semaphore比较
1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行: ...
- Servlet 笔记-Session 跟踪
HTTP 是一种"无状态"协议,这意味着每次客户端检索网页时,客户端打开一个单独的连接到 Web 服务器,服务器会自动不保留之前客户端请求的任何记录. 但是仍然有以下三种方式来维持 ...
- [oracle 使用(2)] Oracle的简单使用
1:oracle的服务 oracleservice + sid # 数据库服务 oracleoradb10g_home1listener # 数据库的监听服务. 2:启动本机数据库: 启动oracle ...
- 通过Struts了解MVC框架,兼说如何在面试中利用Struts证明自己
虽然目前Struts MVC框架不怎么用了,但它确是个能帮助大家很好地入门Web MVC框架,而且,一些历史项目可能还用Struts,反正技多不压身,大家如果能在面试中通过项目证明自己Struts这块 ...
- Java基础笔记9
super关键字 表示父类对象. 1.可以调用父类中被重写的方法. 2.还有调用父类中的构造方法.放在子类构造方法的第一行. 不能和this关键字同时出现. final关键字 1.修饰属性.表示常量. ...
- JavaScript 中的对象引用
ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型,拥有方法的类型和不能拥有方法的类型,还可以分为可变类型和不可变类型,其实这些叫法都是依据这两 ...