React16.3釋出了新的Context API,並且已經確認了將在下一個版本廢棄老的Context API。所以大家更新到新的Context API是無可厚非的事情。而這篇文章會從原理的角度為大家分析為什麼要用新的API--不僅僅是因為React官方要更新,畢竟更新了你也可以用16版本的React來使用老的API--而是因為新的API效能比老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的新能有什麼差距吧,先看一下例子
// 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要慢很多的原因,大家可以先不深究得理解一下,在我之後的原始碼分析環節會有更詳細的講解。
我是Jocky,一個專注於React技巧和深度分析的前端工程師,React絕對是一個越深入學習,越能讓你覺得他的設計精巧,思想超前的框架。關注我獲取最新的React動態,以及最深度的React學習。更多的文章看這裡