为何你应该放弃React老的Context API用新的Context API

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.contextTypesreact

// 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方法,这个方法会返回两个组件ProviderConsumberProvider用来提供context的内容,经过向Provider传递value这个prop,而在须要用到对应context的地方,用相同来源的Consumer来获取contextConsumer有特定的用法,就是他的children必须是一个方法,而且context的值使用参数传递给这个方法。算法

性能对比

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

不知道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的截图前端工程师

不知道Profiler的看这里框架

老API

老API

新API

新API

可见这两个性能差距是很是大的,老的API须要7点几毫秒,而新的API只须要0.4毫秒,并且新的API只有两个节点从新渲染了,而老的API全部节点都从新渲染了(下面还有不少节点没截图进去,虽然每一个可能只有0.1毫秒或者甚至不到,可是聚沙成塔,致使他们的父组件Comp渲染时间很长)dom

进一步举例

在这里可能有些同窗会想,新老API的用法不同,由于老API的context是做为Comp这个functional Component的参数传入的,因此确定会影响该组件的全部子元素,因此我在这个基础上修改了例子,把数组从Comp组件中移除,放到一个新的组件Comp2ide

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

新API

新API

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

从这里能够看出来,结果跟Demo1没什么区别,老API中咱们的arr仍然都被从新渲染了,致使总体的渲染时间被拉长不少。

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

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

新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学习。更多的文章看这里

相关文章
相关标签/搜索