Skip to content

Latest commit

 

History

History
970 lines (753 loc) · 34.6 KB

File metadata and controls

970 lines (753 loc) · 34.6 KB

六、React 组件生命周期

本章的目标是让您了解 React 组件的生命周期以及如何编写响应生命周期事件的代码。首先,我们将简要讨论为什么组件首先需要生命周期。然后,您将实现几个使用这些方法初始化其属性和状态的示例组件。

接下来,您将了解如何通过避免在不必要时进行渲染来优化组件的渲染效率。最后,您将看到如何在 React 组件中封装命令式代码,以及如何在卸载组件时进行清理。

为什么组件需要生命周期

React 组件经历一个生命周期,不管我们的代码是否知道它。事实上,到目前为止,在本书中您在组件中实现的render()方法实际上是一种生命周期方法。渲染只是 React 组件中的一个生命周期事件。

例如,组件将要装入 DOM 时、组件装入 DOM 后、组件更新时等都有生命周期事件。生命周期事件是另一个移动的部分,所以您希望将它们保持在最低限度。正如您将在本章中了解到的,一些组件确实需要响应生命周期事件来执行初始化、呈现启发式,或者在从 DOM 卸载组件后进行清理。

下图给出了组件在其生命周期中的流程,依次调用了相应的方法:

Why components need a lifecycle

这是 React 组件的两个主要生命周期流。第一种情况发生在最初渲染组件时。第二种情况发生在组件重新渲染时。但是,componentWillReceiveProps()方法仅在组件的属性更新时调用。这意味着,如果由于调用setState()而重新呈现组件,则不会调用此生命周期方法,而流将以shouldComponentUpdate()开始。

本图中未包括的另一个生命周期方法是componentWillUnmount()。这是将要删除组件时调用的唯一生命周期方法。我们将在本章末尾看到如何使用此方法的示例。在这一点上,让我们开始编码。

初始化属性和状态

在本节中,您将看到如何在 React 组件中实现初始化代码。这涉及到使用在首次创建组件时调用的生命周期方法。首先,我们将浏览一个基本示例,该示例使用 API 中的数据设置组件。然后,您将看到如何从属性初始化状态,以及如何在属性更改时更新状态。

取元件数据

初始化组件时,首先要做的事情之一是填充它们的状态或属性。否则,该组件除了其骨架标记之外,将没有任何要渲染的内容。例如,假设要呈现以下用户列表组件:

import React from 'react'; 
import { Map as ImmutableMap } from 'immutable'; 

// This component displays the passed-in "error" 
// property as bold text. If it's null, then 
// nothing is rendered. 
const ErrorMessage = ({ error }) => 
  ImmutableMap() 
    .set(null, null) 
    .get( 
      error, 
      (<strong>{error}</strong>) 
    ); 

// This component displays the passed-in "loading" 
// property as italic text. If it's null, then 
// nothing is rendered. 
const LoadingMessage = ({ loading }) => 
  ImmutableMap() 
    .set(null, null) 
    .get( 
      loading, 
      (<em>{loading}</em>) 
    ); 

export default ({ 
  error,  
  loading,  
  users,  
}) => ( 
  <section> 
    { /* Displays any error messages... */ } 
    <ErrorMessage error={error} /> 

    { /* Displays any loading messages, while 
         waiting for the API... */ } 
    <LoadingMessage loading={loading} /> 

    { /* Renders the user list... */ } 
    <ul> 
      {users.map(i => ( 
        <li key={i.id}>{i.name}</li> 
      ))} 
    </ul> 
  </section> 
); 

此 JSX 依赖三条数据:

  • loading:取 API 数据时显示此消息
  • error:如果出现问题,将显示此消息
  • users:从 API 取数

这里还使用了两个助手组件:ErrorMessageLoadingMessage。它们分别用于格式化错误和加载状态。然而,如果errorloading为空,我们既不想呈现任何内容,也不想在这些简单的功能组件中引入命令式逻辑。这就是为什么我们在Immutable.js地图上使用了一个很酷的小把戏。

首先,我们创建一个具有单个键值对的映射。键为 null,值为 null。第二,我们使用errorloading属性调用get()。如果errorloading属性为 null,则会找到密钥,并且不会呈现任何内容。诀窍在于get()接受第二个参数,如果没有找到密钥,则返回该参数。这就是我们传递真实值的地方,同时避免命令式逻辑。这个特定的组件很简单,但当存在两种以上的可能性时,该技术尤其强大。

我们应该如何进行 API 调用并使用响应填充users集合?答案是使用上一章介绍的容器组件进行 API 调用,然后呈现UserList组件:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

import { users } from './api'; 
import UserList from './UserList'; 

export default class UserListContainer extends Component { 

  state = { 
    data: fromJS({ 
      error: null, 
      loading: 'loading...', 
      users: [], 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // When component has been rendered, "componentDidMount()" 
  // is called. This is where we should perform asynchronous 
  // behavior that will change the state of the component. 
  // In this case, we're fetching a list of users from 
  // the mock API. 
  componentDidMount() { 
    users().then( 
      (result) => { 
        // Populate the "users" state, but also 
        // make sure the "error" and "loading" 
        // states are cleared. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', null) 
          .set('users', fromJS(result.users)); 
      }, 
      (error) => { 
        // When an error occurs, we want to clear 
        // the "loading" state and set the "error" 
        // state. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', error); 
      } 
    ); 
  } 

  render() { 
    return ( 
      <UserList {...this.data.toJS()} /> 
    ); 
  } 
} 

让我们看一下这个方法。它的唯一工作是渲染<UserList>组件,并将this.state作为其属性传入。实际的 API 调用发生在componentDidMount()方法中。此方法在组件装入 DOM 后调用。这意味着<UserList>将在 API 中的任何数据到达之前呈现一次。但这很好,因为我们已经将UserListContainer状态设置为具有默认loading消息,UserList将在等待 API 数据时显示此消息。

一旦 API 调用返回数据,users集合将被填充,导致UserList重新呈现自身,只是这一次,它拥有所需的数据。那么,我们为什么要在componentDidMount()中调用这个 API,而不是在组件构造函数中调用呢?这里的经验法则其实很简单。每当异步行为改变 React 组件的状态时,应该从生命周期方法调用它。这样,就很容易推断组件如何以及何时更改状态。

让我们看一下这里使用的模拟 API 函数调用:

// Returns a promise that's resolved after 2 
// seconds. By default, it will resolve an array 
// of user data. If the "fail" argument is true, 
// the promise is rejected. 
export function users(fail) { 
  return new Promise((resolve, reject) => { 
    setTimeout(() => { 
      if (fail) { 
        reject('epic fail'); 
      } else { 
        resolve({ 
          users: [ 
            { id: 0, name: 'First' }, 
            { id: 1, name: 'Second' }, 
            { id: 2, name: 'Third' }, 
          ], 
        }); 
      } 
    }, 2000); 
  }); 
} 

它只是返回一个承诺,并在 2 秒后用数组解析。Promises 是模拟 API 调用之类的东西的好工具,因为这使您能够在 React 组件中使用不仅仅是简单的 HTTP 调用作为数据源。例如,您可能正在从本地文件中读取数据,或者使用某个库返回解析未知源数据的承诺。

以下是当loading状态为字符串,users状态为空数组时UserList组件呈现的内容:

Fetching component data

以下是当loadingnullusers为非空时的渲染:

Fetching component data

我不能保证这是我最后一次在书中提到这一点,但我会尽量把它控制在最低限度。我想强调UserListContainerUserList组件之间的责任分离。因为容器组件处理生命周期管理和实际的 API 通信,这使我们能够创建一个非常通用的用户列表组件。事实上,它是一个不需要任何状态的功能组件,这意味着它很容易在整个应用中重用。

使用属性初始化状态

前面的示例演示了如何通过在componentDidMount()生命周期方法中进行 API 调用来初始化容器组件的状态。但是,组件状态的唯一填充部分是users集合。您可能希望填充不来自 API 端点的其他状态。

例如,errorloading状态消息在初始化状态时设置了默认值。这很好,但是如果呈现UserListContainer的代码想要使用不同的加载消息呢?可以通过允许属性覆盖默认状态来实现这一点。让我们以UserListContainer组件为基础:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

import { users } from './api'; 
import UserList from './UserList'; 

class UserListContainer extends Component { 
  state = { 
    data: fromJS({ 
      error: null, 
      loading: null, 
      users: [], 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // Called before the component is mounted into the DOM 
  // for the first time. 
  componentWillMount() { 
    // Since the component hasn't been mounted yet, it's 
    // safe to change the state by calling "setState()" 
    // without causing the component to re-render. 
    this.data = this.data 
      .set('loading', this.props.loading);  
  } 

  // When component has been rendered, "componentDidMount()" 
  // is called. This is where we should perform asynchronous 
  // behavior that will change the state of the component. 
  // In this case, we're fetching a list of users from 
  // the mock API. 
  componentDidMount() { 
    users().then( 
      (result) => { 
        // Populate the "users" state, but also 
        // make sure the "error" and "loading" 
        // states are cleared. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', null) 
          .set('users', fromJS(result.users)); 
      }, 
      (error) => { 
        // When an error occurs, we want to clear 
        // the "loading" state and set the "error" 
        // state. 
        this.data = this.data 
          .set('loading', null) 
          .set('error', error); 
      } 
    ); 
  } 

  render() { 
    return ( 
      <UserList {...this.data.toJS()} /> 
    ); 
  } 
} 

UserListContainer.defaultProps = { 
  loading: 'loading...', 
}; 

export default UserListContainer; 

您可以看到loading不再有默认的字符串值。相反,我们引入了defaultProps,它为未通过 JSX 标记传入的属性提供默认值。我们添加的新生命周期方法是componentWillMount(),它使用loading属性初始化状态。因为loading属性有一个默认值,所以可以安全地更改状态。但是,在此处调用setState()(通过this.data)不会导致组件重新呈现自身。该方法在组件装载之前被调用,因此初始渲染尚未发生。

现在让我们看看如何将状态数据传递给UserListContainer

import React from 'react'; 
import { render } from 'react-dom'; 

import UserListContainer from './UserListContainer'; 

// Renders the component with a "loading" property. 
// This value ultimately ends up in the component state. 
render(( 
  <UserListContainer 
    loading="playing the waiting game..." 
  /> 
  ), 
  document.getElementById('app') 
); 

很酷吧?仅仅因为组件有状态,并不意味着我们不能灵活地允许定制这种状态。我们将研究这个主题的另一个变体,即通过属性更新组件状态。

以下是首次呈现UserList时初始加载消息的样子:

Initializing state with properties

使用属性更新状态

您已经了解了componentWillMount()componentDidMount()生命周期方法如何帮助您的组件获得所需的数据。还有一个场景,我们应该考虑在这里重新呈现组件容器。

让我们来看看一个简单的 AutoT0x 组件,它跟踪点击次数。

import React from 'react'; 

export default ({ 
  clicks,  
  disabled,  
  text,  
  onClick,  
}) => ( 
  <section> 
    { /* Renders the number of button clicks, 
         using the "clicks" property. */ } 
    <p>{clicks} clicks</p> 

    { /* Renders the button. It's disabled state 
         is based on the "disabled" property, and 
         the "onClick()" handler comes from the 
         container component. */} 
    <button 
      disabled={disabled} 
      onClick={onClick} 
    > 
      {text} 
    </button> 
  </section> 
); 

现在,让我们为该功能实现一个容器组件:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

import MyButton from './MyButton'; 

class MyFeature extends Component { 

  state = { 
    data: fromJS({ 
      clicks: 0, 
      disabled: false, 
      text: '', 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // Sets the "text" state before the initial render. 
  // If a "text" property was provided to the component, 
  // then it overrides the initial "text" state. 
  componentWillMount() { 
    this.data = this.data 
      .set('text', this.props.text);  
  } 

  // If the component is re-rendered with new 
  // property values, this method is called with the 
  // new property values. If the "disabled" property 
  // is provided, we use it to update the "disabled" 
  // state. Calling "setState()" here will not 
  // cause a re-render, because the component is already 
  // in the middle of a re-render. 
  componentWillReceiveProps({ disabled }) { 
    this.data = this.data 
      .set('disabled', disabled); 
  } 

  // Click event handler, increments the "click" count. 
  onClick = () => { 
    this.data = this.data 
      .update('clicks', c => c + 1); 
  } 

  // Renders the "<MyButton>" component, passing it the 
  // "onClick()" handler, and the state as properties. 
  render() { 
    return ( 
      <MyButton 
        onClick={this.onClick} 
        {...this.data.toJS()} 
      /> 
    ); 
  } 
} 

MyFeature.defaultProps = { 
  text: 'A Button', 
}; 

export default MyFeature; 

这里采用与前面示例相同的方法。在安装组件之前,将文本状态的值设置为文本属性的值。但是,我们也在componentWillReceiveProps()方法中设置了文本状态。当特性值更改时,或者换句话说,当组件重新渲染时,将调用此方法。让我们看看如何重新呈现该组件,以及该状态的行为是否符合我们的预期:

import React from 'react'; 
import { render as renderJSX } from 'react-dom'; 

import MyFeature from './MyFeature'; 

// Determines the state of the button 
// element in "MyFeature". 
let disabled = true; 

function render() { 
  // Toggle the state of the "disabled" property. 
  disabled = !disabled; 

  renderJSX( 
    (<MyFeature {...{ disabled }} />), 
    document.getElementById('app') 
  ); 
} 

// Re-render the "<MyFeature>" component every 
// 3 seconds, toggling the "disabled" button 
// property. 
setInterval(render, 3000); 

render(); 

果然,一切都按计划进行。每当单击按钮时,单击计数器都会更新。但正如您所见,<MyFeature>每 3 秒重新渲染一次,切换按钮的禁用状态。当按钮重新启用并单击“继续”时,计数器将从停止处继续。

以下是首次渲染时MyButton组件的外观:

Updating state with properties

单击几次后,按钮进入禁用状态,其外观如下所示:

Updating state with properties

优化渲染效率

您将要学习的下一个生命周期方法用于实现提高组件渲染性能的启发式方法。您将看到,如果组件的状态没有更改,则无需渲染。然后,您将实现一个组件,该组件使用 API 中的特定元数据来确定是否需要重新呈现该组件。

渲染还是不渲染

shouldComponentUpdate()生命周期方法用于确定组件在被要求时是否会呈现自身。例如,如果实现了此方法,并且返回 false,则组件的整个生命周期将被缩短,并且不会发生渲染。如果组件正在渲染大量数据并且频繁地重新渲染,则这可能是一项重要的检查。诀窍在于知道组件状态是否已更改。

这就是不变数据的美妙之处,我们可以很容易地检查它是否发生了变化。如果我们使用Immutable.js之类的库来控制组件的状态,这一点尤其正确。让我们来看一个简单的列表组件:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

export default class MyList extends Component { 
  state = { 
    data: fromJS({ 
      items: new Array(5000) 
        .fill(null) 
        .map((v, i) => i), 
    }), 
  }; 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // If this method returns false, the component 
  // will not render. Since we're using an Immutable.js 
  // data structure, we simply need to check for equality. 
  // If "state.data" is the same, then there's no need to 
  // render because nothing has changed since the last 
  // render. 
  shouldComponentUpdate(props, state) { 
    return this.data !== state.data; 
  } 

  // Renders the complete list of items, even if it's huge. 
  render() { 
    const items = this.data.get('items'); 

    return ( 
      <ul> 
        {items.map(i => ( 
          <li key={i}>{i}</li> 
        ))} 
      </ul> 
    ); 
  } 
} 

items状态初始化为Immutable.js``List,其中包含5000项。这是一个相当大的集合,因此我们不希望内部的虚拟 DOM 不断地计算差异。虚拟 DOM 在它所做的事情上是高效的,但不如可以执行简单的“应该”或“不应该”呈现检查的代码那么高效。我们在这里实现的shouldComponentRender()方法正是这样做的。它将新状态与当前状态进行比较;如果它们是同一个对象,我们就完全避开虚拟 DOM。

现在,让我们将此组件投入使用,看看我们获得了什么样的效率收益:

import React from 'react'; 
import { render as renderJSX } from 'react-dom'; 

import MyList from './MyList'; 

// Renders the "<MyList>" component. Then, it sets 
// the state of the component by changing the value 
// of the first "items" element. However, the value 
// didn't actually change, so the same Immutable.js 
// structure is reused. This means that 
// "shouldComponentUpdate()" will return false. 
function render() { 
  const myList = renderJSX( 
    (<MyList />), 
    document.getElementById('app') 
  ); 

  // Not actually changing the value of the first 
  // "items" element. So, Immutable.js recognizes 
  // that nothing changed, and instead of 
  // returning a new object, it returns the same 
  // "myList.data" reference. 
  myList.data = myList.data 
    .setIn(['items', 0], 0); 
} 

// Instead of performing 500,000 DOM operations, 
// "shouldComponentUpdate()" turns this into 
// 5000 DOM operations. 
for (let i = 0; i < 100; i++) { 
  render(); 
} 

如你所见,我们只是在循环中一遍又一遍地渲染<MyList>。每个迭代有 5000 个列表项要呈现。由于状态不变,对shouldComponentUpdate()的调用在每一次迭代中都返回false。出于性能原因,这一点很重要,因为它们有很多。显然,在实际应用中,我们不会有在紧循环中重新呈现组件的代码。此代码旨在强调 React 的渲染功能。如果你把shouldComponentUpdate()方法注释掉,你就会明白我的意思。

您可能会注意到,我们实际上正在使用Immutable.js地图上的setIn()更改render()函数中的状态。这应该会导致状态改变,对吗?这实际上会返回相同的Immutable.js实例,原因很简单,我们设置的值与当前值相同:0。当没有变化发生时,Immutable.js方法返回相同的对象,因为它没有变化。凉的

使用元数据优化渲染

在本节中,我们将介绍如何使用作为 API 响应一部分的元数据来确定组件是否应该重新呈现自身。下面是一个简单的用户详细信息组件:

import React, { Component } from 'react'; 

export default class MyUser extends Component { 
  state = { 
    modified: new Date(), 
    first: 'First', 
    last: 'Last', 
  }; 

  // The "modified" property is used to determine 
  // whether or not the component should render. 
  shouldComponentUpdate(props, state) { 
    return +state.modified > +this.state.modified; 
  } 

  render() { 
    const { 
      modified, 
      first, 
      last, 
    } = this.state; 

    return ( 
      <section> 
        <p>{modified.toLocaleString()}</p> 
        <p>{first}</p> 
        <p>{last}</p> 
      </section> 
    ); 
  } 
} 

如果你看一下shouldComponentUpdate()方法,你会发现它正在比较新的modified状态和旧的modified状态。此代码假设modified值是一个日期,反映了从 API 返回的数据实际被修改的时间。这种方法的主要缺点是shouldComponentUpdate()方法现在与 API 数据紧密耦合。其优点是,我们可以像处理不可变数据一样获得性能提升。

下面是这种启发式方法的实际效果:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyUser from './MyUser'; 

// Performs the initial rendering of "<MyUser>". 
const myUser = render( 
  (<MyUser />), 
  document.getElementById('app') 
); 

// Sets the state, with a new "modified" value. 
// Since the modified state has changed, the 
// component will re-render. 
myUser.setState({ 
  modified: new Date(), 
  first: 'First1', 
  last: 'Last1', 
}); 

// The "first" and "last" states have changed, 
// but the "modified" state has not. This means 
// that the "First2" and "Last2" values will 
// not be rendered. 
myUser.setState({ 
  first: 'First2', 
  last: 'Last2', 
}); 

如您所见,组件现在完全依赖于modified状态。如果不大于先前修改的值,则不会进行渲染。

以下是渲染两次后组件的外观:

Using metadata to optimize rendering

在这个例子中,我没有使用不变的状态数据。在本书中,我将使用普通 JavaScript 对象作为简单示例的状态。Immutable.js对于这项工作来说是一个很好的工具,所以我会经常使用它。同时,我想明确指出,Immutable.js并不需要在所有情况下都使用。

呈现命令式组件

到目前为止,您在本书中呈现的所有内容都是简单的声明性 HTML。正如您所知,生活从来没有这么简单:有时我们的 React 组件需要在幕后实现一些命令性代码。

这是隐藏命令操作的关键,以便呈现组件的代码不必触及它。在本节中,您将实现一个简单的 jQuery UI button React 组件,以便了解相关的生命周期方法如何帮助我们封装命令式代码。

呈现 jQuery UI 小部件

jQueryUI 小部件库在标准 HTML 之上实现了几个小部件。它使用一种渐进增强技术,在支持更新功能的浏览器中增强基本 HTML。要使这些小部件工作,首先需要以某种方式将 HTML 呈现到 DOM 中;然后,进行命令式函数调用以创建小部件并与之交互。

在本例中,我们将创建一个 React 按钮组件,作为 jQueryUI 小部件的包装器。任何使用 React 组件的人都不需要知道,在幕后,它正在进行命令式调用来控制小部件。让我们看看按钮组件的外观:

import React, { Component } from 'react'; 

// Import all the jQuery UI widget stuff... 
import $ from 'jquery'; 
import 'jquery-ui/ui/widgets/button'; 
import 'jquery-ui/themes/base/all.css'; 

export default class MyButton extends Component { 
  // When the component is mounted, we need to 
  // call "button()" to initialize the widget. 
  componentDidMount() { 
    $(this.button).button(this.props); 
  } 

  // After the component updates, we need to use 
  // "this.props" to update the options of the 
  // jQuery UI button widget. 
  componentDidUpdate() { 
    $(this.button).button('option', this.props); 
  } 

  // Renders the "<button>" HTML element. The "onClick()" 
  // handler will always be a assigned, even if it's a 
  // noop function. The "ref" property is used to assign 
  // "this.button". This is the DOM element itself, and 
  // it's needed by the "componentDidMount()" and 
  // "componentDidUpdate()" methods. 
  render() { 
    return ( 
      <button 
        onClick={this.props.onClick} 
        ref={(button) => { this.button = button; }} 
      /> 
    ); 
  } 
} 

jQueryUIButton 小部件需要一个<button>元素,因此这就是组件呈现的内容。还分配了一个onClick()处理程序,该函数应该在属性中找到。这里还使用了一个ref属性,它将button参数赋值给this.button。这样做的原因是组件可以直接访问组件的底层 DOM 元素。通常,组件不需要访问任何 DOM 元素,但在这里,我们需要向元素发出命令。

例如,在componentDidMount()方法中,我们调用button()函数并从组件传递属性。我们在componentDidUpdate()方法中做了类似的事情,当属性值发生变化时调用该方法。现在,让我们看一下按钮容器组件:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 

import MyButton from './MyButton'; 

class MyButtonContainer extends Component { 
  // The initial state is an empty Immutable map, because 
  // by default, we won't pass anything to the jQuery UI 
  // button widget. 
  state = { 
    data: fromJS({}), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // Before the component is mounted for the first time, 
  // we have to bind the "onClick()" handler to "this" 
  // so that the handler can set the state. 
  componentWillMount() { 
    this.data = this.data 
      .merge(this.props, { 
        onClick: this.props.onClick.bind(this),  
      }); 
  } 

  // Renders the "<MyButton>" component with this 
  // component's state as properties. 
  render() { 
    return ( 
      <MyButton {...this.state.data.toJS()} /> 
    ); 
  } 
} 

// By default, the "onClick()" handler is a noop. 
// This makes it easier because we can always assign 
// the event handler to the "<button>". 
MyButtonContainer.defaultProps = { 
  onClick: () => {}, 
}; 

export default MyButtonContainer; 

再一次,我们有一个控制状态的容器组件,然后作为属性传递给<MyButton>。该组件有一个默认的onClick()处理程序函数。但是,正如您在componentWillMount()方法中看到的,我们可以将不同的单击处理程序作为属性传入。此外,它会自动绑定到组件上下文,这在处理程序需要更改按钮状态时非常有用。让我们看一个这样的例子:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyButtonContainer from './MyButtonContainer'; 

// Simple button event handler that changes the 
// "disabled" state when clicked. 
function onClick() { 
  this.data = this.data 
    .set('disabled', true); 
} 

render(( 
  <section> 
    { /* A simple button with a simple label. */ } 
    <MyButtonContainer label="Text" /> 

    { /* A button with an icon, and a hidden label. */ } 
    <MyButtonContainer 
      label="My Button" 
      icon="ui-icon-person" 
      showLabel={false} 
    /> 

    { /* A button with a click event handler. */ } 
    <MyButtonContainer 
      label="Disable Me" 
      onClick={onClick} 
    /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

在这里,我们有三个 jQueryUI 按钮小部件,每个小部件都由一个 React 组件控制,看不到命令代码。以下是按钮的外观:

Rendering jQuery UI widgets

部件安装后的清理

在本章的最后一节中,我们将考虑组件之后的清理。您不必显式地从 DOM 中卸载组件。有些东西 React 不知道,因此在拆下部件后无法为我们清理。

componentWillUnmount()生命周期方法就是针对这些类型的环境而存在的。清理 React 后组件的主要用例是异步代码。

例如,假设一个组件在首次装入该组件时发出 API 调用以获取一些数据。现在,假设这个组件在 API 响应到达之前从 DOM 中删除。

清理异步调用

如果异步代码试图设置已卸载组件的状态,则不会发生任何事情。将记录警告,并且未设置状态。记录此警告实际上非常重要;否则,我们将很难找出微妙的竞争条件缺陷。

正确的方法是创建可取消的异步操作。这是我们在本章前面实现的users()API 函数的修改版本:

// Adapted from: 
// https://facebook.github.io/react/blog/2015/12/16/ 
// ismounted-antipattern.html 
function cancellable(promise) { 
  let cancelled = false; 

  // Creates a wrapper promise to return. This wrapper is 
  // resolved or rejected based on the wrapped promise, and 
  // on the "cancelled" value. 
  const promiseWrapper = new Promise((resolve, reject) => { 
    promise.then((val) => { 
      return cancelled ? 
        reject({ cancelled: true }) : resolve(val); 
    }, (error) => { 
      return cancelled ? 
        reject({ cancelled: true }) : reject(error); 
    }); 
  }); 

  // Adds a "cancel()" method to the promise, for 
  // use by the React component in "componentWillUnmount()". 
  promiseWrapper.cancel = function cancel() { 
    cancelled = true; 
  }; 

  return promiseWrapper; 
} 

export function users(fail) { 
  // Make sure that the returned promise is "cancellable",  
  // by wrapping it with "cancellable()". 
  return cancellable(new Promise((resolve, reject) => { 
    setTimeout(() => { 
      if (fail) { 
        reject(fail); 
      } else { 
        resolve({ 
          users: [ 
            { id: 0, name: 'First' }, 
            { id: 1, name: 'Second' }, 
            { id: 2, name: 'Third' }, 
          ], 
        }); 
      } 
    }, 4000); 
  })); 
} 

诀窍在于cancellable()函数,它用一个新的承诺来包装一个承诺。新的承诺有一个cancel()方法,如果调用该承诺,该方法将拒绝该承诺。它不会改变承诺正在同步的实际异步行为。但是,它确实提供了一个通用的、一致的接口,供 React 组件使用。

现在让我们来看一个容器组件,它有能力取消异步行为:

import React, { Component } from 'react'; 
import { fromJS } from 'immutable'; 
import { render } from 'react-dom'; 

import { users } from './api'; 
import UserList from './UserList'; 

// When the "cancel" link is clicked, we want to render 
// a new element in "#app". This will unmount the 
// "<UserListContainer>" component. 
const onClickCancel = (e) => { 
  e.preventDefault(); 

  render( 
    (<p>Cancelled</p>), 
    document.getElementById('app') 
  ); 
}; 

export default class UserListContainer extends Component { 
  state = { 
    data: fromJS({ 
      error: null, 
      loading: 'loading...', 
      users: [], 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  componentDidMount() { 
    // We have to store a reference to any async promises, 
    // so that we can cancel them later when the component 
    // is unmounted. 
    this.job = users(); 

    this.job.then( 
      (result) => { 
        this.data = this.data 
          .set('loading', null) 
          .set('error', null) 
          .set('users', fromJS(result.users)); 
      }, 

      // The "job" promise is rejected when it's cancelled. 
      // This means that we need to check for the  
      // "cancelled" property, because if it's true, 
      // this is normal behavior. 
      (error) => { 
        if (!error.cancelled) { 
          this.data = this.data 
            .set('loading', null) 
            .set('error', error); 
        } 
      }, 
    ); 
  } 

  // This method is called right before the component 
  // is unmounted. It is here, that we want to make sure 
  // that any asynchronous behavior is cleaned up so that 
  // it doesn't try to interact with an unmounted component. 
  componentWillUnmount() { 
    this.job.cancel(); 
  } 

  render() { 
    return ( 
      <UserList 
        onClickCancel={onClickCancel} 
        {...this.data.toJS()} 
      /> 
    ); 
  } 
} 

onClickCancel()处理程序实际上替换了用户列表。这就调用了componentWillUnmount()方法,在这里我们可以取消this.job。还值得注意的是,在componentWillMount()中进行 API 调用时,组件中存储了对承诺的引用。这是必要的;否则,我们将无法取消异步调用。

以下是在挂起的 API 调用期间呈现组件时的外观:

Cleaning up asynchronous calls

总结

在本章中,您学习了许多关于 React 组件生命周期的知识。我们首先讨论了 React 组件为什么首先需要生命周期。事实证明,React 不能为我们自动完成所有工作,因此我们需要编写一些代码,在组件生命周期的适当时间运行。

接下来,您实现了几个组件,它们能够从 JSX 属性获取初始数据并初始化状态。然后,您学习了如何通过提供shouldComponentRender()方法来实现更高效的 React 组件。

最后,您学习了如何隐藏某些组件需要实现的命令式代码,以及如何在异步行为之后进行清理。在下一章中,您将学习有助于确保组件传递正确属性的技术。