进击的hooks!实现react中的hooks架构和useState 🚀🚀

hey🖐! 我是小黄瓜😊😊。一枚小透明,期待关注➕ 点赞,共同成长~

写在前面

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

本文致力于实现一个最简单的首次渲染流程,代码均已上传至github,期待star!✨: github.com/kongyich/ti...

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

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

期待点赞!😁😁

食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

上文中我们已经是实现了一个最基本的单节点dom结构的jsx​的初始化渲染逻辑。接下来应该就实现节点更新的流程,但是在此之前我们需要先实现触发更新的"行为"。

我们的"mini-react"采用的是函数式组件的写法,不同于传统的类组件的写法,函数组件更加轻量,减少了很多样板代码。而且高阶组件可以很灵活的满足各种个性化配置需求。

hooks​这一特性出来之前,函数式组件基本只能承载一些ui渲染的任务,所有的组件生命周期及状态变更只能在类组件中处理,函数组件只能被动的接收状态,而在hooks​这一特性诞生之后,函数组件也拥有了与类组件同样的能力。依托官方提供的各类hooks​,我们在函数组件中也可以处理各种状态。这也使得函数组件在react​开发中也逐渐占据主流。

接下来我们搭建一个hooks​架构,然后实现useState​以便于后续对jsx​进行更新。

一. useState基本用法

useState 用于在函数组件中定义变量,具备类组件的 state​,让函数组件拥有更新视图的能力。

js 复制代码
import { useState } from 'react'

const [state, setState] = useState(initial)

参数:

  • initial​参数有两种类型

    • 函数类型:将initial函数的执行结果作为state的初始值
    • 非函数类型:直接讲initial作为state的初始值

返回值:

  • state

数据源,用于渲染UI 层​的数据源

  • setState

改变数据源的函数,用于数据源状态的更新,类似于类组件的 this.setState​方法。

js 复制代码
import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(28);
  const [name, setName] = useState('gua');
  const [todos, setTodos] = useState(() => createTodos());
}

基本用法

js 复制代码
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      add
    </button>
  );
}

这个例子会在点击add​按钮后为count​ 增加1。

此外setState​ 函数也可以接收一个函数。state​的值会作为参数传入setState​ 函数,返回结果将会作为新的值赋值给state​。

js 复制代码
function handleClick() {
    setCount(count => count + 1);
 }

二. 数据共享层

hook​架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook​的逻辑。这样就可以与宿主环境解耦,灵活性更高。

js 复制代码
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

const currentDispatcher = {
	current: null
};

currentDispatcher​为我们本次实现的hook​。

所以对应到我们的render​流程以及hooks​的应用,他们之间的调用关系是这样的:

hooks​怎么知道当前是mount​还是update​?

我们在使用hooks​时,react在内部通过**currentDispatcher.current**赋予不同的函数来处理不同阶段的调用,判断hooks 是否在函数组件内部调用。

三. hooks

hooks​可以看做是函数组件和与其对应的fiber​节点进行沟通和的操作的纽带。在react​中处于不同阶段的fiber​节点会被赋予不同的处理函数执行hooks​:

  • 初始化阶段 -----> HookDispatcherOnMount

  • 更新阶段 -----> HookDispatcherOnUpdate

js 复制代码
const HookDispatcherOnMount = {
	useState: mountState,
	useEffect: mountEffect
};

const HookDispatcherOnUpdate = {
	useState: updateState,
	useEffect: updateEffect
};

但是实现之前,还有几个问题需要解决:

如何确定fiber对应的hooks上下文?

还记得我们在处理函数组件类型的fiber​节点时,调用renderWithHooks​函数进行处理,在我们在执行hooks​相关的逻辑时,将当前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​ 中,我们在处理hooks​ 的初始化及更新逻辑中就可以获取到当前的fiber​节点信息。

hooks是如何存在的?保存在什么地方?

注意hooks​只存在于函数组件中,但是一个函数组件的fiber​节点时如何保存hooks​信息呢?

答案是:memoizedState​。

fiber​节点中保存着非常多的属性(参考上一篇文章),有作为构造fiber​链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。

而对于函数组件类型的fiber​节点,memoizedState​属性保存hooks​信息。hook​在初始化时,会创建一个对象,保存此hook​所产生的计算值,更新队列,hooks​链表。

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>
  );
}

在某个函数组件中存在多个hooks​,此时每个hook​的信息该如何保存呢?这就是上文中hook​对象中next​ 属性的作用,它是一个链表指针。在hook​对象中,next​ 属性指向下一个hook​。

换句话说,如果在一个函数组件中存在多个hook​,那么在该fiber​节点的memoizedState​属性中保存该节点的hooks​链表。

函数组件对应 fiber​ 用 memoizedState​ 保存 hooks​ 信息,每一个 hooks​ 执行都会产生一个 hooks​ 对象,hooks​ 对象中,保存着当前 hooks​的信息,不同 hooks​保存的形式不同。每一个 hooks​ 通过 next​ 链表建立起关系。

对应我们上文的Counter​ 这个函数组件的fiber​节点与hooks​之间的关系:

四. useState

在上一篇介绍react初始化流程中,首先在render​阶段调用beginWork​开始构建fiber​节点,而对于不同的节点,react会进行不同的处理:

js 复制代码
export const beginWork = (wip) => {
	// 返回子fiberNode
	switch (wip.tag) {
		// 根节点
		case HostRoot:
			return updateHostRoot(wip);
		// 原生dom节点
		case HostComponent:
			return updateHostComponent(wip);
		// 文本节点
		case HostText:
			return null;
		// 函数组件
		case FunctionComponent:
			return updateFunctionComponent(wip);
		default:
			if (__DEV__) {
				console.warn('beginWork未实现的类型');
			}
			break;
	}
	return null;
};

针对函数节点,我们调用updateFunctionComponent​函数进行处理,在初始化时,主要任务是执行函数组件,然后生成此函数组件的fiber​节点,在上文中我们使用了两个函数来执行这两个任务:

js 复制代码
function updateFunctionComponent(wip) {
	// 执行函数组件,生成fiber节点
	const nextChildren = renderWithHooks(wip);
	// 根据fiber生成真实DOM节点
	reconcilerChildren(wip, nextChildren);
	return wip.child;
}

而针对于 renderWithHooks​ 函数来说,在我们加入hooks​相关的逻辑后,显然它需要被承载更重要的能力,就是根据不同的处理阶段来为**currentDispatcher.current**​赋值不同的hooks​函数,之所以会在这个函数中处理,是因为可以直接避免在其他无关的环境里调用hooks​函数。

currentDispatcher.current​ 在其他类型的fiber​节点被处理时值都为null​,这样就保证了hooks​函数只在函数组件中被调用。

js 复制代码
// 定义useState函数
export const useState = (initialState) => {
	const dispatcher = resolveDispatcher();
	return dispatcher.useState(initialState);
};

currentDispatcher.current​为null时,说明当前并非函数组件。

js 复制代码
const currentDispatcher = {
	current: null
};
// 错误处理,当currentDispatcher.current为null时,说明当前并非函数组件
export const resolveDispatcher = () => {
	const dispatcher = currentDispatcher.current;

	if (dispatcher === null) {
		throw new Error('hook只能在函数组件中执行');
	}

	return dispatcher;
};

renderWithHooks​中依旧根据alternate​的存在判断当前为初始化/更新?

js 复制代码
// 当前正在render的fiber
let currentlyRenderingFiber = null;
// 当前处理中的hooks
let workInProgressHook = null;
// 当前current树中的hooks
let currentHook = null;

export function renderWithHooks(wip) {
	// 赋值操作
	currentlyRenderingFiber = wip;
	// 重置memoizedState
	wip.memoizedState = null;
	const current = wip.alternate;

	if (current !== null) {
		// update
		currentDispatcher.current = HookDispatcherOnUpdate;
	} else {
		// mount
		currentDispatcher.current = HookDispatcherOnMount;
	}

	// ....

	// 重置操作
	currentlyRenderingFiber = null;
	workInProgressHook = null;
	currentHook = null;
}

renderWithHooks​函数中定义了三个全局变量:currentlyRenderingFiber​代表当前正在处理的fiber​节点。workInProgressHook​ 代表当前处理中的hooks​,也就是workInProgress​树中的。currentHook​ 代表当前的current​树中的hooks​。(上一篇文章双缓存树知识)

currentDispatcher.current​ 在初始化阶段和更新阶段分别被赋值给了HookDispatcherOnMount​ 和HookDispatcherOnUpdate​,分别执行不同的逻辑。

js 复制代码
const HookDispatcherOnMount = {
	useState: mountState
};

const HookDispatcherOnUpdate = {
	useState: updateState
};

HookDispatcherOnMount​ 和HookDispatcherOnUpdate​ 是一个对象,useState​ 属性中保存真正的执行函数,这也就对应了在usestate​函数中的调用方式;

js 复制代码
dispatcher.useState(initialState);

五. Mount阶段

根据useState​的使用规则,useState​返回一个数组。其中第一项为数据源,第二项为触发更新的函数,这也就意味着用户在调用第二个更新函数时,也需要触发react整体的更新流程。

而参数有两种形态,普通值或函数。后者需要将执行结果作为初始值。

到这里在mount​阶段的任务已经很明确了:

  • 生成hooks链表,将当前hooks加入到链表中
  • 处理初始化数据,其中如果参数为函数,将执行结果作为初始值
  • 构建该hooks的更新队列

mountWorkInprogressHook​函数用于构造在mount​阶段的hooks​链表,分为两种情况,如果workInProgressHook​ 为null​,说明当前还没有处理hooks​,当前为处理的第一个hooks​,则将workInProgressHook​ 赋值为当前的hooks​对象,最后保存hooks​链表到memoizedState​ 属性中。

如果workInProgressHook​有值,说明当前处理的是后续的hooks​,那么将当前 workInProgressHook​ 的next​指针指向当前hooks​,然后更新workInProgressHook​。

js 复制代码
function mountWorkInprogressHook() {
	const hook = {
		memoizedState: null,
		updateQueue: null,
		next: null
	};

	if (workInProgressHook == null) {
		// mount 时 第一个hook
		if (currentlyRenderingFiber == null) {
			throw new Error('请在函数组件内调用hook');
		} else {
			workInProgressHook = hook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		}
	} else {
		// mount后续的hook
		workInProgressHook.next = hook;
		workInProgressHook = hook;
	}
	return workInProgressHook;
}

createUpdateQueue​ 创建更新队列(详情上一篇)

js 复制代码
const hook = mountWorkInprogressHook();

// 处理初始化数据
let memoizedState;
// 如果初始值为函数,执行,将返回值作为初始值
if (initialState instanceof Function) {
	memoizedState = initialState();
} else {
	memoizedState = initialState;
}

// 构建更新函数
const queue = createUpdateQueue();
hook.updateQueue = queue;
hook.memoizedState = memoizedState;

如上整个hooks​的初始化和初始值就已经处理完毕了,接下来还需要创建一个更新函数,由用户来触发,更新hooks​保存的值,并触发更新流程。

js 复制代码
const dispatch = dispatchSetState().bind(null, currentlyRenderingFiber, queue);
// 保存更新函数
queue.dispatch = dispatch;

dispatchSetState​ 这个函数与react更新流程的触发方式相似,创建一个更新函数,加入更新队列,然后调用scheduleUpdateOnFiber​函数开启更新流程:

(更新相关的逻辑可查看上一篇文章)

dispatchSetState​函数在初始化及更新阶段都不会使用,是提供给用户调用的。

js 复制代码
function dispatchSetState(
	fiber,
	updateQueue,
	action
) {
	// 创建更新任务
	const update = createUpdate(action);
	// 入队
	enqueueUpdate(updateQueue, update);

	scheduleUpdateOnFiber(fiber);
}

这里有一个地方需要注意一下,dispatchSetState​ 函数是使用bind​进行绑定的,这样做的好处是可以提前传递currentlyRenderingFiber​ 和queue​参数。

我们在使用useState​的时候是这样子的:

js 复制代码
setCount(count => count + 1);

而使用bind​绑定的方式可以提前传递一些参数,而用户定义的副作用函数被当作第三个参数action​来处理。

至此,整个mountState​函数如下:

js 复制代码
function mountState<State>(initialState) {
	// 找到当前useState对应的hook数据
	// 构建hooks链表
	const hook = mountWorkInprogressHook();

	// 处理初始化数据
	let memoizedState;
	if (initialState instanceof Function) {
		memoizedState = initialState();
	} else {
		memoizedState = initialState;
	}

	// 构建更新函数
	const queue = createUpdateQueue();
	hook.updateQueue = queue;
	hook.memoizedState = memoizedState;

	const dispatch = dispatchSetState().bind(null, currentlyRenderingFiber, queue);
	queue.dispatch = dispatch;
	return [memoizedState, dispatch];
}

六. Update阶段

update​阶段也需要处理hooks​链表,只不过稍微有点区别,在mount​阶段中需要构造一个新的hooks​对象。而update​阶段我们需要从current​树中复用已有的hooks​对象。形成新的 hooks​ 链表关系。

  • 当前fiber​存在alternate

    • currentHook​的值为null

      • 获取current树中原始的hooks链表,获取memoizedState 属性,第一个hooks
    • currentHook​有值

      • currentHook.next 取后续的hooks
  • 当前fiber​不存在alternate

    • 与初始化的逻辑类似

js 复制代码
function updateWorkInprogressHook(): Hook {
	let nextCurrentHook: Hook | null;

	if (currentHook == null) {
		// 第一个hook链表
		const current = currentlyRenderingFiber?.alternate;

		if (current !== null) {
			nextCurrentHook = current?.memoizedState;
		} else {
			nextCurrentHook = null;
		}
	} else {
		// 后续的hook
		nextCurrentHook = currentHook.next;
	}

	if (nextCurrentHook === null) {
		throw new Error(
			`组件${currentlyRenderingFiber?.type}本次执行时多了一个hook`
		);
	}

	currentHook = nextCurrentHook as Hook;
	// 复用原始的hooks
	const newHook: Hook = {
		memoizedState: currentHook.memoizedState,
		updateQueue: currentHook.updateQueue,
		next: null
	};

	if (workInProgressHook == null) {
		// mount 时 第一个hook
		if (currentlyRenderingFiber == null) {
			throw new Error('请在函数组件内调用hook');
		} else {
			workInProgressHook = newHook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		}
	} else {
		// mount后续的hook
		workInProgressHook.next = newHook;
		workInProgressHook = newHook;
	}

	return workInProgressHook;
}

其实从更新阶段的hooks​取值的逻辑也就不难看出,为什么在react中的hooks​函数不可以在条件语句中使用的原因了,如果我们有以下使用场景:

js 复制代码
export default function App(){
	let number, setNumber;
    let isShow = true
	if(isShow) {
		const [number, setNumber] = useState(0)
	}
	const [count, setCount] = useState(10)
	const [age, setAge] = useState(18)
}

isShow​ 使其中一个useState​变为有条件使用,所以如果某一次更新isShow​变为false​,那么更新时hooks​链表在复用时:

直接导致整条hooks​链表错乱。

hooks​中的memoizedState​也需要更新,执行processUpdateQueue​ 函数对保存在更新队列的函数。

js 复制代码
// 计算更新新state的逻辑
const queue = hook.updateQueue;
const pending = queue.shared.pending;

if (pending !== null) {
	// 执行更新函数
	const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
	// 执行后的新值,更新memoizedState属性
	hook.memoizedState = memoizedState;
}

js 复制代码
export const processUpdateQueue = (
	baseState,
	pendingUpdate
) => {
	const result = {
		memoizedState: baseState
	};

	if (pendingUpdate !== null) {
		const action = pendingUpdate.action;

		if (action instanceof Function) {
			// baseState 1 update (x) => 4x -> memoizeState 4
			result.memoizedState = action(baseState);
		} else {
			// baseState 1 update 2 -> memoizeState 2
			result.memoizedState = action;
		}
	}

	return result;
};

至此,整个更新阶段的hooks​函数就完成了:

js 复制代码
function updateState() {
	// 构建新的hooks链表
	const hook = updateWorkInprogressHook();

	// 计算更新新state的逻辑
	const queue = hook.updateQueue;
	const pending = queue.shared.pending;

	if (pending !== null) {
		const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
		hook.memoizedState = memoizedState;
	}

	return [hook.memoizedState, queue.dispatch];
}

写在最后

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
csdnLN16 分钟前
$.ajax() 对应事件done() 、fail()、always() 的用法
前端·javascript·ajax
甜味橘阳17 分钟前
echarts地图可视化展示
前端·javascript·echarts
bloxed1 小时前
前端文件下载多方式集合
前端·filedownload
余生H1 小时前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC1 小时前
CSS(四)display和float
前端·css
cwtlw1 小时前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥1 小时前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
米奇妙妙wuu2 小时前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖2 小时前
React 生命周期完整指南
前端·react.js
梦境之冢2 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http