本文由Craig Bilner和Bruno Mota进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
React的声明性组件和虚拟DOM渲染已席卷了前端开发领域,但这并不是唯一基于这些思想的库。 今天,我们将探讨在其他三个类似React的替代方案中构建应用程序的感觉。
我们假设您已经熟悉React及其在生态系统中使用的术语。 如果您需要抓紧时间或只是重新整理,请查阅我们之前的文章之一 。
总览
让我们开始对我们将要比较的库进行高级概述。
德库(2.0.0-rc15)
Deku的目标是成为React的更多功能替代品。 它可以防止组件具有局部状态,从而可以将所有组件编写为与外部状态管理解决方案(例如Redux)通信的纯函数。
预先(4.1.1)
Preact是尝试使用尽可能少的代码来模仿React的核心功能。 假设您将使用ES2015,Preact将采用一些快捷方式并精简React的原始功能集,以产生一个仅3KB的微型库。
虚拟DOM(2.1.1)
在React,Deku和Preact为您提供虚拟DOM之上的组件抽象的地方,virtual-dom包为您提供了您自己创建,比较和呈现虚拟DOM节点树所需的较低级工具。 ( 这与构建React和Preact的虚拟DOM不同! )
像Virtual-DOM这样的低级库似乎可以替代React,但是如果您有兴趣编写高效的移动Web体验,那么观看Pocket大小的JS是一个不错的起点。 实际上,这是我们将Virtual-DOM包含在比较中的原因。
我们将使用这些库中的每一个来构建组件,构建数据流并最终查看每个应用程序的大小和性能。
组件
这是一个React组件,它将使用标记的库来呈现一些Markdown。
import React from 'react';
import marked from 'marked';
const Markdown = React.createClass({
propTypes: {
text: React.PropTypes.string
},
getDefaultProps() {
return { text: '' };
},
render() {
return (
<div
dangerouslySetInnerHTML={{
__html: marked(this.props.text)
}}>
</div>
);
}
});
我们正在使用道具验证来让组件在收到错误类型的道具时警告我们。 它还实现了getDefaultProps()
方法,该方法允许我们在没有传入任何值的情况下为组件提供默认值。最后,我们实现了render方法,该方法返回此组件的用户界面。
为了防止React在渲染Markdown时逃脱它,我们需要将其传递给危险的SetInnerHTML属性。
德库
接下来,我们将使用Deku实现相同的组件。
/** @jsx element */
import { element } from 'deku';
import marked from 'marked';
const Markdown = {
render({ props: { text='' } }) {
return <div innerHTML={marked(text)}></div>;
}
};
第一行是编译器编译指示,告诉编译器将像<h1>Hello</h1>
这样的JSX转换为element('h1', null, 'Hello')
而不是React.createElement('h1', null, 'Hello')
,这使我们可以将JSX与Deku结合使用,而不是React。 也可以使用.babelrc文件配置此选项。
与React相比,我们的Deku组件绝对简单。 Deku组件没有可以引用的实例, this
意味着该组件可能需要的所有数据将作为称为model
的对象传递到方法中。 该对象包含组件的props
,我们可以使用解构语法提取text
道具。
Deku没有prop验证,但是我们至少可以通过在这些解构分配中提供默认值来模拟getDefaultProps()
。
事前
接下来是Preact。
/** @jsx h */
import { h, Component } from 'preact';
import marked from 'marked';
class Markdown extends Component {
render() {
const { text='' } = this.props;
return (
<div
dangerouslySetInnerHTML={{
__html: marked(text)
}}>
</div>
);
}
}
同样,我们需要告诉编译器将JSX转换为Preact可以理解的东西。 Preact组件与React的ES2015类组件非常相似,并且我们能够复制之前的大多数渲染代码。 像Deku一样,Preact不支持道具验证或默认属性,但是我们可以再次使用解构分配模拟默认道具。
虚拟DOM
最后,我们来看一下Virtual-DOM。
/** @jsx h */
import { h } from 'virtual-dom-util';
import marked from 'marked';
function Markdown({ text='' }) {
return <div innerHTML={marked(text)}></div>;
}
我们没有提供任何用于构造组件的工具,因此您在这里看不到诸如this
, props
或state
构造。 实际上,这些“组件”只是返回虚拟DOM节点树的函数。
创建虚拟DOM节点的本机方式与JSX不兼容,因此我们使用virtual-dom-util
包为我们提供了与JSX兼容的替代方案。 在渲染组件之前,我们实际上不需要导入virtual-dom
软件包。
渲染组件
接下来,我们将研究如何将组件呈现到DOM中。 所有这些库都渲染到目标节点中,因此我们将在HTML文件中创建一个。
<div id="app"></div>
反应
import { render } from 'react-dom'
render(
<Markdown text='Hello __world__' />,
document.getElementById('app')
);
要渲染React组件,我们需要使用react-dom
包,该包提供了一个render
功能,该功能可以理解如何将React组件树转变为DOM节点树。
为了使用它,我们传递了一个React组件的实例和一个对DOM节点的引用。 ReactDOM处理其余的事情。
德库
/** @jsx element */
import { createApp, element } from 'deku';
const render = createApp(
document.getElementById('app')
);
render(
<Markdown text='Hello __world__' />
);
Deku呈现组件的方式略有不同。 由于Deku组件不是有状态的,因此它们不会自动重新渲染。 取而代之的是,我们使用createApp()
围绕DOM节点构建渲染函数,每次外部状态更改时,我们都可以调用该函数。
现在,我们可以传递Deku组件的实例以在该节点中渲染它们。
事前
/** @jsx h */
import { h, render } from 'preact';
render(
<Markdown text='Hello __world__' />,
document.getElementById('app')
);
Preact为我们提供了一个类似的接口,用于将组件渲染到DOM节点中,但是与ReactDOM不同,它位于核心Preact包中。 像Preact API一样,没有什么新鲜的东西可以学习,而且React的概念也很容易移植。
虚拟DOM
/** @jsx h */
import { create } from 'virtual-dom';
import { h } from 'virtual-dom-util';
const tree = <Markdown text='Hello __world__' />;
const root = create(tree);
document
.getElementById('app')
.appendChild(root);
虚拟DOM使我们在创建和使用组件方面具有更大的灵活性。 首先,我们创建一个虚拟树的实例,并使用create
函数将其实现为DOM节点。 最终,我们可以随意使用任意方式将此子级添加到DOM中。
数据流
在我们正在考虑的三个库中,有两种不同的方法来管理应用程序状态。
内
与React一样,Preact也允许组件管理自己的状态。
每个组件都跟踪对不变状态对象的引用,该引用可以通过称为setState的特殊组件方法进行更新。 调用此函数时,组件将假定已进行了某些更改并尝试重新渲染。 从状态已更新的组件接收支持的任何组件也将重新呈现。
Preact还为我们提供了一种机制,该机制可以使用shouldComponentUpdate的形式细化控件来覆盖默认行为。
外
Deku做出了将状态管理移到组件之外的刻意决定,而Virtual-DOM级别太低,无法与状态之类的抽象相关。 这意味着,如果要使用它构建应用程序,则需要将状态保留在其他位置。
在这种情况下,我们的状态将移到外部容器中,根组件使用该外部容器为应用程序的其余部分提供数据。 每次状态容器更新时,我们都需要重新渲染整个应用程序。
要更新状态,组件必须与状态容器通信更改。 在类似助焊剂的系统中,这种交流通常以动作的形式出现。
重要的是要记住,尽管React和Preact支持组件的本地状态,但它们也可以与外部状态管理解决方案一起使用。
应用结构
本节将探讨我们如何将有关状态,数据流和重新呈现的这些想法实现为实际代码。 在此过程中,我们将把Markdown
组件构建到实时Markdown编辑器中。 您可以在下一部分中看到完成组件的演示。
德库
Deku应用程序通常由两个主要部分组成: 组件树和store 。
我们将Redux用作商店,因为它可以与Deku很好地配合使用。 树中的组件将分派动作,我们的Redux缩减程序将使用它们来更改状态,并且只要状态发生变化,我们将使用订阅机制重新呈现组件树。
首先,我们将建立一个简单的Redux存储。
import { createStore } from 'redux';
const initState = { text: '' };
const store = createStore((state=initState, action) => {
switch(action.type) {
case 'UPDATE_TEXT':
return { text: action.payload };
default:
return state;
}
});
无需赘述,Redux存储由一个reducer函数构建,该函数以当前状态和一个动作作为参数。 该函数应基于操作中的数据返回新状态。
现在,我们将重新访问渲染代码,以使Deku知道我们的Redux商店。
const render = createApp(
document.getElementById('app'),
store.dispatch
);
因为Deku希望您使用外部状态管理解决方案,所以它的createApp
函数接受调度函数作为第二个参数。 反过来,Deku将为其所有组件提供此分发功能,以便它们可以与Redux商店对话。
我们还将把商店的当前状态传递给render函数。 Deku会将此值作为context
提供给每个组件,从而允许从存储中读取树中的任何组件。
render(
<MarkdownEditor />,
store.getState()
);
我们可以使用store.subscribe()
方法侦听状态更改,以便我们可以重新渲染组件树。
store.subscribe(() => {
render(
<MarkdownEditor />,
store.getState()
);
});
要更新状态,组件应将动作传递给它们的调度功能。 但是,在组件内部创建动作很容易导致组件代码膨胀,因此,我们将创建中间人函数来为我们调度参数化的动作。 这些功能通常被称为“动作创建者”。
const actions = {
updateText: dispatch => text => {
dispatch({
type: 'UPDATE_TEXT',
payload: text
});
}
};
动作创建者采用一个调度功能和一个参数,然后使用它们创建和调度一个适当的动作对象。 为了方便起见,我们正在设计动作,使其符合Flux标准动作 。
为了完全结合起来,我们的组件将从context
读取状态,并使用新的动作创建器来分派动作。
const MarkdownEditor = {
render({ context, dispatch }) {
return (
<main>
<section>
<label>Markdown</label>
<hr />
<Editor onEdit={actions.updateText(dispatch)} />
</section>
<section>
<label>Preview</label>
<hr />
<Markdown text={context.text} />
</section>
</main>
);
}
};
事前
呈现Preact组件后,它将通过侦听对其内部状态的更改来管理自己的重新呈现。
import { Component } from 'preact';
import { bind } from 'decko';
class MarkdownEditor extends Component {
constructor() {
super()
this.state = { text: '' };
}
@bind
onEdit(text) {
this.setState({ text });
}
render() {
return (
<main>
<section>
<label>Markdown</label>
<hr />
<Editor onEdit={this.onEdit} />
</section>
<section>
<label>Preview</label>
<hr />
<Markdown text={this.state.text} />
</section>
</main>
);
}
}
我们使用构造函数初始化此组件的状态。 然后,我们创建一个onEdit
方法,用于基于参数更新状态。 您可能还会注意到,我们在这里使用了@bind
装饰器。
这个装饰器来自一个叫做Decko (不是Deku!)的库,我们正在使用它来确保onEdit
方法具有正确的this
值,即使从组件外部调用它也是如此。
最后,我们将this.state.text
作为道具传递给我们的<Markdown />
组件。 每次调用onEdit
回调时,我们都会更新状态,组件将重新呈现。
虚拟DOM
与React,Deku和Preact不同,Virtual-DOM不假设您如何管理状态或虚拟节点在何处接收其数据。 这意味着我们将不得不做一些额外的工作来进行设置。
值得庆幸的是,Redux的定义还不够完善,我们也可以在这里使用它。 实际上,我们可以借鉴Deku示例中的代码来创建商店。
import { createStore } from 'redux';
const store = createStore((state = initState, action) => {
switch (action.type) {
case 'UPDATE_TEXT':
return {
text: action.payload
};
default:
return state;
}
});
与其将商店的调度功能传递给我们的组件,不如直接从我们的动作创建者那里引用它。
const actions = {
updateText(text) {
store.dispatch({
type: 'UPDATE_TEXT',
payload: text
});
}
}
这可能比我们其他动作创建者更简单,但是由于他们对Redux商店有着不可理解的依赖性,因此使他们更加难以隔离和测试。
我们将初始状态传递给组件进行第一次渲染。
let tree = <MarkdownEditor state={store.getState()} />;
let root = create(tree);
document
.getElementById('app')
.appendChild(root);
然后,我们将使用订阅机制来监听状态更改。
import { diff, patch } from 'virtual-dom';
store.subscribe(function() {
let newTree = <MarkdownEditor state={store.getState()} />;
let patches = diff(tree, newTree);
root = patch(root, patches);
tree = newTree;
});
我们不是手动渲染新树,而是手动执行diff,然后使用返回的补丁集应用必要的最小数量的更改,以使渲染的DOM节点反映newTree
的虚拟DOM节点。
最后,我们覆盖我们的旧树,为下一个渲染做好准备。
演示版
我们将这些组件放在一起,并为每个框架创建了一个简单的分屏实时Markdown编辑器。 您可以在Codepen上查看代码并与完成的编辑器一起玩。
尺寸
当我们开发旨在用于台式机和移动设备的轻量级应用程序时,选择视图层时,必须从服务器传输的数据量是一个重要因素。
在每种情况下,我们都将创建一个包含我们的应用程序代码和依赖项的缩小包,以进行比较。
4.反应
- 代码行数 :61
- 依赖关系 :
react
,react-dom
,marked
- 捆绑包大小 :154.1kb
- 压缩后 :45.3kb
根据React团队的建议,我们使用的是React的预构建生产版本,而不是自己最小化。 独立的Marked缩小版本的大小约为17kb。 最小版本的React和ReactDOM的总时钟约为136kb。
3.德库
- 代码行数 :80
- 依赖项 :
deku
,redux
,marked
- 捆绑包大小 :51.2kb
- 压缩后 :15.3kb
我们的Deku捆绑软件已经比React轻了100kb,并且我们还包括了Redux形式的功能完善的状态管理器。 Redux和Marked的总重约为30kb。 将我们的应用程序代码和对Deku的依赖保留为〜21kb。
2.虚拟DOM
- 代码行数 :85
- 依赖项 :
virtual-dom
,virtual-dom-util
,redux
,已marked
- 捆绑包大小 :50.5kb
- 压缩后 :15.2kb
尽管具有最低限度的低级性质,但我们的Virtual-DOM捆绑包的重量约为50kb(与Deku大致相同)。 同样,Redux和Marked负责该大小的〜30kb。 虚拟域软件包和负责约20kb的应用程序代码一起。
1.预言
- 代码行数 :62
- 依赖关系 :
preact
,decko
,marked
- 捆绑包大小 :30.6kb
- 压缩后 :10.5kb
忠实于其目标,我们的Preact捆绑包令人印象深刻,达到30.6kb。 Decko和Marked共同负责其中的〜19kb,而Preact和我们的应用程序代码仅占11kb。
性能
对于移动网络,我们应该同样意识到,并非所有移动设备处理器都是相同的。 我们将看一看我们的应用程序将其第一帧显示到屏幕上的速度。
4.反应
浏览器在30毫秒左右开始评估JavaScript。 然后,重新计算的风格,回流和更新,以层树后,我们在173.6ms得到油漆事件,那么层复合,最后在183ms浏览器中的第一帧土地。 因此,我们正在考虑大约150ms的周转时间。
3.德库
浏览器大约在55毫秒左右开始评估JavaScript。 然后,我们看到相同的样式重新计算,重排和图层树更新,然后在111ms处看到绘画事件,将图层进行合成,并且第一帧在118ms处着陆 。 Deku将React的周转时间减少了一半以上,将其缩短到大约70ms。
2.预言
我们看到浏览器开始在大约50毫秒处评估脚本,并且绘制事件出现在86.2毫秒处,并且第一帧降落在102毫秒处,周转时间为50毫秒。
1.虚拟DOM
浏览器在32ms时开始评估,绘制事件在80.3ms时着陆(有趣的是,与其他框架相比,浏览器花费多于10倍的时间来合成图层),然后框架在89.9ms时着陆 。 周转时间接近60毫秒。 因此,尽管Virtual-DOM具有最快的成帧时间,但其渲染过程似乎比Preact慢。
当然,我们在这里看到的是微观性能,总体而言,所有这些库都非常快(对于此应用程序而言)。 他们都在200毫秒内在屏幕上显示了第一帧。
这些测试结果也是在Chromebook而非移动设备上捕获的,因此它们仅用于比较这些库之间的相对性能。
您可以在GitHub上找到这些测试的代码。
结论
React改变了我们开发应用程序的思维方式。 没有React,我们将不会有这些出色的替代方案,并且在生态系统,工具和社区方面,它仍然是无可争议的。
npm上已经有成百上千个甚至数千个React软件包可用,并且ReactJS社区组织围绕20多个高质量的开源项目创建,以确保它们得到长期的支持和维护。
React满足了我们在其他库中看到的大多数编程风格。 如果您想将状态转移到Redux这样的商店中并使用无状态组件,React将允许您这样做。 同样,React也支持功能性无状态组件 。
该库本身已经过实战测试, 大量先进技术公司 (包括Facebook)在生产中使用React,npm软件包每周获得数十万次下载。
但是我们在这里考虑使用React的替代方法。 因此,让我们看看您可能想考虑使用其他库的位置,时间,地点和原因。
德库
如果Redux是您工作流程的重要组成部分,那么您可能要尝试使用Deku。 它的重量更轻,并且(在我们的案例中)比React更快一点,采用了一种自以为是的方法,它可以削减很多原始功能集。
Deku非常适合希望React实施更多功能风格的程序员。
虚拟DOM
虚拟DOM非常适合构建您自己的抽象。 它提供的开箱即用的工具不足以构造完整的应用程序,并且遗憾的是它默认情况下不支持JSX,但是这些品质使其非常适合作为不适用于高级抽象的目标反应自己。
对于希望使用基于声明的,基于组件的模型而又不必担心被DOM操作弄伤手的语言开发人员来说,虚拟DOM仍将是一个不错的目标。 例如,它目前已作为Elm的一部分发挥了巨大作用。
事前
精确是这里的惊喜。 它不仅捆绑在最小的应用程序中,而且周转率极低,无法将帧显示在屏幕上。
它是轻量级的,具有一个很小但正在增长的生态系统,并且越来越多的React软件包可以与Preact一起批量使用。 无论您是要构建高性能应用程序,还是需要通过低速网络连接传送页面的页面,Preact都是一个值得关注的好项目。
From: https://www.sitepoint.com/react-alternatives-preact-virtualdom-deku/