[轉(zhuǎn)]深入淺出 React -- 生命周期
這里通過對(duì) React15 和 React16 兩個(gè)版本的生命周期進(jìn)行對(duì)比總結(jié),來建立系統(tǒng)而完善的生命周期知識(shí)體系.
生命周期背后的設(shè)計(jì)思想
React 設(shè)計(jì)的兩個(gè)核心概念:“組件” 和 “虛擬 DOM”
虛擬 DOM
當(dāng)組件初始化時(shí),通過調(diào)用生命周期中的 render 方法经窖,生成虛擬 DOM靠娱;再通過調(diào)用 ReactDOM.render
方法人芽,將虛擬 DOM 轉(zhuǎn)換為真實(shí) DOM索烹。
當(dāng)組件更新時(shí),會(huì)再次調(diào)用生命周期中的 render 方法岸啡,生成新的虛擬 DOM蛇捌;然后通過 diff 算法定位兩次虛擬 DOM 的差異抚恒,對(duì)發(fā)生變化的真實(shí) DOM 做定向更新。
組件化
在一個(gè) React 項(xiàng)目中络拌,幾乎所有的內(nèi)容都可以抽離為各種各樣的組件俭驮,每個(gè)組件既是 “封閉” 的,也是 “開放” 的春贸。
所謂 “封閉”混萝,是針對(duì)組件數(shù)據(jù)改變到組件實(shí)際發(fā)生更新的過程。在組件自身的渲染過程中萍恕,每個(gè)組件都只會(huì)處理它自身內(nèi)部的渲染邏輯逸嘀。在沒有數(shù)據(jù)交流的情況下,組件之間互不干擾允粤。
所謂 “開放”崭倘,是針對(duì)組件間通信的。React 允許開發(fā)者基于單向數(shù)據(jù)流的原則來完成組件之間的通信类垫。組件之間的通信可能使通信組件的渲染結(jié)果產(chǎn)生影響司光。所以說組件之間是相互開放的,可以相互影響的悉患。
React 組件的 “開放” 與 “封閉” 特性残家,使得 React 的組件具備高可重用性和可維護(hù)性。
生命周期方法
生命周期的 render 方法將虛擬 DOM和組件兩者結(jié)合到了一起售躁。
虛擬 DOM 的生成依賴 render跪削,而組件的渲染過程也離不開 render谴仙。所以可以將 render 方法比作組件的“靈魂”迂求。
render 之外的生命周期方法可以理解為組件的“軀干”碾盐。
我們可以省略 render 之外的任何生命周期方法內(nèi)容的編寫,但是 render 函數(shù)不能省略揩局;但是 render 之外的生命周期方法的編寫毫玖,通常是為 render 服務(wù);“靈魂” 和 “軀干” 共同構(gòu)成了 React 組件完整的生命時(shí)間軸凌盯。
React15 生命周期
在 React15 中付枫,需要關(guān)注以下生命周期方法:
constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()
這些生命周期方法的關(guān)系:
下面的示例可以驗(yàn)證:
import React from "react"
import ReactDOM from "react-dom"
// 代碼源自 “深入淺出搞定 React -- 修言”
// 定義子組件
class LifeCycle extends React.Component {
constructor(props) {
console.log("進(jìn)入constructor")
super(props)
// state 可以在 constructor 里初始化
this.state = { text: "子組件的文本" }
}
// 初始化渲染時(shí)調(diào)用
componentWillMount() {
console.log("componentWillMount方法執(zhí)行")
}
// 初始化渲染時(shí)調(diào)用
componentDidMount() {
console.log("componentDidMount方法執(zhí)行")
}
// 父組件修改組件的props時(shí)會(huì)調(diào)用
componentWillReceiveProps(nextProps) {
console.log("componentWillReceiveProps方法執(zhí)行")
}
// 組件更新時(shí)調(diào)用
shouldComponentUpdate(nextProps, nextState) {
console.log("shouldComponentUpdate方法執(zhí)行")
return true
}
// 組件更新時(shí)調(diào)用
componentWillUpdate(nextProps, nextState) {
console.log("componentWillUpdate方法執(zhí)行")
}
// 組件更新后調(diào)用
componentDidUpdate(nextProps, nextState) {
console.log("componentDidUpdate方法執(zhí)行")
}
// 組件卸載時(shí)調(diào)用
componentWillUnmount() {
console.log("子組件的componentWillUnmount方法執(zhí)行")
}
// 點(diǎn)擊按鈕,修改子組件文本內(nèi)容的方法
changeText = () => {
this.setState({
text: "修改后的子組件文本"
})
}
render() {
console.log("render方法執(zhí)行")
return (
<div className="container">
<button onClick={this.changeText} className="changeText">
修改子組件文本內(nèi)容
</button>
<p className="textContent">{this.state.text}</p>
<p className="fatherContent">{this.props.text}</p>
</div>
)
}
}
// 定義 LifeCycle 組件的父組件
class LifeCycleContainer extends React.Component {
// state 也可以像這樣用屬性聲明的形式初始化
state = {
text: "父組件的文本",
hideChild: false
}
// 點(diǎn)擊按鈕驰怎,修改父組件文本的方法
changeText = () => {
this.setState({
text: "修改后的父組件文本"
})
}
// 點(diǎn)擊按鈕阐滩,隱藏(卸載)LifeCycle 組件的方法
hideChild = () => {
this.setState({
hideChild: true
})
}
render() {
return (
<div className="fatherContainer">
<button onClick={this.changeText} className="changeText">
修改父組件文本內(nèi)容
</button>
<button onClick={this.hideChild} className="hideChild">
隱藏子組件
</button>
{this.state.hideChild ? null : <LifeCycle text={this.state.text} />}
</div>
)
}
}
ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"))
掛載階段
組件掛載在一個(gè) React 組件的生命周期中只會(huì)發(fā)生一次,在這個(gè)過程中县忌,組件被初始化掂榔,最后被渲染到真實(shí) DOM;
掛載階段症杏,一個(gè) React 組件所經(jīng)歷的生命周期:
-
constructor()
:對(duì) this.state 初始化装获。 -
componentWillMount()
:在render
方法前被觸發(fā)。 -
render()
:生成需要渲染的內(nèi)容并返回厉颤,不會(huì)操作真實(shí) DOM穴豫。真實(shí) DOM 的渲染由 ReactDOM.render 完成。 -
componentDidMount()
:在渲染結(jié)束后被觸發(fā)逼友,此時(shí)可以訪問真實(shí) DOM 精肃。在這個(gè)生命周期中也可以做類似于異步請(qǐng)求、數(shù)據(jù)初始化的操作帜乞。
更新階段
更新階段司抱,一個(gè) React 組件所經(jīng)歷的生命周期:
componentWillReceiveProps
從圖中可以看出,由父組件觸發(fā)的更新和由組件自身觸發(fā)的更新對(duì)比挖函,多出了一個(gè)生命周期方法:componentWillReceiveProps(nextProps)
状植。
nextProps
表示新 props
內(nèi)容,而現(xiàn)有的 props
可以通過 this.props
獲取怨喘,從而對(duì)比 props
的變化津畸。
如果父組件導(dǎo)致組件重新渲染,即使 props 沒有更改必怜,也會(huì)調(diào)用此方法(componentWillReceiveProps)肉拓。如果只想處理更改,請(qǐng)確保進(jìn)行當(dāng)前值與變更值的比較梳庆。
componentWillReceiveProps 并不是由 props 的變化觸發(fā)的暖途,而是由父組件的更新觸發(fā)的
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
由于 render
方法會(huì)進(jìn)行虛擬 DOM 的構(gòu)建和對(duì)比卑惜,比較耗時(shí)。為了避免不必要的 render
調(diào)用驻售,React 提供了 shouldComponentUpdate
生命周期方法露久。
根據(jù) shouldComponentUpdate()
的返回值,判斷 React 組件的輸出是否受當(dāng)前 state 或 props 更改的影響欺栗。默認(rèn)行為是 state 每次發(fā)生變化組件都會(huì)重新渲染毫痕。大部分情況下,你應(yīng)該遵循默認(rèn)行為迟几。
此方法僅作為性能優(yōu)化的方式而存在消请。不要企圖依靠此方法來“阻止”渲染,因?yàn)檫@可能會(huì)產(chǎn)生 bug类腮。你應(yīng)該考慮使用內(nèi)置的 PureComponent 組件臊泰,而不是手動(dòng)編寫 shouldComponentUpdate()
。PureComponent
會(huì)對(duì) props 和 state 進(jìn)行淺層比較蚜枢,并減少了跳過必要更新的可能性缸逃。
componentWillUpdate 和 componentDidUpdate
componentWillUpdate
在 render
前觸發(fā),和 componentWillMount
類似祟偷,可以在里面做一些與真實(shí) DOM 不相關(guān)的操作察滑。
componentDidUpdate
在組件更新完成后觸發(fā),和 componentDidMount
類似修肠,可以在里面處理 DOM 操作贺辰;作為子組件更新完畢通知父組件的標(biāo)志。
卸載階段
組件銷毀嵌施,只有 componentWillUnmount()
生命周期饲化,可以在里面做一些釋放內(nèi)存,清理定時(shí)器等操作吗伤。
React16 生命周期
React 16.3 生命周期:
示例代碼:
import React from "react"
import ReactDOM from "react-dom"
// 代碼源自 “深入淺出搞定 React -- 修言”
// 定義子組件
class LifeCycle extends React.Component {
constructor(props) {
console.log("進(jìn)入constructor")
super(props)
// state 可以在 constructor 里初始化
this.state = { text: "子組件的文本" }
}
// 初始化/更新時(shí)調(diào)用
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps方法執(zhí)行")
return {
fatherText: props.text
}
}
// 初始化渲染時(shí)調(diào)用
componentDidMount() {
console.log("componentDidMount方法執(zhí)行")
}
// 組件更新時(shí)調(diào)用
shouldComponentUpdate(prevProps, nextState) {
console.log("shouldComponentUpdate方法執(zhí)行")
return true
}
// 組件更新時(shí)調(diào)用
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate方法執(zhí)行")
return "haha"
}
// 組件更新后調(diào)用
componentDidUpdate(nextProps, nextState, valueFromSnapshot) {
console.log("componentDidUpdate方法執(zhí)行")
console.log("從 getSnapshotBeforeUpdate 獲取到的值是", valueFromSnapshot)
}
// 組件卸載時(shí)調(diào)用
componentWillUnmount() {
console.log("子組件的componentWillUnmount方法執(zhí)行")
}
// 點(diǎn)擊按鈕吃靠,修改子組件文本內(nèi)容的方法
changeText = () => {
this.setState({
text: "修改后的子組件文本"
})
}
render() {
console.log("render方法執(zhí)行");
return (
<div className="container">
<button onClick={this.changeText} className="changeText">
修改子組件文本內(nèi)容
</button>
<p className="textContent">{this.state.text}</p>
<p className="fatherContent">{this.props.text}</p>
</div>
)
}
}
// 定義 LifeCycle 組件的父組件
class LifeCycleContainer extends React.Component {
// state 也可以像這樣用屬性聲明的形式初始化
state = {
text: "父組件的文本",
hideChild: false
}
// 點(diǎn)擊按鈕,修改父組件文本的方法
changeText = () => {
this.setState({
text: "修改后的父組件文本"
})
}
// 點(diǎn)擊按鈕足淆,隱藏(卸載)LifeCycle 組件的方法
hideChild = () => {
this.setState({
hideChild: true
})
}
render() {
return (
<div className="fatherContainer">
<button onClick={this.changeText} className="changeText">
修改父組件文本內(nèi)容
</button>
<button onClick={this.hideChild} className="hideChild">
隱藏子組件
</button>
{this.state.hideChild ? null : <LifeCycle text={this.state.text} />}
</div>
)
}
}
ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"))
掛載階段
componentWillMount vs getDerivedStateFromProps
對(duì)比于 React 15 廢棄了 componentWillMount
巢块,新增了 getDerivedStateFromProps
。
componentWillMount
的存在不僅“雞肋”而且危險(xiǎn)巧号,因此它不值得被“替代”族奢,而應(yīng)該直接廢棄。
getDerivedStateFromProps
的設(shè)計(jì)初衷是替換 componentWillReceiveProps
丹鸿,它有且僅有一個(gè)作用:讓組件在 props
變化時(shí)派生/更新 state
越走。
getDerivedStateFromProps
的方法簽名:
static getDerivedStateFromProps(props, state)
-
getDerivedStateFromProps
是一個(gè)靜態(tài)方法;不依賴組件實(shí)例;在這個(gè)方法里不能訪問this
廊敌。 - 兩個(gè)參數(shù):
props
和state
铜跑,分別表示組件接收的來自父組件的props
和自身的state
。 - 需要一個(gè)對(duì)象作為返回值骡澈;如果沒有指定返回值锅纺,React 會(huì)發(fā)出警告;React 需要用這個(gè)返回值來更新/派生組件的
stat
秧廉;如果不需要伞广,最好直接省略這個(gè)方法,否則需要返回null
疼电。 - 對(duì)
state
的更新不是“覆蓋”,而是針對(duì)屬性的定向更新减拭。
更新階段
React 16.4 的掛載和卸載和 React 16.3 保持一致蔽豺,更新階段不同:
React 16.4 生命周期:
- 在 React 16.4 中,任何因素觸發(fā)的組件更新都會(huì)觸發(fā)
getDerivedStateFromProps
拧粪。 - 在 React 16.3 中修陡,只有父組件的更新才會(huì)觸發(fā)
getDerivedStateFromProps
。
getDerivedStateFromProps
-
getDerivedStateFromProps
是為了試圖替換componentWillReceiveProp
而出現(xiàn)的可霎。 -
getDerivedStateFromProps
不能完全等同于componentWillReceiveProps
魄鸦。- 代替實(shí)現(xiàn)基于 props 派生 state。
- 原則上癣朗,它能且只能做這一件事拾因。
為什么要用 getDerivedStateFromProps
替換 componentWillReceiveProps
做 “合理的減法”
getDerivedStateFromProps
直接被定義為 static
方法,使得在其方法內(nèi)部無法拿到組件實(shí)例的 this
旷余,也就不能在里面執(zhí)行類似不合理的 this.setState
(可能會(huì)導(dǎo)致死循環(huán))這類會(huì)產(chǎn)生副作用的操作绢记。
確保生命周期函數(shù)的行為可控可預(yù)測,從源頭上幫助開發(fā)者避免不合理的編碼正卧,同時(shí)也是為新的Fiber 架構(gòu)鋪路蠢熄。
componentWillUpdate vs getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState) {
// ...
}
- 執(zhí)行時(shí)機(jī)在
render
方法之后,真實(shí) DOM 更新之前 - 可以獲得 DOM 更新前后的
state
和props
信息 - 返回值將作為
componentDidUpdate
的第三個(gè)參數(shù)
在實(shí)際編程中很少用到炉旷,但也有特殊場景需要签孔。
例如:實(shí)現(xiàn)一個(gè)內(nèi)容會(huì)發(fā)生變化的滾動(dòng)列表,要求根據(jù)滾動(dòng)列表的內(nèi)容是否發(fā)生變化窘行,來決定是否要記錄滾動(dòng)條的當(dāng)前位置饥追。
這個(gè)例子中要求我們對(duì)比更新前后的數(shù)據(jù)是否發(fā)生變化,還需要獲取真實(shí)的 DOM 位置信息抽高。
與componentDidUpdate
配合編程:
// 組件更新時(shí)調(diào)用
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate方法執(zhí)行")
return "haha"
}
// 組件更新后調(diào)用
componentDidUpdate(prevProps, prevState, valueFromSnapshot) {
console.log("componentDidUpdate方法執(zhí)行")
console.log("從 getSnapshotBeforeUpdate 獲取到的值是", valueFromSnapshot)
}
getSnapshotBeforeUpdate
的設(shè)計(jì)初衷是為了 “與 componentDidUpdate
一起判耕,覆蓋過時(shí)的componentWillUpdate
”。
為什么廢除 componentWillUpdate
翘骂,是因?yàn)樗贿m合 Fiber 架構(gòu)壁熄。
卸載階段
與 React 15 完全一致
React 16 為何做出兩次改變
Fiber 架構(gòu)簡析
使 Virtual DOM 可以進(jìn)行增量式渲染
Fiber 會(huì)使原本同步的渲染過程變成異步的
在 React 16 之前帚豪,每次組件更新,React 都會(huì)構(gòu)建虛擬 DOM草丧,再與舊虛擬 DOM 對(duì)比 diff狸臣,最后對(duì)真實(shí) DOM 定向更新。
同步調(diào)用的調(diào)用棧非常深昌执,需要等到遞歸調(diào)用都返回后烛亦,整個(gè)渲染才算結(jié)束。
這個(gè)“漫長”的同步渲染過程不可被打斷懂拾,存在巨大風(fēng)險(xiǎn)煤禽;同步渲染一旦開始,會(huì)占據(jù)主線程岖赋,直到徹底完成檬果;在這個(gè)過程中,瀏覽器無法處理其他任務(wù)包括用戶交互唐断,甚至可能出現(xiàn)卡頓至卡死的風(fēng)險(xiǎn)选脊。
React 16 引入的 Fiber 架構(gòu),可以解決這個(gè)風(fēng)險(xiǎn):Fiber 會(huì)將一個(gè)大的更新任務(wù)拆解為多個(gè)小任務(wù)脸甘;每次執(zhí)行完成一個(gè)小任務(wù)恳啥,渲染線程都會(huì)交還主線程給瀏覽器,然后處理優(yōu)先級(jí)更高的工作丹诀,進(jìn)而避免同步渲染導(dǎo)致的卡頓钝的。
React 渲染的過程可以被中斷,可以將控制權(quán)交回瀏覽器忿墅,讓位給高優(yōu)先級(jí)的任務(wù)扁藕,瀏覽器空閑后再恢復(fù)渲染。
從 Fiber 架構(gòu)角度看生命周期
Fiber 架構(gòu)的重要特征就是渲染過程可以被中斷疚脐。根據(jù)這個(gè)特征亿柑,React 16 的生命周期被劃分為 Render 和 Commit 兩個(gè)階段,而 Commit 階段又被細(xì)分為 Pre-commit 和 Commit 階段棍弄。
- Render 階段:純凈且不包含副作用望薄。可能會(huì)被 React 暫停呼畸,中止或重新啟動(dòng)痕支。
- Pre-commit 階段:可以讀取 DOM。
- Commit 階段:可以使用 DOM蛮原,運(yùn)行副作用卧须,安排更新。
也就是說在 Render 階段允許被中斷,而 Commit 階段不能花嘶。原因很簡單笋籽,Render 階段的操作對(duì)于用戶不可感知,所以中斷椭员、重啟對(duì)于用戶而言是不可見的车海。而 Commit 階段的操作是對(duì)真實(shí) DOM 的渲染,不能隨意中斷隘击、重渲染侍芝。
React 16 “廢舊立新”背后的思考
Fiber 架構(gòu)下,Render 階段允許被暫停埋同、終止和重啟州叠。當(dāng)一個(gè)任務(wù)執(zhí)行一段后被中斷,下一次搶回渲染線程時(shí)莺禁,這個(gè)任務(wù)會(huì)“重復(fù)執(zhí)行一遍整個(gè)任務(wù)”而不是接著上一次執(zhí)行的地方留量。這導(dǎo)致了 Render 階段的生命周期方法有可能重復(fù)執(zhí)行。
React 16 廢棄的生命周期方法:
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
這些方法都處于 Render 階段哟冬,而且這些方法常年被濫用,在重復(fù)執(zhí)行的過程中存在很大的風(fēng)險(xiǎn)忆绰。
我們的編碼中的一些不好的習(xí)慣浩峡,在 “componentWill” 開頭的生命周期里做一些事情:
- setState()
- fetch 異步請(qǐng)求
- 操作真實(shí) DOM
- ...
這些操作的問題:
- 可以轉(zhuǎn)移到其他生命周期(componentDid...)里去做
- Fiber 架構(gòu)下,可能導(dǎo)致非常嚴(yán)重的 Bug
- 在 React 15 中也有出現(xiàn)過問題(在
componentWillReceiveProps
和componentWillUpdate
里濫用 setState 導(dǎo)致重渲染死循環(huán))
總結(jié)
- React 16 改造生命周期的主要原因是為了配合 Fiber 架構(gòu)帶來的異步渲染機(jī)制错敢。
- 針對(duì)生命周期中長期被濫用的部分推出了具有強(qiáng)制性的最佳實(shí)踐翰灾。
- 確保了 Fiber 架構(gòu)下的數(shù)據(jù)和視圖的安全,以及確保了生命周期方法的行為更加可控稚茅、可預(yù)測纸淮。