[译]React为何在前端开发中大放异彩

原文: Yes, React is taking over front-end development. The question is why.

作者: Samer Buna

原文发布时间: 2017.03.31

译文同时发布于: icyfish’s Blog

译文

本文的重点是阐述React受欢迎的原因, 并不是它与其他框架或库的比较.

React为何在短时间内就获得了那么高的关注度, 下面是其中的一些原因:

  • 使用JavaScript进行前端开发, 不可避免要与浏览器的DOM打交道, 而DOM的API却异常难用. React提出了虚拟浏览器的概念, 虚拟浏览器就像是开发者和浏览器之间的一个代理人, 开发者使用React进行开发时不需要直接处理真实浏览器, 而是处理虚拟浏览器, 使得开发更加快速且友好.
  • React使开发者能够以声明的形式描述用户界面(UI)并模型化描述这些UI的数据. 这样的话, 开发者只需根据最终的state来描述UI(声明一个函数), 不用再费力使用许多步骤描述UI上的数据. 当state发生变化时, React会根据其变化直接处理UI的渲染.
  • 可以说React就是JavaScript, 如果对JavaScript有一定了解, 想要上手React需要学习的API并不多, 仅需要理解几个函数和相关的用法. 理解了React的API之后, 你对JavaScript了解得越多, React编程的能力也越高. 两者之间没有太大的阻碍. 对JavaScript开发者来说, 在1个多小时内成为一名有效率的React开发者应该不在话下.

当然, React的成功不仅仅因为是上述的几个因素. 接下来我们试试看是否能列出所有的原因. 其中有一个重要因素就是虚拟DOM的概念(React的协调算法). 之后会有一个例子用以说明协调算法能给开发带来多少实际效益.

官方的对React的定义是这样的: 渲染UI的JavaScript库. 定义中有两个重点需要把握.

  1. React是库, 不是框架. 它并不是一整套基于MVC(或MVVM, MVP等)的解决方案, React只专注于一方面的任务, 并做得十分出色. React常常和其他库配合使用.
  2. 在构建UI方面, React也做的十分出色. UI指的是开发者所构建的一个界面, 用户在界面上执行操作与设备交互. 到处都是UI, 从一个简单的微波炉按钮, 到航天飞机上的仪表盘. 如果我们需要构建界面的设备理解JavaScript语言, 我们就能利用React描述UI.

浏览器能够理解JavaScript, 因此利用React描述UI毫无问题. 在这里我用了描述一词, 而不是构建, 因为开发者实际上只是告诉React我们所需要的界面是怎样的, 真正构建界面的是React. 如果没有React或与其相类似的库, 开发者就要使用Web API(DOM API)和JavaScript自己构建UI.

“React是声明式的”, 这句话的意思不言自明, 我们用React描述界面, 告诉它我们想要的界面是怎样的, 并非告诉它如何构建, 因为React了解我们的需求之后, 就知道应该如何构建所需的页面, 将我们用React描述的需求转化成真正渲染在浏览器上的视图. React与HTML结合使用功能十分强大, 使用React描述页面时, 我们声明的HTML界面还能够表示动态数据, 不仅仅是静态数据.

React如此受欢迎还因为以下三种设计理念:

1.运用可复用, 可组合, 包含状态的组件

在React中, 我们使用组件(component)来描述UI, 可以将组件看做简单的函数. 利用一些输入调用该函数, 得到函数给我们的输出. 有需要时可以复用这些函数, 同时还能将它们组合起来创建更复杂的组件.

组件几乎都相同, 在React中组件获取输入是通过属性(property)或状态(state)的形式, 组件的输出就是一部分UI的描述(相当于HTML之于浏览器). 我们可以在多个UI中复用同一个组件, 组件中可以包含其他组件.

和纯函数不同, 完整的React组件可以包含私有状态来存储实时变化的数据.

2.根据状态变化实时更新的特性

关于这一个设计理念, 从React这个名字中就可看出. 当组件的状态通过输入(props或state)发生变化时, 它所表示的UI(输出)就会随之发生变化. UI的描述若发生变化, 其结果必须在设备中发生实时变化.

在浏览器中, 完成这样的实时变化必须在DOM中重新渲染HTML视图. 有了React, 考虑如何, 何时渲染这些变化就不是开发者需要伤脑筋的事了, React会对状态的更新做出反应并自动更新DOM.

3.在内存中以虚拟的形式表现视图

在React中, 我们使用JavaScript写HTML(JSX). 用JavaScript根据一些数据生成HTML, 而不是扩展HTML的功能使其与数据交互. 扩展HTML是许多其他框架常用的方法. 比如Angular, 就给HTML扩展了循环, 条件语句等功能.

当我们在后台利用AJAX接受来自服务器的数据时, 仅仅使用HTML是无法处理这些数据的, 要不就是使用扩展了一些功能的HTML, 要不就像React一样利用JavaScript生成HTML. 这两种方法都有其优劣. React团队认为第二种方法的优势大于劣势, 因此在React中采取了第二种方法.

有个重要的优势表明第二种方法是更好的选择, 即利用JavaScript生成HTML使React能更方便地在内存中以虚拟的形式表现视图(即我们常听说的虚拟DOM). 使用虚拟DOM的react渲染HTML树的速度极快, 一旦状态发生改变, React就会提供一个新的HTML树渲染到浏览器的真实DOM中, 而这个更新并不是完全重新渲染, 而是只更新变化的那一部分, 因为React把变化前后的数据都保存在内存中, 因此只更新变化的那一部分是可行的. 这个更新的过程叫做Tree Reconciliation, 我认为这是自AJAX以来Web开发领域的一个里程碑式发明.

在以下的例子中, 我们会专注于最后一个设计理念, 通过一个实例分析tree reconciliation过程, 从中理解这个概念带来的巨大差异. 在这个实例中我们会利用两个完全相同的HTML模板, 第一个使用原生的Web API以及纯JS, 另一个使用React来描述视图.

因为只针对最后一个理念进行试验, 试验的具体内容是创建一个计时器, 在React实例中不使用组件, 也不会使用JSX语法, 虽然JSX语法使代码变得更简单. 在React编程中, 我一直使用JSX语法, 但在这个情况下, 直接使用React API能将概念阐述得更清楚.

React协调算法实例

你可以用浏览器和编辑器跟着以下的步骤自己进行操作, 网页编辑器也可以, 我会在本地建立文件使用浏览器直接进行测试(不需要用到web服务器):

现在我们从零开始构建这个实例, 先创建一个文件夹, 用你最喜欢的编辑器打开这个文件夹:

mkdir react-demo
cd react-demo
atom .

在文件夹内创建index.html, 在里面添加标准的HTML模板, 同时在HTML中引入script.js文件, 在js文件中声明console.log语句测试是否引入成功:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Demo</title>
</head>
<body>
<script src="script.js"></script>
</body>
</html>

用浏览器打开index.html, 确保能看见空的HTML模板, 然后打开控制台, 测试是否看见自己在script.js中声明的console.log语句内容.

open index.html # On Mac
explorer index.html # On Windows

接下来引入React库, React官网中提供了引入的具体方式. 这里选择直接引入托管在CDN中的js文件, 拷贝react和react-dom托管的地址加入index.html中:

<script src="https://unpkg.com/react@15/dist/react.js"></script>
<
script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>

可以看到我们引入了两个分离的js文件, React库可以脱离浏览器环境使用, 而如果想在浏览器中使用React库, 就需要ReactDOM库的帮助.

现在我们刷新浏览器, 在控制台中测试可以发现, 全局环境中已经存在ReactReactDOM变量:

这样简单配置之后, 就可以使用ReactReactDOM的API, 当然同时还有原生的Web API和JavaScript, 在第一个例子中会使用后两者.

使用Web API和JavaScript可以简单实现在浏览器中动态添加HTML. 首先创建div元素用以放置JavaScript生成的 HTML内容 , 给这个div的id属性命名为js. 在index.html的body元素内, script标签之前, 添加:

<div id="js"></div>

然后在script.js中, 通过document.getElementById获取这个id为jsdiv元素, 将其赋值给一个常量jsContainer.

const jsContainer = document.getElementById("js");

要改变jsContainer中的内容, 可以用innerHTML直接在该div元素上执行setter调用. 还可以在其中添加任何想要插入的HTML模板. 现在, 我们在其中添加"class"属性为"demo", 内容为"Hello JS"的div元素:

jsContainer.innerHTML = `
<div class="demo">
Hello JS
</div>
`;

如果各个步骤没有错误, 在浏览器页面上会出现”Hello JS”字符.

这个class为demo的div是我们目前的UI. 这个UI极其简单, 只是在页面上输出了一些文本内容.

document.getElementByIdelement.innerHTML是原生Web API. 使用原生API, 我们直接与浏览器进行交互. 而在用React编程时, 我们使用的是React的API, 让React操纵Web API, 自己不与浏览器直接交互.

React就像是浏览器的代理人, 大部分情况下, 我们只需和React交流, 不需要直接操作浏览器. 上一句提到, 只是大部分情况和React交流, 是因为还有一些情况需要直接操作浏览器, 但这样的情况是少数.

现在我们使用React创建和前一个例子一样的UI, 这次的div元素id为"react", 在index.html中, div#js下添加:

<div id="react"></div>

现在在script.js中, 创建一个新的常量用以插入新的div:

const reactContainer = document.getElementById("react");

常量reactContainer是这个实例中唯一一个需要使用原生web API的地方. 这样ReactDOM库才知道应用应该渲染在DOM的那个位置.

因为声明了reactContainer, 就能使用ReactDOM库将React版的HTML模板渲染到container中:

ReactDOM.render(
/* TODO: React版的HTML模板 */
reactContainer
)

之后要完成的步骤就是我们完全理解React的一个重要步骤. 还记得我之前所说的React是使用JavaScript完成HTML模板的吗, 接下来我们就要完成这项任务.

我们使用JavaScript调用React API来构建这个简单HTML用户界面, 到了实例的最后, 就能了解React为何要这样做.

在第一个例子中, 我们在jsContainer中直接加入字符串, 而这次我们使用对象而不是字符串. 任何HTML字符串都会以对象的形式表示出来, 使用React的APIReact.createElement来实现(这个API是React的核心API).

以下是具体如何用React来构建与第一个例子类似内容的代码:

ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React"
},
reactContainer
);

React.createElement接受许多参数:

  • 第一个参数是HTML标签, 在我们的例子中是div.
  • 第二个参数是一个对象, 内容是我们想要前一个参数所具有的属性, 为了和第一个JS例子相对应, 我们在第二个参数中声明className属性, 其值为”demo”, { className: "demo" }转换为HTML中的属性就是class="demo". 注意我们使用的是className而不是class, 因为在React中属性名是和Web API匹配的, 因此使用className.
  • 第三个参数是元素的内容, 我们在其中添加字符串”Hello React”.

然后就可以进行测试了. 浏览器应该将”Hello JS”和”Hello React”都渲染到页面上. 现在我们给”class”为”demo”的两个div添加上样式, 将两部分分开. 在index.html中:

<style media="screen">
.demo {
border: 1px solid #ccc;
margin: 1em;
padding: 1em;
}
</style>

下图是添加样式之后的页面:

(以上为原文的页面, 但译者自己测试结果与其不一致, 见下图)

现在我们建立了两个节点, 一个由DOM API直接控制, 一个则是由React API控制(间接使用DOM API). 其中最主要的一点不同, 是在仅使用JS的版本中, 我们使用字符串来表示内容, 而在React的版本中, 我们使用了JavaScript函数调用, 采用对象而不是字符串来表示内容.

不管用HTML来表示的UI有多复杂, 任何HTML元素都可以使用React.createElement创建一个JavaScript对象来表示.

现在我们给之前所创建的简单UI添加新样式 — 读取用户输入的文本框.

JS版本的第一个例子中, 在HTML模板中嵌入元素比较简单, 因为只需要直接插入HTML即可. 比如我们需要在”class”为”demo”的div中添加<input />元素, 只需要下面这样简单的操作:

jsContainer.innerHTML = `
<div class="demo">
Hello JS
<input />
</div>
`;

为了在React版本的第二个例子中达到与上面一样的效果, 需要在React.createElement中加入更多参数. 在第四个参数中使用React.createElement添加input元素(注意, 任何HTML元素都是对象):

ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React",
React.createElement("input")
),
reactContainer
);

现在你可能会觉得使用React是”把简单的步骤复杂化”, 确实是这样, 但是我们有充分的理由这样做, 继续看下面的步骤.

我们继续在UI中分别加入时间戳, 在JS版本中, 把时间戳放在段落标签中再插入. 可以调用new Date()来显示一个简单的时间戳:

jsContainer.innerHTML = `
<div class="demo">
Hello JS
<input />
<p>${new Date()}</p>
</div>
`;

要在React版本中加入同样的时间戳样式, 需要在React.createElement的第五个参数中添加元素, 如下:

ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React",
React.createElement("input"),
React.createElement(
"p",
null,
new Date().toString()
)
),
reactContainer
);

现在, JS和React版本的例子依然渲染同样的内容:

可以看到, 直到现在, React版本的例子依旧比JS版本的例子复杂. 究竟React在哪方面做得如此出色, 值得我们耗费那么多的精力用它来替代简单的HTML呢? 答案和最初渲染的HTML视图没有关系, React的长处在于更新DOM中已存在的视图.

现在我们在所创建的DOM中执行一个更新操作, 让时间戳随秒数增加.

将对两个版本的DOM操作添加到render函数中, 再使用Web APIsetInterval重复调用render函数, 每秒调用一次.

下面是script.js文件中的全部完整代码:

const jsContainer = document.getElementById("js");
const reactContainer = document.getElementById("react");
const render = () => {
jsContainer.innerHTML = `
<div class="demo">
Hello JS
<input />
<p>${new Date()}</p>
</div>
`;
ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React ",
React.createElement("input"),
React.createElement(
"p",
null,
new Date().toString()
)
),
reactContainer
);
}
setInterval(render, 1000);

刷新浏览器, 在两个版本中, 时间戳字符串随着时间每秒改变一次. 现在我们就是在更新DOM中的UI.

React让你大吃一惊的时刻即将来到了.如果你打算在JS版本的文本框中输入一些文字, 会发现根本无法输入. 这是可以预料到的结果, 因为在JS版本中, 我们每隔一秒就把整个DOM节点抛弃然后再重新生成. 不过, 在React版本中的文本框输入文字却一点困难都没有!

尽管React部分的代码也是每隔一秒重新渲染一次, 但React版本的实例只重新渲染了时间戳所在元素p的那一部分, 并非改变整个DOM节点, 其中的文本输入框不会随着秒数增加重新渲染, 因此可以在里面输入文字.

在页面中打开Chrome开发者工具可以观察到两种不同更新DOM的方式, 开发者工具会对更新的DOM节点进行高亮处理, 可以发现, 每隔一秒, class为”js”的div都会更新一次, 而React只是更新了含有时间戳字符串的那一部分p元素.

React使用的diffing算法, 使只更新需要更新的部分DOM成为可能. diffing过程之所以可行, 是因为React的虚拟DOM以及存储在内存中的UI.

使用虚拟DOM, React先将DOM的完整版本存储在内存中, 之后如果有更新, 那么更新之后的完整版本就会被存储在内存中, 这样的话, React就能够对比两个版本, 然后根据diffing算法计算出两者的差异(在上例情况下, 不同之处为时间戳).

之后, 根据React的指令, 浏览器就会只更新有差异之处, 而不是整个DOM节点. 不管我们重新生成多少次界面, React都会”指导”浏览器只做出部分更新.

这种方法不仅效率高, 还大大降低了更新UI的复杂程度. 让React来考虑是否需要更新DOM, 这样开发者就能专注于考虑数据(state)以及如何用UI描述数据.

这样我们就可以根据需要控制数据的更新, 不用再考虑如何将这些更新渲染到真正的DOM中(因为React会帮助我们高效地完成这件事).


感谢阅读! 这是本文所完成示例的源代码, 这是demo.

如果对这篇文章或本人所著其他文章有任何疑问, 可以通过这个slack频道联系到我(邀请你自己进入该频道), 在#questions房间中提问.

我还在在线学习网站PluralsightLynda中教授一些课程. 以下是我最近发布的几个课程Getting Started with React.js, Advanced Node.js以及 Learning Full-stack JavaScript.