(三):理解JSX和组件
最后更新于:2022-04-01 23:10:57
> 原文:http://www.infoq.com/cn/articles/react-jsx-and-component
通过前两篇文章的介绍,相信大家对JSX和组件已经有了一定的了解。JSX这种混合使用JavaScript和XML的语言第一眼看上去很“丑”,也很神奇,但是其语法和背后的逻辑却极其简单。相信读完本文你就可以对JSX和组件有一个全面的了解,并能够用JSX来直观的构造用户界面。
**目录**
[TOC]
## 什么是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 =(
{todo.text}
);
});
var ul = (
;
~~~
一般每个组件都定义了一组属性(props,properties的简写)接收输入参数,这些属性通过XML标记的属性来指定。大括号中的语法就是纯JavaScript表达式,返回值会赋予组件的对应属性,因此可以使用任何JavaScript变量或者函数调用。上述代码经过JSX编译后会得到:
~~~
var person = React.createElement(
Person,
{name: window.isLoggedIn ? window.name : ''}
);
~~~
对于子元素也是类似,大括号中使用JavaScript表达式来返回需要展现的元素,例如文章开头提到的例子使用JSX可以写成:
~~~
var node = (
,
document.getElementById('example')
);
}, 500);
~~~
其中声明了一个名为HelloWorld的组件,那么就可以在XML中使用,这个Tag就是JavaScript变量名,我们可以用任意变量名:
~~~
var MyHelloWorld = HelloWorld;
React.render( , …);
~~~
甚至,我们还可以引入命名空间:
~~~
var sampleNameSpace = {
MyHelloWorld: HelloWorld
};
React.render( , …);
~~~
这些语法看上去有点怪,但是如果我们记住JSX语法只是JavaScript语法的一个语法映射,那么这些就非常容易理解了。
## 组件的概念和生命周期
React使用组件来封装界面模块,整个界面就是一个大组件,开发过程就是不断优化和拆分界面组件、构造整个组件树的过程。可以认为组件类似于其他框架中Widget(或Control)的概念,但又有所不同。React中的界面一切皆为组件,而Widget一般只是嵌入到界面中为完成某个功能的独立模块。
如下图,整个页面是一个大的组件,然后再将其拆分成很多小的组件。组件机制加上JSX的语法,让你在构造界面时就像有一套符合项目需求的HTML标记,界面定义变得非常直观。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-31_55baec3a21598.png)
组件自身定义了一组props作为对外接口,展示一个组件时只需要指定props作为XML节点的属性。组件很少需要对外公开方法,唯一的交互途径就是props。这使得使用组件就像使用函数一样简单,给定一个输入,组件给定一个界面输出。当给予的参数一定时,那么输出也是一定的。而传统控件通常提供很多方法让你在外部改变控件的状态和行为,当控件的状态在不同场景不同逻辑中可以被随意控制时,开发和调试也会变得复杂。
而React组件通过唯一的props接口避免了逻辑复杂性,让开发测试都更加容易。这种特性完全得益于虚拟DOM机制,让你可以每次props改变都能以整体刷新页面的思路去考虑界面展现逻辑。
如果整个项目完全采用React,那么界面上就只有一个组件根节点;如果局部使用React,那么每个局部使用的部分都有一个根节点。在Render时,根节点由React.render函数去触发:
~~~
React.render(
,
document.getElementById('react-root')
);
~~~
而所有的子节点则都是通过父节点的render方法去构造的。每个组件都会有一个render方法,这个方法返回组件的实例,最终整个界面得到一个虚拟DOM树,再由React以最高效的方式展现在界面上。
除了props之外,组件还有一个很重要的概念:state。组件规范中定义了setState方法,每次调用时都会更新组件的状态,触发render方法。需要注意,render方法是被异步调用的,这可以保证同步的多个setState方法只会触发一次render,有利于提高性能。和props不同,state是组件的内部状态,除了初始化时可能由props来决定,之后就完全由组件自身去维护。在组件的整个生命周期中,React强烈不推荐去修改自身的props,因为这会破坏UI和Model的一致性,props只能够由使用者来决定。
对于自定义组件,唯一必须实现的方法就是render(),除此之外,还有一些方法会在组件生命周期中被调用,如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-07-31_55baec3b4138e.png)
图中的方法几乎已经包括了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官方博客发布了[一篇文章](http://facebook.github.io/react/blog/2015/06/12/deprecating-jstransform-and-react-tools.html),声明其自身用于JSX语法解析的编译器[JSTransform](https://github.com/facebook/jstransform)已经过期,不再维护,React JS和React Native已经全部采用第三方[Babel](http://babeljs.io/)的JSX编译器实现。原因是两者在功能上已经完全重复,而Babel作为专门的JavaScript语法编译工具,提供了更为强大的功能。在这里笔者也不得不感叹Facebook的胸怀,以非常开放的态度去拥抱开源社区,从而达到共赢的目的。
JSX是一种新的语法,浏览器并不能直接运行,因此需要这种翻译器。在[上一篇文章](http://www.infoq.com/cn/articles/react-and-webpack)中我们推荐使用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算法,了解其背后的运行原理。
';
- First Text Content
- Second Text Content
Welcome back, {{person.firstName}} {{person.lastName}}!
Please log in.
~~~
在[EmberJS](http://emberjs.com/)中:
~~~
{{#if person}}
Welcome back, {{person.firstName}} {{person.lastName}}!
{{else}}
Please log in.
{{/if}}
~~~
在[Knockoutjs](http://knockoutjs.com/)中:
~~~
Welcome back, {{person.firstName}} {{person.lastName}}!
Please log in.
~~~
模板可以直观的定义UI来展现Model中的数据,你不必手动的去拼出一个很长的HTML字符串,几乎每种框架都有自己的模板引擎。传统MVC框架强调界面展示逻辑和业务逻辑的分离,因此为了应对复杂的展示逻辑需求,这些模板引擎几乎都不可避免的需要发展成一门独立的语言,如上面代码所示,每个框架都有自己的模板语言语法。而这无疑增加了框架的门槛和复杂度。
如果说掌握一种模板语言并不是很大的问题,那么其实由模板带来的架构复杂性则是让框架也变得复杂的重要原因之一,例如:
* 模板需要对应数据模型,即上下文,如何去绑定和实现?
* 模板可以嵌套,不同部分界面可能来自不同数据模型,如何处理?
* 模板语言终究是一个轻量级语言,为了满足项目需求,你很可能需要扩展模板引擎的功能。
为了解决这些复杂度,框架本身需要精心的设计,以及创造新的概念(例如Angular的Directive)。这些都会让框架变得复杂和难以掌握,不仅增加了开发成本,各种难以调试的Bug还会降低开发质量。
正因为如此,React直接放弃了模板而发明了JSX。看上去很像模板语言,但其本质是通过代码来构建界面,这使得我们不再需要掌握一门新的语言就可以直观的去定义用户界面:掌握了JavaScript就已经掌握了JSX。这里不妨再引用之前文章举过的例子,在展示一个列表时,模板语言通常提供名为Repeat的语法,例如在Angular中:
~~~
- {{todo.text}}
-
{lis}
{
person ? Welcome back, {person.firstName} {person.lastName}!
: Please log in
}
);
~~~
既然大括号中是JavaScript,而JSX又允许在JavaScript中使用XML,因此在大括号中仍然可以使用XML来声明组件,不断递归使用。
如果需要展现一组子节点,只需表达式返回一个JavaScript数组,数组的每个元素都是一个React组件,例如上一节的例子,其中lis就是有多个“li”元素的数组。:
~~~
var ul = (
-
{lis}
Hello World.
~~~
乍一看,这段JSX中的大括号是双的,有点奇怪,但实际上里面的大括号只是标准的JavaScript对象表达式,外面的大括号是JSX的语法。所以,样式你也可以先赋值给一个变量,然后传进去,代码会更易读:
~~~
var style = {
color: '#ff0000',
fontSize: '14px'
};
var node = HelloWorld.
;
~~~
在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 (
Hello, ! It is {this.props.date.toTimeString()}
); } }; setInterval(function() { React.render(