大家好,核代码我卡颂。心实现行
很多项目的核代码源码非常复杂,让人望而却步。心实现行但在打退堂鼓前,核代码我们应该思考一个问题:源码为什么复杂?
造成源码复杂的心实现行原因不外乎有三个:
如果是原因3,那实际理解起来其实并不难。我们需要的只是有人能帮我们剔除无关功能的干扰。
React Context的实现就是个典型例子,当剔除无关功能的干扰后,他的核心实现,仅需「5行代码」。
本文就让我们看看React Context的核心实现。
Context的完整工作流程包括3步:
以下面的代码举例:
const ctx = createContext(null);function App() { return ( <ctx.Provider value={ 1}> <Cpn /> </ctx.Provider> );}function Cpn() { const num = useContext(ctx); return <div>{ num}</div>;}
其中:
Context数据结构(即createContext方法的返回值)也很简单:
function createContext(defaultValue) { const context = { $$typeof: REACT_CONTEXT_TYPE, Provider: null, _currentValue: defaultValue }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context }; return context;}
其中context._currentValue保存context当前值。
context工作流程的三个步骤其实可以概括为:
了解了工作流程后我们会发现,Context的核心实现其实就是步骤2。
核心实现需要考虑什么呢?还是以上面的示例为例,当前只有一层<ctx.Provider>包裹<Cpn />:
function App() { return ( <ctx.Provider value={ 1}> <Cpn /> </ctx.Provider> );}
在实际项目中,消费ctx的组件(示例中的<Cpn/>)可能被多级<ctx.Provider>包裹,比如:
const ctx = createContext(0);function App() { return ( <ctx.Provider value={ 1}> <ctx.Provider value={ 2}> <ctx.Provider value={ 3}> <Cpn /> </ctx.Provider> <Cpn /> </ctx.Provider> <Cpn /> </ctx.Provider> );}
在上面代码中,ctx的值会从0(默认值)逐级变为3,再从3逐级变为0,所以沿途消费ctx的<Cpn />组件取得的值分别为:3、2、1。
整个流程就像「操作一个栈」,1、2、3分别入栈,3、2、1分别出栈,过程中栈顶的值就是context当前的值。
基于此,context的核心逻辑包括两个函数:
function pushProvider(context, newValue) { // ...}function popProvider(context) { // ...}
其中:
每次执行pushProvider时将context._currentValue更新为当前值:
function pushProvider(context, newValue) { context._currentValue = newValue;}
同理,popProvider执行时将context._currentValue更新为上一个context._currentValue:
function popProvider(context) { context._currentValue = /* 上一个context value */}
该如何表示上一个值呢?我们可以增加一个全局变量prevContextValue,用于保存「上一个同类型的context._currentValue」:
let prevContextValue = null;function pushProvider(context, newValue) { // 保存上一个同类型context value prevContextValue = context._currentValue; context._currentValue = newValue;}function popProvider(context) { context._currentValue = prevContextValue;}
在pushProvider中,执行如下语句前:
context._currentValue = newValue;
context._currentValue中保存的就是「上一个同类型的context._currentValue」,将其赋值给prevContextValue。
以下面代码举例:
const ctx = createContext(0);function App() { return ( <ctx.Provider value={ 1}> <Cpn /> </ctx.Provider> );}
进入ctx.Provider时:
当<Cpn />消费ctx时,取得的值就是1。
离开ctx.Provider时:
但是,我们当前的实现只能应对一层ctx.Provider,如果是多层ctx.Provider嵌套,我们不知道沿途ctx.Provider对应的prevContextValue。
所以,我们可以增加一个栈,用于保存沿途所有ctx.Provider对应的prevContextValue:
const prevContextValueStack = [];let prevContextValue = null;function pushProvider(context, newValue) { prevContextValueStack.push(prevContextValue); prevContextValue = context._currentValue; context._currentValue = newValue;}function popProvider(context) { context._currentValue = prevContextValue; prevContextValue = prevContextValueStack.pop();}
其中:
至此,完成了React Context的核心逻辑,其中pushProvider三行代码,popProvider两行代码。
关于Context的实现,有两个有意思的点。
第一个点:这个实现太过简洁(核心就5行代码),以至于让人严重怀疑是不是有bug?
比如,全局变量prevContextValue用于保存「上一个同类型的context._currentValue」,如果我们把不同context嵌套使用时会不会有问题?
在下面代码中,ctxA与ctxB嵌套出现:
const ctxA = createContext('default A');const ctxB = createContext('default B');function App() { return ( <ctxA.Provider value={ 'A0'}> <ctxB.Provider value={ 'B0'}> <ctxA.Provider value={ 'A1'}> <Cpn /> </ctxA.Provider> </ctxB.Provider> <Cpn /> </ctxA.Provider> );}
当离开最内层ctxA.Provider时,ctxA._currentValue应该从'A1'变为'A0'。考虑到prevContextValue变量的唯一性以及栈的特性,ctxA._currentValue会不会错误的变为'B0'?
答案是:不会。
JSX结构的确定意味着以下两点是确定的:
第一点保证了当进入与离开同一个ctx.Provider时,prevContextValue的值始终与该ctx相关。
第二点保证了不同ctx.Provider的prevContextValue被以正确的顺序入栈、出栈。
第二个有意思的点:我们知道,Hook的使用有个限制 —— 不能在条件语句中使用hook。
究其原因,对于同一个函数组件,Hook的数据保存在一条链表上,所以必须保证遍历链表时,链表数据与Hook一一对应。
但我们发现,useContext获取的其实并不是链表数据,而是ctx._currentValue,这意味着useContext其实是不受这个限制影响的。
以上五行代码便是React Context的核心实现。在实际的React源码中,Context相关代码远不止五行,这是因为他与其他特性耦合在一块,比如:
所以,当我们面对复杂代码时,不要轻言放弃。仔细分析下,没准儿核心代码只有几行呢?
责任编辑:姜华 来源: 魔术师卡颂 ReactContext(责任编辑:知识)
又一款骁龙8 Gen1旗舰官宣:25日发布 定制35mm大师镜头
鲁西化工(000830.SZ)公布消息:拟开展外汇衍生品交易业务