存在以下疑问:
1.context实现跨层级组件传递的原理
2.context如何实现跳过中间组件,只更新订阅组件的功能的?
React的更新过程
协调过程包含
beginWork和completeUnitOfWork
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)
// 结果
// 点击之后,只会打印
订阅组件
说明:
-
Provider中的value变化必须是React更新引起的(遵循setState数据流),才能引起强制更新。(Provider中存放一个window.a,改变window.a不会引起更新)
-
为什么子孙组件都用React.memo包一层?如果不包,setState的时候,其内部组件都会更新。 -
为什么只更新了订阅组件?这里存在一个误区,认为不走父组件的render,怎么走子组件(订阅组件)的render?
代码中的render函数,其实是协调过程之后的提交过程(执行更新的过程),当其中一个组件更新时,组件到父组件的root组件都会执行beginWork过程,也就是diff dom的过程,查询组件是否订阅同名context,如果没有订阅且props、state都没有变化,就不会更新,沿用旧fiber节点。
如果订阅了context,会被打上强制更新的标签,然后继续diff其子节点。