写点什么

深入浅出 React(三):理解 JSX 和组件

2015 年 7 月 06 日

编者按:自 2013 年 Facebook 发布以来,React 吸引了越来越多的开发者,基于它的衍生技术,如 React Native、React Canvas 等也层出不穷。InfoQ 精心策划“深入浅出React ”系列文章,为读者剖析React 开发的技术细节。

通过前两篇文章的介绍,相信大家对JSX 和组件已经有了一定的了解。JSX 这种混合使用JavaScript 和XML 的语言第一眼看上去很“丑”,也很神奇,但是其语法和背后的逻辑却极其简单。相信读完本文你就可以对JSX 和组件有一个全面的了解,并能够用JSX 来直观的构造用户界面。

什么是JSX

React 的核心机制之一就是虚拟 DOM:可以在内存中创建的虚拟 DOM 元素。React 利用虚拟 DOM 来减少对实际 DOM 的操作从而提升性能。类似于真实的原生 DOM,虚拟 DOM 也可以通过 JavaScript 来创建,例如:

复制代码
var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);

使用这样的机制,我们完全可以用 JavaScript 构建完整的界面 DOM 树,正如我们可以用 JavaScript 创建真实 DOM。但这样的代码可读性并不好,于是 React 发明了 JSX,利用我们熟悉的 HTML 语法来创建虚拟 DOM:

复制代码
var root =(
<ul className="my-list">
<li>First Text Content</li>
<li>Second Text Content</li>
</ul>
);

这两段代码是完全等价的,后者将 XML 语法直接加入到 JavaScript 代码中,让你能够高效的通过代码而不是模板来定义界面。之后 JSX 通过翻译器转换到纯 JavaScript 再由浏览器执行。在实际开发中,JSX 在产品打包阶段都已经编译成纯 JavaScript,JSX 的语法不会带来任何性能影响。另外,由于 JSX 只是一种语法,因此 JavaScript 的关键字 class, for 等也不能出现在 XML 中,而要如例子中所示,使用 className, htmlFor 代替,这和原生 DOM 在 JavaScript 中的创建也是一致的。

因此,JSX 本身并不是什么高深的技术,可以说只是一个比较高级但很直观的语法糖。它非常有用,却不是一个必需品,没有 JSX 的 React 也可以正常工作:只要你乐意用 JavaScript 代码去创建这些虚拟 DOM 元素。

为什么使用 JSX

前端界面的最基本功能在于展现数据,为此大多数框架都使用了模板引擎,例如在 AngularJS 中:

复制代码
<div ng-if="person != null">
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
</div>
<div ng-if="person == null">
Please log in.
</div>

EmberJS 中:

复制代码
{{#if person}}
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
{{else}}
Please log in.
{{/if}}

Knockoutjs 中:

复制代码
<div data-bind="if: person != null">
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
</div>
<div data-bind="if: person == null">
Please log in.
</div>

模板可以直观的定义 UI 来展现 Model 中的数据,你不必手动的去拼出一个很长的 HTML 字符串,几乎每种框架都有自己的模板引擎。传统 MVC 框架强调界面展示逻辑和业务逻辑的分离,因此为了应对复杂的展示逻辑需求,这些模板引擎几乎都不可避免的需要发展成一门独立的语言,如上面代码所示,每个框架都有自己的模板语言语法。而这无疑增加了框架的门槛和复杂度。

如果说掌握一种模板语言并不是很大的问题,那么其实由模板带来的架构复杂性则是让框架也变得复杂的重要原因之一,例如:

  • 模板需要对应数据模型,即上下文,如何去绑定和实现?
  • 模板可以嵌套,不同部分界面可能来自不同数据模型,如何处理?
  • 模板语言终究是一个轻量级语言,为了满足项目需求,你很可能需要扩展模板引擎的功能。

为了解决这些复杂度,框架本身需要精心的设计,以及创造新的概念(例如 Angular 的 Directive)。这些都会让框架变得复杂和难以掌握,不仅增加了开发成本,各种难以调试的 Bug 还会降低开发质量。

正因为如此,React 直接放弃了模板而发明了 JSX。看上去很像模板语言,但其本质是通过代码来构建界面,这使得我们不再需要掌握一门新的语言就可以直观的去定义用户界面:掌握了 JavaScript 就已经掌握了 JSX。这里不妨再引用之前文章举过的例子,在展示一个列表时,模板语言通常提供名为 Repeat 的语法,例如在 Angular 中:

复制代码
<ul class="unstyled">
<li ng-repeat="todo in todoList.todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>

而使用 JSX,则代码如下:

复制代码
var lis = this.todoList.todos.map(function (todo) {
return (
<li>
<input type="checkbox" checked={todo.done}>
<span className={'done-' + todo.done}>{todo.text}</span>
</li>
);
});
var ul = (
<ul class="unstyled">
{lis}
</ul>
);

可以看到,JSX 完美利用了 JavaScript 自带的语法和特性,我们只要记住 HTML 只是代码创建 DOM 的一种语法形式,就很容易理解 JSX。而这种使用代码构建界面的方式,完全消除了业务逻辑和界面元素之间的隔阂,让代码更加直观和易于维护。

JSX 的语法

JSX 本身就和 XML 语法类似,可以定义属性以及子元素。唯一特殊的是可以用大括号来加入 JavaScript 表达式,例如:

var person = <Person name={window.isLoggedIn ? window.name : ''} />;一般每个组件都定义了一组属性(props,properties 的简写)接收输入参数,这些属性通过 XML 标记的属性来指定。大括号中的语法就是纯 JavaScript 表达式,返回值会赋予组件的对应属性,因此可以使用任何 JavaScript 变量或者函数调用。上述代码经过 JSX 编译后会得到:

复制代码
var person = React.createElement(
Person,
{name: window.isLoggedIn ? window.name : ''}
);

对于子元素也是类似,大括号中使用 JavaScript 表达式来返回需要展现的元素,例如文章开头提到的例子使用 JSX 可以写成:

复制代码
var node = (
<div className="container">
{
person ? <span>Welcome back, <b>{person.firstName} {person.lastName}</b>!</span>
: <span>Please log in</span>
}
</div>
);

既然大括号中是 JavaScript,而 JSX 又允许在 JavaScript 中使用 XML,因此在大括号中仍然可以使用 XML 来声明组件,不断递归使用。

如果需要展现一组子节点,只需表达式返回一个 JavaScript 数组,数组的每个元素都是一个 React 组件,例如上一节的例子,其中 lis 就是有多个“li”元素的数组。:

复制代码
var ul = (
<ul class="unstyled">
{lis}
</ul>
);

在 JSX 中使用事件

如果你在 90 年代写过 HTML,那么也许会有点怀念那时的事件绑定是多么的直观和简单:

<button onclick="checkAndSubmit(this.form)">Submit</button>那时的 JavaScript 应用范围非常有限,最有用的也许就是做表单有效性验证。因为逻辑都很简单,直接写到 HTML 中并没有问题,而且这种方式非常直观易读。但是现在因为 Web 程序变的越来越复杂,我们就需要使用 JavaScript 来绑定事件,例如在 jQuery 中:

$('#my-button').on('click', this.checkAndSubmit.bind(this));在看到这段事件绑定和验证逻辑之前,你无法直观的看到有事件绑定在某个元素上,这种隐藏的界面元素和业务逻辑的耦合是很多 Bug 和内存泄露产生的根源。幸运的是,现在 JSX 可以让事件绑定返璞归真:

<button onClick={this.checkAndSubmit.bind(this)}>Submit</button>和原生 HTML 定义事件的唯一区别就是 JSX 采用驼峰写法来描述事件名称,大括号中仍然是标准的 JavaScript 表达式,返回一个事件处理函数。在 JSX 中你不需要关心什么时机去移除事件绑定,因为 React 会在对应的真实 DOM 节点移除时就自动解除了事件绑定。

React 并不会真正的绑定事件到每一个具体的元素上,而是采用事件代理的模式:在根节点 document 上为每种事件添加唯一的 Listener,然后通过事件的 target 找到真实的触发元素。这样从触发元素到顶层节点之间的所有节点如果有绑定这个事件,React 都会触发对应的事件处理函数。这就是所谓的 React 模拟事件系统。

尽管整个事件系统由 React 管理,但是其 API 和使用方法与原生事件一致。这种机制确保了跨浏览器的一致性:在所有浏览器(IE8 及以上)都可以使用符合 W3C 标准的 API,包括 stopPropagation(),preventDefault() 等等。对于事件的冒泡(bubble)和捕获(capture)模式也都完全支持。

在 JSX 中使用样式

尽管在大部分场景下我们应该将样式写在独立的 CSS 文件中,但是有时对于某个特定组件而言,其样式相当简单而且独立,那么也可以将其直接定义在 JSX 中。在 JSX 中使用样式和真实的样式也很类似,通过 style 属性来定义,但和真实 DOM 不同的是,属性值不能是字符串而必须为对象,例如:

<div style={{color: '#ff0000', fontSize: '14px'}}>Hello World.</div>乍一看,这段 JSX 中的大括号是双的,有点奇怪,但实际上里面的大括号只是标准的 JavaScript 对象表达式,外面的大括号是 JSX 的语法。所以,样式你也可以先赋值给一个变量,然后传进去,代码会更易读:

复制代码
var style = {
color: '#ff0000',
fontSize: '14px'
};
var node = <div style={style}>HelloWorld.</div>;

在 JSX 中可以使用所有的的样式,基本上属性名的转换规范就是将其写成驼峰写法,例如“background-color”变为“backgroundColor”, “font-size”变为“fontSize”,这和标准的 JavaScript 操作 DOM 样式的 API 是一致的。

使用自定义组件

在 JSX 中,我们不仅可以使用 React 自带 div, input…这些虚拟 DOM 元素,还可以自定义组件。组件定义之后,也都可以利用 XML 语法去声明,而能够使用的 XML Tag 就是在当前 JavaScript 上下文的变量名,这一点非常好用,你不必再去考虑某个 Tag 是如何对应到相应的组件实现。例如 React 官方教程中的例子:

复制代码
class HelloWorld extends React.Component{
render() {
return (
<p>
Hello, <input type="text" placeholder="Your name here" />!
It is {this.props.date.toTimeString()}
</p>
);
}
};
setInterval(function() {
React.render(
<HelloWorld date={new Date()} />,
document.getElementById('example')
);
}, 500);

其中声明了一个名为 HelloWorld 的组件,那么就可以在 XML 中使用,这个 Tag 就是 JavaScript 变量名,我们可以用任意变量名:

复制代码
var MyHelloWorld = HelloWorld;
React.render(<MyHelloWorld />, …);

甚至,我们还可以引入命名空间:

复制代码
var sampleNameSpace = {
MyHelloWorld: HelloWorld
};
React.render(<sampleNameSpace.MyHelloWorld />, …);

这些语法看上去有点怪,但是如果我们记住 JSX 语法只是 JavaScript 语法的一个语法映射,那么这些就非常容易理解了。

组件的概念和生命周期

React 使用组件来封装界面模块,整个界面就是一个大组件,开发过程就是不断优化和拆分界面组件、构造整个组件树的过程。可以认为组件类似于其他框架中 Widget(或 Control)的概念,但又有所不同。React 中的界面一切皆为组件,而 Widget 一般只是嵌入到界面中为完成某个功能的独立模块。

如下图,整个页面是一个大的组件,然后再将其拆分成很多小的组件。组件机制加上 JSX 的语法,让你在构造界面时就像有一套符合项目需求的 HTML 标记,界面定义变得非常直观。

组件自身定义了一组 props 作为对外接口,展示一个组件时只需要指定 props 作为 XML 节点的属性。组件很少需要对外公开方法,唯一的交互途径就是 props。这使得使用组件就像使用函数一样简单,给定一个输入,组件给定一个界面输出。当给予的参数一定时,那么输出也是一定的。而传统控件通常提供很多方法让你在外部改变控件的状态和行为,当控件的状态在不同场景不同逻辑中可以被随意控制时,开发和调试也会变得复杂。

而 React 组件通过唯一的 props 接口避免了逻辑复杂性,让开发测试都更加容易。这种特性完全得益于虚拟 DOM 机制,让你可以每次 props 改变都能以整体刷新页面的思路去考虑界面展现逻辑。

如果整个项目完全采用 React,那么界面上就只有一个组件根节点;如果局部使用 React,那么每个局部使用的部分都有一个根节点。在 Render 时,根节点由 React.render 函数去触发:

复制代码
React.render(
<App />,
document.getElementById('react-root')
);

而所有的子节点则都是通过父节点的 render 方法去构造的。每个组件都会有一个 render 方法,这个方法返回组件的实例,最终整个界面得到一个虚拟 DOM 树,再由 React 以最高效的方式展现在界面上。

除了 props 之外,组件还有一个很重要的概念:state。组件规范中定义了 setState 方法,每次调用时都会更新组件的状态,触发 render 方法。需要注意,render 方法是被异步调用的,这可以保证同步的多个 setState 方法只会触发一次 render,有利于提高性能。和 props 不同,state 是组件的内部状态,除了初始化时可能由 props 来决定,之后就完全由组件自身去维护。在组件的整个生命周期中,React 强烈不推荐去修改自身的 props,因为这会破坏 UI 和 Model 的一致性,props 只能够由使用者来决定。

对于自定义组件,唯一必须实现的方法就是 render(),除此之外,还有一些方法会在组件生命周期中被调用,如下图所示:

图中的方法几乎已经包括了 React 的所有 API,自定义组件时根据需要在组件生命周期的不同阶段实现不同的逻辑。除了必须的 render 方法之外,其它常用的方法包括:

componentDidMount: 在组件第一次 render 之后调用,这时组件对应的 DOM 节点已被加入到浏览器。在这个方法里可以去实现一些初始化逻辑。

componentWillUnmount: 在 DOM 节点移除之后被调用,这里可以做一些相关的清理工作。

shouldComponentUpdate: 这是一个和性能非常相关的方法,在每一次 render 方法之前被调用。它提供了一个机会让你决定是否要对组件进行实际的 render。例如:

复制代码
shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id;
}

当此函数返回 false 时,组件就不会调用 render 方法从而避免了虚拟 DOM 的创建和内存中的 Diff 比较,从而有助于提高性能。当返回 true 时,则会进行正常的 render 的逻辑。

组件是 React 的核心,虽然功能很强大,但是其 API 和概念却十分简单,以至于你只要实现一个 render 方法就可以创建一个组件。这大大降低了 React 学习门槛。

使用 Babel 进行 JSX 编译

就在本文撰写过程中,React 官方博客发布了一篇文章,声明其自身用于JSX 语法解析的编译器 JSTransform 已经过期,不再维护,React JS 和 React Native 已经全部采用第三方 Babel 的 JSX 编译器实现。原因是两者在功能上已经完全重复,而 Babel 作为专门的 JavaScript 语法编译工具,提供了更为强大的功能。在这里笔者也不得不感叹 Facebook 的胸怀,以非常开放的态度去拥抱开源社区,从而达到共赢的目的。

JSX 是一种新的语法,浏览器并不能直接运行,因此需要这种翻译器。在上一篇文章中我们推荐使用 Webpack 进行 React 的开发,要将 JSX 的编译器从 JSTransform 切换到 Babel 非常简单,首先通过 npm 安装 Babel:

复制代码
npm install —save-dev babel-loader

只需稍微改变一下 webpack.config.js 的配置,将原来的 jsx-loader 变为 babel-loader:

复制代码
module: {
loaders: [
{ test: /\.jsx?$/, loaders: ['babel-loader']}
]
}

小结

本文主要介绍了 React 中最重要的组件机制,以及声明组件的语法 JSX。看似有点神秘的 JSX 背后的原理非常简单:只是一种用于创建组件的 XML 语法。让代码直观易懂是软件项目质量的重要保证之一,这意味着代码更加容易理解和维护,出现 Bug 时更容易调试和修复。因此 React 这种采用 JSX 语法,以声明式的方法来直观的定义用户界面的方式,正是其最大的价值。

整个组件机制运行的基础是虚拟 DOM,正因为 React 能够以极高的性能去比较两个虚拟 DOM 树的 Diff,才实现了每次局部更新都通过刷新整个页面这种思考模式,降低了开发复杂度。在下一篇文章中就将会和大家一起研究虚拟 DOM 的 Diff 算法,了解其背后的运行原理。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 7 月 06 日 00:1139507

评论

发布
暂无评论
发现更多内容

架构师训练营第7周总结

时来运转

JVM 类加载机制

Alex🐒

JVM 深入理解JVM

并发必备基础知识汇总

itlemon

并发 基础

Spring Security入门到实践(一)HTTP Basic在Spring Security中的应用原理浅析

itlemon

源码分析 spring security

JVM 运行时数据区

Alex🐒

JVM 深入理解JVM

JVM 垃圾回收机制

Alex🐒

JVM 深入理解JVM

架构师训练营第7周作业

时来运转

【译文】创建 Kubernetes manifest 的初学者指南

FeiLong

Kubernetes

Java并行程序基础

itlemon

Java 高并发 并行

JVM 对象内存布局

Alex🐒

JVM 深入理解JVM

vcenter 5.5故障处理

小小文

vcenter

记一次bem命名规范使用优化方案

前端有的玩

Vue npm React bem

【数据结构】Java 常用集合类 PriorityQueue

Alex🐒

Java 源码 数据结构

Flask 中的 Sessions

Leetao

Python flask Web框架

Ubuntu 20.04 上安装和配置 VNC

酱紫的小白兔

【数据结构】Java 常用集合类 ArrayDeque

Alex🐒

Java 源码 数据结构

【干货分享】通过命令操作来学习Git

itlemon

git git入门

架构师训练营第七周作业-性能测试

sunnywhy

解决 Harbor 启动失败故障

FeiLong

Docker Harbor Docker-compose

优雅地利用c++编程从1乘到20 | 技术总结

chaozh

c++

深入理解 JS 中的 this

墨子苏

Java 前端

深入理解 JS 中的变量提升

墨子苏

Java 前端

程序员面试必备战衣 | T恤衫 - 程序员穿搭

chaozh

GEEK

创业使人成长系列 (5)-申请国家高新企业

石云升

高新企业

玩转混合加密 | 精美配图

阿宝哥

安全 加密解密 数据加密

JVM 垃圾回收器 CMS

Alex🐒

JVM 深入理解JVM GC

压测工具如何选择?

elfkingw

彻底弄懂C++11右值引用 | 技术总结

chaozh

c++

架构师训练营 - 命题作业 第 7 周

铁血杰克

JVM 垃圾回收器 G1

Alex🐒

JVM 深入理解JVM GC

深入Java Web技术内幕(一)浅析Web请求过程

itlemon

Java

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

深入浅出React(三):理解JSX和组件-InfoQ