你真的瞭解 React 生命週期嗎

前言

  • 本來我覺得對 React 生命週期已經熟的不能再熟了,直到前幾天實現一個功能時,就由於沒有吃透 React 生命週期,把我坑的不要不要的,因此痛定思痛,從新學習一遍 React 生命週期

舊版生命週期

  • 初始化的時候不會把賦值算做更新,因此不會執行更新階段
import React, { Component } from 'react'

export default class LifeCycle extends Component {
    //// props = {age:10,name:'計數器'}
  static defaultProps = {
      name:'計數器'
  }
  constructor(props){
      //Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    super();//this.props = props;
    this.state = {number:0,users:[]};//初始化默認的狀態對象
    console.log('1. constructor 初始化 props and state');
  
  }  
  //componentWillMount在渲染過程當中可能會執行屢次
  componentWillMount(){
    console.log('2. componentWillMount 組件將要掛載');
    //localStorage.get('userss');
  }
  //componentDidMount在渲染過程當中永遠只有執行一次
  //通常是在componentDidMount執行反作用,進行異步操做
  componentDidMount(){
    console.log('4. componentDidMount 組件掛載完成');
    fetch('https://api.github.com/users').then(res=>res.json()).then(users=>{
        console.log(users);
        this.setState({users});
    });
  }
  shouldComponentUpdate(nextProps,nextState){
    console.log('Counter',nextProps,nextState);
    console.log('5. shouldComponentUpdate 詢問組件是否須要更新');
    return true;
  }
  componentWillUpdate(nextProps, nextState){
    console.log('6. componentWillUpdate 組件將要更新');
  }
  componentDidUpdate(prevProps, prevState)){
    console.log('7. componentDidUpdate 組件更新完畢');
  }
  add = ()=>{
      this.setState({number:this.state.number});
  };
  render() {
    console.log('3.render渲染,也就是掛載')
    return (
      <div style={{border:'5px solid red',padding:'5px'}}> <p>{this.props.name}:{this.state.number}</p> <button onClick={this.add}>+</button> <ul> { this.state.users.map(user=>(<li>{user.login}</li>)) } </ul> {this.state.number%2==0&&<SubCounter number={this.state.number}/>} </div> ) } } class SubCounter extends Component{ constructor(props){ super(props); this.state = {number:0}; } componentWillUnmount(){ console.log('SubCounter componentWillUnmount'); } //調用此方法的時候會把新的屬性對象和新的狀態對象傳過來 shouldComponentUpdate(nextProps,nextState){ console.log('SubCounter',nextProps,nextState); if(nextProps.number%3==0){ return true; }else{ return false; } } //componentWillReceiveProp 組件收到新的屬性對象 componentWillReceiveProps(){ console.log('SubCounter 1.componentWillReceiveProps') } render(){ console.log('SubCounter 2.render') return( <div style={{border:'5px solid green'}}> <p>{this.props.number}</p> </div> ) } } 複製代碼

洋蔥模型

image.png

新版生命週期

static getDerivedStateFromProps

  • static getDerivedStateFromProps(nextProps,prevState):接收父組件傳遞過來的 props 和組件以前的狀態,返回一個對象來更新 state 或者返回 null 來表示接收到的 props 沒有變化,不須要更新 state
  • 該生命週期鉤子的做用: 將父組件傳遞過來的 props 映射 到子組件的 state 上面,這樣組件內部就不用再經過 this.props.xxx 獲取屬性值了,統一經過 this.state.xxx 獲取。映射就至關於拷貝了一份父組件傳過來的 props ,做爲子組件本身的狀態。注意:子組件經過 setState 更新自身狀態時,不會改變父組件的 props
  • 配合 componentDidUpdate,能夠覆蓋 componentWillReceiveProps 的全部用法
  • 該生命週期鉤子觸發的時機:
    • 在 React 16.3.0 版本中:在組件實例化、接收到新的 props 時會被調用
    • 在 React 16.4.0 版本中:在組件實例化、接收到新的 props 、組件狀態更新時會被調用
    • 在線 demo —— 測試 16.3.0 和 16.4.0 版本中,該生命週期鉤子什麼狀況下會被觸發
  • 使用: 在線 demo
    • 注意:派生狀態時,不須要把組件自身的狀態也設置進去
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

function App() {
  return (
    <div className="App"> <AAA /> </div>
  );
}

class AAA extends React.Component {
  state = {
    age: 666
  };

  add = () => {
    this.setState({ age: this.state.age + 1 });
  };

  render() {
    return (
      <div> <ChildA onChangeParent={this.add} age={this.state.age} /> </div> ); } } class ChildA extends React.Component { state = { num: 888 }; // 根據新的屬性對象派生狀態對象 // nextProps——新的屬性對象 prevState——舊的狀態對象 static getDerivedStateFromProps(nextprops, state) { console.log('props',nextprops); // 返回一個對象來更新 state 或者返回 null 來表示接收到的 props 不須要更新 state if (nextprops.age !== state.age) { console.log("更新吧"); return { onChangeParent:nextprops.onChangeParent, age: nextprops.age, // 注意:這裏不須要把組件自身的狀態也放進來 // num:state.num }; } return null; } add = () => { this.setState({ num: this.state.num + 1 }); }; render() { const { onChangeParent } = this.state; console.log('state',this.state); return ( <> <div onClick={onChangeParent}>change</div> <div onClick={this.add}>add</div> </> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 複製代碼

getSnapshotBeforeUpdate

  • getSnapshotBeforeUpdate(prevProps, prevState)接收父組件傳遞過來的 props 和組件以前的狀態,今生命週期鉤子必須有返回值,返回值將做爲第三個參數傳遞給 componentDidUpdate。必須和 componentDidUpdate 一塊兒使用,不然會報錯
  • 該生命週期鉤子觸發的時機 :被調用於 render 以後、更新 DOMrefs 以前
  • 該生命週期鉤子的做用: 它能讓你在組件更新 DOMrefs 以前,從 DOM 中捕獲一些信息(例如滾動位置)
  • 配合 componentDidUpdate, 能夠覆蓋 componentWillUpdate 的全部用法
  • 在線 demo:每次組件更新時,都去獲取以前的滾動位置,讓組件保持在以前的滾動位置
import React, { Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  return (
    <div className="App"> <GetSnapshotBeforeUpdate /> </div>
  );
}

class GetSnapshotBeforeUpdate extends Component {
  constructor(props) {
    super(props);
    this.wrapper = React.createRef();
    this.state = { messages: [] };
  }
  componentDidMount() {
    setInterval(() => {
      this.setState({
        messages: ["msg:" + this.state.messages.length, ...this.state.messages]
      });
      //this.setState({messages:[...this.state.messages,this.state.messages.length]});
    }, 1000);
  }
  getSnapshotBeforeUpdate() {
    // 返回更新內容的高度 300px
    return this.wrapper.current.scrollHeight;
  }
  componentDidUpdate(prevProps, prevState, prevScrollHeight) {
    this.wrapper.current.scrollTop =
      this.wrapper.current.scrollTop +
      (this.wrapper.current.scrollHeight - prevScrollHeight);
  }
  render() {
    let style = {
      height: "100px",
      width: "200px",
      border: "1px solid red",
      overflow: "auto"
    };
    return (
      <ul style={style} ref={this.wrapper}> {this.state.messages.map((message, index) => ( <li key={index}>{message}</li> ))} </ul>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement); 複製代碼

版本遷移

  • componentWillMountcomponentWillReceivePropscomponentWillUpdate 這三個生命週期由於常常會被誤解和濫用,因此被稱爲 不安全(不是指安全性,而是表示使用這些生命週期的代碼,有可能在將來的 React 版本中存在缺陷,可能會影響將來的異步渲染) 的生命週期。
  • React 16.3 版本:爲不安全的生命週期引入別名 UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。(舊的生命週期名稱和新的別名均可以在此版本中使用
  • React 16.3 以後的版本:爲 componentWillMountcomponentWillReceivePropscomponentWillUpdate 啓用棄用警告。(舊的生命週期名稱和新的別名均可以在此版本中使用,但舊名稱會記錄DEV模式警告
  • React 17.0 版本: 推出新的渲染方式——異步渲染( Async Rendering),提出一種可被打斷的生命週期,而能夠被打斷的階段正是實際 dom 掛載以前的虛擬 dom 構建階段,也就是要被去掉的三個生命週期 componentWillMountcomponentWillReceivePropscomponentWillUpdate。(從這個版本開始,只有新的「UNSAFE_」生命週期名稱將起做用

常見問題

當外部的 props 改變時,如何再次執行請求數據、更改狀態等操做

使用 componentWillReceiveProps

class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentWillReceiveProps(nextProps) {
    // 當父組件的 props 改變時,從新請求數據
    if (nextProps.id !== this.props.id) {
      this.setState({externalData: null});
      this._loadAsyncData(nextProps.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}
複製代碼

使用 getDerivedStateFromProps + componentDidUpdate 加載數據

class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.id !== prevState.prevId) {
      return {
        externalData: null,
        prevId: nextProps.id,
      };
    }
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }
  
  // 藉助 componentDidUpdate
  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) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}
複製代碼

使用 getDerivedStateFromProps 更改狀態

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  return (
    <div className="App"> <AAA /> </div>
  );
}

class AAA extends React.Component {
  state = {
    age: 66
  };

  add = () => {
    this.setState({ age: this.state.age + 1 });
  };
  render() {
    return (
      <div> <ChildA onChangeParent={this.add} age={this.state.age} /> </div> ); } } class ChildA extends React.Component { state = { num: 88 }; static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.age !== prevState.age) { return { age: nextProps.age }; } return null; } add = () => { this.setState({ num: this.state.num + 1 }); }; render() { const { onChangeParent } = this.props; console.log("render", this.state); return ( <> <div onClick={onChangeParent}>change</div> <div onClick={this.add}>add</div> </> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 複製代碼

只用 componentDidUpdate 的寫法

  • 不必定要使用 getDerivedStateFromProps 或者 componentWillReceiveProps
  • 在線 demo
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  return (
    <div className="App"> <AAA /> </div>
  );
}

class AAA extends React.Component {
  state = {
    age: 66
  };

  add = () => {
    this.setState({ age: this.state.age + 1 });
  };
  render() {
    return (
      <div> <ChildA onChangeParent={this.add} age={this.state.age} /> </div> ); } } class ChildA extends React.Component { state = { num: 88, age: this.props.age }; add = () => { this.setState({ num: this.state.num + 1 }); }; componentDidUpdate() { if (this.props.age !== this.state.age) { console.log("componentDidUpdate", this.props.age); this.setState({ age: this.props.age }); } } render() { const { onChangeParent } = this.props; console.log("render", this.state); return ( <> <div onClick={onChangeParent}>change</div> <div onClick={this.add}>add</div> </> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 複製代碼

使用 key 的寫法

  • 經過改變 key ,來從新初始化組件 在線 demo
  • 這聽起來很慢,可是這點的性能是能夠忽略的。若是在組件樹的更新上有很重的邏輯,這樣反而會更快,由於省略了子組件的 diff
  • React 官方建議的模式
  • 我以爲這種寫法,很是適合:當你調用同事寫的業務 UI 組件時,若是他沒有考慮到組件內部狀態須要跟隨外部 props 的更改的狀況(巴不得上去就給他個膝蓋重錘 😂😂😂),可使用 key 來快速實現
class ExampleComponent extends React.Component {
  state = {
    id: '123456',
  };
  render(){
    const {id} = this.state;
    // 當 id 變化時,key 也隨之改變,那麼組件就會從新初始化
    return <ExampleComponent key={id} id={id}/>; } } class ExampleComponent extends React.Component { state = { externalData: null, }; // 不須要使用 getDerivedStateFromProps 或者 componentWillReceiveProps // static getDerivedStateFromProps(nextProps, prevState) { // if (nextProps.id !== prevState.prevId) { // return { // externalData: null, // prevId: nextProps.id, // }; // } // return null; // } componentDidMount() { this._loadAsyncData(this.props.id); } componentWillUnmount() { if (this._asyncRequest) { this._asyncRequest.cancel(); } } render() { if (this.state.externalData === null) { // Render loading state ... } else { // Render real UI ... } } _loadAsyncData(id) { this._asyncRequest = asyncLoadData(id).then( externalData => { this._asyncRequest = null; this.setState({externalData}); } ); } } 複製代碼

getDerivedStateFromProps 是一個靜態方法,而組件實例沒法繼承靜態方法,因此該生命週期鉤子內部沒法經過使用 this 獲取組件實例的屬性/方法。

  • 有些狀況下,咱們須要對父組件傳遞過來的數據進行過濾/篩選等操做,而這些操做通常都會放在一個單獨的函數中(單一原則),而後將該生命週期鉤子獲取到的 props 傳遞進這些方法中進行處理。
    • 若是選擇把這些方法放在 class 組件上,那麼這些方法得申明成靜態方法,而後在該生命週期鉤子中經過 className.xxx 調用這些方法。
class AAA extends React.Component {

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.id !== prevState.prevId) {
      const data = AAA.filterFn(nextProps.data);
      return {
        data,
        prevId: nextProps.id,
      };
    }
    return null;
  }
  
  static filterFn(data){
  	// 過濾數據
    
    ...
    
    return newData;
  }
  
  ...
}

複製代碼
  • 或者把這些方法放在 class 組件外面,就不用申明成靜態方法,在該生命週期鉤子中直接調用這些方法。
function filterFn(data){
  	// 過濾數據
    ...
    return newData;
}


class AAA extends React.Component {

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.id !== prevState.prevId) {
      const data = filterFn(nextProps.data);
      return {
        data,
        prevId: nextProps.id,
      };
    }
    return null;
  }
 
  ...
}

複製代碼
  • 在使用以上兩種方法時,我我的認爲的一個缺點:若是這些方法比較複雜,內部還調用了別的函數,此時,要麼全部的處理函數都申明成靜態方法,要麼全部的方法都提到組件外部去,而且須要一層層的往下傳遞 props 值。沒法像組件實例的方法同樣,能夠在每一個組件實例方法內,經過 this.props.xxx / this.state.xxx 訪問屬性,會比較麻煩。
  • 還有一種方法: 結合 componentDidUpdate 使用 在線 demo
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  return (
    <div className="App"> <AAA /> </div>
  );
}

class AAA extends React.Component {
  state = {
    age: 66
  };

  add = () => {
    this.setState({ age: this.state.age + 1 });
  };
  render() {
    return (
      <div> <ChildA onChangeParent={this.add} age={this.state.age} /> </div> ); } } class ChildA extends React.Component { state = { num: 88 }; static getDerivedStateFromProps(nextprops, state) { console.log("getDerivedStateFromProps", nextprops); if (nextprops.age !== state.age) { return { // 給一個標識 status: false, // age: nextprops.age, onChangeParent: nextprops.onChangeParent }; } return null; } add = () => { this.setState({ num: this.state.num + 1 }); }; processData(){ console.log("process",this.props); return this.props.age; } componentDidUpdate() { // 根據標識來更新狀態 if (!this.state.status) { this.setState({ age: this.processData(), status: true }); console.log("componentDidUpdate"); } } componentDidMount() { this.setState({ age: this.props.age, status: true }); } render() { const { onChangeParent } = this.state; console.log("render", this.state); return ( <> <div onClick={onChangeParent}>change</div> <div onClick={this.add}>add</div> </> ); } } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 複製代碼

使用 getDerivedStateFromProps 派生狀態時,不須要把組件自身的狀態也設置進去

class AAA extends React.Component {
  // 必須給 state 設置一個值,哪怕是一個空對象
  state = {
  	num:666
  };
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.id !== prevState.prevId) {
      return {
        data:nextProps.data,
        prevId: nextProps.id,
        // 只須要映射屬性,不須要把組件自身的狀態也加進去
        // num:prevState.num
      };
    }
    return null;
  }
 
  ...
}

複製代碼

若是 setState 更新的值不變,那麼還會觸發這些生命週期鉤子嗎?

  • 哪怕每次都設置一樣的值,仍是會觸發更新
import React, {Component} from 'react'

export default class LifeCycle extends Component {
    static defaultProps = {
        name: '計數器'
    };

    constructor(props) {
        super(props);
        this.state = {number: 0};//初始化默認的狀態對象
        console.log('1. constructor 初始化 props and state');
    }
    
    componentWillMount() {
        console.log('2. componentWillMount 組件將要掛載');
    }
    
    componentDidMount() {
        console.log('4. componentDidMount 組件掛載完成');
    }

    shouldComponentUpdate(nextProps, nextState) {
        console.log('Counter', nextProps, nextState);
        console.log('5. shouldComponentUpdate 詢問組件是否須要更新');
        return true;
    }

    componentWillUpdate() {
        console.log('6. componentWillUpdate 組件將要更新');
    }

    componentDidUpdate() {
        console.log('7. componentDidUpdate 組件更新完畢');
    }

    add = () => {
        this.setState({number: this.state.number });
    };

    render() {
        console.log('3.render渲染')
        return (
            <div style={{border: '5px solid red', padding: '5px'}}> <p>{this.state.number}</p> <button onClick={this.add}>+</button> </div>
        )
    }
}

複製代碼

不要在 componentWillMount 中添加事件監聽

  • componentDidMount 中添加事件監聽
  • componentWillMount 能夠被打斷或調用屢次,所以沒法保證事件監聽能在 unmount 的時候被成功卸載,可能會引發內存泄露

因爲 React 將來的版本中推出了異步渲染,在 dom 被掛載以前的階段均可以被打斷重來,致使 componentWillMountcomponentWillUpdatecomponentWillReceiveProps 在一次更新中可能會被觸發屢次,所以那些只但願觸發一次的反作用應該放在 componentDidMount

  • 這也就是爲何要把異步請求放在 componentDidMount 中,而不是放在 componentWillMount 中的緣由,爲了向後兼容

最多見的誤解就是 getDerivedStateFromPropscomponentWillReceiveProps 只會在 props 「改變」時纔會調用。實際上只要父組件從新渲染時,這兩個生命週期函數就會從新調用,無論 props 有沒有「變化」

參考

Update on Async Renderingcss

React v16.9.0 and the Roadmap Updatehtml

你可能不須要使用派生 statereact

推薦閱讀

傻傻分不清之 Cookie、Session、Token、JWTgit

React Hooks 詳解 【近 1W 字】+ 項目實戰github

React SSR 詳解【近 1W 字】+ 2個項目實戰json

從 0 到 1 實現一款簡易版 Webpackapi

Webpack 轉譯 Typescript 現有方案安全

相關文章
相關標籤/搜索