React16.3發佈了新的Context API,而且已經確認了將在下一個版本廢棄老的Context API。因此你們更新到新的Context API是無可厚非的事情。而這篇文章會從原理的角度爲你們分析爲何要用新的API--不只僅是由於React官方要更新,畢竟更新了你也能夠用16版本的React來使用老的API--而是由於新的API性能比老API 高出太多css
咱們先來看一下兩個版本的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
react
// 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的新能有什麼差距吧,先看一下例子api
// 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
的截圖markdown
不知道Profiler的看這裏前端工程師
可見這兩個性能差距是很是大的,老的API須要7點幾毫秒,而新的API只須要0.4毫秒,並且新的API只有兩個節點從新渲染了,而老的API全部節點都從新渲染了(下面還有不少節點沒截圖進去,雖然每一個可能只有0.1毫秒或者甚至不到,可是聚沙成塔,致使他們的父組件Comp渲染時間很長)框架
在這裏可能有些同窗會想,新老API的用法不同,由於老API的context
是做爲Comp
這個functional Component
的參數傳入的,因此確定會影響該組件的全部子元素,因此我在這個基礎上修改了例子,把數組從Comp
組件中移除,放到一個新的組件Comp2
中dom
// 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>
,咱們再來看一下狀況
忽視比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依然沒有什麼改觀,他依然從新渲染全部子節點。
再進一步我給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要慢不少的緣由,你們能夠先不深究得理解一下,在我以後的源碼分析環節會有更詳細的講解。
我是Jocky,一個專一於React技巧和深度分析的前端工程師,React絕對是一個越深刻學習,越能讓你以爲他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裏