一步一步带你实现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. jquery取前、后、父、子元素

    前.prev(); 后.next(); 父.parent(); 子.children(); 注意:前的前是.prev().prev(),例如前元素无i,但前的前的i元素有i,不能写成.prev('i' ...

  2. gd库的安装

    gd库简介 主要用途编辑 在网站上GD库通常用来生成缩略图,或者用来对图片加水印,或者用来生成汉字验证码,或者对网站数据生成报表等.在PHP处理图像,可使用GD库,而GD库开始时是支持GIF的,但由于 ...

  3. matlab获取文件夹中的所有文件名(dir)

    当前目录中包含文件及目录如下: abc111.txt abc112.txt abc113.txt a\ (文件夹) CODE: >> dir('test')   %目录 .         ...

  4. 关于MyEclipse启动报错:Error starting static Resources;下面伴随Failed to start component [StandardServer[8005]]; A child container failed during start.的错误提示解决办法.

    最后才发现原因是Tomcat的server.xml配置文件有问题:apache-tomcat-7.0.67\conf的service.xml下边多了类似与 <Host appBase=" ...

  5. Node.js:上传文件,服务端如何获取文件上传进度

    内容概述 multer是常用的Express文件上传中间件.服务端如何获取文件上传的进度,是使用的过程中,很常见的一个问题.在SF上也有同学问了类似问题<nodejs multer有没有查看文件 ...

  6. 面试中的TCP协议

    TCP的三次握手和四次挥手 三次握手 TCP连接是通过三次握手来连接的. 第一次握手 当客户端向服务器发起连接请求时,客户端会发送同步序列标号SYN到服务器,在这里我们设SYN为x,等待服务器确认,这 ...

  7. VS工程中的Windows.h

    才发现这个Windows.h是有些奥秘的,不是随便引用就可以的. 1,C++工程,头文件引用要讲求一定顺序.如果cpp文件先引用a.h,再引用b.h,则后者自动包含a.h.这一点很重要. 2,Wind ...

  8. iOS-电子书开发 笔记

    前言 刚接手电子书项目时,和安卓开发者pt Cai老师[aipiti Cai,一个我很敬佩很资深的开发工程师,设计领域:c++.Java.安卓.QT等]共同商议了一下,因为项目要做要同步,移动端[手机 ...

  9. 《Thinking in Java》学习笔记(二)

    1.Java方法的重载 如果几个方法都有相同的名字,区分它们就需要重载来完成. 构成重载的种类: 1)形参的个数不同  2) 形参的类型不同 3) 形参的顺序不同 注意,返回值.抛出的异常和修饰符的不 ...

  10. 洛谷 [P1119] 灾后重建

    我们发现每次询问都是对于任意两点的,所以这是一道多源最短路径的题,多源最短路径,我们首先想到floyd,因为询问的时间是不降的,所以对于每次询问,我们将还没有进行松弛操作的的点k操作. #includ ...