博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
为什么你应该放弃React老的Context API用新的Context API
阅读量:4084 次
发布时间:2019-05-25

本文共 5343 字,大约阅读时间需要 17 分钟。

React16.3发布了新的Context API,并且已经确认了将在下一个版本废弃老的Context API。所以大家更新到新的Context API是无可厚非的事情。而这篇文章会从原理的角度为大家分析为什么要用新的API--不仅仅是因为React官方要更新,毕竟更新了你也可以用16版本的React来使用老的API--而是因为新的API性能比老API 高出太多

用法

我们先来看一下两个版本的Context API如何使用

// old versionclass Parent extends Component{  getChildContext() {    return {type: 123}  }}Parent.childContextType = {  type: PropTypes.number}const Child = (props, context) => (  

{context.type}

)Child.contextTypes = { type: PropTypes.number}复制代码

通过在父组件上声明getChildContext方法为其子孙组件提供context,我们称其ProviderComponent。注意必须要声明Parent.childContextType才会生效,而子组件如果需要使用context,需要显示得声明Child.contextTypes

// new versionconst { Provider, Consumer } = React.createContext('defaultValue')const Parent = (props) => (  
{props.children}
)const Child = () => {
{ (value) =>

{value}

}
}复制代码

新版本的API,React提供了createContext方法,这个方法会返回两个组件ProviderConsumberProvider用来提供context的内容,通过向Provider传递value这个prop,而在需要用到对应context的地方,用相同来源的Consumer来获取contextConsumer有特定的用法,就是他的children必须是一个方法,并且context的值使用参数传递给这个方法。

性能对比

正好前几天React devtool发布了Profiler功能,就用这个新功能来查看一下两个API的新能有什么差距吧,先看一下例子

// old api demoimport 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(

{i}

) } return (

{context.type}

{arr}
)}Comp.contextTypes = { type: PropTypes.number}复制代码
// new api demoimport 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 (      
{this.props.children}
) }}export const Comp = () => { const arr = [] for (let i=0; i<100; i++) { arr.push(

{i}

) } return (
{(type) =>

{type}

}
{arr}
)}复制代码
// index.jsimport 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(  
, document.getElementById('root'))复制代码

代码基本相同,主要变动就是一个interval,每500毫秒给type加1,然后我们来分别看一下Profiler的截图

老API

 

老API

 

 

新API

 

新API

 

 

可见这两个性能差距是非常大的,老的API需要7点几毫秒,而新的API只需要0.4毫秒,而且新的API只有两个节点重新渲染了,而老的API所有节点都重新渲染了(下面还有很多节点没截图进去,虽然每个可能只有0.1毫秒或者甚至不到,但是积少成多,导致他们的父组件Comp渲染时间很长)

进一步举例

在这里可能有些同学会想,新老API的用法不一样,因为老API的context是作为Comp这个functional Component的参数传入的,所以肯定会影响该组件的所有子元素,所以我在这个基础上修改了例子,把数组从Comp组件中移除,放到一个新的组件Comp2

// Comp2export class Comp2 extends React.Component {  render() {    const arr = []    for (let i=0; i<100; i++) {      arr.push(

{i}

) } return arr }}// new old api Compexport const Comp = (props, context) => { return (

{context.type}

)}// new new api Compexport const Comp = () => { return (
{(type) =>

{type}

}
)}复制代码

现在受context影响的渲染内容新老API都是一样的,只有<p>{type}</p>,我们再来看一下情况

老API

 

老API

 

 

新API

 

新API

 

 

忽视比demo1时间长的问题,应该是我电脑运行时间长性能下降的问题,只需要横向对比新老API就可以了

从这里可以看出来,结果跟Demo1没什么区别,老API中我们的arr仍然都被重新渲染了,导致整体的渲染时间被拉长很多。

事实上,这可能还不是最让你震惊的地方,我们再改一下例子,我们在App中不再修改type,而是新增一个statenum,然后对其进行递增

// Appexport 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 (      

inside update {this.state.num}

{this.props.children}
) }}复制代码

老API

 

老API

 

 

新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要慢很多的原因,大家可以先不深究得理解一下,在我之后的源码分析环节会有更详细的讲解。

作者:Jokcy
链接:https://juejin.im/post/5baa1f09f265da0a867c3b78
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的文章
微信小程序 Audio API
查看>>
[React Native]react-native-scrollable-tab-view(进阶篇)
查看>>
Vue全家桶+Mint-Ui打造高仿QQMusic,搭配详细说明
查看>>
React Native for Android 发布独立的安装包
查看>>
React Native应用部署/热更新-CodePush最新集成总结(新)
查看>>
react-native-wechat
查看>>
基于云信的react-native聊天系统
查看>>
网易云音乐移动客户端Vue.js
查看>>
ES7 await/async
查看>>
ES7的Async/Await
查看>>
React Native WebView组件实现的BarCode(条形码)、(QRCode)二维码
查看>>
每个人都能做的网易云音乐[vue全家桶]
查看>>
JavaScript专题之数组去重
查看>>
Immutable.js 以及在 react+redux 项目中的实践
查看>>
Vue2.0全家桶仿腾讯课堂(移动端)
查看>>
React+Redux系列教程
查看>>
react-native 自定义倒计时按钮
查看>>
19 个 JavaScript 常用的简写技术
查看>>
ES6这些就够了
查看>>
微信小程序:支付系列专辑(开发指南+精品Demo)
查看>>