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

相关推荐
y先森27 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy27 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891130 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐4 小时前
前端图像处理(一)
前端