前言
- 原本我以為對 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>
)
}
}
複製程式碼
洋蔥模型
新版生命週期
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 版本中,該生命週期鉤子什麼情況下會被觸發
- 在 React 16.3.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
之後、更新DOM
和refs
之前 - 該生命週期鉤子的作用: 它能讓你在元件更新
DOM
和refs
之前,從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);
複製程式碼
版本遷移
componentWillMount
,componentWillReceiveProps
,componentWillUpdate
這三個生命週期因為經常會被誤解和濫用,所以被稱為 不安全(不是指安全性,而是表示使用這些生命週期的程式碼,有可能在未來的 React 版本中存在缺陷,可能會影響未來的非同步渲染) 的生命週期。- React 16.3 版本:為不安全的生命週期引入別名
UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和UNSAFE_componentWillUpdate
。(舊的生命週期名稱和新的別名都可以在此版本中使用) - React 16.3 之後的版本:為
componentWillMount
,componentWillReceiveProps
和componentWillUpdate
啟用棄用警告。(舊的生命週期名稱和新的別名都可以在此版本中使用,但舊名稱會記錄DEV模式警告) - React 17.0 版本: 推出新的渲染方式——非同步渲染( Async Rendering),提出一種可被打斷的生命週期,而可以被打斷的階段正是實際
dom
掛載之前的虛擬dom
構建階段,也就是要被去掉的三個生命週期componentWillMount
,componentWillReceiveProps
和componentWillUpdate
。(從這個版本開始,只有新的“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
被掛載之前的階段都可以被打斷重來,導致 componentWillMount
、componentWillUpdate
、componentWillReceiveProps
在一次更新中可能會被觸發多次,因此那些只希望觸發一次的副作用應該放在 componentDidMount
中
- 這也就是為什麼要把非同步請求放在
componentDidMount
中,而不是放在componentWillMount
中的原因,為了向後相容
最常見的誤解就是 getDerivedStateFromProps
和 componentWillReceiveProps
只會在 props
“改變”時才會呼叫。實際上只要父元件重新渲染時,這兩個生命週期函式就會重新呼叫,不管 props
有沒有“變化”
參考
React v16.9.0 and the Roadmap Update