vlambda博客
学习文章列表

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

 关注
脚本之家
,与百万开发者在一起

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

context 是 React 提供的特性,可以实现任意层级组件之间的数据传递。

可能大家用过 context,但是不知道它是怎么实现的。

本文就从源码层面来讲下 cotnext 的原理,而且我们能从中发现一些 hack 的小技巧。

首先,我们先过一下 context 的使用方式:

context 的使用

有这样的 3个组件,One、Two、Three:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

我们想不通过 props 从 One 传递数据到 Three,这时候就可以用 context 了:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

调用 createContext 来创建 context 对象,初始化数据是 'dong'。

Three 组件里就可以通过 useContext 把 context 数据取出来了:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

除了初始化的时候可以传入,后面也可以通过 Provider 来修改 context 数据:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

我们通过 Provider 传入了新的 value,覆盖了初始值,这样 useContext 拿到的值就变了:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

函数组件是用 useContext 的 hook 来取,class 组件则是用 Consumer 来取:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

我们依然是通过 createContext 创建 context 对象,通过 Provider 修改了 value。

现在 Three 变成了 class 组件,所以使用 context 的方式要通过 Consumer,它的 children 部分传入 render 函数,参数里就能拿到 context 数据:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

这分别是 class 组件和 function 组件使用 context 的方式,我们小结一下:

context 使用 React.createContext 创建,可以传入初始值,后面也可以通过 Provider 来修改其中的值,使用 context 值的时候,如果是 function 组件,可以通过 useContext 的 hook 来取,而 class 组件是用 Consumer 传入一个 render 函数的方式来取。

学会了 context 怎么用,我们再来看下它的实现原理:

context 的实现

首先我们看下 createContext 的源码:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

它创建了一个 context 对象,有 _currentValue 属性,一看就是保存值的,还有 Consumer 和 Provider 两个属性。

Consumer 和 Provider 都是通过 _context 保存了  context 对象的引用。

并且它们都有 $$typeof 来标识类型。

这就是 context 对象的结构:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

那这个 context 对象是怎么结合到 React 渲染流程里的呢?

就是通过 jsx 结合的呀:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

jsx 编译以后会产生 render function。

比如上面那段 jsx 编译后是这样的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

新版 React 不调用 React.createElement 了,而是 jsx 函数,也就是上面的 jsxDev。

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

看这第一个参数是啥,有 $$typeof 和 _context 属性,不就是我们传入的 context.Provider 么:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了
看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

value 是在 props 参数里传入的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

jsx 执行会产生一个个 vdom:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

这样 context 就保存到了 vdom 节点上。

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

那递归渲染的时候不就能从 vdom 拿到 context 了么?

别着急,React 现在不是 vdom 直接渲染了,而是会先把 vdom 转成 fiber,这个过程叫做 reconcile:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

fiber 的结构中保存了兄弟节点和父节点的引用,这是 vdom 所没有的,vdom 只有子节点的引用,所以 vdom 变成 fiber 以后就变成了可打断的,因为就算断了也能找到兄弟节点和父节点继续处理。

那保存在 vdom 中的 context 不就自然的转移到了 fiber 节点上了么?

创建 fiber 的代码是这样的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

调用 createFiber 最终会 new FiberNode

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

然后也是创建一个对象:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

这个和 vdom 差别不大,只不过属性不一样了。

你看这里的  fiber.type,不就是保存在 vdom 上的 context.Provider 对象么?

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了
看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

那之后这个 fiber 节点是怎么处理的呢?

你会发现在处理 fiber 节点的时候,会判断 fiber.tag,不同的类型做不同的处理:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

FunctionComponent 和 ClassComponent 的 fiber 节点的处理就不同。

下面可以找到 ContextProvider 的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

它的实现就是修改了 context 中的值:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

pushProvider 就是最终修改 context 值的地方:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

通过 _context 拿到了 Provider 所引用的 context 对象,然后修改它的 _currentValue 属性,也就是 context 中的值。

对照着这个 context 对象的结构一看就明白了:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

同理,后面处理到 Consumer 的时候也是这样拿到 context 的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

ContextConsumer 的 fiber 节点也会做专门的处理:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

workInProgress 就是当前 fiber 节点,它的 type 保存了 context.Consumer 对象。

我们通过 readContext 拿到其中的值,也就是取 _currentValue 属性。

拿到最新的 context 中的值后就触发子组件的渲染:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

所以说为什么 Consumer 必须要传入一个 render 函数作为子节点不就清楚了么:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

这样我们就取到了 context 中的值,并触发了子组件的渲染。

使用 context 的方式还有 useContext 的 hook,其实那个也是一样的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

useContext 也是调用了 readContext 来读取了 context 的 _currentValue 属性:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

当然,useContext 的 context 不是从 fiber.type 来取的,而是用的传入的 context:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

但是都是引用的同一个对象,在 Provider 里修改了 context 的 value,这里取到的 context 同样也是新的。

我们小结下 context 的实现原理:

createContext 会创建 context 对象,它有 _currentValue 保存值,还引用了 Provider 和 Consumer 两个对象,Provider 和 Consumer 里有 _context 属性引用了 context。

在 jsx 渲染的时候会把 Provider 和 Consumer 对象保存到 vdom 上,后面 reconcile 的时候会转移到 fiber 的 type 属性上,处理 fiber 节点的时候会根据类型做不同的处理:

如果是 Provider,就会根据传入的 value 修改 context 的值,也就是 _currentValue 属性

如果是 Consumer,则会读取 context 的值然后触发子组件的渲染。

函数组件会使用 useContext 的 hook,最终也是读取了同一个 context 的 _currentValue 的 值

理清了 context 的实现原理,我们是不是能发现一些 hack 的技巧呢?

比如 Provider 其实就是修改 _currentValue 的,那我们自己修改 context._currentValue 不就不用 Provider 了?

试一下:

用 Provider 是这样的:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

其实直接修改 _currentValue 也可以:

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了

但是不推荐这样写,因为这是个私有属性,万一哪一天变了呢?

总结

context 是 React 提供的任意层级组件之间通信的机制,我们先过了一遍它的使用方式:

通过 createContext 来创建 context 对象,可以传入初始值,可以通过 jsx 里的 context.Provider 来修改 context 值,通过 context.Consumer 来拿到 context 的值,如果是函数组件,是通过 useContext 的 hook 来取。

然后通过源码理清了 context 的实现原理:

jsx 里的 Provider 和 Consumer 对象会被保存到 vdom 中,最后会转移到 fiber 节点的 type 属性上,fiber 处理的时候,对 Provider 会修改 context 的 value,而 Consumer 则会取出 context 的值然后触发子组件渲染。函数组件的 useContext 的 hook 也是从同一个 context 对象读取的数据。

然后我们发现了绕过 Provider 修改 context 的方式,就是直接修改 _currentValue,但是不推荐这样做,因为私有属性不一定啥时候就变了。

context 是这样实现的,其他特性的实现原理也大同小异,只不过是挂到了不同的属性上,处理 fiber 的时候做了分成了不同的类型来处理。

理清 context 的原理之后,你是否对 vdom、fiber,还有 reconcile 的过程都有更深的理解了呢?

       
         
         
       
         
           
           
         

程序员专属卫衣

商品直购链接  👇

看完 React Conext 源码,就知道怎么绕过 Provider 修改它了 脚本之家严选 交易担保 放心买 程序员极客连帽卫衣

         
           
           
         

  推荐阅读:

!!!


推荐:

                   
                     
                     
                   

每日打卡赢积分兑换书籍入口