HTML5教程

React 生命周期小结

本文主要是介绍React 生命周期小结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

在这篇文章中,我会介绍 React 各个生命周期在实际项目中的应用场景,并给出代码示例。
React 生命周期图谱:https://projects.wojtekmaj.pl...

挂载阶段(Mounting)

组件实例创建并插入到 DOM 中。这个阶段调用生命周期函数顺序依次是: coustructor()、static getDerivedStateFromProps()、render()、componentDidMount()。

更新阶段(Updating)

组件的 state 或者 props 发生变化时会触发更新。这个阶段调用生命周期函数顺序依次是:static getDerivedStateFromProps()、shouldComponentUpdate()、render()、getSnapshotBeforeUpdate()、componentDidUpdate()。

卸载阶段(UnMounting)

组件从 DOM 中移除时,会调用 componentWillUnmount() 方法。

错误处理(Error boundaries)

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:static getDerivedStateFromError()、componentDidCatch()。

render()

注意⚠️:
  1. render() 函数是 class 组件中唯一必须实现的方法;
  2. render 函数的返回值:React 元素、数组、Fragments、Portals、字符串或数值、布尔值或 null;
  3. render 应为纯函数,并且不会与浏览器产生交互,这样使组件更加容易理解。

constructor(props)

调用时间:

React 组件在挂载之前,会调用它的构造函数。

使用场景:
  1. 初始化 state
  2. 给事件处理函数绑定实例
class Test extends React.Component {
   constructor(props) {
      super(props);
      this.state = { counter: 0 };
      this.handleClick = this.handleClick.bind(this);
   }

   handleClick() {
      const { counter } = this.state;
      this.setState({ counter: counter + 1 });
   }

   render() {
      return (
         <div>
             {this.state.counter}
             <button onClick={this.handleClick}>click</button> 
         </div>
      );
   }
}
ReactDOM.render(
   <Test />,
 document.getElementById('root')
);
注意⚠️:
  1. 不要在构造函数中调用 setState 方法;
  2. 在为 React.Component 子类实现构造函数时,应在其他语句之前调用 super(props);
  3. 避免在构造函数中引入副作用或者订阅;
  4. 避免将 props 的值赋值给 state(why?)。

componentDidMount()

调用时间:

React 组件挂载后(插入DOM 树中)立即调用。

使用场景:
  1. 依赖于 DOM 节点的初始化操作;
  2. 通过网络请求获取数据;
  3. 添加订阅;
注意⚠️:

在此处如果调用 setState(),将会触发渲染,这个渲染将会发生在浏览器更新屏幕之前,因此用户不会有感知,但是要谨慎使用该模式,因为会导致性能问题。 如果你的渲染依赖于 DOM 节点的位置和大小,比如 Modal 和 Tooltip ,则可以使用该模式。

componentDidUpdate(prevProps, prevState, snapshot)

调用时间:

React 组件更新后立即调用,首次渲染不会调用此方法。

使用场景:
  1. 在组件更新后,在此处对 DOM 进行操作;
  2. 对更新前后的 props 进行比较,如果有变化,执行操作(比如网络请求、执行 setState() 等);
componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.id !== prevProps.id) {
    this.fetchData(this.props.id);
  }
}
注意⚠️:
  1. 不要将 props “镜像”给 state(why?)
  2. 如果组件实现了 getSnapshotBeforeUpdate() 生命周期,则它的返回值将作为 componentDidUpdate 的第三个参数(snapshot)传入,否则,第三个参数将为 undefined;
  3. 如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()。

componentWillUnmount()

调用时间:

React 组件卸载或销毁之前调用

使用场景:
  1. 清除 timer(setInterval());
  2. 取消网络请求;
  3. 取消在 componentDidMount() 中创建的订阅。
注意⚠️:

在该生命周期中不要调用 setState(),因为组件卸载后,不会重新渲染。

shouldComponentUpdate(nextProps, nextState)

调用时间:

当 state 或者 props 发生变化时,shouldComponentUpdate() 将会在 render() 之前调用,首次渲染或者使用 forceUpdate() 时不会调用。

使用场景:

可以通过手写覆盖 shouldComponentUpdate() 方法对 React 组件进行性能优化,但是大部分情况下,可以通过继承 React.pureComponent 代替手写 shouldComponentUpdate(),React.pureComponent 实现了 shouldComponentUpdate() 方法(用当前和之前的 props 和 state 进行浅比较)。

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button>
    );
  }
}
注意⚠️:
  1. 此方法默认返回true,其遵循默认行为——“state 或者 props 发生变化时,组件重新渲染”;如果返回 false,则组件不会重新渲染,大部分情况下,你该遵循默认行为。
  2. 此方法为了性能优化而存在,不要试图使用此方法阻止渲染,因为这可能产生 bug;
  3. 如果你一定要手动编写此方法,需要对 this.props 和 nextProps、this.state 和 nextState 进行比较。
  4. 不建议在 shouldComponentUpdate() 中使用 JSON.stringfy() 或者进行深层次比较,这样会极其的耗费性能。

static getDerivedStateFromProps(props,state)

调用时间:

render() 方法之前调用,在初始挂载和后续更新都会调用。

使用场景:

适用于罕用例,即 state 的值在任何时候都取决于 props。

// 例子1:
class Example extends React.Component {
  state = { 
    isScrollingDown: false,
    lastRow: null
  };

  static getDerivedStateFromProps(props, state) { 
    if (props.currentRow !== state.lastRow) { 
      return { 
        isScrollingDown: props.currentRow > state.lastRow, 
        lastRow: props.currentRow
      }; 
    }
    // 返回 null 表示无需更新 state。
    return null;
   }
}
// 例子2:根据 props 获取 externalData
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(props, state) {
  // 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。 
  // 清除之前加载的数据(这样我们就不会渲染旧的内容)。 
    if (props.id !== state.prevId) { 
      return { 
        externalData: null,
        prevId: props.id
      };
    } 
    // 无需更新 state 
    return null;
  }
   
  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) { 
    if (this.state.externalData === null) { 
      this._loadAsyncData(this.props.id);
    } 
  }
  
  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // 渲染加载状态 ...
    } else {
      // 渲染真实 UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = loadMyAsyncData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}
注意⚠️:

派生状态会导致代码冗余,并使组件难以维护,可以使用如下替换方案:

(1)memoization 模式:如果只是为了缓存基于当前 props 计算之后的结果的话,没有必要使用 getDerivedStateFromProps(),因为管理 state 的复杂度会随着需要管理的属性的增多而越来越庞大,比如,如果我们想在组件 state 里添加第二个派生 state,那就需要写两份跟踪变化的逻辑。为了让代码变得简单和易于管理,可以尝试使用 memoization。

  // *******************************************************
  // 注意:这个例子不是建议的方法。
  // 下面的例子才是建议的方法。
  // *******************************************************

  static getDerivedStateFromProps(props, state) {
    // 列表变化或者过滤文本变化时都重新过滤。
    // 注意我们要存储 prevFilterText 和 prevPropsList 来检测变化。
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }
// 使用 memoization
import memoize from "memoize-one";

class Example extends Component {
  // state 只需要保存当前的 filter 值:
  state = { filterText: "" };

  // 在 list 或者 filter 变化时,重新运行 filter:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 计算最新的过滤后的 list。
    // 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} /> 
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

(2)使用完全受控的组件:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

(3)使用有 key 的非可控组件(当 key 变化的时候,React 就会创建一个新的而不是一个既有的组件),大部分时候,这是处理重置 state 的最好的办法。

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

但是在某些情况下,key 可能不起作用,这时候可以使用 getDerivedStateFromProps() 来观察属性变化。

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };

  static getDerivedStateFromProps(props, state) {
    // 只要当前 user 变化,
    // 重置所有跟 user 相关的状态。
    // 这个例子中,只有 email 和 user 相关。
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}

getSnapshotBeforeUpdate(prevProps, prevState)

调用时间:

在最近一次渲染输出(提交到 DOM 节点)之前调用。

使用场景:

组件在发生更改之前可以从 DOM 中获取一些信息,这个方法可能用在 UI 处理中,例如滚动位置。

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们是否在 list 中添加新的 items ?
    // 捕获滚动​​位置以便我们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

static getDerivedStateFromError(error)

调用时间:

该方法在组件抛出错误时被调用。它将抛出的错误作为参数,并且返回一个值以更新 state。

使用场景:

显示降级 UI

注意⚠️:

该方法会在渲染阶段调用,因此不允许出现副作用,如遇此类情况,可以用 componentDidCatch()。

componentDidCatch(error, info)

调用时间:

该方法在“提交”的阶段被调用。因此允许执行副作用。

使用场景:

用于记录错误,该方法的第二个参数包含有关引发组件错误的栈信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }
  
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显示降级 UI
    return { error: true };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo })
  }
  
  render() {
    if (this.state.errorInfo) {
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
             {this.state.error && this.state.error.toString()}
             <br />
             {this.state.errorInfo.componentStack}
          </details>
        </div> 
      );
    }
    return this.props.children;
  }
}
class BuggyCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
    
  handleClick() {
    this.setState(({counter}) => ({ counter: counter + 1 }));
  }
  
  render() {
    if (this.state.counter === 5) {
      // 此处渲染会产生错误
      return [1,2,3];
    }
    return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
  }
}

function App() {
  return (
    <div>
       <p>
           <b> This is an example of error boundaries in React 16.
               <br /><br />
               Click on the numbers to increase the counters.
               <br />
               The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
           </b>
       </p>
       <hr />
       <ErrorBoundary>
           <p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.
           </p>
       <BuggyCounter /> 
           <BuggyCounter />
       </ErrorBoundary>
       <hr />
       <p>These two counters are each inside of their own error boundary. So if one crashes, the other is not affected.
       </p>
       <ErrorBoundary>
           <BuggyCounter />
       </ErrorBoundary> 
       <ErrorBoundary>
           <BuggyCounter />
       </ErrorBoundary> 
    </div>
  );
}
这篇关于React 生命周期小结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!