一步一步带你实现virtual dom(一)
一步一步带你实现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’] }
] }
);
应用我们的DOM展示
现在我们的DOM树用纯的JS对象来代表了。很酷了。但是我们需要根据这些创建实际的DOM。因为我们不能只是把虚拟节点转换后直接加载DOM里。
首先我们来定义一些假设和一些术语:
- 实际的DOM都会使用
$
开头的变量来表示。所以$parent
是一个实际的DOM。 - 虚拟DOM使用node变量表示
- 和React一样,你只可以有一个根节点。其他的节点都在某个根节点里。
我们来写一个方法:createElement()
,这个方法可以接收一个虚拟节点之后返回一个真实的DOM节点。先不考虑props
和children
,这个之后会有介绍。
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
,它接收三个参数:$parent
、newNode
和oldNode
。$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(一)的更多相关文章
- 一步一步带你实现virtual dom(二) -- Props和事件
很高兴我们可以继续分享编写虚拟DOM的知识.这次我们要讲解的是产品级的内容,其中包括:设置和DOM一致性.以及事件的处理. 使用Babel 在继续之前,我们需要弥补前一篇文章中没有详细讲解的内容.假设 ...
- 抛开react,如何理解virtual dom和immutability
去年以来,React的出现为前端框架设计和编程模式吹来了一阵春风.很多概念,无论是原本已有的.还是由React首先提出的,都因为React的流行而倍受关注,成为大家研究和学习的热点.本篇分享主要就聚焦 ...
- 如何实现一个 Virtual DOM 及源码分析
如何实现一个 Virtual DOM 及源码分析 Virtual DOM算法 web页面有一个对应的DOM树,在传统开发页面时,每次页面需要被更新时,都需要手动操作DOM来进行更新,但是我们知道DOM ...
- Virtual DOM的简单实现
了解React的同学都知道,React提供了一个高效的视图更新机制:Virtual DOM,因为DOM天生就慢,所以操作DOM的时候要小心翼翼,稍微改动就会触发重绘重排,大量消耗性能. 1.Virtu ...
- [翻译]Review——The Inner Workings Of Virtual DOM
The Inner Workings Of Virtual DOM 虚拟DOM的内部工作机制 原文地址:https://medium.com/@rajaraodv/the-inner-workings ...
- 【转】Virtual DOM
前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常多的介绍 React.Virtual DOM 的文章.但是直到前不久我专门花时间去学习 ...
- 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 ...
- 如何理解Virtual DOM
什么是虚拟DOM 接下来用vdom(Virtual DOM)来简称为虚拟DOM. 指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做.换而言之,虚拟DOM就是JS对象.如下DOM结构: & ...
- Virtual DOM 简直就是挥霍
彻底澄清"Virtual DOM 飞快"的神话. 注意:原文发表于2018-12-27,随着框架不断演进,部分内容可能已不适用. 近年来,如果你有使用过 JavaScript 框架 ...
随机推荐
- 【转】shell学习笔记(一)——学习目的性、特殊字符、运算符等
1 学习shell的目的性 写之前我们先来搞清楚为什么要学shell,学习要有目的性 shell简单.灵活.高效,特别适合处理一些系统管理方面的小问题 shell可以实现自动化管理,让系统管理员的工作 ...
- Java NIO 之 Buffer
Java NIO 之 Buffer Java NIO (Non Blocking IO 或者 New IO)是一种非阻塞IO的实现.NIO通过Channel.Buffer.Selector几个组件的协 ...
- 【视频编解码·学习笔记】4. H.264的码流封装格式
一.码流封装格式简单介绍: H.264的语法元素进行编码后,生成的输出数据都封装为NAL Unit进行传递,多个NAL Unit的数据组合在一起形成总的输出码流.对于不同的应用场景,NAL规定了一种通 ...
- OpenStreetMap数据清洗(SQL&MonogoDB版本)
目标:通过网上下载的OpenStreetMap.xml数据格式,将该文件的格式进行统计,清洗,并导出成CSV格式的文件,最后倒入到SQLite中 本案例中所需的包 import csv import ...
- 精通libGDX-RPG开发实战
从今天开始,我会陆陆续续做一个五脏俱全的rpg小品游戏. 素材使用<圣剑英雄传II>的素材 游戏名称< Inspiration > 教程目录(暂定): Chapter 1: 开 ...
- ASP.NET MVC 5 ABP DataTables (二)
1)ABP DataTables 应用(一) 2) ABP DataTables 应用(二) JS DataTables 这个组件绑定数据必须要有自己的返回数据格式.但是ABP返回的格式直接绑定是错 ...
- SpringMVC源码情操陶冶-HandlerAdapter适配器简析
springmvc中对业务的具体处理是通过HandlerAdapter适配器操作的 HandlerAdapter接口方法 列表如下 /** * Given a handler instance, re ...
- 利用回调实现Java的异步调用
异步是指调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通知调用者,或通过回调函数处理这个调用. 回调简单地说就是B中有一个A,这样A在调用B的某个方法时实际上是调用到了自己的方法. 利 ...
- Java随感
创新项目要用java,而我只大概会C++,只能靠自学咯~~~随时将一些重要的概念做笔记在这里吧>_< 1.一个源文件中只能有一个public类,一个源文件可以有多个非public类 2.所 ...
- ES6字符串
1.unicode表示法: alert("\u0061"); alert("\uD842\uDFB7"); alert("\u20BB7") ...