React 16是最近一年多React更新最大的版本。除了讓大家喜聞樂見的向下兼容的Fiber,防止了客戶端react在進行渲染的時候阻塞頁面的其他交互行為。Fiber源碼速覽
參考https://juejin.im/post/5bea68a6e51d450cb20fdd70
新的生命周期過程
先來看看最新版本react的生命周期圖:
看看它的變化
新增:getDerivedStateFromProps,getSnapshotBeforeUpdate
UNSAFE:UNSAFE_componentWillMount,UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps
getDerivedStateFromProps
React生命周期的命名一直都是非常語義化的,這個生命周期的意思就是從props中獲取state
,可以說是太簡單易懂了。
可以說,這個生命周期的功能實際上就是將傳入的props映射到state上面
。
由于16.4的修改,這個函數(shù)會在每次re-rendering之前被調用
,這意味著什么呢?
意味著即使你的props沒有任何變化,而是父state發(fā)生了變化,導致子組件發(fā)生了re-render,這個生命周期函數(shù)依然會被調用。看似一個非常小的修改,卻可能會導致很多隱含的問題。
使用
這個生命周期函數(shù)是為了替代componentWillReceiveProps
存在的,所以在你需要使用componentWillReceiveProps
的時候,就可以考慮使用getDerivedStateFromProps
來進行替代了。
兩者的參數(shù)是不相同的,而getDerivedStateFromProps
是一個靜態(tài)函數(shù),也就是這個函數(shù)不能通過this訪問到class的屬性,也并不推薦直接訪問屬性。而是應該通過參數(shù)提供的nextProps以及prevState來進行判斷,根據(jù)新傳入的props來映射到state。
需要注意的是,如果props傳入的內容不需要影響到你的state,那么就需要返回一個null,這個返回值是必須的,所以盡量將其寫到函數(shù)的末尾。
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 當傳入的type發(fā)生變化的時候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否則,對于state不進行任何操作
return null;
}
Case1 -- 多來源的不同狀態(tài)
假設我們有一個列表,這個列表受到頁面主體,也就是根組件的驅動,也受到其本身數(shù)據(jù)加載的驅動。
因為這個頁面在開始渲染的時候,所有的數(shù)據(jù)請求可能是通過batch進行的,所以要在根組件進行統(tǒng)一處理,而其列表的分頁操作,則是由其本身控制。
這會出現(xiàn)什么問題呢?該列表的狀態(tài)受到兩方面的控制,也就是re-render可能由props驅動,也可能由state驅動。這就導致了getDerivedStateFromProps會在兩種驅動狀態(tài)下被重新渲染。
當這個函數(shù)被多次調用的時候,就需要注意到,state和props的變化將會怎樣影響到你的組件變化。
// 組件接收一個type參數(shù)
static propTypes = {
type: PropTypes.number
}
// 組件還具有自己的狀態(tài)來渲染列表
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
type: 0,
}
}
}
如上面代碼的例子所示,組件既受控,又控制自己。當type發(fā)生變化,會觸發(fā)一次getDerivedStateFromProps
,在這里更新組件的type狀態(tài),然而,在進行異步操作之后,組件又會更新list狀態(tài),這時你的getDerivedStateFromProps
函數(shù)就需要注意,不能夠僅僅判斷type是否變化來更新狀態(tài),因為list的變化也會更新到組件的狀態(tài)。這時就必須返回一個null,否則會導致組件無法更新并且報錯。
Case2 -- 組織好你的組件
考慮一下,如果你的組件內部既需要修改自己的type,又需要接收從外部修改的type。
是不是非常混亂?getDerivedStateFromProps中你根本不知道該做什么。
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// type可能由props驅動,也可能由state驅動,這樣判斷會導致state驅動的type被回滾
if (type !== prevState.type) {
return {
type,
};
}
// 否則,對于state不進行任何操作
return null;
}
如何解決這個棘手的問題呢?
好好組織你的組件,在非必須的時候,摒棄這種寫法。type要么由props驅動,要么完全由state驅動。
如果實在沒有辦法解耦,那么就需要一個hack來輔助:綁定props到state上。
constructor(props) {
super(props);
this.state = {
type: 0,
props,
}
}
static getDerivedStateFromProps(nextProps, prevState) {
const {type, props} = nextProps;
// 這段代碼可能看起來非常混亂,這個props可以被當做緩存,僅用作判斷
if (type !== props.type) {
return {
type,
props: {
type,
},
};
}
// 否則,對于state不進行任何操作
return null;
}
上面的代碼可以保證在進行多數(shù)據(jù)源驅動的時候,狀態(tài)能夠正確改變。當然,這樣的代碼很多情況下是會影響到別人閱讀你的代碼的,對于維護造成了非常大的困難。
從這個生命周期的更新來看,react更希望將受控的props
和state
進行分離,就如同Redux
作者Dan Abramov在redux文檔當中寫的一樣Presentational and Container Components,將所有的組件分離稱為展示型組件和容器型組件,一個只負責接收props
來改變自己的樣式,一個負責保持其整個模塊的state
。這樣可以讓代碼更加清晰。但是在實際的業(yè)務邏輯中,我們有時很難做到這一點,而且這樣可能會導致容器型組件變得非常龐大以致難以管理,如何進行取舍還是需要根據(jù)實際場景決定的。
Case3 -- 異步
以前,我們可以在props發(fā)生改變的時候,在componentWillReceiveProps中進行異步操作,將props的改變驅動到state的改變。
componentWillReceiveProps(nextProps) {
if (props.type !== nextProps.type) {
// 在這里進行異步操作或者更新狀態(tài)
this.setState({
type: props.type,
});
this._doAsyncOperation();
}
}
這樣的寫法已經(jīng)使用了很久,并且并不會存在什么功能上的問題,但是將componentWillReceiveProps標記為deprecated的原因也并不是因為功能問題,而是性能問題。
當外部多個屬性在很短的時間間隔之內多次變化,就會導致componentWillReceiveProps被多次調用。這個調用并不會被合并,如果這次內容都會觸發(fā)異步請求,那么可能會導致多個異步請求阻塞。
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
這個生命周期函數(shù)會在每次調用render之前被觸發(fā),而讀過一點react源碼的童鞋都會了解,reactsetState操作是會通過transaction進行合并的,由此導致的更新過程是batch的,而react中大部分的更新過程的觸發(fā)源都是setState,所以render觸發(fā)的頻率并不會非常頻繁(感謝 @leeenx20 的提醒,這里描述進行了修改)。
在使用getDerivedStateFromProps的時候,遇到了上面說的props在很短的時間內多次變化,也只會觸發(fā)一次render,也就是只觸發(fā)一次getDerivedStateFromProps。這樣的優(yōu)點不言而喻。
那么如何使用getDerivedStateFromProps進行異步的處理呢?
If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
官方教你怎么寫代碼系列,但是其實也沒有其他可以進行異步操作的地方了。為了響應props的變化,就需要在componentDidUpdate中根據(jù)新的props和state來進行異步操作,比如從服務端拉取數(shù)據(jù)。
// 在getDerivedStateFromProps中進行state的改變
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.type !== prevState.type) {
return {
type: nextProps.type,
};
}
return null;
}
// 在componentDidUpdate中進行異步操作,驅動數(shù)據(jù)的變化
componentDidUpdate() {
this._loadAsyncData({...this.state});
}
小結
以上是本期開發(fā)過程中使用新的生命周期函數(shù)的時候遇到的一點小問題和一些相關思考。react為了防止部分開發(fā)者濫用生命周期,可謂非常盡心盡力了。既然你用不好,我就干脆不讓你用。一個靜態(tài)的生命周期函數(shù)可以讓狀態(tài)的修改更加規(guī)范和合理。
至于為什么全文沒有提到getSnapshotBeforeUpdate
,因為自己并沒有用到#誠實臉。簡單看了一下,這個函數(shù)返回一個update之前的快照,并且傳入到componentDidUpdate
中,組件更新前后的狀態(tài)都可以在componentDidUpdate
中獲取了。一些需要在組件更新完成之后進行的操作所需要的數(shù)據(jù),就可以不需要掛載到state
或者是cache下來了。比如官方例子中所舉例的保留更新之前的頁面滾動距離,以便在組件update完成之后恢復其滾動位置。也是一個非常方便的周期函數(shù)。