深入了解JSX
最近开始学着使用React写东西。在写代码时会使用JSX,不了解JSX的相关知识写起代码的效率还是蛮低的。为了能更好的进入状态,打算先把JSX相关的知识和细节了解一下。在这篇文章中我们主要就是来学习一下JSX的相关知识。希望对于像我这样的初学React(或初次接触JSX)的同学有所帮助。
什么是JSX
JSX是JavaScript中的一种语法扩展。是React组件编写UI逻辑的语言扩展(JSX除了能在React中使用之外还可以用于别的地方)。虽然在不使用JSX的情况之下,React可以完全正常工作,但它是一种理想的组件处理技术,所以React从JSX中获益良多。
首先,你可能认为使用JSX就是将HTML和JavaScript混合在一起,事实并非如此,因为在使用JSX语法时,描述这个UI不是用字符串,使用的是JavaScript。这让我们能做很多事。
将JavaScript和标记(Markup)放在同一位置被认为是一种坏习惯,但事实证明,将视图与功能结合起来可以直接对视图进行推理。
为了了解这意味着什么,假设我们有一个React组件,它只渲染一个<h1>
标签。JSX允许我们以种非常类似HTML的方式声明这个元素:
class HelloWorld extends React.Component {
render() {
return (
<h1 className="title">Hello World</h1>
)
}
}
HelloWorld
组件中的render()
函数看起来返回的是HTML,但实际上这是JSX。JSX在运行时被转换为常规则JavaScript。翻译后的组件看起来是这样的:
class HelloWorld extends React.Component {
render() {
return (
React.createElement(
'h1',
{className: 'title'},
'Hello World'
)
)
}
}
虽然JSX看起来像HTML,但实际上它只是一种编写React.createElement()
声明的更简洁的方法。当组件渲染时,它输出一个React元素树或该组件输出的HTML元素的虚拟DOM。然后,React将根据这个React元素表示确定对实际DOM进行哪些更改。HelloWorld
组件渲染出来的React的HTML看起来像下面这样:
<h1 class='title'>Hello World</h1>
为什么使用JSX
React认为渲染逻辑本质上与其他UI逻辑内在耦合,比如,在UI中需要绑定处理事件、在某些时刻状态发生变化时需要通知到UI,以及需要在UI中展示准备好的数据。
React并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为组件的松散耦合单元之中,来实现“关注点分离”。React不强制要求使用JSX,但大多数人发现,在JavaScript代码中将JSX和UI放在一起时,会在视觉上有辅助作用。它还可以使React显示更多有用的错误和警告消息。
JSX原理
在继续往下阅读之前,先稍微的了解一些有关于JSX原理相关的内容。即JSX是经过怎么样的转化变成页面的元素的。
比如说下面这个最简单的DOM元素为例,怎么使用JavaScript的对象来表现一个DOM元素的结果:
<h1 class="title">Hello World!</h1>
每个DOM元素的结构都可以用JavaScript的对象来表示。你会发现,DOM元素包含的信息其实只有三个:标签名(HTML元素)、属性(HTML元素属性)和子元素(HTML元素或文本节点)。那么上面的这个HTML标签对应的所有信息可以用下面这个对象来描述:
{
tag: 'div',
attrs: {className: 'title'},
children: 'Hello World!'
}
你会发现,HTML的信息和JavaScript所包含的结构和信息其实是一样的,我们可以用JavaScript对象来描述所有能用HTML表示的UI信息。只不过写起来有点麻烦,结构看起来不太清晰。
在React中会把类似HTML的JSX结构转换成JavaScript的对象结构。比如:
<h1 class="title">Hello World!</h1>
JSX转换成JavaScript就会像下面这样:
React.createElement(
"h1",
{
class: "title"
},
"Hello World!"
);
React.createElement()
会构建一个JavaScript对象来描述HTML结构的信息,包括标签名、属性和子元素等。上面是一个较为简单的示例,对于复杂的示例,我们可以借助在线的工具来帮助我们,比如Babel:
所谓的JSX其实就是JavaScript对象。
有了之个表示HTML结构和信息的对象以后,就可以去构造真正的DOM元素,然后使用ReactDOM.render()
函数把构造好的DOM元素塞到页面中:
ReactDOM.render(
<HelloWorld />,
document.getElementById('root')
)
ReactDOM.render()
函数就是把组件渲染并且构造DOM树,然后插入到页面上某个特定的元素上div#root
。
简单地总结一下,JSX经过Babel编译和React构造器转换成了JavaScript对象,然后通过ReactDOM.render()
函数转换成DOM元素,再插入到页面中渲染出来:
接下来简单看看JSX的具体使用。
JSX的使用
在JSX中可以像下面这样定义一个包含字符串的<h1>
标签:
const element = <h1> Hello World!~</h1>
看起来有点像是JavaScript和HTML的结合特,但实际上它就是JavaScript。在JSX中是用来定义组件及其在标记中的位置的语法糖。事实element
是一个Object
:
JSX中嵌入表达式
在下面的示例中,声明了一个名为eleId
的变量,然后在JSX中使用它,并且放置在{}
中,比如:
const eleId = 'title'
const element = <h1 id={eleId}>Hello World!</h1>
在JSX语法中,可以在大括号内放置任何有效的JavaScript表达式,比如:
const Hello = (user) => {
return `Hello ${user}`;
}
const element = <h1>{ Hello('W3cplus.com') }</h1>
在属性中嵌入JavaScript表达式时,不要在大括号外面加上引号。应该仅使用引用(字符串)或大括号(表达式)中的一个,对于同一个属性不能同时使用这两种符号。
JSX中的特定属性
在HTML中标签中有时候会用到带有-
中折号的属性,比如aira-hidden
,也会有多个词组合在一起的属性,比如tabindex
。类似这些属性在JSX中需要使用驼峰写法,比如airaHidden
或tabIndex
。
const eleId='title'
const element = <h1 id={eleId} tabIndex="0">Hello World!</h1>
需要特别注意的是,在JSX中要是会用到class
和for
属性时,需要将class
换成 className
,for
换成 htmlFor
。那是因为class
和for
是JavaScript中的关键字。
const element = (
<div className="control">
<label htmlFor="user">Name:</label>
<input id="user" type="text" placholder="Enter your name" />
</div>
)
在JSX中有些写法和HTML类似,比如一个标签里没有内容,可以使用/>
来闭合标签,比如上面的<input>
元素:
<input id="user" type="text" placholder="Enter your name" />
如果在ReactDOM.render()
包含多个子元素时(JSX标签里包含多个子元素),需要用一个标签元素括起来,比如上面示例中的<div className="control">
。另外将其放置在一个括号()
中。这是因为render()
函数只能返回一个节点,所以如果你想返回两个兄弟节点,就需要添加一个父节点,如上面示例所示。
JSX是一个对象
浏览器不能直接执行包含JSX代码的JavaScript文件。它们必须首先转换成普通的JavaScript。需要一个叫做转置的过程。当然,在React中JSX是可选的,因为对于每个JSX代码都有对应的纯JavaScript代码替代,这就是JSX的换位符。
正如文章开头所提到的,Babel会把JSX转译成一个名为React.createElement()
函数调用。比如下面的两段代码,起到的作用是同等的:
// JSX代码
ReactDOM.render(
<div id="box">
<h1>title</h1>
<p>paragraph</p>
</div>,
document.getElementById('root')
)
// JavaScript
ReactDOM.render(
React.createElement(
"div",
{id: "box"},
React.createElement(
"h1",
null,
"title"
),
React.createElement(
"p",
null,
"paragraph"
)
),
document.getElementById('root')
);
JSX和JavaScript两段代码同样可以借助在线的Babel工具查看。
上面的示例也再次验证了,虽然JSX和JavaScript所起的效果是等效的,但相比而言,JavaScript要比JSX复杂的多。另外React.createElement()
会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
// 简化后的结构
React.createElement(
"h1",
null,
"title"
)
实际上,JSX仅仅只是React.createElement(component, props, ...children)
函数的语法糖。如下JSX代码:
// JSX代码
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
编译成JavaScript代码如下:
// JavaScript代码
React.createElement(
MyButton,
{
color: "blue",
shadowSize: 2
},
"Click Me"
);
JSX中的JS
JSX接受任何混合的JavaScript。当你需要添加一些JavaScript时,只需要将它放到大括号{}
中(前面也有简单的提到过)。例如,下面的示例就是向你展示了在JSX中怎么引用其他地方声明的常量:
const name = '大漠'
ReactDOM.render(
<h1> Hello, { name }</h1>,
document.getElementById('root')
)
正如你所看到的,我们可以将JavaScript嵌套在JSX中定义的JSX中,而且你想多深就可以多深(你想放置在JSX代码中的位置):
const items = ['大漠', 'w3cplus', '杭州']
ReactDOM.render(
<ul>
{items.map((item, i) => {
return <li>({i}):{item}</li>
})}
</ul>,
document.getElementById('root')
)
JSX中的HTML
JSX看上去和HTML非常相似,但实际上是XML语法。但在页面中最终呈现的依旧是HTML,因此我们有必要了解HTML中如何定义某些内容与在JSX中定义的差异性。
需要关闭所有标记
就像在XHTML中一样,需要关闭所有的标记,比如<img>
标签需要使用自闭标记<img />
,其他标记也是类似。<img>
标记有点特殊,那么对于其他HTML标记也是有相通的原理,比如<div>
这样的常规标记,如果没有包含子元素(或子节点)也可以像这样使用<div />
来关闭标签,如果有子元素之类的就需要<div></div>
这样使用。
驼峰写法是新的标准
在HTML中,你会发现属性没有任何大小写,比如onclick
。但在JSX中,它们被重命名,需要用驼峰的方式来命名:
onchange => onChange
onclick => onClick
onsubmit => onSubmit
前面也提到过了,HTML中的
class
和for
是JavaScript的关键词,所以在JSX中需要以新的方式提供class=>className
,for=>htmlFor
。
JSX中的表单
JSX中对于表单的部分做一些更改,主要目的是让开发人员更容易地完成工作。
value和defaultValue
value
属性始终保存字段的当前值;defaultValue
属性保存创建字段时设置的默认值。这有助于解决在检查输入(input
)时常规DOM交互的一些奇怪行为。input.value
和input.getAttribute('value')
返回一个当前值和一个初始默认值。
这个也适用于textarea
标签,比如:
<textarea>多行文本域的文本</textarea>
需要用下面的方式来替代:
<textarea defaultValue={'多行文本域的文本'} />
<select>
也类似:
<select>
<option value="x" selected></option>
</select>
用下面的这种方式来替代:
<select defaultValue="x">
<option value="x"></option>
</select>
JSX中的HTML实体符
在HTML中编写代码的时候,有时候会使用到HTML的实体符,比如©
用©
来表示。在JSX中为了降低XSS攻击的风险,JSX强制在表达式中自动会将HTML实体符进行转义。比如:
<p>{'© 2019'}</p>
在JSX中,©
会被转义为"\xA9 2019"
。那么上面的代码,在JSX中应该:
<p>{'\xA9 2019'}</p>
<!-- 或者 -->
<p>{'\u00A9 2019'}</p>
渲染出来的结果如下:
JSX中的空格符
在JSX中添加空格符有两条规则:
- 水平空白符会被裁剪为一个空白符:如果在同一行元素之间有空白,则所有元素都会被裁剪为一个空白符
- 消除垂直换行的空白符:如果是垂直换行的空白符会完全消除
比如:
<!-- JSX -->
<p>Something becomes this</p>
<p>
Something
becomes
this
</p>
<!-- 编译后结果 -->
<p>Something becomes this</p>
<p>Somethingbecomesthis</p>
要解决这个问题,需要显式地通过添加一个空格符表达式来处理,像下面这样:
<p>
Something
{' '}becomes
{' '}this
</p>
<!-- 或像下面这样 -->
<p>
Something
{' becomes '}
this
</p>
JSX中的注释
在编码时难免少不了添加一些注释。在不同的语言中有不同的注释方式,比如:
<!-- HTML中的注释方式 -->
/* CSS中的单行注释方式 */
/*
* CSS 中的多行注释方式
*
*/
// JavaScript中的注释
// 1. JavaScript的多行注释
// 2. JavaScript的多行注释
/*
* JavaScript中的多行注释
*/
JSX中的注释会放在{}
中,像下面这样:
{/* JSX中的单行注释*/}
{
// JSX中的单行注释
}
{
/*
* JSX中的多行注释
*/
}
JSX中的条件渲染
在JavaScript中,对于if ... else
和switch
语句并不会感到陌生,在JSX中其实也可以使用它们。
在JSX中根据条件渲染中,组件根据一个或多个条件决定返回哪些元素。例如可以返回一个项目列表,也可以返回一条消息。当组件具有条件渲染时,渲染组件的实例可以具有不同的外观(组件样式不一样)。
JSX中的if ... else
条件渲染最简单的方式就是使用if ... else
方式。例如,当没有项列表时,列表组件不应该渲染列表。我们可以使用if
语句从渲染生命周期中提前返回。
function List({ list }) {
if (!list) {
return null;
}
return (
<div>
{list.map(item => <ListItem item={item} />)}
</div>
);
}
上面的JSX编译后的结果如下:
function List(_ref) {
var list = _ref.list;
if (!list) {
return null;
}
return React.createElement(
"div",
null,
list.map(function (item) {
return React.createElement(
ListItem,
{item: item}
);
})
);
}
返回null
的组件将不会渲染任何内容。然而,你可能想在列表为空时显示文本,以便为你的应用程序给用户提供一些反馈,提供较好的用户体验:
function List({ list }) {
if (!list) {
return null;
}
if (!list.length) {
return <p>对不起,列表是空的!</p>;
} else {
return (
<div>
{list.map(item => <ListItem item={item} />)}
</div>
);
}
}
JSX中的三元运算符
使用三元运算符可以使if ... else
语句更简洁。
来看一个示例,假设组件中一个切换按钮,可以在高亮和暗黑两种模式之间切换。决定切换到哪种状态只需要一个简单条件的布尔值,该布尔值可以决定要返回哪个元素(切换到哪种模式):
function Item({ item, mode }) {
const isDarkMode = mode === 'DARK';
return (
<div>
{ isDarkMode
? <DarkMode item={item} />
: <HighlightMode item={item} />
}
</div>
);
}
如果三元运算符中的两个分支会用到多个子元素时,可以使用()
括起来:
function Item({ item, mode }) {
const isDarkMode = mode === 'DARK';
return (
<div>
{
isDarkMode ? (
<DarkMode item={item} />
) : (
<HighlightMode item={item} />
)
}
</div>
);
}
JSX中的&&
操作符
在写组件的时候,经常会碰到这种情况,要么渲染出组件,要么什么都不渲染,例如有一个Card
组件,它渲染一个卡片或什么都不渲染。我们可以使用上面提到的if ... else
语句或三元运算符来实现:
{/* if ... else */}
function CardFun({ isCard }) {
if (isCard) {
return (
<Card />
)
} else {
return null
}
}
{/* 三元操作符 */}
function CardFun({ isCard }) {
return (
<div>
{isCard ? <Card /> : null}
</div>
)
}
但还有一种方法会更简单,可以省略返回null
的必要性。即使用&&
操作符会更简单:
const result = true && 'Hello World';
console.log(result);
// Hello World
const result = false && 'Hello World';
console.log(result);
// false
使用&&
运算符,上面的示例可以像下面这样使用:
function CardFun({ isCard }) {
return (
<div>
{isCard && <Card />}
</div>
)
}
JSX中的switch操作符
在JSX中如果要根据多个不同的条件渲染出组件不同的状态,那么可以使用switch ... case
的方式来处理,比如:
function Notification({ text, state }) {
switch(state) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return null;
}
}
在React中,如果组件基于状态的条件渲染时,还可以使用React.PropTypes
描述组件接口:
function Notification({ text, state }) {
switch(state) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return null;
}
}
Notification.propTypes = {
text: React.PropTypes.string,
state: React.PropTypes.oneOf(['info', 'warning', 'error'])
}
有关于React中的
React.PropTypes
相关知识不在这里阐述,后续我们会专门花一定的篇幅来介绍这方面的知识。
现在有一个通用组件来显示不同类型的通知。例如,基于state
的值,组件可以有不同的外观。error
是红色的,warning
可能是黄色的,info
可能是蓝色的。
在JSX中还可以用内联开关的方式,需要一个自调用JavaScript函数。
function Notification({ text, state }) {
return (
<div>
{(function() {
switch(state) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return null;
}
})()}
</div>
);
}
上面的示例,还可以使用ES6箭头函数来写,会更简洁:
function Notification({ text, state }) {
return (
<div>
{(() => {
switch(state) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return null;
}
})()}
</div>
);
}
JSX中使用枚举条件来渲染
在JavaScript中,当对象用作键值的映射时,对象可以用作枚举。
const ENUM = {
a: '1',
b: '2',
c: '3',
};
枚举用作多个条件渲染也是一个较好的方式,比如上面的示例,使用枚举可以改写成下面这样:
function Notification({ text, state }) {
return (
<div>
{{
info: <Info text={text} />,
warning: <Warning text={text} />,
error: <Error text={text} />,
}[state]}
</div>
);
}
state
属性键帮助我们从对象中检索值。相比前面的示例是不是更简洁,是吧。而且更具可读性。在本例中,我们必须使用内联对象,因为对象的值依赖于text
属性。如果不依赖text
,就需要使用外部静态枚举方式:
const NOTIFICATION_STATES = {
info: <Info />,
warning: <Warning />,
error: <Error />,
};
function Notification({ state }) {
return (
<div>
{NOTIFICATION_STATES[state]}
</div>
);
}
如果依赖text
属性可以使用函数来检索值:
const getSpecificNotification = (text) => ({
info: <Info text={text} />,
warning: <Warning text={text} />,
error: <Error text={text} />,
});
function Notification({ state, text }) {
return (
<div>
{getSpecificNotification(text)[state]}
</div>
);
}
JSX中多级条件渲染
JSX中还会有多级条件(嵌套条件)渲染的场景。比如下面这个示例,List
组件,它既可以显示列表、空文本,也可以什么都不显示:
function List({ list }) {
const isNull = !list;
const isEmpty = !isNull && !list.length;
return (
<div>
{ isNull
? null
: ( isEmpty
? <p>Sorry, the list is empty.</p>
: <div>{list.map(item => <ListItem item={item} />)}</div>
)
}
</div>
);
}
// Usage
<List list={null} /> // => <div></div>
<List list={[]} /> // => <div><p>Sorry, the list is empty.</p></div>
<List list={['a', 'b', 'c']} /> // => <div><div>a</div><div>b</div><div>c</div><div>
上面的示例还可以继续优化,将嵌套条件保持在最小值状态。建议将复杂的组件拆分成更小的组件,这些组件自身具备有条件地渲染。
function List({ list }) {
const isList = list && list.length;
return (
<div>
{ isList
? <div>{list.map(item => <ListItem item={item} />)}</div>
: <NoList isNull={!list} isEmpty={list && !list.length} />
}
</div>
);
}
function NoList({ isNull, isEmpty }) {
return (!isNull && isEmpty) && <p>Sorry, the list is empty.</p>;
}
特别声明:JSX中条件渲染这部分示例代码来自于@rwieruch的《All React Conditional Rendering Techniques》一文。
有关于条件渲染相关的还可以阅读@Kristina Grujic的《React JSX: How to Do It the Right Way, Part II》和《条件渲染》。
JSX中的循环
如果你有一组需要循环的元素来生成JSX部分,可以创建一个循环,然后将JSX添加到一个数组中:
const elements = [] //..some array
const items = []
for (const [index, value] of elements.entries()) {
items.push(<Element key={index} />)
}
现在,当渲染JSX时,可以通过用大括号{}
来嵌套items
数组:
const elements = ['one', 'two', 'three'];
const items = []
for (const [index, value] of elements.entries()) {
items.push(<li key={index}>{value}</li>)
}
return (
<div>
{items}
</div>
)
如果使用map
还可以在JSX中直接执行相同的操作:
const elements = ['one', 'two', 'three'];
return (
<ul>
{elements.map((value, index) => {
return <li key={index}>{value}</li>
})}
</ul>
)
有关于JSX中的循环相关的使用,更详细的介绍还可以阅读:
JSX中表达式
前面其实提到过,在JSX中可以通过使用大括号{}
来引用表达式,用于将变量传递给元素和属性、计算布尔表达式等。比如下面这样的一个示例:
class App extends Component {
constructor(props) {
super(props);
this.state = {
address: {
houseNumber: '12',
street: 'Oakwood Drive',
city: 'Red Hook',
state: 'New York'
zipcode: '1234',
country: 'USA'
}
}
}
formatAddress() {
const { address } = this.state
return `${address.houseNumber}
${address.street}
${address.city}
${address.state}
${address.zipcode}
${address.country}
`
}
render() {
return (
<div className="App">
{ this.formatAddress() }
</div>
)
}
}
const rootElement = document.getElementById(root)
ReactDOM.render(<App />, rootElement)
如果想在字符串中显示变量内部的值,那么ES6的模板字符串(Template Literals)就非常有用,比如:
cosnt monthAndYear = `${month} - ${year}`
在标签元素中属性也可以使用表达多:
const profilePicture = <img src = {user.picture} alt="me!" />
另外如果想根据条件给元素添加类名,也可以使用表达式直接处理:
<h1 className={isLogo ? 'logo' : 'title'}></h1>
JSX 中的 Props
在JSX中有多种方式可以指定Props。
JavaScript 表达式作为 Props
你可以把包裹在 {}
中的 JavaScript 表达式作为一个 prop
传递给 JSX 元素。例如,如下的 JSX:
<MyComponent foo={1 + 2 + 3 + 4} />
在 MyComponent
中,props.foo
的值等于 1 + 2 + 3 + 4
的执行结果 10
。
if
语句以及 for
循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中。比如:
function NumberDescriber(props) {
let description;
if (props.number % 2 == 0) {
description = <strong>even</strong>;
} else {
description = <i>odd</i>;
}
return <div>{props.number} is an {description} number</div>;
}
字符串字面量
你可以将字符串字面量赋值给 prop
。 如下两个 JSX 表达式是等价的:
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
当你将字符串字面量赋值给 prop
时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:
<MyComponent message="<3" />
<MyComponent message={'<3'} />
这种行为通常是不重要的,这里只是提醒有这个用法。
Props 默认值为 “True”
如果你没给 prop
赋值,它的默认值是 true
。以下两个 JSX 表达式是等价的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
通常,我们不建议这样使用,因为它可能与 ES6 对象简写混淆,{foo}
是 {foo: foo}
的简写,而不是 {foo: true}
。这样实现只是为了保持和 HTML 中标签属性的行为一致。
属性展开
如果你已经有了一个 props
对象,你可以使用展开运算符 ...
来在 JSX 中传递整个 props
对象。以下两个组件是等价的:
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
你还可以选择只保留当前组件需要接收的 props
,并使用展开运算符将其他 props
传递下去。
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
const App = () => {
return (
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
在上述例子中,kind
的 prop
会被安全的保留,它将不会被传递给 DOM 中的 <button>
元素。 所有其他的 props
会通过 ...other
对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClick
和 children
属性。
属性展开在某些情况下很有用,但是也很容易将不必要的 props
传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。
JSX 中的子元素
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children
传递给外层组件。有几种不同的方法来传递子元素:
字符串字面量
你可以将字符串放在开始和结束标签之间,此时 props.children
就只是该字符串。这对于很多内置的 HTML 元素很有用。例如:
<MyComponent>Hello world!</MyComponent>
这是一个合法的 JSX,MyComponent
中的 props.children
是一个简单的未转义字符串 "Hello world!"
。因此你可以采用编写写 HTML 的方式来编写写 JSX。如下所示:
<div>This is valid HTML & JSX at the same time.</div>
JSX 子元素
子元素允许由多个 JSX 元素组成。这对于嵌套组件非常有用:
<MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
你可以将不同类型的子元素混合在一起,因此你可以将字符串字面量与 JSX 子元素一起使用。这也是 JSX 类似 HTML 的一种表现,所以如下代码是合法的 JSX 并且也是合法的 HTML:
<div>
Here is a list:
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
React 组件也能够返回存储在数组中的一组元素:
render() {
// 不需要用额外的元素包裹列表元素!
return [
// 不要忘记设置 key :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}
JavaScript 表达式作为子元素
JavaScript 表达式可以被包裹在 {}
中作为子元素。例如,以下表达式是等价的:
<MyComponent>foo</MyComponent>
<MyComponent>{'foo'}</MyComponent>
这对于展示任意长度的列表非常有用。例如,渲染 HTML 列表:
function Item(props) {
return <li>{props.message}</li>;
}
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
JavaScript 表达式也可以和其他类型的子元素组合。这种做法可以方便地替代模板字符串:
function Hello(props) {
return <div>Hello {props.addressee}!</div>;
}
函数作为子元素
通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children
和其他 prop
一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。例如,如果你有一个自定义组件,你可以把回调函数作为 props.children
进行传递:
// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX。
布尔类型、Null 以及 Undefined 将会忽略
false
, null
, undefined
和 true
是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeader
为 true
时,才会渲染 <Header />
:
<div>
{showHeader && <Header />}
<Content />
</div>
值得注意的是有一些 “falsy” 值,如数字 0
,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages
是空数组时,0
仍然会被渲染:
<div>
{props.messages.length &&
<MessageList messages={props.messages} />
}
</div>
要解决这个问题,确保 &&
之前的表达式总是布尔值:
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
反之,如果你想渲染 false
、true
、null
、undefined
等值,你需要先将它们转换为字符串:
<div>
My JavaScript variable is {String(myVariable)}.
</div>
指定 React 元素类型
JSX 标签的第一部分指定了 React 元素的类型。
大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX <Foo />
表达式时,Foo
必须包含在作用域内。
React 必须在作用域内
由于 JSX 会编译为 React.createElement
调用形式,所以 React 库也必须包含在 JSX 代码作用域内。
例如,在如下代码中,虽然 React
和 CustomButton
并没有被直接使用,但还是需要导入:
import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
// return React.createElement(CustomButton, {color: 'red'}, null);
return <CustomButton color="red" />;
}
如果你不使用 JavaScript 打包工具而是直接通过 <script>
标签加载 React
,则必须将 React
挂载到全局变量中。
在 JSX 类型中使用点语法
在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker
是一个组件,你可以在 JSX 中直接使用:
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
用户定义的组件必须以大写字母开头
以小写字母开头的元素代表一个 HTML 内置组件,比如 <div>
或者 <span>
会生成相应的字符串 'div'
或者 'span'
传递给 React.createElement
(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 <Foo />
会编译为 React.createElement(Foo)
。
我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。
例如,以下的代码将无法按照预期运行:
import React from 'react';
// 错误!组件应该以大写字母开头:
function hello(props) {
// 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
// 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
return <hello toWhat="World" />;
}
要解决这个问题,我们需要重命名 hello
为 Hello
,同时在 JSX 中使用 <Hello />
:
import React from 'react';
// 正确!组件需要以大写字母开头:
function Hello(props) {
// 正确! 这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签:
return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
// 正确!React 知道 <Hello /> 是一个组件,因为它是大写字母开头的:
return <Hello toWhat="World" />;
}
在运行时选择类型
你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop
来渲染不同组件的情况下:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 错误!JSX 类型不能是一个表达式。
return <components[props.storyType] story={props.story} />;
}
要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
JSX中行内样式
在HTML中,可以通过style
给标签元素添加行内样式:
<h1 class="title" style="color:red;">Hello World!~</h1>
在JSX同样可以使用style
来给元素添加样式,但和HTML的使用略有不同,需要以一个对象的方式给style
赋值,如下:
<h1 className="title" style={{color: 'red'}}>Hello world!~</h1>
可以通过Babel在线转换工具,把下面的代码转换一下:
{/* JSX 转换前*/}
const element = <h1 className="title" style={{color: 'red'}}>Hello world!~</h1>;
ReactDOM.render(
<div>
{ element }
</div>,
document.getElementById('root')
)
// Babel 转换出来的代码
var element = React.createElement(
"h1",
{
className: "title",
style: {
color: 'red'
}
},
"Hello world!~"
);
ReactDOM.render(React.createElement("div", null, element), document.getElementById('root'));
编译后在浏览器中看到的效果和我们平时写HTML并无差异:
在JSX中,可以将上面的代码进行优化,可以声明一个对象,比如titleStyles
,然后在需要的地方通过style
来引用:
const titleStyle = {
color: 'red'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
如果h1
中有多个样式时,可以在titleStyle
对象中添加你所需要的样式:
const titleStyle = {
color: 'red',
backgroundColor: '#604D17',
padding: '5px 10px',
display: 'inline-flex',
'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
注意,如果CSS的属性带有中折号-
时,在JSX中需要将其换成驼峰写法或用引号括起来,比如上面例示中的:
background-color => 换成驼峰写法: backgroundColor
border-radius => 用引号括起来: 'border-radius'
更建议使用驼峰方式来处理。
JSX中还更强大一些,比如说,可以给CSS属性赋值一个函数值,比如下面示例中的backgroundColor
就是一个随机生成的颜色:
let rgb = []
for (var i = 0; i < 3; i++) {
let r = Math.floor(Math.random() * 256)
rgb.push(r)
}
const titleStyle = {
color: 'red',
backgroundColor: `rgb(${rgb})`,
padding: '5px 10px',
display: 'inline-flex',
'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
在JSX中,可以给CSS属性添加变量、函数等值。
除此之外,在JSX中还可以共享一些样式,比如:
const TodoComponent = {
width: "300px",
margin: "30px auto",
backgroundColor: "#44014C",
minHeight: "200px",
boxSizing: "border-box"
}
const Header = {
padding: "10px 20px",
textAlign: "center",
color: "red",
fontSize: "22px"
}
const ErrorMessage = {
color: "white",
fontSize: "13px"
}
const styles = {
TodoComponent: TodoComponent,
Header: Header,
ErrorMessage: ErrorMessage
}
const element = <h1 className="title" style={ styles.Header }>Hello world!~</h1>;
这只是JSX中添加行内样式的使用。这种方式也常常被称为inline style。其实在React中处理CSS的方式还有其他方式,比如Styled Components,CSS Modules等。据2019年的CSS 状态报告统计,在React中处理样式(CSS-in-JS)就属Styled Components和CSS Modules占比最高:
如果你对2019年CSS状态报告中提到的CSS特性感兴趣的话,可以阅读《从9102年的CSS状态报告中看CSS特性的使用》一文。
上面提到的仅是在JSX中怎么写行内样式,如果你对React项目中怎么写样式,维护样式方面感兴趣的话,欢迎关注后续的更新,我将会花一篇的篇幅来和大家一起探讨如何在React中撸CSS。
小结
对于撸习惯了HTML或其他模板语言的同学来说,使用JSX还是会略有一点不习惯。但JSX本身并不是模板语言,他是JavaScript的扩展。初次使用JSX可能会存在一定疑惑,虽然该文介绍了一些JSX的使用和具有的特性,以及需要注意的一些小细节。但难免无法解决所有的困惑,如果你和我一样,在使用JSX感到困惑的话,可以花点时间阅读@Brad Westfall的这篇博文,还是蛮有帮助的。如果你在这方面有更多的经验和技巧,欢迎在下面的评论中与我们一起共享。