一步一步带你实现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. Android之View绘制流程源码分析

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 对于稍有自定义View经验的安卓开发者来说,onMeasure,onLayout,onDraw这三个方法都不会陌生,起码多少都有所接触吧. 在安卓中 ...

  2. ABAP 内表数据 与 Json串 相互转换

    内表: A B C IMINGZHA  HAIMINGZ AIMINGZH 1 2 3 4 5 6 Json串:  [{a: "IMINGZHA", b: "HAIMIN ...

  3. kafka 的 createDirectStream

    一入大数据深似海,脑袋不够用了,先留下只言片语. kafka api中给出2类直接获取流的接口:createStream和createDirectStream. createStream比较简单,只需 ...

  4. Spring Cloud官方文档中文版-Spring Cloud Config(上)

    官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...

  5. HDFS Basic Operation

    1.如何启动一个命令行的hadoop客户端 任何一个Hadoop集群中的节点,只要有hadoop安装包,就可以通过# hadoop fs来启动 2.Hadoop基本命令格式 # hadoop  fs  ...

  6. 数据处理不等式:Data Processing Inequality

    我是在差分隐私下看到的,新解决方案的可用性肯定小于原有解决方案的可用性,也就是说信息的后续处理只会降低所拥有的信息量. 那么如果这么说的话为什么还要做特征工程呢,这是因为该不等式有一个巨大的前提就是数 ...

  7. CodeForces 11D(状压DP 求图中环的个数)

    Given a simple graph, output the number of simple cycles in it. A simple cycle is a cycle with no re ...

  8. linux dig 命令

    dig 命令主要用来从 DNS 域名服务器查询主机地址信息. 查询单个域名的 DNS 信息 dig 命令最典型的用法就是查询单个主机的信息. $ dig baidu.com dig 命令默认的输出信息 ...

  9. Android 开发笔记___实战项目:购物车

    购物车的应用很广泛,电商app基本上都有它的身影.由于它用到了多种存储方式,通过项目对数据的存储有更高层次的了解. 1.设计思路 首先看看购物车的外观.第一次进入时里面是空的,去购物页面加入购物车以后 ...

  10. Android 开发笔记___登陆app

    package com.example.alimjan.hello_world; /** * Created by alimjan on 7/4/2017. */ import android.con ...