[譯] React 實現條件渲染的多種方式和效能考量

卷頭驢先生發表於2018-07-05

JSX 是對 JavaScript 強大的擴充套件,允許我們來定義 UI 元件。但是它不直接支援迴圈和條件表示式(儘管新增 條件表示式已經被討論過了)。

如果你想要遍歷一個列表來渲染多個元件或者實現一些條件邏輯,你不得不使用純 Javascript,你也並沒有很多的選擇來處理迴圈。更多的時候,map 將會滿足你的需要。

但是條件表示式呢?

那就是另外一回事了。

有幾種方案可供你選擇

在 React 中有多種使用條件語句的方式。並且,和程式設計中的大多數事情一樣,依賴於你所要解決的實際問題,有些方式是更適合的。

本教程介紹了最流行的條件渲染方法:

  • If/Else
  • 避免渲染空元素
  • 元素變數
  • 三元運算子
  • 與運算 (&&)
  • 立即呼叫函式(IIFE)
  • 子元件
  • 高階元件(HOCs)

作為所有這些方法如何工作的示例,接下來將實現具有檢視/編輯功能的元件:

[譯] React 實現條件渲染的多種方式和效能考量

你可以在 JSFiddle 中嘗試和拷貝(fork)所有例子。

讓我們從使用 if/else 這種最原始的實現開始並在這裡構建它。

If/else

讓我們使用如下狀態來構建一個元件:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
  }
}
複製程式碼

你將使用一個屬性來儲存文字,並且使用另外一個屬性儲存正在被編輯的文字。第三個屬性將用來表示你是在 edit 還是 view 模式下。

接下來,新增一些方法來處理輸入文字、儲存和輸入事件:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
}
複製程式碼

現在,對於渲染方法,除了儲存的文字之外,還要檢查模式狀態屬性,以顯示編輯按鈕或文字輸入框和儲存按鈕:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
}
複製程式碼

下面是完整的程式碼,可以在 fiddle 中嘗試執行它:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  render () {
    if(this.state.mode === 'view') {
      return (
        <div>
          <p>Text: {this.state.text}</p>
          <button onClick={this.handleEdit}>
            Edit
          </button>
        </div>
      );
    } else {
      return (
        <div>
          <p>Text: {this.state.text}</p>
            <input
              onChange={this.handleChange}
              value={this.state.inputText}
            />
          <button onClick={this.handleSave}>
            Save
          </button>
        </div>
      );
    }
  }
}

ReactDOM.render(
    <App />,
  document.getElementById('root')
);
複製程式碼

if/else 是最簡單的方式來解決這個問題,但是我確定你知道這並不是一種好的實現方式。

它適用於簡單的用例,每個程式設計師都知道它是如何工作的。但是有很多重複,render 方法看起來並不簡潔。

所以讓我們通過將所有條件邏輯提取到兩個渲染方法來簡化它,一個來渲染文字框,另一個來渲染按鈕:

class App extends React.Component {
  // …
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return <div></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>
    );
  }
}
複製程式碼

下面是完整的程式碼,可以在 fiddle 中嘗試執行它:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return <div></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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

需要注意的是當元件在預覽模式下時,方法 renderInputField 返回了一個空的 div 元素。

然而這並不是必要的。

避免渲染空元素

如果你想要隱藏一個元件,你可以讓它的渲染方法返回 null,因為沒必要渲染一個空的(和不同的)元素來佔位。

需要注意的重要一點是當返回 null 時,即使元件並不會被看見,但是生命週期方法仍然被觸發了。

舉個例子,下面的程式碼實現了兩個元件之間的計數器:

Babel + JSX:

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 元件只有在父元件傳遞偶數時渲染父元件傳遞的值,否則,將返回 null。然後,當觀察控制檯輸出時,將會發現不管 render 返回什麼, componentDidUpdate 總是會被呼叫。

回頭來看我們的例子,像這樣來改變 renderInputField 方法:

  renderInputField() {
    if(this.state.mode === 'view') {
      return null;
    } else {
      return (
          <p>
            <input
              onChange={this.handleChange}
              value={this.state.inputText}
            />
          </p>
      );
    }
  }
複製程式碼

下面是完整的程式碼:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  renderInputField() {
    if(this.state.mode === 'view') {
      return null;
    } 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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

返回 null 來替代一個空元素的優勢在於這將會對組建的效能有一些改善,因為 React 不必要解綁元件來替換它。

例如,當執行返回空 div 元素的程式碼時,開啟檢閱頁面元素,將會看到在跟元素下的 div 元素是如何被重新整理的:

[譯] React 實現條件渲染的多種方式和效能考量

對比這個例子,當返回 null 來隱藏元件時,Edit 按鈕被點選時 div 元素是不更新的:

[譯] React 實現條件渲染的多種方式和效能考量

這裡,你將明白更多關於 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;
  }
複製程式碼

這樣做是等同於那些返回 null 的方法的。

以下是優化後的完整程式碼:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  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 () {
    return (
      <div>
        <p>Text: {this.state.text}</p>
        {this.renderInputField()}
        {this.renderButton()}
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

使用這種方式使主 render 方法更有可讀性,但是可能並沒有必要使用 if/else 判斷(或者像 switch 這樣的語句)和輔助的渲染方法。

讓我們嘗試一種更簡單的方法。

三元運算子

我們可以使用 三元運算子 來代替 if/else 語句:

condition ? expr_if_true : expr_if_false
複製程式碼

該運算子用大括號包裹,表示式可以包含JSX,可選擇將其包含在圓括號中以提高可讀性。

它可以應用於元件的不同部分。讓我們將它應用到示例中,以便您可以看到這個例項。

我將在 render 方法中刪除 renderInputFieldrenderButton,並新增一個變數用來表示元件是在 view 還是 edit 模式:

render () {
  const view = this.state.mode === 'view';

  return (
      <div>
      </div>
  );
}
複製程式碼

現在,你可以使用三元運算子,當元件被設定為 view 模式時返回 null,否則返回輸入框:

  // ...

  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>
  );
複製程式碼

正如前面所說,三元運算子可以應用在元件的不同位置。

可以在 fiddle 中執行檢視效果:

jsfiddle.net/eh3rrera/y6…

與運算子

在某種特殊情況下,三元運算子是可以簡化的。

當你想要一種條件下渲染元素,另一種條件下不渲染元素時,你可以使用 && 運算子。

不同於 & 運算子,當左側的表示式可以決定最終結果時,&& 是不會再執行右側表示式的判斷的。

例如,如果第一個表示式被判定為 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>
)
複製程式碼

下面是可在 fiddle 中執行的完整程式碼:

Banel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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>
);
複製程式碼

這可能會很快變得混亂。

出於這個原因,有時您可能想要使用其他技術,例如立即執行函式。

立即執行函式表示式 (IIFE)

顧名思義,立即執行函式就是在定義之後被立即呼叫的函式,他們不需要被顯式地呼叫。

通常情況下,你一般會這樣定義並執行(定義後執行)一個函式:

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>
    );
  })()
} 
複製程式碼

下面是可以在 fiddle 中執行的完整程式碼:

Babel + JSX:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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>
          )
        }
        
        {
          (() => {
            const handler = view 
                ? this.handleEdit 
                : this.handleSave;
            const label = view ? 'Edit' : 'Save';
          
            return (
              <button onClick={handler}>
                {label}
              </button>
            );
          })()
        }  
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼
<div id="root"></div>
複製程式碼

子元件

很多時候,立即執行函式看起來可能是一種不那麼優雅的解決方案。

畢竟,我們在使用 React,React 推薦使用的方案是將你的應用邏輯分解為儘可能多的元件,並且推薦使用函數語言程式設計而非指令式程式設計。

所以修改條件渲染邏輯為一個子元件,這個子元件會依據父元件傳遞的 props 來決定在不同情況下的渲染,這將會是一個更好的方案。

但在這裡,我將做一些有點不同的事情,向您展示如何從一個命令式的解決方案轉向更多的宣告式和函式式解決方案。

我將從建立一個 SaveComponent 元件開始:

const SaveComponent = (props) => {
  return (
    <div>
      <p>
        <input
          onChange={props.handleChange}
          value={props.text}
        />
      </p>
      <button onClick={props.handleSave}>
        Save
      </button>
    </div>
  );
};
複製程式碼

正如函數語言程式設計的屬性,SaveComponent 的功能邏輯都來自於它接收的引數所指定的。同樣的方式定義另一個元件 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>
    );
}
複製程式碼

下面是可以在 fiddle 中執行的完整程式碼:

Babel + JSX:

const SaveComponent = (props) => {
  return (
    <div>
      <p>
        <input
          onChange={props.handleChange}
          value={props.text}
        />
      </p>
      <button onClick={props.handleSave}>
        Save
      </button>
    </div>
  );
};

const EditComponent = (props) => {
  return (
    <button onClick={props.handleEdit}>
      Edit
    </button>
  );
};

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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
            ? <EditComponent handleEdit={this.handleEdit}  />
            : (
              <SaveComponent 
               handleChange={this.handleChange}
               handleSave={this.handleSave}
               text={this.state.inputText}
             />
            )
        } 
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

If 元件

有像 jsx-control-statements 這樣的庫可以擴充套件JSX來新增如下條件語句:

<If condition={ true }>

  <span>Hi!</span>

</If>
複製程式碼

這些庫提供更高階的元件,但是如果我們需要簡單的 if/else,我們可以參考 Michael J. Ryanissue 下的 評論

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>
    );
}
複製程式碼

下面是可以在 fiddle 中執行的完整程式碼:

Babel + JSX:

const SaveComponent = (props) => {
  return (
    <div>
      <p>
        <input
          onChange={props.handleChange}
          value={props.text}
        />
      </p>
      <button onClick={props.handleSave}>
        Save
      </button>
    </div>
  );
};

const EditComponent = (props) => {
  return (
    <button onClick={props.handleEdit}>
      Edit
    </button>
  );
};

const If = (props) => {
  const condition = props.condition || false;
  const positive = props.then || null;
  const negative = props.else || null;
  
  return condition ? positive : negative;
};

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  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';
    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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

高階元件

高階元件(HOC)是一個函式,它接收一個已經存在的元件並且基於這個元件返回一個新的帶有更多附加功能的元件:

const EnhancedComponent = higherOrderComponent(component);
複製程式碼

應用於條件渲染時,一個元件被傳遞給一個高階元件,高階元件可以依據一些條件返回一個不同於原元件的元件:

function higherOrderComponent(Component) {
  return function EnhancedComponent(props) {
    if (condition) {
      return <AnotherComponent { ...props } />;
    }

    return <Component { ...props } />;
  };
}
複製程式碼

這裡有一篇 Robin Wieruch 寫的 關於高階元件的精彩好文,這篇文章深入討論了高階元件在條件渲染中的應用。

在我們這篇文章中,我將會借鑑一些 EitherComponent 的概念。

在函數語言程式設計中,Either 這一類方法的實現通常是作為一個包裝,來返回兩個不同的值。

所以讓我們從定義一個接收兩個引數的函式開始,另一個函式返回一個布林值(判斷條件的結果),如果這個布林值為 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 } />;
複製程式碼

通過這個方式,使用原來定義的 SaveComponentEditComponent,你可以建立一個 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>
    );
}
複製程式碼

下面是可以在 fiddle 中執行的完整程式碼:

Babel + JSX:

const SaveComponent = (props) => {
  return (
    <div>
      <p>
        <input
          onChange={props.handleChange}
          value={props.text}
        />
      </p>
      <button onClick={props.handleSave}>
        Save
      </button>
    </div>
  );
};

const EditComponent = (props) => {
  return (
    <button onClick={props.handleEdit}>
      Edit
    </button>
  );
};

const withEither = (conditionalRenderingFn, EitherComponent) => (Component) => (props) =>
  conditionalRenderingFn(props)
    ? <EitherComponent { ...props } />
    : <Component { ...props } />;

const isViewConditionFn = (props) => props.mode === 'view';

const withEditContionalRendering = withEither(isViewConditionFn, EditComponent);
const EditSaveWithConditionalRendering = withEditContionalRendering(SaveComponent);

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {text: '', inputText: '', mode:'view'};
    
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }
  
  handleChange(e) {
    this.setState({ inputText: e.target.value });
  }
  
  handleSave() {
    this.setState({text: this.state.inputText, mode: 'view'});
  }

  handleEdit() {
    this.setState({mode: 'edit'});
  }
  
  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>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
複製程式碼

效能考量

條件渲染可能是複雜的。就像前面我所展示的那樣,每種方式的效能也可能是不同的。

然而,在大多數時候這種差別是不成問題的。但當它確實造成問題時,你將需要深入理解 React 的虛擬 DOM 的工作原理,並且使用一些技巧來優化效能

這裡有一篇關於很好的文章,關於 優化React的條件渲染,我非常推薦你讀一下。

基本的思想是條件渲染導致改變元件的位置將會引起迴流,從而導致應用內元件的解綁/繫結。

基於這篇文章的例子,我寫了兩個例子:

第一個例子使用 if/else 來控制 SubHeader 元件的顯示/隱藏:

Babel + JSX:

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')
);
複製程式碼

第二個例子使用與運算(&&)做同樣的事情:

Babel + JSX:

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 是被如何處理的。

結論

就像程式設計中的很多事情一樣,在 React 中有很多種方式實現條件渲染。

我會說除了第一種方式(有多種返回的if/else),你可以任選你喜歡的方式。

基於下面的原則,你可以決定哪一種方式在你的實際情況中是最好的:

  • 你的程式設計風格
  • 條件邏輯的複雜程度
  • 使用 JavaScript、JSX和高階的 React 概念(比如高階元件)的舒適度。

如果所有的事情都是相當的,那麼就追求簡明度和可讀性。


Plug: LogRocket, a DVR for web apps

[譯] React 實現條件渲染的多種方式和效能考量

LogRocket 是一款前端日誌工具,能夠在你自己的瀏覽器上覆現問題。而不是去猜為什麼發生錯誤或者向使用者要截圖和日誌,LogRocket 幫助你復現場景來快速理解發生了什麼錯誤。 它適用於任何應用程式,且和框架無關,並且具有從Redux,Vuex和@ngrx/store記錄其他上下文的外掛。

除了記錄Redux動作和狀態之外,LogRocket 還記錄控制檯日誌,JavaScript 錯誤,堆疊跟蹤,帶有頭資訊+主體的網路請求/響應,瀏覽器後設資料和自定義日誌。它還可以檢測 DOM 來記錄頁面上的 HTML 和 CSS,即使是最複雜的單頁面應用,也能還原出畫素級的視訊。

免費試用。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章