前言
本文是譯者第一次做完整的全篇翻譯,主要目的是學習一下這類文章的寫作風格,所以挑了一篇相對入門、由淺入深的文章,全篇採用直譯,即使有時候覺得作者挺囉嗦的,也依然翻譯了原文內容。
相較於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中的高階概念(例如高階元件)的接受程度
當然,有些事是始終重要的,那就是保持簡單和可讀性。