為什麼你應該放棄React老的Context API用新的Context API
我們先來看一下兩個版本的Context API如何使用
// old version class Parent extends Component{ getChildContext() { return {type: 123} } } Parent.childContextType = { type: PropTypes.number } const Child = (props, context) => ( <p>{context.type}</p> ) Child.contextTypes = { type: PropTypes.number } 複製程式碼
通過在父元件上宣告 getChildContext
方法為其子孫元件提供 context
,我們稱其 ProviderComponent
。注意必須要宣告 Parent.childContextType
才會生效,而子元件如果需要使用 context
,需要顯示得宣告 Child.contextTypes
// new version const { Provider, Consumer } = React.createContext('defaultValue') const Parent = (props) => ( <Provider value={'realValue'}> {props.children} </Provider> ) const Child = () => { <Consumer> { (value) => <p>{value}</p> } </Consumer> } 複製程式碼
新版本的API,React提供了 createContext
方法,這個方法會返回兩個 元件 : Provider
和 Consumber
, Provider
用來提供 context
的內容,通過向 Provider
傳遞 value
這個 prop
,而在需要用到對應 context
的地方,用 相同來源的 Consumer
來獲取 context
, Consumer
有特定的用法,就是他的 children
必須是一個方法,並且 context
的值使用引數傳遞給這個方法。
效能對比
正好前幾天React devtool釋出了 Profiler
功能,就用這個新功能來檢視一下兩個API的新能有什麼差距吧,先看一下例子
ofollow,noindex">不知道Profiler的看這裡
// old api demo import React from 'react' import PropTypes from 'prop-types' export default class App extends React.Component { state = { type: 1, } getChildContext() { return { type: this.state.type } } componentDidMount() { setInterval(() => { this.setState({ type: this.state.type + 1 }) }, 500) } render() { return this.props.children } } App.childContextTypes = { type: PropTypes.number } export const Comp = (props, context) => { const arr = [] for (let i=0; i<100; i++) { arr.push(<p key={i}>{i}</p>) } return ( <div> <p>{context.type}</p> {arr} </div> ) } Comp.contextTypes = { type: PropTypes.number } 複製程式碼
// new api demo import React, { Component, createContext } from 'react' const { Provider, Consumer } = createContext(1) export default class App extends Component { state = { type: 1 } componentDidMount() { setInterval(() => { this.setState({ type: this.state.type + 1 }) }, 500) } render () { return ( <Provider value={this.state.type}> {this.props.children} </Provider> ) } } export const Comp = () => { const arr = [] for (let i=0; i<100; i++) { arr.push(<p key={i}>{i}</p>) } return ( <div> <Consumer> {(type) => <p>{type}</p>} </Consumer> {arr} </div> ) } 複製程式碼
// index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App, {Comp} from './context/OldApi' // import App, { Comp } from './context/NewApi' ReactDOM.render( <App><Comp /></App>, document.getElementById('root') ) 複製程式碼
程式碼基本相同,主要變動就是一個 interval
,每500毫秒給 type
加1,然後我們來分別看一下 Profiler
的截圖
老API

新API

可見這兩個效能差距是非常大的,老的API需要7點幾毫秒,而新的API只需要0.4毫秒,而且新的API只有兩個節點重新渲染了,而老的API所有節點都重新渲染了(下面還有很多節點沒截圖進去,雖然每個可能只有0.1毫秒或者甚至不到,但是積少成多,導致他們的父元件Comp渲染時間很長)
進一步舉例
在這裡可能有些同學會想,新老API的用法不一樣,因為老API的 context
是作為 Comp
這個 functional Component
的引數傳入的,所以肯定會影響該元件的所有子元素,所以我在這個基礎上修改了例子,把陣列從 Comp
元件中移除,放到一個新的元件 Comp2
中
// Comp2 export class Comp2 extends React.Component { render() { const arr = [] for (let i=0; i<100; i++) { arr.push(<p key={i}>{i}</p>) } return arr } } // new old api Comp export const Comp = (props, context) => { return ( <div> <p>{context.type}</p> </div> ) } // new new api Comp export const Comp = () => { return ( <div> <Consumer> {(type) => <p>{type}</p>} </Consumer> </div> ) } 複製程式碼
現在受 context
影響的渲染內容新老API都是一樣的,只有 <p>{type}</p>
,我們再來看一下情況
老API

新API

忽視比demo1時間長的問題,應該是我電腦執行時間長效能下降的問題,只需要橫向對比新老API就可以了
從這裡可以看出來,結果跟Demo1沒什麼區別,老API中我們的 arr
仍然都被重新渲染了,導致整體的渲染時間被拉長很多。
事實上,這可能還不是最讓你震驚的地方,我們再改一下例子,我們在 App
中不再修改 type
,而是新增一個 state
叫 num
,然後對其進行遞增
// App export default class App extends React.Component { state = { type: 1, num: 1 } getChildContext() { return { type: this.state.type } } componentDidMount() { setInterval(() => { this.setState({ num: this.state.num + 1 }) }, 500) } render() { return ( <div> <p>inside update {this.state.num}</p> {this.props.children} </div> ) } } 複製程式碼
老API

新API

可以看到老API依然沒有什麼改觀,他依然重新渲染所有子節點。
再進一步我給 Comp2
增加 componentDidUpdate
生命週期鉤子
componentDidUpdate() { console.log('update') } 複製程式碼
在使用老API的時候,每次App更新都會列印

而新API則不會
總結
從上面測試的結果大家應該可以看出來結果了,這裡簡單的講一下原因,因為要具體分析會很長並且要涉及到原始碼的很多細節,所以有空再寫一片續,來詳細得講解原始碼,大家有興趣的可以關注我。
要分析原理要了解React對於每次更新的處理流程,React是一個樹結構,要進行更新只能通過某個節點執行 setState、forceUpdate
等方法,在某一個節點執行了這些方法之後,React會向上搜尋直到找到 root
節點,然後把 root
節點放到更新佇列中,等待更新。
所以React的更新都是從 root
往下執行的,他會嘗試重新構建一個新的樹,在這個過程中能複用之前的節點就會複用, 而我們現在看到的情況,就是因為複用演算法根據不同的情況而得到的不同的結果
我們來看一小段原始碼
if ( !hasLegacyContextChanged() && (updateExpirationTime === NoWork || updateExpirationTime > renderExpirationTime) ) { // ... return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } 複製程式碼
如果能滿足這個判斷條件並且進入 bailoutOnAlreadyFinishedWork
,那麼有極高的可能這個節點以及他的子樹都不需要更新,React會直接跳過,我們使用新的 context API
的時候就是這種情況, 但是使用老的 context API
是永遠不可能跳過這個判斷的
老的 context API
使用過程中,一旦有一個節點提供了 context
,那麼他的所有子節點都會被視為有 side effect
的,因為React本身並不判斷子節點是否有使用 context
,以及提供的 context
是否有變化,所以一旦檢測到有節點提供了 context
,那麼他的子節點在執行 hasLegacyContextChanged
的時候,永遠都是true的,而沒有進入 bailoutOnAlreadyFinishedWork
,就會變成重新 reconcile
子節點,雖然最終可能不需要更新DOM節點,但是重新計算生成 Fiber
物件的開銷還是又得,一兩個還好,數量多了時間也是會被拉長的。
以上就是使用老的 context API
比新的API要慢很多的原因,大家可以先不深究得理解一下,在我之後的原始碼分析環節會有更詳細的講解。