React context更新过程

1,316 阅读3分钟

存在以下疑问:

1.context实现跨层级组件传递的原理

2.context如何实现跳过中间组件,只更新订阅组件的功能的?

React的更新过程

协调过程

包含beginWorkcompleteUnitOfWork

beginWork就是进入节点向下遍历的过程,深度优先

completeUnitOfWork就是回溯的过程

提交过程

实现更新的过程

以上,协调过程可以执行多次。因为有可能被打断(diff dom)。

但是提交过程只能执行一次,不能被打断。

React.createContext 原理

var symbolFor = Symbol.for;
const REACT_CONTEXT_TYPE = symbolFor("react.context");
const REACT_PROVIDER_TYPE = symbolFor("react.provider");
function createContext(defaultValue) {
  var context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    Provider: null,
    Consumer: null,
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = {
    $$typeof: REACT_CONTEXT_TYPE,
    _context: context,
  };

  return context;
}
const context = createContext({ count: 0 });
console.log("context....", context);

createContext 负责创建一个 context 对象,包含 Provider 和 Consumer 属性,其中 _currentValue 用于存储全局共享状态,订阅了 context 的组件都是从 context._currentValue 中读取最新值的。

Context.Provider原理

Provider功能:

1.通过value向下传递context的值

2.内层的Provider会覆盖外层的Provider的值,组件只会受最近一层(外层)Provider的影响

3.如果没有匹配到Provider,defaultValue才会起作用

4.Provider的value值改变后,会强制更新所有订阅组件,不受shouldComponentUpdate的影响

实现功能1原理:

改变context对象实例的_currentValue的值。(改变全局变量的值,这样useContext取出的值就是最新的值)

实现功能2原理:

既然contex中的_currentValue是全局变量,Provider如何做到只影响到其内部组件,而不影响外部组件?

利用栈。

当我们遇到Provider标签,会把此时value当作_currentValue push到栈中,后面如果需要取出context,就会使用这个_currentValue,直到遇到Provider的闭合标签,就会将这个值pop。

实现功能3原理:

如果没有Provider设置_currentValue,取到的就是context的默认值。

实现功能4原理:

这里涉及到react的更新过程,在下面详细说,概括来说,就是Provider中的value变化时,会遍历其子组件,找到订阅了context的组件,打上更新标签,在提交阶段强制更新它。

消费 Context 原理

如果说Provider是改变_currentValue的值,那消费context就是读取_currentValue的值。

详细的源码分析可参考这篇文章

关于context会引起强制更新的实验:

// index.js
import MyContext from './context'
import { useState } from 'react'
import Child from './child'

const A = () => {
  const [val, setVal] = useState(1)
  return (
    <>
      <span onClick={() => setVal(val + 1)}>点击加一</span>
      <MyContext.Provider value={val}>
        <Child />
      </MyContext.Provider>
    </>
  )
}

export default A
// child.js
import Child1 from './child1'
import Child2 from './child2'
import React from 'react'

class B extends React.Component {
  render() {
    console.log('中间组件')
    return (
      <>
        <Child2 />
        <Child1 />
      </>
    )
  }
}

export default React.memo(B)
// child1.js
import MyContext from './context'
import React, { useContext } from 'react'

const C = () => {
  const MyContextVal = useContext(MyContext)
  console.log('订阅组件')
  return <div>{MyContextVal}</div>
}

export default React.memo(C)

// child2.js
import React from 'react'

const D = () => {
  console.log('非订阅组件')
  return <div></div>
}

export default React.memo(D)


// 结果

// 点击之后,只会打印
订阅组件

说明:

  1. Provider中的value变化必须是React更新引起的(遵循setState数据流),才能引起强制更新。(Provider中存放一个window.a,改变window.a不会引起更新)

  2. 为什么子孙组件都用React.memo包一层? 如果不包,setState的时候,其内部组件都会更新。

  3. 为什么只更新了订阅组件?这里存在一个误区,认为不走父组件的render,怎么走子组件(订阅组件)的render?

代码中的render函数,其实是协调过程之后的提交过程(执行更新的过程),当其中一个组件更新时,组件到父组件的root组件都会执行beginWork过程,也就是diff dom的过程,查询组件是否订阅同名context,如果没有订阅且props、state都没有变化,就不会更新,沿用旧fiber节点。

如果订阅了context,会被打上强制更新的标签,然后继续diff其子节点。