react useContext 执行机制解析 🚀🚀

本系列会实现一个简单的react,包含最基础的首次渲染,更新,hooklane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘

本文致力于实现一个最简单的useContext,代码均已上传至github,期待star!✨:

github.com/kongyich/ti...

本文是系列文章,阅读的联系性非常重要!!

手写mini-react!超万字实现mount首次渲染流程🎉🎉

进击的hooks!实现react(反应)(反应)中的hooks架构和useState 🚀🚀

更新!更新!实现react更新及diff流程

深入react源码!实现react事件模型🎉🎉

爆肝第五篇! 实现react调度更新与Lane模型✌️✌️

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

手写mini-react!实现react并发更新机制 🎉🎉

进击的hooks!useEffect执行机制大揭秘 🥳🥳

react能力值+1!useTransition是如何实现的?

面试官:说一下 useRef 是如何实现的?我:? 😭

期待点赞!😁😁

食用前指南!本文涉及到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​的用法可知,主要有两个功能:

  1. 保存数据

在消费数据时,通过useContext​获取数据,可以看到通过_currentValue​属性获取值:

js 复制代码
const value = context._currentValue;
  1. 拥有一个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-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍ ‍ ‍

相关推荐
算法与编程之美8 分钟前
通过两个类计算一个长方形的周长和面积
java·开发语言·javascript·jvm·servlet
ceek@21 分钟前
HTML增加复制模块(使用户快速复制内容到剪贴板)
前端·javascript·html
小于负无穷23 分钟前
前端面试题(十)
前端
黄毛火烧雪下42 分钟前
前端注释规范
前端
声声codeGrandMaster1 小时前
Vue入门2
前端·vue.js·vue
爱吃水果和蔬菜丫1 小时前
解决sortablejs+el-table表格内限制回撤和拖拽回撤失败问题
前端
NiNg_1_2341 小时前
Axios 和 Ajax的区别和联系
前端·javascript·ajax
曲辒净1 小时前
js中的事件冒泡是什么?
前端·javascript·html
小满zs1 小时前
React第九章(组件通信)
前端·react.js