前言
本文是译者第一次做完整的全篇翻译,主要目的是学习一下这类文章的写作风格,所以挑了一篇相对入门、由浅入深的文章,全篇采用直译,即使有时候觉得作者挺啰嗦的,也依然翻译了原文内容。
相较于Javascript,JSX是一个很好的扩展,它允许我们定义UI组件。但是,它不提供条件、循环表达式的原生支持(增加条件表达式在该issue中被讨论过)。
译者注:条件、循环表达式一般是模板引擎默认提供的最基本语法
假设你需要遍历一个列表,去渲染多个组件或者实现一些条件判断逻辑,都必须用到JS。不过大部分情况下,可选的方法很少,Array.prototype.map
都能满足需求。
但,条件表达式呢?
那就是另一个故事了。
你有很多选择
在React中有好几种方法可以实现条件表达式。并且,不同的方法适用于不同的场景,取决于你需要处理什么样的问题。
本文包含了最常见的几种条件渲染方法:
- If/Else
- 返回null阻止渲染
- 变量
- 三元运算符
- 短路运算符(&&)
- 自执行函数(IIFE)
- 子组件
- 高阶组件(HOCs)
为了说明这些方法都是如何使用的,本文实现了一个编辑/展示态互相切换的组件:
你可以在JSFiddle运行、体验所有示例代码。
译者注:JSFiddle在墙内打开实在太慢了,故本文不贴出完整示例地址,如有需要,可自行查看原文链接。如果有合适的替代产品,欢迎告知
If/Else
首先,我们创建一个基础组件:
class App extends React.Component {
state = {
text: '',
inputText: '',
mode: 'view',
}
}
text
属性存储已存的文案,inputText
属性存储输入的文案,mode
属性来存储当前是编辑态还是展示态。
接下来,我们增加一些方法来处理input输入以及状态切换:
class App extends React.Component {
state = {
text: '',
inputText: '',
mode: 'view',
}
handleChange = (e) => {
this.setState({ inputText: e.target.value });
}
handleSave = () => {
this.setState({text: this.state.inputText, mode: 'view'});
}
handleEdit = () => {
this.setState({mode: 'edit'});
}
}
现在到了render
方法,我们需要检测state中的mode
属性来决定是渲染一个编辑按钮还是一个文本输入框+一个保存按钮:
class App extends React.Component {
// …
render () {
if(this.state.mode === 'view') {
return (
<div>
<p>Text: {this.state.text}</p>
<button onClick={this.handleEdit}>
Edit
</button>
</div>
);
} else {
// 译者注:如果if代码块里有return时,一般不需要写else代码块,不过为了贴合标题还是保留了
return (
<div>
<p>Text: {this.state.text}</p>
<input
onChange={this.handleChange}
value={this.state.inputText}
/>
<button onClick={this.handleSave}>
Save
</button>
</div>
);
}
}
If/Else是最简便的实现条件渲染的方法,不过我肯定,你不认为这是一个好的实现方式。
它的优势是,在简单场景下使用方便,并且每个程序员都理解这种使用方式;它的劣势是,会存在一些重复代码,并且render方法会变得臃肿。
那我们来简化一下,我们把所有的条件判断逻辑放入两个render方法,一个用来渲染输入框,另一个用来渲染按钮:
class App extends React.Component {
// …
renderInputField() {
if (this.state.mode === 'view') {
return <div />;
} else {
return (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText}
/>
</p>
);
}
}
renderButton() {
if (this.state.mode === 'view') {
return (
<button onClick={this.handleEdit}>
Edit
</button>
);
} else {
return (
<button onClick={this.handleSave}>
Save
</button>
);
}
}
render() {
return (
<div>
<p>Text: {this.state.text}</p>
{this.renderInputField()}
{this.renderButton()}
</div>
);
}
}
注意在示例中,renderInputField
函数在视图模式下,返回的是一个空div。通常来说,不推荐这么做。
返回null阻止渲染
如果想隐藏一个组件,你可以通过让该组件的render函数返回null
,没必要使用一个空div或者其他什么元素去做占位符。
需要注意的是,即使返回了null,该组件“不可见”,但它的生命周期依然会运行。
举个例子,下面的例子用两个组件实现了一个计数器:
class Number extends React.Component {
constructor(props) {
super(props);
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
render() {
if (this.props.number % 2 == 0) {
return (
<div>
<h1>{this.props.number}</h1>
</div>
);
} else {
return null;
}
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }
}
onClick(e) {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
<div>
<Number number={this.state.count} />
<button onClick={this.onClick.bind(this)}>Count</button>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Number
组件只有在偶数时才会展示。因为奇数时,render函数返回了null。但是,当你查看console时会发现,componentDidUpdate
函数每次都会执行,无论render
函数返回什么。
回到本文的例子,我们对renderInputField
函数稍作修改:
renderInputField() {
if (this.state.mode === 'view') {
return null;
} else {
return (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText}
/>
</p>
);
}
}
此外,返回null而不是空div的另一个好处是,这可以略微提升整个React应用的性能,因为React不需要在更新的时候unmount这个空div。
举个例子,如果是返回空div,在控制台中,你可以发现,root节点下的div
元素会始终更新:
相对的,如果是返回null,当Edit
按钮被点击时,这个div
元素不会更新:
你可以在这里继续深入了解React是如何更新DOM元素,以及调和算法是如何工作的。
在这个简单的例子中,也许这点性能差距是微不足道的,但如果是一个大型组件,性能差距就不容忽视。
我会在下文继续讨论条件渲染的性能影响。不过现在,让我们先继续聚焦在这个例子上。
变量
有时候,我不喜欢在一个方法中包含多个return
。所以,我会使用一个变量去指向这个JSX元素,并且只有当条件为true
的时候才去初始化。
renderInputField() {
let input;
if (this.state.mode !== 'view') {
input =
<p>
<input
onChange={this.handleChange}
value={this.state.inputText}
/>
</p>;
}
return input;
}
renderButton() {
let button;
if (this.state.mode === 'view') {
button =
<button onClick={this.handleEdit}>
Edit
</button>;
} else {
button =
<button onClick={this.handleSave}>
Save
</button>;
}
return button;
}
这些方法的返回结果和上一节的两个方法返回一致。
现在,render函数会变得更易读,不过在本例中,其实没必要使用if/else(或者switch)代码块,也没必要使用多个render方法。
我们可以写得更简洁一些。
三元运算符
我们可以使用三元运算符替代if/else代码块:
condition ? expr_if_true : expr_if_false
整个运算符可以放在jsx的{}
中,每一个表达式可以用()
来包裹JSX来提升可读性。
三元运算符可以用在组件的不同地方(?),让我们在例子中实际应用看看。
译者注:标记?的这句话我个人不是很理解
我先移除renderInputField
和renderButton
方法,并在render
中增加一个变量来表示组件是处于view
模式还是edit
模式:
render () {
const view = this.state.mode === 'view';
return (
<div>
</div>
);
}
接下来,添加三元运算符——当处于view
模式时,返回null;处于edit
模式时,返回输入框:
// ...
return (
<div>
<p>Text: {this.state.text}</p>
{
view
? null
: (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText} />
</p>
)
}
</div>
);
通过三元运算符,你可以通过改变组件内的标签或者回调函数来渲染一个保存/编辑按钮:
// ...
return (
<div>
<p>Text: {this.state.text}</p>
{
...
}
<button
onClick={
view
? this.handleEdit
: this.handleSave
} >
{view ? 'Edit' : 'Save'}
</button>
</div>
);
短路运算符
三元运算符在某些场景下可以更加简化。例如,当你要么渲染一个组件,要么不做渲染,你可以使用&&
运算符。
不像&
运算符,如果&&
执行左侧的表达式就可以确认结果的话,右侧表达式将不会执行。
举个例子,如果左侧表达式结果为false(false && ...
),那么下一个表达式就不需要执行,因为结果永远都是false。
在React中,你可以这样运用:
return (
<div>
{ showHeader && <Header /> }
</div>
);
如果showHeader
结果为true
,那么<Header />
组件就会被返回;如果showHeader
结果为false,那么<Header />
组件会被忽略,返回的会是一个空div
。
上文的代码中:
{
view
? null
: (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText} />
</p>
)
}
可以被改为:
!view && (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText} />
</p>
)
现在,完整的例子如下:
class App extends React.Component {
state = {
text: '',
inputText: '',
mode: 'view',
}
handleChange = (e) => {
this.setState({ inputText: e.target.value });
}
handleSave = () => {
this.setState({ text: this.state.inputText, mode: 'view' });
}
handleEdit = () => {
this.setState({mode: 'edit'});
}
render () {
const view = this.state.mode === 'view';
return (
<div>
<p>Text: {this.state.text}</p>
{
!view && (
<p>
<input
onChange={this.handleChange}
value={this.state.inputText} />
</p>
)
}
<button
onClick={
view
? this.handleEdit
: this.handleSave
}
>
{view ? 'Edit' : 'Save'}
</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
这样看上去是不是好了很多?
然而,三元运算符有时候会让人困扰,比如如下的复杂代码:
return (
<div>
{ condition1
? <Component1 />
: ( condition2
? <Component2 />
: ( condition3
? <Component3 />
: <Component 4 />
)
)
}
</div>
);
很快,这些代码会变为一团乱麻,因此,有时候你需要一些其他技巧,比如:自执行函数。
自执行函数
顾名思义,自执行函数就是在定义以后会被立刻执行,没有必要显式地调用他们。
通常来说,函数是这么被定义并执行的:
function myFunction() {
// ...
}
myFunction();
如果你期望一个函数在被定以后立刻执行,你需要使用括号将整个定义包起来(将函数作为一个表达式),然后传入需要使用的参数。
示例如下:
( function myFunction(/* arguments */) {
// ...
}(/* arguments */) );
或:
( function myFunction(/* arguments */) {
// ...
} ) (/* arguments */);
如果这个函数不会在其他地方被调用,你可以省略名字:
( function (/* arguments */) {
// ...
} ) (/* arguments */);
或使用箭头函数:
( (/* arguments */) => {
// ...
} ) (/* arguments */);
在React中,你可以用一个大括号包裹一整个自执行函数,把所有逻辑都放在里面(if/else、switch、三元运算符等等),然后返回你需要渲染的东西。
举个例子,如果使用自执行函数去渲染一个编辑/保存按钮,代码会是这样的:
{
(() => {
const handler = view
? this.handleEdit
: this.handleSave;
const label = view ? 'Edit' : 'Save';
return (
<button onClick={handler}>
{label}
</button>
);
})()
}
子组件
有时候,自执行函数看上去像是黑科技。
使用React的最佳实践是,尽可能地将逻辑拆分在各个组件内,使用函数式编程,而不是命令式编程。
所以,将条件渲染的逻辑放入一个子组件,子组件通过props来渲染不同的内容会是一个不错的方案。
但在这里,我不这么做,在下文中我会向你展示一种更声明式、更函数式的写法。
首先,我创建一个SaveComponent
:
const SaveComponent = (props) => {
return (
<div>
<p>
<input
onChange={props.handleChange}
value={props.text}
/>
</p>
<button onClick={props.handleSave}>
Save
</button>
</div>
);
};
通过props它接受足够的数据来供它展示。同样的,我再写一个EditComponent
:
const EditComponent = (props) => {
return (
<button onClick={props.handleEdit}>
Edit
</button>
);
};
render
方法现在看起来会是这样:
render () {
const view = this.state.mode === 'view';
return (
<div>
<p>Text: {this.state.text}</p>
{
view
? <EditComponent handleEdit={this.handleEdit} />
: (
<SaveComponent
handleChange={this.handleChange}
handleSave={this.handleSave}
text={this.state.inputText}
/>
)
}
</div>
);
}
If组件
有些库,例如JSX Control Statements,它们通过扩展JSX去支持条件状态:
<If condition={ true }>
<span>Hi!</span>
</If>
这些库提供了更多高级的组件,不过,如果我们只需要一些简单的if/else,我们可以写一个组件,类似Michael J. Ryan在这个issue的回复中提到的:
const If = (props) => {
const condition = props.condition || false;
const positive = props.then || null;
const negative = props.else || null;
return condition ? positive : negative;
};
// …
render () {
const view = this.state.mode === 'view';
const editComponent = <EditComponent handleEdit={this.handleEdit} />;
const saveComponent = <SaveComponent
handleChange={this.handleChange}
handleSave={this.handleSave}
text={this.state.inputText}
/>;
return (
<div>
<p>Text: {this.state.text}</p>
<If
condition={ view }
then={ editComponent }
else={ saveComponent }
/>
</div>
);
}
高阶组件
高阶组件(HOC)指的是一个函数,它接受一个已存在的组件,然后返回一个新的组件并且新增了一些方法:
const EnhancedComponent = higherOrderComponent(component);
应用在条件渲染中,一个高阶组件可以通过一些条件,返回不同的组件:
function higherOrderComponent(Component) {
return function EnhancedComponent(props) {
if (condition) {
return <AnotherComponent { ...props } />;
}
return <Component { ...props } />;
};
}
这篇Robin Wieruch写的精彩文章中,他对使用高阶组件来完成条件渲染有更深入的研究。
通过这篇文章,我准备借鉴EitherComponent
的概念。
在函数式编程中,Ether
经常被用来做一层包装以返回两个不同的值。
让我们先定义一个函数,它接受两个函数类型的参数,第一个函数会返回一个布尔值(条件表达式执行的结果),另一个是当结果为true
时返回的组件。
function withEither(conditionalRenderingFn, EitherComponent) {
}
这种高阶组件的名字一般以with
开头。
这个函数会返回一个函数,它接受原始组件为参数,并返回一个新组件:
function withEither(conditionalRenderingFn, EitherComponent) {
return function buildNewComponent(Component) {
}
}
再内层的函数返回的组件将是你在应用中使用的,所以它需要接受一些属性来运行:
function withEither(conditionalRenderingFn, EitherComponent) {
return function buildNewComponent(Component) {
return function FinalComponent(props) {
}
}
}
因为内层函数可以拿到外层函数的参数,所以,基于conditionalRenderingFn
的返回值,你可以返回EitherComponent
或者是原始的Component
:
function withEither(conditionalRenderingFn, EitherComponent) {
return function buildNewComponent(Component) {
return function FinalComponent(props) {
return conditionalRenderingFn(props)
? <EitherComponent { ...props } />
: <Component { ...props } />;
}
}
}
或者,使用箭头函数:
const withEither = (conditionalRenderingFn, EitherComponent) => (Component) => (props) =>
conditionalRenderingFn(props)
? <EitherComponent { ...props } />
: <Component { ...props } />;
你可以用到之前定义的SaveComponent
和EditComponent
来创建一个withEditConditionalRendering
高阶组件,最终,创建一个EditSaveWithConditionalRendering
组件:
const isViewConditionFn = (props) => props.mode === 'view';
const withEditContionalRendering = withEither(isViewConditionFn, EditComponent);
const EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);
译者注:苍了个天,杀鸡用牛刀
最终,在render
中,你传入所有需要用到的属性:
render () {
return (
<div>
<p>Text: {this.state.text}</p>
<EditSaveWithConditionalRendering
mode={this.state.mode}
handleEdit={this.handleEdit}
handleChange={this.handleChange}
handleSave={this.handleSave}
text={this.state.inputText}
/>
</div>
);
}
性能的注意事项
条件渲染有时很微妙,上文中提到了很多方法,它的性能是不一样的。
然而,大部分场景下,这些差异不算什么。但是当你需要做的时候,你需要对React的虚拟DOM是如何运转有很好的理解,并且掌握一些优化技巧:
这里有篇关于优化条件渲染的文章,我推荐阅读。
核心点是,如果条件渲染的组件会引起位置的变更,那它会引起重排,从而导致app中的组件装载/卸载。
译者注:这里的重排指的不是浏览器渲染的重排,算是虚拟DOM的概念
基于文中的例子,我做了如下两个例子。
第一个使用if/else来展示/隐藏SubHeader
组件:
const Header = (props) => {
return <h1>Header</h1>;
}
const Subheader = (props) => {
return <h2>Subheader</h2>;
}
const Content = (props) => {
return <p>Content</p>;
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
if(this.state.isToggleOn) {
return (
<div>
<Header />
<Subheader />
<Content />
<button onClick={this.handleClick}>
{ this.state.isToggleOn ? 'ON' : 'OFF' }
</button>
</div>
);
} else {
return (
<div>
<Header />
<Content />
<button onClick={this.handleClick}>
{ this.state.isToggleOn ? 'ON' : 'OFF' }
</button>
</div>
);
}
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
另一个使用短路运算符(&&
)实现:
const Header = (props) => {
return <h1>Header</h1>;
}
const Subheader = (props) => {
return <h2>Subheader</h2>;
}
const Content = (props) => {
return <p>Content</p>;
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<div>
<Header />
{ this.state.isToggleOn && <Subheader /> }
<Content />
<button onClick={this.handleClick}>
{ this.state.isToggleOn ? 'ON' : 'OFF' }
</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
打开控制台,并多次点击按钮,你会发现Content
组件的表现在两种实现中式不一致的。
译者注:例子1中的写法,Content每次都会被重新渲染
结论
就像编程中的其他事情一样,在React中实现条件渲染有很多种实现方式。
你可以自由选择任一方式,除了第一种(if/else并且包含了很多return)。
你可以基于这些理由来找到最适合当前场景的方案:
- 你的编程风格
- 条件逻辑的复杂度
- 你对于Javascript、JSX和React中的高级概念(例如高阶组件)的接受程度
当然,有些事是始终重要的,那就是保持简单和可读性。