一步一步带你实现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. commonjs模块和es6模块的区别

    commonjs模块与es6模块的区别 到目前为止,已经实习了3个月的时间了.最近在面试,在面试题里面有题目涉及到模块循环加载的知识.趁着这个机会,将commonjs模块与es6模块之间一些重要的的区 ...

  2. 当谈到 GitLab CI 的时候,我们都该聊些什么(下篇)

    上篇主要介绍了 GitLab WorkFlow 以及 CI/CD 做的事情,并且详细分析 GitLab CI 跟 Runner 信息交互是如何进行的.接下来将为大家讲解 Executor 的实现,再通 ...

  3. canvas图表详解系列(3):动态饼状图(原生Js仿echarts饼状图)

    本章建议学习时间4小时 学习方式:详细阅读,并手动实现相关代码(如果没有canvas基础,需要先学习前面的canvas基础笔记) 学习目标:此教程将教会大家如何使用canvas绘制各种图表,详细分解步 ...

  4. win7下安装Ubuntu后进不去win7的解决方法

    win7下安装Ubuntu后进不去win7的解决方法 刚刚给同学在win7下安装了Ubuntu16.04,结果在安装完后竟然无法在电脑重启后,找到win7的进入选项. 在网上找了找,都不行!就差点重装 ...

  5. visio流程图软件

    microsoft visio 2013 64位 链接:http://pan.baidu.com/s/1bnjWa1T 密码:9ylc 标准图表 使用现有的数据,您可以生成许多种类的 Visio 标准 ...

  6. angular 4 实现的tab栏切换

    管理系统 tab 切换页,是一种常见的需求,大概如下: 点击左边菜单,右边显示相应的选项卡,然后不同的选项卡面可以同时编辑,切换时信息不掉失! 用php或.net,java的开发技术,大概是切换显示, ...

  7. KDevelop使用笔记【中文】

    师从官方文档: https://userbase.kde.org/KDevelop4/Manual https://docs.kde.org/trunk5/en/extragear-kdevelop/ ...

  8. multiprocessing模块

    multiprocessing模块 由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程. multiproce ...

  9. 薪资那么高的Web前端,你该怎么学?

    由于前端开发的火热和一些IT巨头公司 对 web前端开发人员的需求旺盛,让越来越多的人转入前端.前端开发领域 是IT技术语言领域唯一一个男女老少都可以快速入门并快速提升兴趣的领域,今天就来聊聊前端到底 ...

  10. codeblocks无法编译的问题

    (题外话:网上垃圾资源太多,良心推荐下载 codeblocks的码农们,别TM用DevC++,百度搜索100个不用devc++的理由加上我自己亲身经历!!!) https://jingyan.baid ...