<img src="https://pic4.zhimg.com/v2-4273d0f7dfc900d8e63856240d7d6bc7_b.png" data-rawwidth="1000" data-rawheight="364" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic4.zhimg.com/v2-4273d0f7dfc900d8e63856240d7d6bc7_r.png">原文链接:Yes, React is taking over front-end development. The question is why.
作者:Samer Buna
译者:余博伦
转载请注明出处。
TL;DR 太长不看(译者注)
- 最主要的 React 核心 API 非常少,在你对 ES6/7 等新语法 和 Modern JavaScript 技术栈比较熟悉的前提下,React 上手是非常快的。
- React 实现了一层抽象的 “虚拟 DOM” 来表示用户界面,这也就意味着,只要有合适的 renderer 来渲染,你可以把 React 编写的用户界面渲染到任何设备或平台上(Web/Native/VR)
- React 采用声明式的方法编写用户界面,你只需要向 React “描述” 你想要什么样的界面,然后规划好应用会有哪些状态数据的改变,剩下的具体如何更新如何渲染 React 会自动帮你完成。
- 注意:所有的框架或库都不会比原生的 JavaScript 快,React 再快也快不过你直插 DOM ,但是 React 实现的 “虚拟 DOM” 以及 “核心 Diff 算法” 可以只更新界面必要的部分,而不是整体替换,这是你手工操作 DOM 很难实现的效果。
以下是React如此迅速流行的几个原因:
- 使用 DOM API 很困难。 React 实现的虚拟 DOM 对开发人员来说更加友好。 就像开发者和真实浏览器之间的代理一样。
- React允许开发人员采用声明式的方法描述他们的用户界面,并对这些接口的状态进行建模。 这意味着开发人员只是根据最终状态(如函数)来描述接口,而不需要考虑过程和中间环节。 当一些事件触发应用状态改变时,React会根据具体的情况自动更新用户界面(想象我们的界面是一段动画,React可以让我们把动画切分成一帧一帧的片段,我们只需要关注其中某个片段,以及片段有哪些变化,至于怎么变化,过程如何,React可以自动帮我们处理)。
- React 单纯是 JavaScript,你只需要了解非常少的核心API,外加几个简单的函数的使用方法。 除此之外,只要你的 JavaScript 技术足够好,你就能成为一个很棒的 React 开发者。 上手门槛也比较低,只要你熟悉 JavaScript ,仅需几个小时你就能熟练运用 React 了。
但除了这些表象之外还有更多重要的原因。接下来我们就仔细地来聊聊。首先一点是它实现的 虚拟DOM(Virtual DOM) 以及 核心调度算法(reconciliation algorithm)。我们可以通过实例来证明这些实现的价值。
React 官方的定义是它是一个用于构建用户界面的库。这其中有两个比较重要的部分:
1.首先React是一个库,而不是框架。它并不是一个完整的解决方案,我们在使用React的同时还需要加入别的库来配合(不像Angular那种)。React 只专注于一件事,并且把它做到很好。
2.第二部分也就是 React 专注要做的事了:构建用户界面。用户界面就是用户能够看到并通过它与程序交互的部分。广义的用户界面无处不在,从微波炉上的按钮到宇宙飞船上的控制面板。而只要这个设备支持 JavaScript,我们就能用 React 为它编写界面(想起来了《西部世界》里Hosts的人工智能貌似都在用React)。
浏览器都能够运行 JavaScript ,所以我们自然能够通过 React 来 描述 Web 的界面。注意到我这里用到的 描述 一词,这也就是通常情况下我们运用 React 做的事情,我们只是告诉 React 如何来构建具体的用户界面。如果没有 React 的话,我们只能通过原始的 Web APIs 和 JavaScript 来手动编写了。
所以当你听到 “React 是声明式” 的这样的定义时,它其实就是字面意思,我们通过使用 React 来描述用户界面(只是告诉 React 是什么或如何做)。剩下的 React 会帮我们搞定,它会把我们声明式的描述转换成浏览器当中的用户界面。React 的这些功能是构建在原生的 HTML 基础之上的,不过有了 React 除了静态内容之外我们还可以描述动态的数据。
React 三条主要的设计理念成就了它的走红:
1——可复用、嵌套、有状态的组件
在 React 中,我们使用组件描述用户界面。 您可以将组件视为简单的函数(不论任何编程语言)。 我们用一些输入调用函数,并给我们一些输出。 我们可以根据需要复用函数,并从简单的函数中构建更复杂的函数。
组件的作用也完全一样; 我们把调用组件时的输入称之为“属性 properties”和“状态 state”,组件输出就是对用户界面的描述(与浏览器的HTML类似)。 我们可以在多个用户界面中复用单个组件,组件可以嵌套其他组件。
然而,与纯函数不同,完整的 React 组件可以具有私有 状态state 来保存变化的数据。
2——响应式更新的性质
React 这个单词本身的含义(反应)就是对这个概念的简单解释。 当组件(输入)的状态发生变化时,它所表示的用户界面(输出)也会发生变化。 用户界面描述中的这种变化同样会在我们使用的设备中响应。
在浏览器中,我们需要重新生成文档对象模型(DOM)中的 HTML 视图。 使用 React ,我们就不需要担心如何响应这些更改,甚至管理什么时候应用更改到浏览器; React会直接对状态进行更改,并在需要时自动更新 DOM 。
3——在内存中对视图的虚拟
在 React 中,我们使用 JavaScript 编写 HTML 。 我们依靠 JavaScript 的功能通过某些数据来生成相应的 HTML,而不是扩展 HTML 的功能。 扩展 HTML 是其他 JavaScript 框架的选择。 例如,Angular 扩展了 HTML 使用循环,条件和其他的功能。
当我们收到来自服务器的数据(在后台使用 Ajax )时,我们需要比 HTML 更多的东西来处理该数据。 要么使用扩展了功能的 HTML ,要么使用 JavaScript 本身的能力来生成 HTML 。 这两种方法各有优劣。 React 选择了后一种瑕不掩瑜的方式。
事实上,使用这种方式有一个最主要的优点; 使用 JavaScript 来渲染 HTML 使 React 能够直接在内存中保存 HTML 的虚拟表示(通常称为 虚拟DOM)。 React使用虚拟DOM先渲染一个 HTML树,然后,每当状态数据发生变化,我们就可以更新这个代表 DOM 的树状结构数据,然后 React 不是重新渲染整个 DOM ,而只会写入 新树和之前的区别(因为 React 在内存中保留了用于比较差异的两个版本)。 这个过程被称为 Tree Reconciliation ,我认为,这是自 Ajax 以来Web开发中最伟大的发明!
在下面的示例中,我们将重点介绍第三个概念,并且可以看到一个简单的树对照过程的实例及其带来的重大改变。 我们将编写相同的 HTML 示例两次,首先使用原生 Web API 和 JavaScript,然后我们再来看看如何使用 React 描述相同的 HTML树。
为了专注说明这一概念,我们不会使用组件,我们将使用 JavaScript 定时器来模拟状态数据的改变。 我们也不会使用 JSX(在 React 中使用 JSX 可以极大地提高我们编写代码的效率)在这个例子中直接使用 React API,以帮助大家更好地了解这个概念。
React 的调度算法示例
要尝试此示例,你需要一个浏览器和一个代码编辑器。 实际上也可以使用在线编码应用,不过我将使用本地文件并直接在浏览器中进行测试(不需要 Web 服务器):
我们将从零开始编写这个例子。 创建一个新目录,并在你习惯的编辑器中打开:
mkdir react-demo
cd react-demo
atom .
在该目录中创建一个 index.html 文件,并在其中编写一个标准的 HTML 模板。 在该模板中包含一个 script.js 文件,并在脚本中添加了一个 console.log 语句,以测试我们的文件正确引入了:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Demo</title>
</head>
<body>
<script src="script.js"></script>
</body>
</html>
script.js
console.log('Included!')
在浏览器中打开 index.html 文件,并确保您可以看到空的模板没有问题,你可以在 控制台中看到您放在 script.js 中的 console.log 测试消息:
open index.html # Mac
explorer index.html # Windows
现在让我们来引入 React 库文件,我们可以直接使用它的 CDN ,复制 react 和 react-dom 的脚本标签添加到 index.html 中:
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
我们需要引入两个 React 库文件是因为 React 实现的功能是独立的,也可以在别的平台使用,而为了把 React 描述的界面在 DOM 中渲染出来则需要用到 ReactDOM 这个库:
现在我们刷新一下页面,在控制台试着输出 React 和 ReactDOM ,应该就能够看到了它们暴露的全局对象了:
通过简单的配置,我们现在可以访问 React 和 ReactDOM 的 API 了。
&amp;lt;img src="https://pic3.zhimg.com/v2-ccdde5f3df072652363bdd2c7ac1a66a_b.png" data-rawwidth="752" data-rawheight="349" class="origin_image zh-lightbox-thumb" width="752" data-original="https://pic3.zhimg.com/v2-ccdde5f3df072652363bdd2c7ac1a66a_r.png"&amp;gt;
想要在页面中动态插入 HTML ,我们需要先声明一个 DOM 标签作为容器:
<div id="js"></div>
然后在我们的 script.js 脚本文件中编写代码获取到这个容器:
// 这里是 ES6 的 const 关键字声明常量
const jsContainer = document.getElementById("js");
加入我们想要简单地插入 HTML 片段的话,只需要使用原生的 innerHTML 方法即可:
// 这里是 ES6 的字符串模板
jsContainer.innerHTML = `
<div class="demo">
Hello JS
</div>
`;
如果这一切都工作正常,你应该可以在页面里看到一个 "Hello JS" 的字样了。
为了更简洁地说明我们的概念,代码就保持这么简单。注意到我们使用的所有方法都是原生的 JS 方法。而接下来我们使用 React 时则需要调用 React 的 API,再有 React 来进行同 DOM 原生 API 之间的交互(事实上 React 源码最后把 DOM 插入的时候用的也就是 innerHTML 方法)。
React 的角色就好像是我们与浏览器之间的代理,我们只需要告诉 React 该干什么,React 会自己去和浏览器打交道。我们仅有的需要直接使用原生 JS 的情况可能也就只有获取 DOM 容器这些操作了。
接下来我们使用 React 实现一下刚才的示例,还是一样的,新建一个 id 为 react 的页面容器:
<div id="react"></div>
同样的,在我们的 script.js 脚本文件中获取这个容器:
const reactContainer = document.getElementById("react");
接下来我们需要使用到 ReactDOM 这可库中的 render 方法:
ReactDOM.render(
/* TODO: React's version of the HTML template */,
reactContainer
)
我们接下来要做的,是你理解 React 至关重要的一点。记得我们之前提到过的,React 是通过 JavaScript 来书写 HTML 的吧。
我们可以通过调用 React 的 API 来生成一个简单的 HTML 界面
并不像之前一样操作字符串,在 React 当中,我们通过定义对象来定义 DOM 的内容,下面的方法可以生成和刚才原生 JS 示例一样的内容:
ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React"
),
reactContainer
);
React.createElement 方法接受很多个参数:
- 首先是生成 HTML 的标签名,例如 div
- 然后是标签的属性,不过这里我们使用的是对象来描述,例如 { className: "demo" } 会生成 class="demo"
- 第三个则是标签中包含的内容,例如我们示例中的 "Hello React" 字符串
我们现在可以稍微添加一点点样式,在浏览器中查看一下:
<style media="screen">
.demo {
border: 1px solid #ccc;
margin: 1em;
padding: 1em;
}
</style>
&amp;lt;img src="https://pic2.zhimg.com/v2-42f52cd6d8d8bcb5c6819056246bd13d_b.png" data-rawwidth="525" data-rawheight="245" class="origin_image zh-lightbox-thumb" width="525" data-original="https://pic2.zhimg.com/v2-42f52cd6d8d8bcb5c6819056246bd13d_r.png"&amp;gt;
现在我们有了两个节点,一个由原生 DOM Web API 创建,另一个则是通过 React API 创建。最主要的一个区别是,原生的方法用字符串表示节点内容,而 React 则是通过一个方法传入对象来表示内容。
不论我们的界面多么复杂,使用 React 时,所有的页面元素都是通过 React.createElement 方法传入对象来表示。
接下来我们再来试着添加点其他内容,我们来尝试一下嵌套结构的 HTML 内容,原生的 JS 还是一样的,用字符串表示:
jsContainer.innerHTML = `
<div class="demo">
Hello JS
<input />
</div>
`;
使用 React 也很简单,我们只需要修改 React.createElement 方法的第三个参数:
ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React",
React.createElement("input")
),
reactContainer
);
看到这一步,你可能会疑惑。使用 React 是不是把简单的问题复杂化了?表面上看起来是这样,但我们有这么做的理由,请接着往下读:
我们再来添加一个显示时间戳的标签:
jsContainer.innerHTML = `
<div class="demo">
Hello JS
<input />
<p>${new Date()}</p>
</div>
`;
使用 React 时呢,我们则需要传入 5 个参数来表示:
ReactDOM.render(
React.createElement(
"div",
{ className: "demo" },
"Hello React",
React.createElement("input"),
React.createElement(
"p",
null,
new Date().toString()
)
),
reactContainer
);
现在原生 JS 和 React 在页面上仍旧显示相同的内容。
&amp;lt;img src="https://pic1.zhimg.com/v2-62ca25207fdb9825413778ca3ff8bb80_b.png" data-rawwidth="528" data-rawheight="345" class="origin_image zh-lightbox-thumb" width="528" data-original="https://pic1.zhimg.com/v2-62ca25207fdb9825413778ca3ff8bb80_r.png"&amp;gt;
就目前的示例来看,使用 React 看起来要比原生的方法复杂困难许多。那么 React 究竟有什么价值能让我们放弃编写原生的 HTML 转而用它的 API 方法来书写呢?问题的关键不在于第一次生成页面,而是之后如何更新页面的内容。
接下来我们用示例演示一下,让时间戳随着每一秒变化。
最直接的方法是通过 setInterval 函数每秒钟执行一词我们定义好的方法:
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);
运行一下我们的代码,现在两个节点中的计时器应该都自动工作了。不过,你现在试一下原生 JS 的节点,你会发现 input 是无法输入的,因为每一秒,这个节点的 DOM 元素就都被重新构建了。而你会发现 Rect 节点的输入框是可以正常使用的。
虽然 React 也是重复调用 render 方法,但实际上,React 只会重新渲染计时器数字变化的部分,其他内容会保持不变,输入框自然也就能正常使用了。
在控制台里面,你可以很具体的观察到两个节点之间的异同,原生节点的所有内容都被重新渲染了,而 React 则只更新了计数器的节点。
&amp;lt;img src="https://pic3.zhimg.com/v2-366ef3ed2edad75788e063f038fdb2da_b.gif" data-rawwidth="669" data-rawheight="361" data-thumbnail="https://pic3.zhimg.com/v2-366ef3ed2edad75788e063f038fdb2da_b.jpg" class="origin_image zh-lightbox-thumb" width="669" data-original="https://pic3.zhimg.com/v2-366ef3ed2edad75788e063f038fdb2da_r.gif"&amp;gt;React 有一个智能的差分diff算法,通过这一算法我们能实现只生成 DOM 节点当中需要被重新渲染的部分。这一算法之所以能够行得通,是因为 React 的虚拟 DOM 技术,把 DOM 的结构和内容保存在了 JS 当中。
使用虚拟 DOM ,React 将内存中的最后一个 DOM 版本保留下来,当它有一个新的 DOM 版本生成到浏览器时,新的 DOM 版本也将保存在内存中,所以React可以计算新版本和旧版本之间的差异(在我们的例子中,区别是时间戳段落)。
然后React会指示浏览器只更新计算出的差异,而不是整个 DOM 节点。无论我们重新生成我们的界面多少次,React将只向浏览器添加新的“部分”更新。
这种方法不仅可以提高效率,而且为了更新用户界面的方式,也会消除一大堆复杂性。有了React来做所有关于我们是否应该更新 DOM 的计算,都使我们能够专注于思考我们的数据(状态)和描述用户界面的方式。
然后,我们根据需要管理我们的数据更新,而不用担心在浏览器中将实际用户界面反映这些更新所需的步骤(因为我们知道 React 将会自动地并以最高效的方式做到这一点)!