资讯专栏INFORMATION COLUMN

为什么你应该放弃老的React Context API改用新的Context API

william / 1360人阅读

摘要:发布了新的,并且已经确认了将在下一个版本废弃老的。所以大家更新到新的是无可厚非的事情。

</>复制代码

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

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

</>复制代码

  1. // old version
  2. class Parent extends Component{
  3. getChildContext() {
  4. return {type: 123}
  5. }
  6. }
  7. Parent.childContextType = {
  8. type: PropTypes.number
  9. }
  10. const Child = (props, context) => (
  11. {context.type}

  12. )
  13. Child.contextTypes = {
  14. type: PropTypes.number
  15. }

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

</>复制代码

  1. // new version
  2. const { Provider, Consumer } = React.createContext("defaultValue")
  3. const Parent = (props) => (
  4. {props.children}
  5. )
  6. const Child = () => {
  7. {
  8. (value) =>

    {value}

  9. }
  10. }

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

性能对比

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

不知道Profiler的看这里

</>复制代码

  1. // old api demo
  2. import React from "react"
  3. import PropTypes from "prop-types"
  4. export default class App extends React.Component {
  5. state = {
  6. type: 1,
  7. }
  8. getChildContext() {
  9. return {
  10. type: this.state.type
  11. }
  12. }
  13. componentDidMount() {
  14. setInterval(() => {
  15. this.setState({
  16. type: this.state.type + 1
  17. })
  18. }, 500)
  19. }
  20. render() {
  21. return this.props.children
  22. }
  23. }
  24. App.childContextTypes = {
  25. type: PropTypes.number
  26. }
  27. export const Comp = (props, context) => {
  28. const arr = []
  29. for (let i=0; i<100; i++) {
  30. arr.push(

    {i}

    )
  31. }
  32. return (
  33. {context.type}

  34. {arr}
  35. )
  36. }
  37. Comp.contextTypes = {
  38. type: PropTypes.number
  39. }

</>复制代码

  1. // new api demo
  2. import React, { Component, createContext } from "react"
  3. const { Provider, Consumer } = createContext(1)
  4. export default class App extends Component {
  5. state = {
  6. type: 1
  7. }
  8. componentDidMount() {
  9. setInterval(() => {
  10. this.setState({
  11. type: this.state.type + 1
  12. })
  13. }, 500)
  14. }
  15. render () {
  16. return (
  17. {this.props.children}
  18. )
  19. }
  20. }
  21. export const Comp = () => {
  22. const arr = []
  23. for (let i=0; i<100; i++) {
  24. arr.push(

    {i}

    )
  25. }
  26. return (
  27. {(type) =>

    {type}

    }
  28. {arr}
  29. )
  30. }

</>复制代码

  1. // index.js
  2. import React from "react";
  3. import ReactDOM from "react-dom";
  4. import "./index.css";
  5. import App, {Comp} from "./context/OldApi"
  6. // import App, { Comp } from "./context/NewApi"
  7. ReactDOM.render(
  8. ,
  9. document.getElementById("root")
  10. )

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

不知道Profiler的看这里

老API

新API

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

进一步举例

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

</>复制代码

  1. // Comp2
  2. export class Comp2 extends React.Component {
  3. render() {
  4. const arr = []
  5. for (let i=0; i<100; i++) {
  6. arr.push(

    {i}

    )
  7. }
  8. return arr
  9. }
  10. }
  11. // new old api Comp
  12. export const Comp = (props, context) => {
  13. return (
  14. {context.type}

  15. )
  16. }
  17. // new new api Comp
  18. export const Comp = () => {
  19. return (
  20. {(type) =>

    {type}

    }
  21. )
  22. }

现在受context影响的渲染内容新老API都是一样的,只有

{type}

,我们再来看一下情况

老API

新API

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

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

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

</>复制代码

  1. // App
  2. export default class App extends React.Component {
  3. state = {
  4. type: 1,
  5. num: 1
  6. }
  7. getChildContext() {
  8. return {
  9. type: this.state.type
  10. }
  11. }
  12. componentDidMount() {
  13. setInterval(() => {
  14. this.setState({
  15. num: this.state.num + 1
  16. })
  17. }, 500)
  18. }
  19. render() {
  20. return (
  21. inside update {this.state.num}

  22. {this.props.children}
  23. )
  24. }
  25. }
老API

新API

可以看到老API依然没有什么改观,他依然重新渲染所有子节点。

再进一步我给Comp2增加componentDidUpdate生命周期钩子

</>复制代码

  1. componentDidUpdate() {
  2. console.log("update")
  3. }

在使用老API的时候,每次App更新都会打印

而新API则不会

总结

从上面测试的结果大家应该可以看出来结果了,这里简单的讲一下原因,因为要具体分析会很长并且要涉及到源码的很多细节,所以有空再写一片续,来详细得讲解源码,大家有兴趣的可以关注我。

要分析原理要了解React对于每次更新的处理流程,React是一个树结构,要进行更新只能通过某个节点执行setState、forceUpdate等方法,在某一个节点执行了这些方法之后,React会向上搜索直到找到root节点,然后把root节点放到更新队列中,等待更新。

所以React的更新都是从root往下执行的,他会尝试重新构建一个新的树,在这个过程中能复用之前的节点就会复用,而我们现在看到的情况,就是因为复用算法根据不同的情况而得到的不同的结果

我们来看一小段源码

</>复制代码

  1. if (
  2. !hasLegacyContextChanged() &&
  3. (updateExpirationTime === NoWork ||
  4. updateExpirationTime > renderExpirationTime)
  5. ) {
  6. // ...
  7. return bailoutOnAlreadyFinishedWork(
  8. current,
  9. workInProgress,
  10. renderExpirationTime,
  11. );
  12. }

如果能满足这个判断条件并且进入bailoutOnAlreadyFinishedWork,那么有极高的可能这个节点以及他的子树都不需要更新,React会直接跳过,我们使用新的context API的时候就是这种情况,但是使用老的context API是永远不可能跳过这个判断的

老的context API使用过程中,一旦有一个节点提供了context,那么他的所有子节点都会被视为有side effect的,因为React本身并不判断子节点是否有使用context,以及提供的context是否有变化,所以一旦检测到有节点提供了context,那么他的子节点在执行hasLegacyContextChanged的时候,永远都是true的,而没有进入bailoutOnAlreadyFinishedWork,就会变成重新reconcile子节点,虽然最终可能不需要更新DOM节点,但是重新计算生成Fiber对象的开销还是又得,一两个还好,数量多了时间也是会被拉长的。

以上就是使用老的context API比新的API要慢很多的原因,大家可以先不深究得理解一下,在我之后的源码分析环节会有更详细的讲解。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97980.html

相关文章

  • react new context API的一次实践(补充)

    摘要:作为数据的发布方,它拥有一个名为的属性,用于维护数据内容,通过传递给的数据会被发布出去。最后,组件将自己的原封不动的传递给。但是通过这次的实践,也算是熟悉的的用法,对也加深了了解吧。 这是一篇我发在掘金上的文章,原文有一个我没有解决的问题,在网友的解答下我找到了答案,我把文章重新修改编辑后,同步发送到这里,希望能对大家有所帮助。本文原发布于掘金:https://juejin.im/po...

    kaka 评论0 收藏0
  • [译]开发类 redux 库来理解状态管理

    摘要:第一个功能是普通经典类组件,也就是众所周知的有状态组件。我们准备创建一个上下文环境来存放全局状态,然后把它的包裹在一个有状态组件中,然后用来管理状态。接下来我们需要用有状态组件包裹我们的,利用它进行应用状态的管理。 原文地址对于想要跳过文章直接看结果的人,我已经把我写的内容制作成了一个库:use-simple-state,无任何依赖(除了依赖 react ),只有3kb,相当轻量。 ...

    KoreyLee 评论0 收藏0
  • “别更新了,学不动了” 之:全栈开发者 2019 应该学些什么

    摘要:但是,有一件事是肯定的年对全栈开发者的需求量很大。有一些方法可以解决这个问题,例如模式,或者你可以这么想,其实谷歌机器人在抓取单页应用程序时没有那么糟糕。谷歌正在这方面努力推进,但不要指望在年会看到任何突破。 对于什么是全栈开发者并没有一个明确的定义。但是,有一件事是肯定的:2019 年对全栈开发者的需求量很大。在本文中,我将向你概述一些趋势,你可以尝试根据这些趋势来确定你可能要投入的...

    NervosNetwork 评论0 收藏0
  • “别更新了,学不动了” 之:全栈开发者 2019 应该学些什么

    摘要:但是,有一件事是肯定的年对全栈开发者的需求量很大。有一些方法可以解决这个问题,例如模式,或者你可以这么想,其实谷歌机器人在抓取单页应用程序时没有那么糟糕。谷歌正在这方面努力推进,但不要指望在年会看到任何突破。 对于什么是全栈开发者并没有一个明确的定义。但是,有一件事是肯定的:2019 年对全栈开发者的需求量很大。在本文中,我将向你概述一些趋势,你可以尝试根据这些趋势来确定你可能要投入的...

    sutaking 评论0 收藏0
  • “别更新了,学不动了” 之:全栈开发者 2019 应该学些什么

    摘要:但是,有一件事是肯定的年对全栈开发者的需求量很大。有一些方法可以解决这个问题,例如模式,或者你可以这么想,其实谷歌机器人在抓取单页应用程序时没有那么糟糕。谷歌正在这方面努力推进,但不要指望在年会看到任何突破。 对于什么是全栈开发者并没有一个明确的定义。但是,有一件事是肯定的:2019 年对全栈开发者的需求量很大。在本文中,我将向你概述一些趋势,你可以尝试根据这些趋势来确定你可能要投入的...

    ormsf 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<