React Context源码浅析 #111
zhangyu1818
announced in
zh-cn
Replies: 1 comment
-
|
太酷啦 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
简述
在
React中,有一个valueStack,是一个栈结构,其中会存入Context信息,在beginWork阶段,当Fiber节点为ContextProvider时,会将当前的Context的旧值压入栈,并赋予新值,当此Fiber节点执行到completeWork阶段时,会将旧值弹出,以保证Fiber节点之间的层级关系。Context的值就存在Context对象本身的_currentValue字段,当Fiber节点读取Context值时,会直接从Context上获取值,同时会创建Fiber节点的dependencies并将Context信息存入,在Context值改变时,会从当前ContextProvider向下遍历,找到所有depenencies里与Context相同的Fiber节点,标识它们需要更新。Context值本身改变是不会触发更新的,依旧需要使用setState这类方法。以下源码浅析的React版本为17.0.1,需要先了解Fiber树的构建流程。
valueStack
valueStack定义在ReactFiberStack.js文件中,valueStack存储了几种数据,并不是只存储Context的值。其中有一种数据的类型为
StackCursor,该类型也定义在ReactFiberNewContext.js文件中,用来存储Context的新值,它的作用就是传递valueStack里的值。后文有关
Context处理的方法都定义在这个文件里。从
Context的创建开始看源码。createContext
createContext方法实际是创建了一个对象,该对象会作为ReactElement的type,同时使用了$$typeof字段区分REACT_PROVIDER_TYPE类型和REACT_CONTEXT_TYPE类型。当我们将
Provider以JSX模式使用时,会创建对应的Fiber节点,也会进入beginWork和completeWork阶段。通过
createContext方法可以知道Provider和Consumer为一个对象,首先会进入Fiber节点的创建。Fiber节点的创建
createFiberFromTypeAndProps方法会创建并返回Fiber节点,在这个方法里会判断Fiber节点的类型,Provider和Consumer都是对象,进入default判断后会以$$typeof来判断类型。beginWork阶段
beginWork阶段会以Fiber节点的tag判断进入哪一个方法,在Fiber节点创建的时候已经为Provider和Consumer设置了对应的tag。updateContextProvider
ContextProvider类型会进入updateContextProvider方法。pushProvider
pushProvider方法是Context的值变化的核心,它会将旧的值压入valueStack,同时为Context赋新值。propagateContextChange
propagateContextChange方法在Context更新时使用,从当前Fiber节点开始遍历节点树,为使用了当前context的子节点设置优先级。设置优先级的目的是为了子节点在进入
beginWork阶段的时候不会进入bailout的复用流程。updateContextConsumer
Consumer是使用Context值的一种最基础的方式,ContextConsumer类型会进入updateContextConsumer方法。在这里与
context相关的是prepareToReadContext方法和readContext方法。prepareToReadContext
readContext
readContext方法不止在ContextConsumer会用到,使用了contextType的类组件和使用了useContext的函数组件都会使用(后文),该方法不仅会返回context的值,同时也记录了该Fiber节点使用了Context,后续Context改变会触发此节点的更新。completeWork阶段
completedWork阶段会将调用popProvider将当前valueStack栈中的旧值弹出并赋值给ContextProvider。popProvider
为什么会赋值为旧值呢?如以下情况。
当
beginWork执行到value = 0到ContextPrivder时,将默认值-1压入栈,同时赋予新值0,接下来执行value = 1的ContextProvider,将旧值0压入栈,同时赋予新值1,这时候A组件读取的值为1。接下来执行
completeWork阶段,当到value = 1的ContextProvider时,将旧值0从栈弹出,同时赋予旧值。接下来在
B组件的beginWork阶段,读取的ContextProvider的值才会为正确的0,最后依次执行completeWork阶段,将ContextProvider值还原为默认值-1。为了保证这样的层级关系,所以需要保留旧值来还原。
子级如何判断有更新?
在Fiber树构建流程中,如果当前更新的
renderLanes不包含WorkInProgress的lane,就会进入bailoutOnAlreadyFinishedWork方法,就不会走更新流程了。所以在
propagateContextChange方法里,会对使用了Context对子级节点设置lane,确保不会进入bailoutOnAlreadyFinishedWork方法。类组件和函数组件使用Context
类组件使用方式是利用
contextType,函数组件则是简单的useContext。类组件
类组件流程和
ContextConsumer是一样的,同样先调用prepareToReadContext重置全局变量,在通过调用readContext获取context并添加依赖关系。函数组件
函数组件同样是需要先调用
prepareToReadContext重置全局变量,再调用useContext来获取值。而
useContext本质上就是readContext,和其他Hooks非常不一样。总结
文章逻辑写的有点狗屁不通,难受啊。
当Fiber节点为
ContextProvider时,会将旧值压入栈,并为Context赋予新值,当有更新时,会遍历子级节点,找到有依赖关系的Fiber节点,标识它们需要更新。当Fiber节点需要使用
Context时,会先调用prepareToReadContext方法来设置全局变量,读取Context需要调用readContext方法,该方法同时会记录此节点与Context的依赖关系。类组件和函数组件调用
Context的逻辑实际上和ContextConsumer是一样的。在这里还发现一个有意思的东西,
createContext的第二个参数calculateChangedBits,在文档上是没有使用的,看逻辑应该是和是否需要更新节点有关,原来并不是Context一改变,所有使用了的节点都需要更新啊!Beta Was this translation helpful? Give feedback.
All reactions