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

一步一步带你实现virtual dom(二)--Props和事件

要写你自己的虚拟DOM,有两件事你必须知道。你甚至都不用翻看React的源代码,或者其他的基于虚拟DOM的代码。他们代码量都太大,太复杂。然而要实现一个虚拟DOM的主要部分只需要大约50行的代码。50行代码!!

下面就是那两个你要知道的事情:

  • 虚拟DOM和真实DOM的有某种对应关系
  • 我们在虚拟DOM树的更改会生成另外一个虚拟DOM树。我们会用一种算法来比较两个树有哪些不同,然后对真实的DOM做最小的更改。

下面我们就来看看这两条是如何实现的。

生成虚拟DOM树

首先我们需要在内存里存储我们的DOM树。只要使用js就可以达到这个目的。假设我们有这样的一个树:

<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ur>

看起来非常简单对吧。我们怎么用js的对象来对应到这个树呢?

{ type: 'ul', props: {'class': 'list}, children: [
{type: 'li', props: {}, children: ['item 1']},
{type: 'li', props: {}, children: ['item 2']}
]}

这里我们会注意到两件事:

  • 我们使用这样的对象来对应到真实的DOM上:{type: '...', props: {...}, children: [...]}
  • DOM的文本节点会对应到js的字符串上。

    但是如果用这个方法来对应到巨大的DOM树的话那将是非常困难的。所以我们来写一个helper方法,这样结构上也就容易理解一些:
function h(type, props, ...children) {
return {type, props, children};
}

现在我们可以这样生成一个虚拟DOM树:

h('ul', {'class': 'list'},
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
)

这样看起来就清晰了很多。但是我们还可以做的更好。你应该听说过JSX对吧。是的,我们也要用那种方式。但是,这个应该如何下手呢?

如果你读过Babel的JSX文档的话,你就会知道这些都是Babel的功劳。Babel会把下面的代码转码:

<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>

转码为:

React.createElement('ul', {className: 'list'}),
React.createElement('li', {}, 'item 1'),
React.createElement('li', {}, 'item 2')
);

你注意到多相似了吗?如果把React.createElement(...)体换成我们自己的h方法的话,那我们也已使用类似于JSX的语法。我们只需要在我们的文件最顶端加这么一句话:

/** @jsx h */
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>

这一行/** @jsx h */就是在告诉Babel“大兄弟,按照jsx的方式转码,但是不要用React.createElement, 使用h。你可以使用任意的东西来代替h。

那么把上面我们说的总结一下,我们会这样写我们的虚拟DOM:

/** @jsx h */
const a = {
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>
};

然后Babel就会转码成这样:

const a = {
h('ul', {className: 'list'},
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
)
};

当方法h执行的时候,它就会返回js的对象--我们的虚拟DOM树。

const a = (
{ type: ‘ul’, props: { className: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
);

JSFiddle里运行一下试试

应用我们的DOM展示

现在我们的DOM树用纯的JS对象来代表了。很酷了。但是我们需要根据这些创建实际的DOM。因为我们不能只是把虚拟节点转换后直接加载DOM里。

首先我们来定义一些假设和一些术语:

  • 实际的DOM都会使用$开头的变量来表示。所以$parent是一个实际的DOM。
  • 虚拟DOM使用node变量表示
  • 和React一样,你只可以有一个根节点。其他的节点都在某个根节点里。

我们来写一个方法:createElement(),这个方法可以接收一个虚拟节点之后返回一个真实的DOM节点。先不考虑propschildren,这个之后会有介绍。

function createElement(node) {
if(typeof node === 'string') {
return document.createTextNode(node);
}
return document.createElement(node.type);
}

因为我们不仅需要处理文本节点(js的字符串),还要处理各种元素(element)。这些元素都是想js的对象一样的:

{ type: '-', props: {...}, children: [...]}

我们可以用这个结构来处理文本节点和各种element了。

那么子节点如何处理呢,他们也基本是文本节点或者各种元素。这些子节点也可以用createElement()方法来处理。父节点和子节点都使用这个方法,看到了么?其实这就是递归处理了。我们可以调用createElement方法来创建子节点,然后用appendChild方法来把他们添加到根节点上。

function createElement(node) {
if(typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}

看起来还不错,我们先不考虑节点的props。要理解虚拟节点的概念并不需要这些东西却会增加很多的复杂度。

处理修改

我们可以把虚拟节点转化为真实的DOM了。现在该考虑比较我们的虚拟树了。基本上我们需要写一点算法了。虚拟树的比较需要用到这个算法,比较之后只做必要的修改。

如何比较树的不同?

  • 如果新节点的子节点增加了,那么我们就需要调用appendChild方法来添加。
//new
<ul>
<li>item 1</li>
<li>item 2</li>
</ul> //old
<ul>
<li>item 1</li>
</ul>
  • 新节点比旧节点的子节点少,那么就需要调用removeChild方法来删除掉多余的子节点。
//new
<ul>
<li>item 1</li>
</ul> //old
<ul>
<li>item 1</li>
<li>item 2</li> // 这个要被删掉
</ul>
  • 新旧节点的某个子节点不同,也就是某个节点上发生了修改。那么,我们就调用replaceChild方法。
//new
<div>
<p>hi there!</p>
<p>hello</p>
</div> //old
<div>
<p>hi there!</p>
<button>click it</button> //发生了修改,变成了new里的<p />节点
</div>
  • 各节点都一样。那么我们就需要做进一步的比较
//new
<ul>
<li>item 1</li>
<li> //*
<span>hello</span>
<span>hi!</span>
</li>
</ul> //old
<ul>
<li>item 1</li>
<li> //*
<span>hello</span>
<div>hi!</div>
</li>
</ul>

加醒的两个节点可以看到都是<li>,是相等的。但是它的子节点里面却有不同的节点。

我们来写一个方法updateElement,它接收三个参数:$parentnewNodeoldNode$parent是真的DOM元素。它是我们虚拟节点的父节点。现在我们来看看如何处理上面提到的全部问题。

没有旧节点

这个问题很简单:

function updateElement($parent, newNode, oldNode) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}

没有新节点

如果当前没有新的虚拟节点,我们就应该把它从真的DOM里删除掉。但是,如何做到呢?我们知道父节点(作为参数传入了方法),那么我们就可以调用$parent.removeChild方法,并传入真DOM的引用。但是我们无法得到它,如果我们知道的节点在父节点的位置,就可以用$parent.childNodes[index]来获取它的引用。index就是节点的位置。

假设index也作为参数传入了我们的方法,我们的方法就可以这么写:

function updateElement($parent, newNode, oldNode, index = 0) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode);
);
} else if(!newNode) {
$parent.removeChild(
$parent.childNodes[index];
);
}
}

节点改变

首先写一个方法来比较两个节点(新的和旧的)来区分节点是否发生了改变。要记住,节点可以是文本节点,也可以是元素(element):

function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === 'string' && node1 !== node2 ||
node1.type !== node2.type;
}

现在有了当前节点的index了,index就是当前节点在父节点的位置。这样可以很容易用新创建的节点来代替当前节点了。

function updateElement($parent, newNode, oldNode, index = 0) {
if(!oldNode) {
$parent.appendChild(
createElement(newNode);
);
} else if(!newNode) {
$parent.removeChild(
$parent.childNOdes[index];
);
} else if(chianged(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}

对比子节点的不同

最后,需要遍历新旧节点的子节点,并比较他们。可以在每个节点上都使用updateElement方法。是的,递归。

但是在开始代码之前需要考虑一些问题:

  • 只有在节点是一个元素(element)的时候再去比较子节点(文本节点不可能有子节点)。
  • 当前节点作为父节点传入方法中。
  • 我们要一个一个的比较子节点,即使会遇到undefined的情况。没有关系,我们的方法可以处理。
  • index,当前节点在直接父节点中的位置。
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) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}

JSFiddle里看看代码把!

结语

祝贺你!我们搞定了。我们写出了虚拟节点的实现。从上面的例子中你已经可以理解虚拟节点的概念了,也大体可以知道React是如何运作的了。

当时还有很多需要讲述的内容,其中包括:

  • 设置节点的属性(props)和比较、更新他们
  • 处理事件,在元素上添加事件监听器
  • 让我们的节点像React的Component那样运作
  • 获取实际DOM的引用
  • 虚拟节点和其他的库一起使用来修改真实的DOM,这些库有jQuery等其他的类似的库。
  • 更多。。

原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

一步一步带你实现virtual dom(一)的更多相关文章

  1. 一步一步带你实现virtual dom(二) -- Props和事件

    很高兴我们可以继续分享编写虚拟DOM的知识.这次我们要讲解的是产品级的内容,其中包括:设置和DOM一致性.以及事件的处理. 使用Babel 在继续之前,我们需要弥补前一篇文章中没有详细讲解的内容.假设 ...

  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. web项目各个clean

    project clean:清楚tomcat下的已编译的java类.class文件,包括js但不包括jsp server clean:clean tomcat work dictionary:清除to ...

  2. CopyOnWriteArrayList集合排序异常问题

    1.集合自定义排序实现 对List集合的自定义排序想必大家都知道要使用如下的方式,通过实现Comparator接口并实现compare方法来实现. /** * * @方法名 changeChain * ...

  3. Execption:the database returned no natively generated identity value

    org.hibernate.HibernateException: The database returned no natively generated identity value at org. ...

  4. apache编译安装参数说明

    apache编译安装参数说明 ./configure //配置源代码树--prefix=/usr/local/apache2 //体系无关文件的顶级安装目录prefix ,也就apache的安装目录. ...

  5. 【转】Linux Oracle服务启动&停止脚本与开机自启动

    在CentOS 6.3下安装完Oracle 10g R2,重开机之后,你会发现Oracle没有自行启动,这是正常的,因为在Linux下安装Oracle的确不会自行启动,必须要自行设置相关参数,首先先介 ...

  6. 4个强大的Linux服务器监控工具[转]

    本文介绍了一些可以用来监控网络使用情况的Linux命令行工具.这些工具可以监控通过网络接口传输的数据,并测量目前哪些数据所传输的速度.入站流量和出站流量分开来显示. 一些命令可以显示单个进程所使用的带 ...

  7. phpstorm中配置真正的远程调试(xdebug)

    这里说的是真正的远程调试,不是本地,本地不需要安装任何php程序!!! 这里略去xdebug的安装,安装很简单可以下载源码包,动态编译进去! 环境: Dev 服务器(IP:192.168.2.100) ...

  8. iOS-image图片压缩

    ///压缩图片 + (NSData *)imageCompressToData:(UIImage *)image{ NSData *data=UIImageJPEGRepresentation(ima ...

  9. HTML图片上下之间的空隙是什么原因

    在这个问题上,<权威指南>该书第三版第 146 页有明确说到: 如果一个垂直元素没有基线——也就是说,这是一个图像或表单输入元素,或者其它替换元素——那么该元素低端与其父元素的基线对齐.这 ...

  10. UOJ Round #15 [构造 | 计数 | 异或哈希 kmp]

    UOJ Round #15 大部分题目没有AC,我只是水一下部分分的题解... 225[UR #15]奥林匹克五子棋 题意:在n*m的棋盘上构造k子棋的平局 题解: 玩一下发现k=1, k=2无解,然 ...