本系列会实现一个简单的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
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳