本系列会实现一个简单的react,包含最基础的首次渲染,更新,hook,lane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的useContext,代码均已上传至github,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
进击的hooks!实现react(反应)(反应)中的hooks架构和useState 🚀🚀
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
react能力值+1!useTransition是如何实现的?
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
一. 基本概念
useContext用于在函数式组件中访问上下文(Context)的值。
Context是一种在react不同组件(跨层级,例如父子组件,父孙组件)之间共享,传递数据的机制。
useContext 的参数是由 createContext 创建,或者是父级上下文 context传递的,通过 Context.Provider 包裹的组件,才能通过 useContext 获取对应的值。可以理解为 useContext 代替之前 Context.Consumer 来获取 Provider 中保存的 value 值。
基本使用:
ts
const contextVal = useContext(context)
params:
context:经过createContext()执行后返回的context对象。
result:
contextVal:返回的数据,也就是context对象内保存的value值。
js
import React, { createContext, useContext } from 'react';
// 创建一个上下文
const GuaContext = createContext();
const Index = () => {
const name = 'gua';
return (
// 通过context对象包裹子组件
<GuaContext.Provider value={name}>
<Son />
</GuaContext.Provider>
);
};
const Son = () => {
// 使用 useContext 获取上下文的值
const name = useContext(GuaContext);
return <h1>hi, {name}</h1>;
};
const App = () => {
return <Index />;
};
export default App;
在开始实现useContext之前,先来了解一下react中的hook架构。
二. 数据共享层
hook架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook的逻辑。这样就可以与宿主环境解耦,灵活性更高。
js
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
const currentDispatcher = {
current: null
};
currentDispatcher为我们本次实现的hook。
所以对应到我们的render流程以及hook的应用,他们之间的调用关系是这样的:

hook怎么知道当前是mount还是update?
我们在使用hook时,react在内部通过 currentDispatcher.current 赋予不同的函数来处理不同阶段的调用,判断hook 是否在函数组件内部调用。
三. hook
hook可以看做是函数组件和与其对应的fiber节点进行沟通和的操作的纽带。在react中处于不同阶段的fiber节点会被赋予不同的处理函数执行hook:
- 初始化阶段 ----->
HookDispatcherOnMount - 更新阶段 ----->
HookDispatcherOnUpdate
js
const HookDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
useTransition: mountTransition,
};
const HookDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
useTransition: updateTransition,
};
但是实现之前,还有几个问题需要解决:
如何确定fiber对应的hook上下文?
还记得我们在处理函数组件类型的fiber节点时,调用renderWithHooks函数进行处理,在我们在执行hook相关的逻辑时,将当前fiber节点信息保存在一个全局变量中:
js
// 当前正在render的fiber
let currentlyRenderingFiber = null;
js
export function renderWithHooks(wip: FiberNode) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
// hooks更新阶段
} else {
// mount
// hooks初始化阶段
}
const Component = wip.type;
const props = wip.pendingProps;
const children = Component(props);
// 重置操作
// 处理完当前fiber节点后清空currentlyRenderingFiber
currentlyRenderingFiber = null;
return children;
}
将当前正在处理的fiber节点保存在全局变量currentlyRenderingFiber 中,我们在处理hook 的初始化及更新逻辑中就可以获取到当前的fiber节点信息。
hook是如何存在的?保存在什么地方?
注意hook只存在于函数组件中,但是一个函数组件的fiber节点时如何保存hook信息呢?
答案是:memoizedState。
fiber节点中保存着非常多的属性,有作为构造fiber链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。
而对于函数组件类型的fiber节点,memoizedState属性保存hooks信息。hook在初始化时,会创建一个对象,保存此hook所产生的计算值,更新队列,hook链表。
js
const hook = {
// hooks计算产生的值 (初始化/更新)
memoizedState: "";
// 对此hook的更新行为
updateQueue: "";
// hooks链表指针
next: null;
}
多个hook如何处理?
例如有以下代码:
js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(10);
function handleClick() {
setCount(count + 1);
}
function handleAgeClick() {
setCount(age + 18);
}
return (
<button onClick={handleClick}>
add
</button>
<button onClick={handleAgeClick}>
age
</button>
);
}
在某个函数组件中存在多个hook,此时每个hook的信息该如何保存呢?这就是上文中hook对象中next 属性的作用,它是一个链表指针。在hook对象中,next 属性指向下一个hook。
换句话说,如果在一个函数组件中存在多个hook,那么在该fiber节点的memoizedState属性中保存该节点的hooks链表。
函数组件对应 fiber 用 memoizedState 保存 hook 信息,每一个 hook 执行都会产生一个 hook 对象,hook 对象中,保存着当前 hook的信息,不同 hook保存的形式不同。每一个 hook 通过 next 链表建立起关系。
三. useContext
js
export const useContext = (context) => {
// 通过resolveDispatcher获取真正的执行函数
const dispatcher = resolveDispatcher();
return dispatcher.useContext(context);
};
根据useContext这个函数的功能可见,其实它的职责只是获取Context的值,无论是在mount阶段还是update阶段。
js
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
// mount阶段
useContext: readContext
};
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
// update阶段
useContext: readContext
};
相比于其他hook函数在不同阶段需要执行的功能不一致,useContext在初始化和更新阶段的功能是一致的:获取Context中保存的值。
传入一个Context,然后通过useContext函数的返回值获取value。
js
function readContext(context) {
// 获取当前fiber节点
const consumer = currentlyRenderingFiber;
if (consumer === null) {
throw new Error('只能在函数组件中调用useContext');
}
// 获取value值
const value = context._currentValue;
return value;
}
currentlyRenderingFiber是一个全局变量,保存当前正在处理的fiber节点,因为Context功能存在于函数组件中,所以当前处理的节点必然是一个函数组件类型fiber(FunctionComponent)。
到这里我们只是实现了整个Context功能的一小份部分,useContext只是代表"消费context"的功能。接下来首先看一下Context是如何被创建的?
四. createContext
根据Context的用法可知,主要有两个功能:
- 保存数据
在消费数据时,通过useContext获取数据,可以看到通过_currentValue属性获取值:
js
const value = context._currentValue;
- 拥有一个
Provider标签,用来包裹子子节点
js
const GuaContext = createContext();
// 通过context对象包裹子组件
<GuaContext.Provider value={name}>
<Son />
</GuaContext.Provider>
context与Provider相互引用,context对象中保存原始值,作为函数返回值导出。而Provider作为一个标签节点使用,同时也可以通过_context属性获取到保存在context中的值。
js
export function createContext(defaultValue) {
const context = {
$$typeof: REACT_CONTEXT_TYPE,
Provider: null,
// 初始值
_currentValue: defaultValue
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
};
return context;
}
$$typeof为节点标记。例如普通的dom节点标记为REACT_ELEMENT_TYPE。
js
export const REACT_CONTEXT_TYPE = supportSymbol
? Symbol.for('react.context')
: 0xeacc;
export const REACT_PROVIDER_TYPE = supportSymbol
? Symbol.for('react.provider')
: 0xeac2;
在生成fiber阶段时会根据$$typeof这个节点标记为fiber节点生成不同的tag:
diff
export const ContextProvider = 8;
export function createFiberFromElement(element) {
const { type, key, props, ref } = element;
// 默认为函数组件类型的tag
let fiberTag = FunctionComponent;
// dom类型tag 比如:div,span...
if (typeof type === 'string') {
fiberTag = HostComponent;
} else if (
++ typeof type === 'object' &&
++ type.$$typeof === REACT_PROVIDER_TYPE
) {
// Provider类型
++ fiberTag = ContextProvider;
} else if (typeof type !== 'function' && __DEV__) {
console.warn('为定义的type类型', element);
}
const fiber = new FiberNode(fiberTag, props, key);
fiber.type = type;
fiber.ref = ref;
return fiber;
}
五. Context的逻辑
需要实现两部分内容:
- 对
ContextProvider类型FiberNode的支持 Context逻辑的实现
由于jsx结构最终会被babel编译为element对象的树结构,最终react会在render阶段根据element对象生成fiber节点,进而构建整棵fiber树。
由于我们在createContext函数中已经导出了一个包含有Provider对象返回值。所以我们在使用GuaContext.Provider作为标签使用时,这个标签在编译后的已经是一个$$typeof类型为REACT_PROVIDER_TYPE的element对象了,所以在生成fiber节点时这个标签的tag被标记为ContextProvider。
js
<GuaContext.Provider value={name}>
<Son />
</GuaContext.Provider>
在render阶段创建fiber时,beginWork函数根据不同的tag类型执行不同的处理,在这里增加对ContextProvider类型的支持:
js
export const beginWork = (wip, renderLane) => {
switch (wip.tag) {
case HostRoot:
// ...
case HostComponent:
// ...
case HostText:
return null;
case FunctionComponent:
// ...
case ContextProvider:
// ContextProvider类型
return updateContextProvider(wip);
default:
if (__DEV__) {
console.warn('beginWork未实现的类型');
}
break;
}
return null;
};
数据保存与 context 嵌套
既然已经通过createContext函数生成了一个Context对象,那么在处理ContextProvider类型的节点时,首要任务就是将新的值更新,支持Context._currentValue的变化。
新的值是通过value属性传递,我们可以通过fiber节点的pendingProps属性获取新的值。
以上面的基本使用为例:

在创建fiber节点的过程中,遇到ContextProvider标记的节点代表当前是一个Provider标签,更新与当前节点对应的Context对象的值。当子孙节点使用useContext函数时传入同一个Context对象即可获取最新值。
如果整棵fiber树中之存在一个Context,可以直接更新Context对象,但是在实际的情况中可能会有多层嵌套:
js
<ctx.Provider value={0}>
<Cpn />
<ctx.Provider value={1}>
<Cpn />
<ctx.Provider value={2}>
<Cpn />
</ctx.Provider>
</ctx.Provider>
</ctx.Provider>
所以我们使用栈的形式来存储值,由于整个render阶段的处理过程是先深度遍历,到达最深处节点后回溯,直到根节点。beginWork由上至下,completeWork由下至上。
所以在beginWork流程中入栈,completeWork流程出栈,可以满足每一层的对应关系。

js
function updateContextProvider(wip) {
// 从element对象的type属性获取Context对象
const providerType = wip.type;
const context = providerType._context;
// 获取新的值
const newProps = wip.pendingProps;
// 入栈
pushProvider(context, newProps.value);
// 处理子节点
// ...
}
定义两个全局变量,prevContextValue代表当前正在处理的context对象,prevContextValueStack保存context对象栈。
js
let prevContextValue = null;
const prevContextValueStack = [];
export function pushProvider(context, newValue) {
prevContextValueStack.push(prevContextValue);
// 保存当前正在处理的context对象
prevContextValue = context._currentValue;
// 更新_currentValue
context._currentValue = newValue;
}
completeWork回溯的过程与beginWork类似,同样也是通过tag对不同的fiber类型的节点调用不同的处理逻辑。
js
export const completeWork = (wip: FiberNode) => {
// 递归中的归
const newProps = wip.pendingProps;
const current = wip.alternate;
switch (wip.tag) {
case HostComponent:
// ...
case HostText:
// ...
case HostRoot:
// ...
case ContextProvider:
const context = wip.type._context;
// 出栈
popProvider(context);
return null;
default:
if (__DEV__) {
console.warn('未处理的completeWork情况', wip);
}
break;
}
};
由于当前fiber将要回溯到父级,所以更新为父级的值。同时prevContextValueStack栈出栈。
js
export function popProvider(context) {
context._currentValue = prevContextValue;
// 出栈
prevContextValue = prevContextValueStack.pop();
}
写在最后 ⛳
未来可能会更新实现mini-react和antd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳