React Context源码浅析 #111
zhangyu1818
announced in
zh-cn
Replies: 1 comment
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
简述
在
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