? React Fiber原理
React架構(gòu)
- 1)Virtual DOM 層牲距,描述頁面長什么樣
- 2)Reconciler 層攻礼,負責調(diào)用組件生命周期方法虚婿,進行Diff運算等
- 3)Renderer 層屋剑,根據(jù)不同的平臺茵臭,渲染出相應的頁面震叮,如 ReactDOM 和 ReactNative
React15遺留問題
- 1)瀏覽器的整體渲染是多線程的胧砰,包括GUI渲染線程、JS引擎線程苇瓣、事件觸發(fā)線程尉间、定時觸發(fā)器線程和異步http請求線程。頁面繪制和JS運算是互斥的線程击罪,兩者不能同時進行哲嘲。
- 2)React15使用JS的函數(shù)調(diào)用棧(Stack Reconciler)遞歸渲染界面,因此在處理DOM元素過多的復雜頁面的頻繁更新時媳禁,大量同步進行的任務(樹diff和頁面render)會導致界面更新阻塞眠副、事件響應延遲、動畫卡頓等竣稽,因此React團隊在16版本重寫了React Reconciler架構(gòu)囱怕。
React16問題解決
- 1)
Fiber Reconciler
架構(gòu)可以允許同步阻塞的任務拆分成多個小任務霍弹,每個任務占用一小段時間片,任務執(zhí)行完成后判斷有無空閑時間娃弓,有則繼續(xù)執(zhí)行下一個任務典格,否則將控制權(quán)交由瀏覽器以讓瀏覽器去處理更高優(yōu)先級的任務,等下次拿到時間片后台丛,其它子任務繼續(xù)執(zhí)行耍缴。整個流程類似CPU調(diào)度邏輯,底層是使用了瀏覽器APIrequestIdleCallback
挽霉。 - 2)為了實現(xiàn)整個Diff和Render的流程可中斷和恢復防嗡,單純的VirtualDom Tree不再滿足需求,React16引入了采用單鏈表結(jié)構(gòu)的Fiber樹炼吴,如下圖所示本鸣。
- 3)FiberReconciler架構(gòu)將更新流程劃分成了兩個階段:1.diff(由多個diff任務組成疫衩,任務時間片消耗完后被可被中斷硅蹦,中斷后由requestIdleCallback再次喚醒) => 2.commit(diff完畢后拿到fiber tree更新結(jié)果觸發(fā)DOM渲染,不可被中斷)闷煤。左邊灰色部分的樹即為一顆fiber樹童芹,右邊的workInProgress為中間態(tài),它是在diff過程中自頂向下構(gòu)建的樹形結(jié)構(gòu)鲤拿,可用于斷點恢復假褪,所有工作單元都更新完成之后,生成的workInProgress樹會成為新的fiber tree近顷。
- 4)fiber tree中每個節(jié)點即一個工作單元生音,跟之前的VirtualDom樹類似,表示一個虛擬DOM節(jié)點窒升。workInProgress tree的每個fiber node都保存著diff過程中產(chǎn)生的effect list缀遍,它用來存放diff結(jié)果,并且底層的樹節(jié)點會依次向上層merge effect list饱须,以收集所有diff結(jié)果域醇。注意的是如果某些節(jié)點并未更新,workInProgress tree會直接復用原fiber tree的節(jié)點(鏈表操作)蓉媳,而有數(shù)據(jù)更新的節(jié)點會被打上tag標簽譬挚。
<FiberNode> : {
stateNode, // 節(jié)點實例
child, // 子節(jié)點
sibling, // 兄弟節(jié)點
return, // 父節(jié)點
}
? React新舊生命周期
React16.3之前的生命周期
componentWillMount()
此生命周期函數(shù)會在在組件掛載之前被調(diào)用,整個生命周期中只被觸發(fā)一次酪呻。開發(fā)者通常用來進行一些數(shù)據(jù)的預請求操作减宣,以減少請求發(fā)起時間,建議的替代方案是考慮放入constructor構(gòu)造函數(shù)中玩荠,或者componentDidMount后漆腌;另一種情況是在在使用了外部狀態(tài)管理庫時丰歌,如Mobx,可以用于重置Mobx Store中的的已保存數(shù)據(jù)屉凯,替代方案是使用生命周期componentWilUnmount在組件卸載時自動執(zhí)行數(shù)據(jù)清理立帖。componentDidMount()
此生命周期函數(shù)在組件被掛載之后被調(diào)用,整個生命周期中只觸發(fā)一次悠砚。開發(fā)者同樣可以用來進行一些數(shù)據(jù)請求的操作晓勇;除此之外也可用于添加事件訂閱(需要在componentWillUnmount中取消事件訂閱);因為函數(shù)觸發(fā)時dom元素已經(jīng)渲染完畢灌旧,第三種使用情況是處理一些界面更新的副作用绑咱,比如使用默認數(shù)據(jù)來初始化一個echarts組件,然后在componentDidUpdate后進行echarts組件的數(shù)據(jù)更新枢泰。componentWillReceiveProps(nextProps, nexState)
此生命周期發(fā)生在組件掛載之后的組件更新階段描融。最常見于在一個依賴于prop屬性進行組件內(nèi)部state更新的非完全受控組件中,非完全受控組件即組件內(nèi)部維護state更新衡蚂,同時又在某個特殊條件下會采用外部傳入的props來更新內(nèi)部state窿克,注意不要直接將props完全復制到state,否則應該使用完全受控組件Function Component
毛甲,一個例子如下:
class EmailInput extends Component {
state = { email: this.props.email };
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
handleChange = e => his.setState({ email: e.target.value });
componentWillReceiveProps(nextProps) {
if (nextProps.userID !== this.props.userID) {
this.setState({ email: nextProps.email });
}
}
}
- shouldComponentUpdate(nextProps)
此生命周期發(fā)生在組件掛載之后的組件更新階段年叮。
值得注意的是子組件更新不一定是由于props或state改變引起的,也可能是父組件的其它部分更改導致父組件重渲染而使得當前子組件在props/state未改變的情況下重新渲染一次玻募。
函數(shù)被調(diào)用時會被傳入即將更新的nextProps
和nextState
對象只损,開發(fā)者可以通過對比前后兩個props對象上與界面渲染相關(guān)的屬性是否改變,再決定是否允許這次更新(returntrue
表示允許執(zhí)行更新七咧,否則忽略更新跃惫,默認為true
)。常搭配對象深比較函數(shù)用于減少界面無用渲染次數(shù)艾栋,優(yōu)化性能爆存。在一些只需要簡單淺比較props變化的場景下,并且相同的state和props會渲染出相同的內(nèi)容時裹粤,建議使用React.PureComponnet
替代终蒂,在props更新時React會自動幫你進行一次淺比較,以減少不必要渲染遥诉。
class EmailInput extends Component {
state = { email: this.props.email };
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
handleChange = e => his.setState({ email: e.target.value });
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.userID === this.props.userID &&
nextState.email == this.state.email
) return false;
}
}
- componenetWillUpdate(newProps, newState)
此生命周期發(fā)生在組件掛載之后的更新階段拇泣。當組件收到新的props或state,并且shouldComponentUpdate
返回允許更新時矮锈,會在渲染之前調(diào)此方法霉翔,不可以在此生命周期執(zhí)行setState
。在此生命周期中開發(fā)者可以在界面實際渲染更新之前拿到最新的nextProps
和nextState
苞笨,從而執(zhí)行一些副作用:比如觸發(fā)一個事件债朵、根據(jù)最新的props緩存一些計算數(shù)據(jù)到組件內(nèi)子眶、平滑界面元素動畫等:
// 需要搭配css屬性transition使用
componentWillUpdate : function(newProps,newState){
if(!newState.show)
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
else
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
},
componentDidUpdate : function(oldProps,oldState){
if(this.state.show)
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
else
$(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
}
- componenetDidUpdate(prevProps, prevState)
此生命周期發(fā)生在組件掛載之后的更新階段,組件初次掛載不會觸發(fā)序芦。當組件的props和state改變引起界面渲染更新后臭杰,此函數(shù)會被調(diào)用,不可以在此生命周期執(zhí)行setState
谚中。我們使用它用來執(zhí)行一些副作用:比如條件式觸發(fā)必要的網(wǎng)絡請求來更新本地數(shù)據(jù)渴杆、使用render后的最新數(shù)據(jù)來調(diào)用一些外部庫的執(zhí)行(例子:定時器請求接口數(shù)據(jù)動態(tài)繪制echarts折線圖):
...
componentDidMount() {
this.echartsElement = echarts.init(this.refs.echart);
this.echartsElement.setOption(this.props.defaultData);
...
}
componentDidUpdate() {
const { treeData } = this.props;
const optionData = this.echartsElement.getOption();
optionData.series[0].data = [treeData];
this.echartsElement.setOption(optionData, true);
}
- componentWillUnmount()
此生命周期發(fā)生在組件卸載之前,組件生命周期中只會觸發(fā)一次宪塔。開發(fā)者可以在此函數(shù)中執(zhí)行一些數(shù)據(jù)清理重置磁奖、取消頁面組件的事件訂閱等。
React16.3之后的生命周期
React16.3之后React的Reconciler
架構(gòu)被重寫(Reconciler用于處理生命周期鉤子函數(shù)和DOM DIFF)某筐,之前版本采用函數(shù)調(diào)用棧遞歸同步渲染機制即Stack Reconciler比搭,dom的diff階段不能被打斷,所以不利于動畫執(zhí)行和事件響應南誊。React團隊使用Fiber Reconciler架構(gòu)之后身诺,diff階段根據(jù)虛擬DOM節(jié)點拆分成包含多個工作任務單元(FiberNode)的Fiber樹(以鏈表實現(xiàn)),實現(xiàn)了Fiber任務單元之間的任意切換和任務之間的打斷及恢復等等弟疆。Fiber架構(gòu)下的異步渲染導致了componentWillMount
戚长、componentWillReceiveProps
盗冷、componentWillUpdate
三個生命周期在實際渲染之前可能會被調(diào)用多次怠苔,產(chǎn)生不可預料的調(diào)用結(jié)果,因此這三個不安全生命周期函數(shù)不建議被使用仪糖。取而代之的是使用全新的兩個生命周期函數(shù):getDerivedStateFromProps
和getSnapshotBeforeUpdate
柑司。
- getDerivedStateFromProps(nextProps, currentState)
- 1)定義
此生命周期發(fā)生在組件初始化掛載和組件更新階段,開發(fā)者可以用它來替代之前的componentWillReceiveProps
生命周期锅劝,可用于根據(jù)props變化來動態(tài)設(shè)置組件內(nèi)部state攒驰。
函數(shù)為static靜態(tài)函數(shù),因此我們無法使用this
直接訪問組件實例故爵,也無法使用this.setState
直接對state進行更改玻粪,以此可以看出React團隊想通過React框架的API式約束來盡量減少開發(fā)者的API濫用。函數(shù)調(diào)用時會被傳入即將更新的props和當前組件的state數(shù)據(jù)作為參數(shù)诬垂,我們可以通過對比處理props然后返回一個對象來觸發(fā)的組件state更新劲室,如果返回null則不更新任何內(nèi)容。 - 2)濫用場景一:直接復制props到state上面
這會導致父層級重新渲染時结窘,SimpleInput組件的state都會被重置為父組件重新傳入的props很洋,不管props是否發(fā)生了改變。如果你說使用shouldComponentUpdate
搭配著避免這種情況可以嗎隧枫?代碼層面上可以喉磁,不過可能導致后期shouldComponentUpdate
函數(shù)的數(shù)據(jù)來源混亂谓苟,任何一個prop的改變都會導致重新渲染和不正確的狀態(tài)重置,維護一個可靠的shouldComponentUpdate
會更難协怒。
class SimpleInput extends Component {
state = { attr: '' };
render() {
return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
}
static getDerivedStateFromProps(nextProps, currentState) {
// 這會覆蓋所有組件內(nèi)的state更新涝焙!
return { attr: nextProps.attr };
}
}
- 3)使用場景: 在props變化后選擇性修改state
class SimpleInput extends Component {
state = { attr: '' };
render() {
return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.attr !== currentState.attr) return { attr: nextProps.attr };
return null;
}
}
可能導致的bug:在需要重置SimpleInput組件的情況下,由于props.attr
未改變孕暇,導致組件無法正確重置狀態(tài)纱皆,表現(xiàn)就是input輸入框組件的值還是上次遺留的輸入。
- 4)優(yōu)化的使用場景一:使用完全可控的組件
完全可控的組件即沒有內(nèi)部狀態(tài)的功能組件芭商,其狀態(tài)的改變完全受父級props控制派草,這種方式需要將原本位于組件內(nèi)的state和改變state的邏輯方法抽離到父級。適用于一些簡單的場景铛楣,不過如果父級存在太多的子級狀態(tài)管理邏輯也會使邏輯冗余復雜化近迁。
function SimpleInput(props) {
return <input onChange={props.onChange} value={props.attr} />;
}
- 5)優(yōu)化的使用場景二:使用有key值的非可控組件
如果我們想讓組件擁有自己的狀態(tài)管理邏輯,但是在適當?shù)臈l件下我們又可以控制組件以新的默認值重新初始化簸州,這里有幾種方法參考:
/*
1. 設(shè)置一個唯一值傳入作為組件重新初始化的標志
通過對比屬性手動讓組件重新初始化
*/
class SimpleInput extends Component {
state = { attr: this.props.attr, id="" }; // 初始化默認值
render() {
return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.id !== currentState.id)
return { attr: nextProps.attr, id: nextProps.id };
return null;
}
}
/*
2. 設(shè)置一個唯一值作為組件的key值
key值改變后組件會以默認值重新初始化
*/
class SimpleInput extends Component {
state = { attr: this.props.attr }; // 初始化默認值
render() {
return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
}
}
<SimpleInput
attr={this.props.attr}
key={this.props.id}
/>
/*
3. 提供一個外部調(diào)用函數(shù)以供父級直接調(diào)用以重置組件狀態(tài)
父級通過refs來訪問組件實例鉴竭,拿到組件的內(nèi)部方法進行調(diào)用
*/
class SimpleInput extends Component {
state = { attr: this.props.attr }; // 初始化默認值
resetState = (value) => {
this.setState({ attr: value });
}
render() {
return <input onChange={(e) => this.setState({ attr: e.target.value })} value={this.state.attr} />;
}
}
<SimpleInput
attr={this.props.attr}
ref={this.simpleInput}
/>
componentDidMount()
...shouldComponentUpdate(nextProps, nexState)
...getSnapshotBeforeUpdate(prevProps, prevState)
此生命周期發(fā)生在組件初始化掛載和組件更新階段,界面實際render之前岸浑。開發(fā)者可以拿到組件更新前的prevProps
和prevState
搏存,同時也能獲取到dom渲染之前的狀態(tài)(比如元素寬高、滾動條長度和位置等等)矢洲。此函數(shù)的返回值會被作為componentWillUpdate
周期函數(shù)的第三個參數(shù)傳入璧眠,通過搭配componentDidUpdate
可以完全替代之前componentWillUpdate
部分的邏輯,見以下示例读虏。
class ScrollingList extends Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 判斷是否在list中添加新的items
// 捕獲滾動??位置以便我們稍后調(diào)整滾動位置责静。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 調(diào)整滾動位置使得這些新items不會將舊的items推出視圖
// snapshot是getSnapshotBeforeUpdate的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...list items... */}</div>
);
}
}
componenetDidUpdate(prevProps, prevState, shot)
此生命周期新增特性:getSnapshotBeforeUpdate
的返回值作為此函數(shù)執(zhí)行時傳入的第三個參數(shù)。componenetWillUnmount
...