context是React提供的一个特性,可以实现任意层级组件之间的数据传递。也许你用过context,但你不知道它是如何实现的。本文将从源码层面讲解cotnext的原理,我们可以从中找到一些hacktips。首先我们回顾一下context的使用方法:context的使用有三个组成部分,一,二,三:我们不想通过props将数据从一传给三,那么我们可以使用context:调用createContext创建上下文对象,初始化数据为'dong'。三组件中,可以通过useContext取出上下文数据:除了初始化时传入,后期还可以通过Provider修改上下文数据:我们通过Provider传入一个新值,覆盖初始值,这样useContext就把接收到的值改了:函数组件是useContexthook取的,类组件是Consumer取的:我们还是通过createContext创建context对象,通过Provider修改值。现在Three变成了类组件,所以使用context的方式是通过Consumer,它的children部分传递给render函数,在参数中可以获取到context数据:这就是类组件使用context的方式,分别是功能组件。总结一下:context是使用React.createContext创建的,可以传入初始值,后期通过Provider修改值。在使用context值的时候,如果是函数组件,可以通过useContext的钩子获取,而类组件是使用Consumer传入一个render函数来获取。了解了context的使用方法,再来看看它的实现原理:context的实现先来看看createContext的源码:创建一个context对象,带有_currentValue属性,一看就保存了值,还有作为Consumer和Provider的两个属性。Consumer和Provider都通过_context保存了context对象的引用。他们都有$$typeof来标识类型。这是上下文对象的结构:这个上下文对象是如何集成到React渲染过程中的?通过jsx结合:jsx编译后,会生成一个render函数。比如上面的jsx编译之后是这样的:新版本的React并没有调用React.createElement,而是调用了一个jsx函数,也就是上面的jsxDev。看第一个参数是什么,有$$typeof和_context属性,不就是我们传入的context.Provider:props参数中传入value:jsx执行会一个一个生成vdoms:所以context被保存到vdom节点。递归渲染的时候不能从vdom获取context吗?不用担心,React不会直接渲染vdom,而是先将vdom转换成fiber。这个过程叫做reconcile:兄弟节点在fiber结构中保存了对父节点的引用,这在vdom中是没有的,vdom只有子节点的引用,所以vdom变成fiber之后,就变成了可中断的,因为即使它坏了,它仍然可以找到兄弟节点和父节点继续处理。存储在vdom中的context不是自然而然的转移到fiber节点了吗?创建一个fiber的代码是这样的:调用createFiber最终会创建一个新的FiberNode,然后创建一个object:这个和vdom没什么区别,只是属性不一样而已。看这里的fiber.type,不就是存储在vdom上的context.Provider对象吗?那么fiber节点是怎么处理的呢?你会发现在处理fiber节点的时候,会判断fiber.tag,对不同的Type进行不同的处理:FunctionComponent和ClassComponent的fiber节点是不同处理的。下面可以找到ContextProvider:它的实现是修改context中的值:pushProvider是最后修改context值的地方:通过_context获取Provider引用的context对象,然后修改它的_currentValue属性,在上下文值。对比一下这个context对象的结构,一目了然:同理,后面处理到Consumer的时候,context也是这样获取的:ContextConsumer的fiber节点也会做特殊处理:workInProgress就是当前的fiber节点,其类型保存了context.Consumer对象。我们通过readContext获取值,也就是取_currentValue属性。获取到context中的最新值后,触发子组件的渲染:所以不清楚为什么Consumer必须作为子节点传入一个render函数:这样,我们获取到context中的值,并触发subcomponent组件的渲染。context的使用方式也是useContext的hook,其实是一样的:useContext也是调用readContext来读取context的_currentValue属性:当然useContext的context并不是从fiber.type中获取的,而是通过importedcontext:但是他们都引用了同一个对象,context的值在Provider中被修改了,这里取的context也是new的。总结一下context的实现原理:createContext会创建一个context对象,这个对象有_currentValue保存值,同时也引用了Provider和Consumer对象。Provider和Consumer中的_context属性指的是上下文。jsx渲染的时候会把Provider和Consumer对象保存到vdom中,后面reconcile会调到fiber的type属性中。在处理fiber节点时,会根据类型做不同的处理:如果是Provider,会根据传入的值修改context的值,即如果_currentValue属性是Consumer,会读取上下文的值并触发子组件的渲染。函数组件会使用useContext的hook,最后读取同一个context的_currentValue的值,明确context的实现原理。我们能找到一些黑客技巧吗?比如Provider其实是修改了_currentValue,那如果我们自己修改context._currentValue,就不需要Provider了?试一试:像这样使用Provider:其实直接修改_currentValue:也是可以的,但是不建议这样写,因为这是私有属性。万一哪天变了呢?摘要Context是React提供的任何级别的组件之间进行通信的机制。先看下它的用法:在jsx中通过createContext创建context对象,传入初值,通过context.Provider修改context值。通过context.Consumer获取context值,如果是函数组件,通过useContext的hook获取。然后通过源码明确了context的实现原理:jsx中的Provider和Consumer对象会保存在vdom中,最后传递给fiber节点的type属性。当处理Fiber时,会为Provider修改context的值,Consumer会去取context的值,触发子组件渲染。函数组件的useContext钩子也从同一个上下文对象中读取数据。然后我们找到了绕过Provider修改context的方法,就是直接修改_currentValue,但是不推荐这样做,因为private属性不一定随时变化。Context就是这样实现的,其他feature的实现原理也是类似的,只是挂在不同的属性上,在处理fiber的时候,分成不同的type进行处理。理清了context的原理之后,是不是对vdom、fiber、reconcile的过程有了更深的理解呢?
