深入浅出 React 和 Redux
内容简介
本书由浅入深地介绍如何用 React 和 Redux 构建现代化的前端项目,产出高质量的前端代码。共有12章。第1章介绍 React 应用开发的特点。第2章介绍开发高质量 React 组件的原则,详细介绍 React 组件的生命周期和数据管理方式。第3章由 Flux 引出 Redux框架,比较了不同框架的优劣。第4章开发了一个 Todo 应用,介绍将 React 和 Redux 结合的方法。第5章通过对 Todo 应用的性能优化,介绍提高 React 组件渲染性的方法。第6章介绍高阶组件和“以函数为子组件”的模式。第7章介绍如何在 React 和 Redux 的环境中实现和服务器通信。第8章介绍针对 React 和 Redux 的单元测试技巧。第9章介绍创建中间件和 StoreEnhancer 的技巧。第10章介绍在 React 中实现动画的技巧。第11章介绍如何创建多页面路由以及代码分片技巧。第12章介绍同构的概念和方法,使得 React 组件能够在服务器端和浏览器端进行渲染。
作者简介
程墨,资历架构师,曾任职于摩托罗拉、雅虎和微软,云鸟配送平台联合创始人,目前服务于美国视频服务公司 Hulu。
本书内容
前言
互联网技术发展一日千里,网页应用开发技术也不例外,这本书介绍的是在这一领域备受瞩目的两个工具 React 和 Redux。
自从 jQuery 问世以来,它就在网页开发领域占据统治地位,同时,还有许多 MVC 框架如雨后春笋般出现。但是业界也普遍发现,jQuery 和各种 MVC 框架在开发大型复杂应用时,依然面临很多难以克服的困难。
当2014年 Facebook 推出 React 时,给整个业界带来全新的看待网页应用开发的方式,和 React 一同问世的 Flux,也克服传统 MVC 框架的很多弊病。技术在不断发展,在2015年,Flux 的一个变体 Redux 出现,进一步优化了 Flux 的功能。
React 和 Redux 的结合,让网页开发的方式耳目一新,写这本书的初衷,是为了让国内读者能够一睹 React 和 Redux 的内在原理并深入实践。
在这里深入介绍 React 和 Redux,绝不是贬抑其他前端框架,事实上,开发者应该接触不同的开发模式,才能融会贯通,对技术有一个全面的认识,若要掌握某种技术,就要深入学习,这就是本书的目的。对 React 和 Redux 的了解不要只是停留在能用的表面功夫,重要的是理解内在的原理。
本书的内容
希望读者把阅读这本书的过程当做一个旅程,由浅入深地了解 React 和 Redux,如果你对 React 和 Redux 技术已经有一些了解,可以直接跳到感兴趣的章节。本书包括12章,如下所示。
第1章,React 新的前端思维方式。实际操作快速创建一个 React 应用,介绍和传统网页开发相比 React 应用开发的独特方式。
第2章,设计高质量的 React 组件。React 提倡基于组件的设计,这一章通过开发一个 ControlPanel 组件的实践,介绍了开发高质量 React 组件的原则,详细介绍 React 组件的生命周期和数据管理方式。
第3章,从 Flux 到 Redux。通过 Flux 介绍了单向数据流的框架模式,由此引出比 Flux 更优秀的 Redux 框架,通过用不同框架实现 ControlPanel 应用可以比较框架的优劣。
第4章,模块化 React 和 Redux 应用。这一章通过开发一个 Todo 应用介绍将 React 和 Redux 结合的方法。
第5章,React 组件的性能优化。通过对 Todo 应用的性能优化,介绍提高 React 组件渲染性的方法,以及提高从 Store 获取数据性能的方法。
第6章,React 高级组件。介绍高阶组件和“以函数为子组件”的模式。
第7章,Redux 和服务器通信。通过开发一个天气信息应用的实践,介绍应如何在 React 和 Redux 的环境中实现与服务器的通信。
第8章,单元测试。介绍针对 React 和 Redux 的单元测试技巧。
第9章,扩展 Redux。介绍创建中间件和 Store Enhancer 的技巧。
第10章,动画。介绍在 React 中通过 ReactTransitionGroup 和 React-Motion 库实现动画的技巧。
第11章,多页面应用。介绍如何创建多页面路由,以及为了提高网页装载性能的代码分片技巧。
第12章,同构。创建让 React 组件能够在服务器端和浏览器端渲染的技术。
本书的目标读者
阅读这本书只需要一些基本的 JavaScript、HTML 和 CSS 知识,了解网页应用的工作原理,就足够具备体验 React 和 Redux 这种全新的开发方式。
如果你熟悉传统的 jQuery 应用开发,那么通过阅读本书会让你发现不一样的应用构建模式;如果你之前学习过 Angular.js 或者 Vue.js,那么对理解 React 和 Redux 的工作机理很有帮助,同时有机会体验同样一种思想的不同实现之道。
即使你对 React 和 Redux 已经有了一定认识,相信阅读此书也不会让你觉得是浪费时间,因为书中不只是介绍“如何去做”,更多地还解释了“为什么这么做”,相信阅读此书会让你对 React 和 Redux 会有更多更深的认识。
源代码
本书每章都附带大量的实际代码例子,因为篇幅所限,在书中不可能包含所有代码,读者可以在 Github(网址https://github.com/mocheng/react-and-redux)上找到所有代码,代码按照所属章节内容组织。
如果读者发现代码或者书中的错误,可以直接在上面网址对应的代码库中提交问题,请不吝斧正。
致谢
首先要感谢我的家人,没有他们的帮助和理解,这本书不可能完成。
感谢 Hulu 公司,本书中的很多内容都是和 Hulu 的研发团队协同合作中得到的体会。
感谢机械工业出版社的吴怡编辑,因为她的鼓励和帮助,这本书才得以问世。
最后要感谢 React 和 Redux 社区,因为千千万万开发者以开放的心态贡献代码和积极讨论,前端开发技术才获得巨大的飞跃,这个世界才变得更加美好。
第1章 React 新的前端思维方式
我们先来直观认识 React,对任何一种工具,只有使用才能够熟练掌握,React 也不例外。通过对 React 快速入手,我们会解析 React 的工作原理,并通过与功能相同的 jQuery 程序对比,从而看出 React 的特点。
在这一章中,我们会介绍:
如何初始化一个 React 项目;
如何创建一个 React 组件;
React 的工作方式。
让我们开始旅程吧!
1.1 初始化一个 React 项目
为了开发 React 应用,你的电脑是运行微软 Windows 操作系统,还是苹果 Mac,或者是 Linux,并不重要,只需要保证具备以下条件:
安装了浏览器,如果是 Windows 操作系统,请保证微软 IE 浏览器版本不低于8.0版,因为 React 不支持比 IE8 更低版本的浏览器;
有一个命令行环境,在 Windows 操作系统中有命令行界面,在苹果 Mac 电脑中可以使用 Terminal 应用,对于 Linux 用户,命令行环境我想不用过多解释;
一个你最喜欢的代码编辑器,用于编辑 React 应用的代码,本书内容注重实践,只有实际编码才能有深入体会。
作为开发者,推荐使用谷歌 Chrome 浏览器,因为 Chrome 浏览器自带的开发辅助工具非常友好,而且还可以安装辅助 React 和 Redux 的扩展工具,具体的开发工具在第4章4.2节中有详细介绍。
React 是一个 JavaScript 语言的工具库,在这个 JavaScript 工具铺天盖地的时代,没有意外,你需要安装 Node.js,React 本身并不依赖于 Node.js,但是我们开发中用到的诸多工具需要 Node.js 的支持。
在 Node.js 的官网(https://nodejs.org/)可以找到合适的安装方式,安装 Node.js 的同时也就安装了 npm,npm 是 Node.js 的安装包管理工具,因为我们不可能自己开发所有功能,会大量使用现有的安装包,就需要 npm 的帮助。
1.1.1 create-react-app 工具
React 技术依赖于一个很庞大的技术栈,比如,转译 JavaScript 代码需要使用 Babel,模块打包工具又要使用 Webpack,定制 build 过程需要 grunt 或者 gulp……这些技术栈都需要各自的配置文件,还没有开始写一行 React 相关代码,开发人员就已经被各种技术名词淹没。
针对这种情况,React 的创建者 Facebook 提供了一个快速开发 React 应用的工具,名叫 create-react-app,这个工具的目的是将开发人员从配置工作中解脱出来,无需过早关注这些技术栈细节,通过创建一个已经完成基本配置的应用,让开发者快速开始 React 应用的开发。
本书中所有应用实例都由 create-react-app 创建,我们用这种最简单的方式创建可运行的应用,必要的时候才会介绍底层技术栈的细节,毕竟,没有什么比一个能运行的应用更加增强开发者的信心。
create-react-app 是一个通过 npm 发布的安装包,在确认 Node.js 和 npm 安装好之后,命令行中执行下面的命令安装 create-react-app:
npm install–-global create-react-app
安装过程结束之后,你的电脑中就会有 create-react-app 这样一个可以执行的命令,这个命令会在当前目录下创建指定参数名的应用目录。
我们在命令行中执行下面的命令:
create-react-app first_react_app
这个命令会在当前目录下创建一个名为 first_react_app 的目录,在这个目录中会自动添加一个应用的框架,随后我们只需要在这个框架的基础上修改文件就可以开发 React 应用,避免了大量的手工配置工作:
在 create-react-app 命令一大段文字输出之后,根据提示,输入下面的命令:
cd first_react_appnpm start
这个命令会启动一个开发模式的服务器,同时也会让你的浏览器自动打开了一个网页,指向本机地址 http://localhost3000/,显示界面如图1-1所示。
注意
本书中的截图是根据 create-react-app 1.0.0版本所得,其他版本产生的页面可能略有不同。
图1-1 由 create-react-app 创造的 React 应用界面
恭喜你,你的第一个 React 应用诞生了!
接下来,我们会用 React 开发一个简单的功能,让我们继续吧。
1.2 增加一个新的 React 组件
React 的首要思想是通过组件(Component)来开发应用。所谓组件,简单说,指的是能完成某个特定功能的独立的、可重用的代码。
基于组件的应用开发是广泛使用的软件开发模式,用分而治之的方法,把一个大的应用分解成若干小的组件,每个组件只关注于某个小范围的特定功能,但是把组件组合起来,就能够构成一个功能庞大的应用。如果分解功能的过程足够巧妙,那么每个组件可以在不同场景下重用,那样不光可以构建庞大的应用,还可以构建出灵活的应用。打个比方,每个组件是一块砖,而一个应用是一座楼,想要一次锻造就创建一座楼是不现实的。实际上,总是先锻造很多砖,通过排列组合这些砖,才能构建伟大的建筑。
React 非常适合构建用户交互组件,让我们从创建一个 React 组件开始。
学习任何一门语言或者任何一门框架,往往是从写 Hello World 程序开始,不过只是展示一句 Hello World 并不足以体现 React 的神奇能力,所以,我们要做一个不那么简单的组件,为了体现 React 对交互功能的支持,我们做一个显示点击次数的组件。
我们先看一看 create-react-app 给我们自动产生的代码,在 first-react-app 目录下包含如下文件和目录:
src/public/README.mdpackage.jsonnode_modules/
在开发过程中,我们主要关注 src 目录中的内容,这个目录中是所有的源代码。
create-react-app 所创建的应用的入口是 src/index.js 文件,我们看看中间的内容,代码如下:
import React from 'react';import ReactDOM from 'react-dom';import App from './App';import './index.css';ReactDOM.render( <App />, document.getElementById('root'));
这个应用所做的事情,只是渲染一个名叫 App 的组件,App 组件在同目录下的 App.js 文件中定义,渲染出来的效果就是在图1-1中看到的界面。
我们要定义一个新的能够计算点击数组件,名叫 ClickCounter,所以我们修改 index.js 文件如下:
import React from 'react';import ReactDOM from 'react-dom';import ClickCounter from './ClickCounter';import './index.css';ReactDOM.render( <ClickCounter />, document.getElementById('root'));
我们接下来会介绍代码的含义。现在我们先来看看如何添加一个新组件,在 src 目录下增加一个新的代码文件 ClickCounter.js,代码如下:
import React, { Component } from 'react';class ClickCounter extends Component { constructor(props) { super(props); this.onClickButton = this.onClickButton.bind(this); this.state = {count: 0}; } onClickButton() { this.setState({count: this.state.count + 1}); } render() { return ( <div> <button onClick={this.onClickButton}>Click Me</button> <div> Click Count: {this.state.count} </div> </div> ); }}export default ClickCounter;
如果你是从上一节不停顿直接读到这一节,而且没有关闭命令行中的 npm start 命令,当你保存完这个文件之后,不需要主动做刷新网页的动作,就会发现网页中的内容已经发生改变,如图1-2所示。
图1-2 ClickCounter 组件界面效果
去点击那个“Click Me”按钮,可以看到“Click Count”后面的数字会随之增加,每点击一次加一。
恭喜你,现在你已经构建了一个有交互性的组件!
import ClickCounter from './ClickCounter';
现在让我们来逐步详细解释代码中各部分的要义。
在 index.js 文件中,使用 import 导入了 ClickCounter 组件,代替了之前的 App 组件。
import 是 ES6(EcmaScript6)语法中导入文件模块的方式,ES6 语法是一个大集合,大部分功能都被最新浏览器支持。不过这个 import 方法却不在广泛支持之列,这没有关系,ES6 语法的 JavaScript 代码会被 webpack 和 babel 转译成所有浏览器都支持的 ES5 语法,而这一切都无需开发人员做配置,create-react-app 已经替我们完成了这些工作。
在 ClickCounter.js 文件的第一行,我们从 react 库中引入了 React 和 Component,如下所示:
import React, { Component } from 'react';
Component 作为所有组件的基类,提供了很多组件共有的功能,下面这行代码,使用的是 ES6 语法来创建一个叫 ClickCounter 的组件类,ClickCounter 的父类就是 Component:
class ClickCounter extends Component {
在 React 出现之初,使用的是 React.createClass 方式来创造组件类,这种方法已经被废弃了,但是在互联网上依然存在大量的文章基于 React.createClass 来讲解 React,这些文章中依然有很多真知灼见的部分,但是读者要意识到,使用 React.createClass 是一种过时的方法。在本书中,我们只使用 ES6 的语法来构建组件类。
细心的读者会发现,虽然我们导入的 Component 类在 ClickCounter 组件定义中使用了,可是导入的 React 却没有被使用,难道在这里引入 React 没有必要吗?
事实上,引入 React 非常必要,你可以尝试删掉第一行中的 React,在网页中立刻会出现错误信息,如图1-3所示。
图1-3 缺失 React 的错误
这个错误信息的含义是:“在使用 JSX 的范围内必须要有 React。”
也就是说,在使用 JSX 的代码文件中,即使代码中并没有直接使用 React,也一定要导入这个 React,这是因为 JSX 最终会被转译成依赖于 React 的表达式。
接下来,我们就要认识什么是 JSX。
1.2.1 JSX
所谓 JSX,是 JavaScript 的语法扩展(eXtension),让我们在 JavaScript 中可以编写像 HTML 一样的代码。在 ClickCounter.js 的 render 函数中,就出现了类似这样的 HTML 代码,在 index.js 中,ReactDOM.render 的第一个参数<App/>
也是一段 JSX 代码。
JSX 中的这几段代码看起来和 HTML 几乎一模一样,都可以使用<div><button>
之类的元素,所以只要熟悉 HTML,学习 JSX 完全不成问题,但是,我们一定要明白两者的不同之处。
首先,在 JSX 中使用的“元素”不局限于 HTML 中的元素,可以是任何一个 React 组件,在 App.js 中可以看到,我们创建的 ClickCounter 组件被直接应用在 JSX 中,使用方法和其他元素一样,这一点是传统的 HTML 做不到的。
React 判断一个元素是 HTML 元素还是 React 组件的原则就是看第一个字母是否大写,如果在 JSX 中我们不用 ClickCounter 而是用 clickCounter,那就得不到我们想要的结果。
其次,在 JSX 中可以通过 onClick 这样的方式给一个元素添加一个事件处理函数,当然,在 HTML 中也可以用 onclick(注意和 onClick 拼写有区别),但在 HTML 中直接书写 onclick 一直就是为人诟病的写法,网页应用开发界一直倡导的是用 jQuery 的方法添加事件处理函数,直接写 onclick 会带来代码混乱的问题。
这就带来一个问题,既然长期以来一直不倡导在 HTML 中使用 onclick,为什么在 React 的 JSX 中我们却要使用 onClick 这样的方式来添加事件处理函数呢?
1.2.2 JSX 是进步还是倒退
在 React 出现之初,很多人对 React 这样的设计非常反感,因为 React 把类似 HTML 的标记语言和 JavaScript 混在一起了,但是,随着时间的推移,业界逐渐认可了这种方式,因为大家都发现,以前用 HTML 来代表内容,用 CSS 代表样式,用 JavaScript 来定义交互行为,这三种语言分在三种不同的文件里面,实际上是把不同技术分开管理了,而不是逻辑上的“分而治之”。
根据做同一件事的代码应该有高耦合性的设计原则,既然我们要实现一个 ClickCounter,那为什么不把实现这个功能的所有代码集中在一个文件里呢?
这点对于初学者可能有点难以接受,但是相信当你读完此书后,观点会随之改变。
那么,在 JSX 中使用 onClick 添加事件处理函数,是否代表网页应用开发兜了一个大圈,最终回到了起点了呢?
不是这样,JSX 的 onClick 事件处理方式和 HTML 的 onclick 有很大不同。
即使现在,我们还是要说在 HTML 中直接使用 onclick 很不专业,原因如下:
onclick 添加的事件处理函数是在全局环境下执行的,这污染了全局环境,很容易产生意料不到的后果;
给很多 DOM 元素添加 onclick 事件,可能会影响网页的性能,毕竟,网页需要的事件处理函数越多,性能就会越低;
对于使用 onclick 的 DOM 元素,如果要动态地从 DOM 树中删掉的话,需要把对应的事件处理器注销,假如忘了注销,就可能造成内存泄露,这样的 bug 很难被发现。
上面说的这些问题,在 JSX 中都不存在。
首先,onClick 挂载的每个函数,都可以控制在组件范围内,不会污染全局空间。
我们在 JSX 中看到一个组件使用了 onClick,但并没有产生直接使用 onclick(注意是 onclick 不是 onClick)的 HTML,而是使用了事件委托(event delegation)的方式处理点击事件,无论有多少个 onClick 出现,其实最后都只在 DOM 树上添加了一个事件处理函数,挂在最顶层的 DOM 节点上。所有的点击事件都被这个事件处理函数捕获,然后根据具体组件分配给特定函数,使用事件委托的性能当然要比为每个 onClick 都挂载一个事件处理函数要高。
因为 React 控制了组件的生命周期,在 unmount 的时候自然能够清除相关的所有事件处理函数,内存泄露也不再是一个问题。
除了在组件中定义交互行为,我们还可以在 React 组件中定义样式,我们可以修改 ClickCounter.js 中的 render 函数,代码如下:
render() { const counterStyle = { margin: '16px' } return ( <div style={counterStyle}> <button onClick={this.onClickButton}>Click Me</button> <div> Click Count: <span id=”clickCount”>{this.state.count}</span> </div> </div> );}
我们在 JavaScript 代码中定义一个 counterStyle 对象,然后在 JSX 中赋值给顶层 div 的 style 属性,可以看到网页中这个部分的 margin 真的变大了。
你看,React 的组件可以把 JavaScript、HTML 和 CSS 的功能在一个文件中,实现真正的组件封装。
1.3 分解 React 应用
前面我们提到过,React 应用实际上依赖于一个很大很复杂的技术栈,我们使用 create-react-app 避免在一开始就费太多精力配置技术栈,不过现在是时候了解一下这个技术栈了。
我们启动 React 应用的命令是 npm start,看一看 package.json 中对 start 脚本的定义,如下所示:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject"}
可以看到,start 命令实际上是调用了 react-scripts 这个命令,react-scripts 是 create-react-app 添加的一个 npm 包,所有的配置文件都藏在 node_modules/react-scripts 目录下,我们当然可以钻进这个目录去一探究竟,但是也可以使用 eject 方法来看清楚背后的原理。
你可以发现 package.json 文件中和 start 并列还有其他几个命令,其中 build 可以创建生产环境优化代码,test 用于单元测试,还有一个 eject 命令很有意思。
这个 eject(弹射)命令做的事情,就是把潜藏在 react-scripts 中的一系列技术栈配置都“弹射”到应用的顶层,然后我们就可以研究这些配置细节了,而且可以更灵活地定制应用的配置。
注意
eject 命令是不可逆的,就好像战斗机飞行员选择“弹射”出驾驶舱,等于是放弃了这架战斗机,是不可能再飞回驾驶舱的。所以,当你执行 eject 之前,最好做一下备份。
我们在命令行下执行下面的命令,完成“弹射”操作:
npm run eject
这个命令会让改变一些文件,也会添加一些文件。
当前目录下会增加两个目录,一个是 scripts,另一个是 config,同时,package.json 文件中的 scripts 部分也发生了变化:
"scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js --env=jsdom"},
从此之后,start 脚本将使用 scripts 目录下的 start.js,而不是 node_modules 目录下的 react-scripts,弹射成功,再也回不去了。
在 config 目录下的 webpack.config.dev.js 文件,定制的就是 npm start 所做的构造过程,其中有一段关于 babel 的定义:
{ test: /\\.(js|jsx)$/, include: paths.appSrc, loader: 'babel', query: { // This is a feature of \`babel-loader\` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ // directory for faster rebuilds. cacheDirectory: true }},
代码中 paths.appSrc 的值就是 src,所以这段配置的含义指的是所有以 js 或者 jsx 为扩展名的文件,都会由 babel 所处理。
并不是所有的浏览器都支持所有 ES6 语法,但是有了 babel,我们就可以不用顾忌太多,因为 babel 会把 ES6 语法的 JavaScript 代码转译(transpile)成浏览器普遍支持的 JavaScript 代码,实际上,在 React 的社区中,不使用 ES6 语法写代码才显得奇怪。
1.4 React 的工作方式
在继续深入学习 React 的其他知识之前,我们先就这个简单的 ClickCounter 组件思考一下 React 的工作方式,要了解一样东西的特点,最好的方法当然是拿这个东西和另一样东西做比较。我们就拿 React 和 jQuery 来比较。
1.4.1 jQuery 如何工作
假设我们用 jQuery 来实现 ClickCounter 的功能,该怎么做呢?首先,我们要产生一个网页的 HTML,写一个 index.html 文件如下所示:
<!doctype html><html> <body> <div> <button id="clickMe">Click Me</button> <div> Click Count: <span id="clickCount">0</span> </div> </div> <script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script> <script src="./clickCounter.js"></script> </body></html>
实际产品中,产生这样的 HTML 可以用 PHP、Java、Ruby on Rails 或者任何一种服务器端语言和框架来做,也可以在浏览器中用 Mustache、Hogan 这样的模板产生,这里我们只是把问题简化,直接书写 HTML。
jQuery 已经发展到 3.x 版,但已经不支持比较老的浏览器了,我们这里使用1.9.1的 jQuery 只是为了让这个网页在 IE8 上也能够运行。
上面的 HTML 只是展示样式,并没有任何交互功能,现在我们用 jQuery 来实现交互功能,和 jQuery 的传统一样,我们把 JavaScript 代码写在一个独立的文件 clickCounter.js 里面,如下所示:
$(function() { $('#clickMe').click(function() { var clickCounter = $('#clickCount'); var count = parseInt(clickCounter.text(), 10); clickCounter.text(count+1); })})
用浏览器打开上面创造的 index.html,可以看到实际效果和我们写的 React 应用一模一样,但是对比这两段程序可以看出差异。
在 jQuery 的解决方案中,首先根据 CSS 规则找到 id 为 clickCount 的按钮,挂上一个匿名事件处理函数,在事件处理函数中,选中那个需要被修改的 DOM 元素,读取其中的文本值,加以修改,然后修改这个 DOM 元素。
选中一些 DOM 元素,然后对这些元素做一些操作,这是一种最容易理解的开发模式。jQuery 的发明人 John Resig 就是发现了网页应用开发者的这个编程模式,才创造出了 jQuery,其一问世就得到普遍认可,因为这种模式直观易懂。但是,对于庞大的项目,这种模式会造成代码结构复杂,难以维护,每个 jQuery 的使用者都会有这种体会。
1.4.2 React 的理念
与 jQuery 不同,用 React 开发应用是另一种体验,我们回顾一下,用 React 开发的 ClickCounter 组件并没有像 jQuery 那样做“选中一些 DOM 元素然后做一些事情”的动作。
打一个比方,React 是一个聪明的建筑工人,而 jQuery 是一个比较傻的建筑工人,开发者你就是一个建筑的设计师,如果是 jQuery 这个建筑工人为你工作,你不得不事无巨细地告诉 jQuery“如何去做”,要告诉他这面墙要拆掉重建,那面墙上要新开一个窗户,反之,如果是 React 这个建筑工人为你工作,你所要做的就是告诉这个工人“我想要什么样子”,只要把图纸递给 React 这个工人,他就会替你搞定一切,当然他不会把整个建筑拆掉重建,而是很聪明地把这次的图纸和上次的图纸做一个对比,发现不同之处,然后只去做适当的修改就完成任务了。
显而易见,React 的工作方式把开发者从繁琐的操作中解放出来,开发者只需要着重“我想要显示什么”,而不用操心“怎样去做”。
这种新的思维方式,对于一个简单的例子也要编写不少代码,感觉像是用高射炮打蚊子,但是对于一个大型的项目,这种方式编写的代码会更容易管理,因为整个 React 应用要做的就是渲染,开发者关注的是渲染成成什么样子,而不用关心如何实现增量渲染。
React 的理念,归结为一个公式,就像下面这样:
UI=render(data)
让我们来看看这个公式表达的含义,用户看到的界面(UI),应该是一个函数(在这里叫 render)的执行结果,只接受数据(data)作为参数。这个函数是一个纯函数,所谓纯函数,指的是没有任何副作用,输出完全依赖于输入的函数,两次函数调用如果输入相同,得到的结果也绝对相同。如此一来,最终的用户界面,在 render 函数确定的情况下完全取决于输入数据。
对于开发者来说,重要的是区分开哪些属于 data,哪些属于 render,想要更新用户界面,要做的就是更新 data,用户界面自然会做出响应,所以 React 实践的也是“响应式编程”(Reactive Programming)的思想,这也就是 React 为什么叫做 React 的原因。
1.4.3 Virtual DOM
既然 React 应用就是通过重复渲染来实现用户交互,你可能会有一个疑虑:这样的重复渲染会不会效率太低了呢?毕竟,在 jQuery 的实现方式中,我们可以清楚地看到每次只有需要变化的那一个 DOM 元素被修改了;可是,在 React 的实现方式中,看起来每次 render 函数被调用,都要把整个组件重新绘制一次,这样看起来有点浪费。
事实并不是这样,React 利用 Virtual DOM,让每次渲染都只重新渲染最少的 DOM 元素。
要了解 Virtual DOM,就要先了解 DOM,DOM 是结构化文本的抽象表达形式,特定于 Web 环境中,这个结构化文本就是 HTML 文本,HTML 中的每个元素都对应 DOM 中某个节点,这样,因为 HTML 元素的逐级包含关系,DOM 节点自然就构成了一个树形结构,称为 DOM 树。
浏览器为了渲染 HTML 格式的网页,会先将 HTML 文本解析以构建 DOM 树,然后根据 DOM 树渲染出用户看到的界面,当要改变界面内容的时候,就去改变 DOM 树上的节点。
Web 前端开发关于性能优化有一个原则:尽量减少 DOM 操作。虽然 DOM 操作也只是一些简单的 JavaScript 语句,但是 DOM 操作会引起浏览器对网页进行重新布局,重新绘制,这就是一个比 JavaScript 语句执行慢很多的过程。
如果使用 mustache 或者 hogan 这样的模板工具,那就是生成 HTML 字符串塞到网页中,浏览器又要做一次解析产生新的 DOM 节点,然后替换 DOM 树上对应的子树部分,这个过程肯定效率不高。虽然 JSX 看起来很像是一个模板,但是最终会被 Babel 解析为一条条创建 React 组件或者 HTML 元素的语句,神奇之处在于,React 并不是通过这些语句直接构建 DOM 树,而是首先构建 Virtual DOM。
既然 DOM 树是对 HTML 的抽象,那 Virtual DOM 就是对 DOM 树的抽象。Virutal DOM 不会触及浏览器的部分,只是存在于 JavaScript 空间的树形结构,每次自上而下渲染 React 组件时,会对比这一次产生的 Virtual DOM 和上一次渲染的 Virtual DOM,对比就会发现差别,然后修改真正的 DOM 树时就只需要触及差别中的部分就行。
以 ClickCounter 为例,一开始点击计数为0,用户点击按钮让点击计数变成1,这一次重新渲染,React 通过 Virtual DOM 的对比发现其实只是 id 为 clickCount 的 span 元素中内容从0变成了1而已:
<span id="clickCount">{this.state.count}</span>
React 发现这次渲染要做的事情只是更换这个 span 元素的内容而已,其他 DOM 元素都不需要触及,于是执行类似下面的语句,就完成了任务:
document.getElementById("clickCount").innerHTML = "1";
React 对比 Virtual DOM 寻找差异的过程比较复杂,在第5章,我们会详细介绍对比的过程。
1.4.4 React 工作方式的优点
毫无疑问,jQuery 的方式直观易懂,对于初学者十分适用,但是当项目逐渐变得庞大时,用 jQuery 写出的代码往往互相纠缠,形成类似图1-4的状况,难以维护。
图1-4 jQuery 方式造成的纠缠代码结构
使用 React 的方式,就可以避免构建这样复杂的程序结构,无论何种事件,引发的都是 React 组件的重新渲染,至于如何只修改必要的 DOM 部分,则完全交给 React 去操作,开发者并不需要关心,程序的流程简化为图1-5的样式。
React 利用函数式编程的思维来解决用户界面渲染的问题,最大的优势是开发者的效率会大大提高,开发出来的代码可维护性和可阅读性也大大增强。
图1-5 React 的程序流程
React 等于强制所有组件都按照这种由数据驱动渲染的模式来工作,无论应用的规模多大,都能让程序处于可控范围内。
1.5 本章小结
在这一章里,我们用 create-react-app 创造了一个简单的 React 应用,在一开始,我们就按照组件的思想来开发应用,React 的主要理念之一就是基于组件来开发应用。
通过和同样功能的 jQuery 实现方式对比,我们了解了 React 的工作方式,React 利用声明式的语法,让开发者专注于描述用户界面“显示成什么样子”,而不是重复思考“如何去显示”,这样可以大大提高开发效率,也让代码更加容易管理。
虽然 React 是通过重复渲染来实现动态更新效果,但是借助 Virtual DOM 技术,实际上这个过程并不牵涉太多的 DOM 操作,所以渲染效率很高。
理解 React 的工作方式,是踏入 React 世界的关键一步,接下来我们详细介绍如何构建高质量的 React 组件。
第2章 设计高质量的 React 组件(上)
第2章 设计高质量的 React 组件(下)
第3章 从 Flux 到 Redux(上)
第3章 从 Flux 到 Redux(下)
第4章 模块化 React 和 Redux 应用(上)
第4章 模块化 React 和 Redux 应用(中)
第4章 模块化 React 和 Redux 应用(下)
第5章 React 组件的性能优化(上)
第5章 React 组件的性能优化(下)
第6章 React 高级组件
第7章 Redux 和服务器通信(上)
第7章 Redux 和服务器通信(下)
第8章 单元测试
第9章 扩展 Redux
第10章 动画
第11章 多页面应用(上)
第11章 多页面应用(下)
第12章 同构(上)
第12章 同构(下)
结语
阅读全文: http://gitbook.cn/gitchat/geekbook/5b83afd713fe9d4fcd05c222