一步一步带你实现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和事件的更多相关文章

  1. 一步一步带你实现virtual dom(一)

    一步一步带你实现virtual dom(一) 一步一步带你实现virtual dom(二)--Props和事件 要写你自己的虚拟DOM,有两件事你必须知道.你甚至都不用翻看React的源代码,或者其他 ...

  2. 抛开react,如何理解virtual dom和immutability

    去年以来,React的出现为前端框架设计和编程模式吹来了一阵春风.很多概念,无论是原本已有的.还是由React首先提出的,都因为React的流行而倍受关注,成为大家研究和学习的热点.本篇分享主要就聚焦 ...

  3. 如何实现一个 Virtual DOM 及源码分析

    如何实现一个 Virtual DOM 及源码分析 Virtual DOM算法 web页面有一个对应的DOM树,在传统开发页面时,每次页面需要被更新时,都需要手动操作DOM来进行更新,但是我们知道DOM ...

  4. Virtual DOM的简单实现

    了解React的同学都知道,React提供了一个高效的视图更新机制:Virtual DOM,因为DOM天生就慢,所以操作DOM的时候要小心翼翼,稍微改动就会触发重绘重排,大量消耗性能. 1.Virtu ...

  5. [翻译]Review——The Inner Workings Of Virtual DOM

    The Inner Workings Of Virtual DOM 虚拟DOM的内部工作机制 原文地址:https://medium.com/@rajaraodv/the-inner-workings ...

  6. 【转】Virtual DOM

    前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常多的介绍 React.Virtual DOM 的文章.但是直到前不久我专门花时间去学习 ...

  7. 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 ...

  8. 如何理解Virtual DOM

    什么是虚拟DOM 接下来用vdom(Virtual DOM)来简称为虚拟DOM. 指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做.换而言之,虚拟DOM就是JS对象.如下DOM结构: & ...

  9. Virtual DOM 简直就是挥霍

    彻底澄清"Virtual DOM 飞快"的神话. 注意:原文发表于2018-12-27,随着框架不断演进,部分内容可能已不适用. 近年来,如果你有使用过 JavaScript 框架 ...

随机推荐

  1. 【特效】select美化

    select的默认样式往往很丑,为保证页面样式风格统一,需要对select进行美化.虽然其美化的插件很多,一搜一大把,但是需要引入长长的css文件和js文件实在是件头痛的事.其实select的实现原理 ...

  2. 吾八哥学Python(四):了解Python基础语法(下)

    咱们接着上篇的语法学习,继续了解学习Python基础语法. 数据类型大体上把Python中的数据类型分为如下几类:Number(数字),String(字符串).List(列表).Dictionary( ...

  3. CSS之 relative 特性

    1. 自身特性: 如left,right,top,bottom定位都是相对于自身位置定位. 当left与right同时存在,lfet生效. 当top与bottom同时存在,top生效. 无侵入,保留原 ...

  4. win10 uwp 修改Pivot Header 颜色

    我们在xaml创建一个Pivot <Pivot Grid.Row="1"> <PivotItem Header="lindexi">&l ...

  5. session文件无法并发操作

    session_start():打开服务器上的session文件. session_commit():会把$_SESSION数组的内容写入到服务器上的session文件中,但不会清空$_SESSION ...

  6. 【机器学习实战】第 10 章 K-Means(K-均值)聚类算法

    第 10 章 K-Means(K-均值)聚类算法 K-Means 算法 聚类是一种无监督的学习, 它将相似的对象归到一个簇中, 将不相似对象归到不同簇中.相似这一概念取决于所选择的相似度计算方法.K- ...

  7. request的getServletPath(),getContextPath(),getRequestURI(),getRealPath("/")区别

    假定你的web application 名称为news,你在浏览器中输入请求路径: http://localhost:8080/news/main/list.jsp 则执行下面向行代码后打印出如下结果 ...

  8. MarkDown的快速入门

    简介 简单的去解释MarkDown就是html,但是将html中的元素用符号去代替使用.本文用的编译软件是Atom(神器),不多说直接上图看效果. 语法 文本 列表 区块 分割符 表格 链接 mark ...

  9. 利用wsdl.exe自动将wsdl文档转换为C#代码

    1.获取完整的wsdl文档 获取下面这个博客中提到的wsdl http://www.cnblogs.com/LCCRNblog/p/3716406.html 将获取到的wsdl放到一个文本中,改后缀( ...

  10. css超过一定长度显示省略号

    overflow: hidden; white-space: nowrap; text-overflow: ellipsis;