你不知道的函数式组件在mount阶段的流程和其hook所执行的时机

你不知道的函数式组件在mount阶段的流程和其hook所执行的时机

提出问题

想必用过React的对UseState应该并不陌生,但是你知道其在mount阶段的执行的时机吗?

​ 先思考一个问题:当我们在非FC(函数式组件)中使用hook时会出现什么情况呢?会出现如下的错误:

那么Hook本身作为一个函数是如何知道当前它的上下文的呢?React的解决方法就是:在不同上下文中所调用的Hook是不同的,并且实现了一个内部数据共享层。大致的情况如下:

​ 还有一个问题是Hook数据保存在哪里,在类组件中我们知道保存在state中,而在FC中每次更新都是函数的重新执行,所以不能保存在函数里,那么只能保存在FC对应的fiberNode上,只要这个FC还存在那么这个fiberNode就不会销毁。其保存的形式大致如下:

mount的大致流程:

在这我们只考虑单节点,如下结构

typescript 复制代码
function App() {
	const [num] = useState(100);
	return (
		<div>
			<sapn>{num}</sapn>
		</div>
	);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

调度准备阶段

​ 在我们的React的项目中src文件夹下的index.js文件中会有如下代码

js 复制代码
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

//index.html
<div id="root"></div>

这就是mount阶段的入口,我们看一下ReactDOM下createRoot函数所做的事情,下面为导出的函数:

typescript 复制代码
export function createRoot(container: Container) {
	const root = createContainer(container);
	//...其他代码
	return {
		render(element: ReactElementType) {
			return updateContainer(element, root);
		}
	};
}

这个container你可以想象成一个容器,即为传进来的div元素,createContainer函数所做的事情就是创建HostRootFiber和FiberRootNode,那么可能又有人会问HostRootFiber和FiberRootNode是什么呢?这个在这就不介绍过多,想要了解更多请查看别的资料,我就用一张图片来表示:

这个FiberRootNode用来切换current和workInProgress树来实现双缓冲树,这个HostRootFiber对应的即为root对应的element对应的Fiber。

可以看到返回一个对象其中包含一个render函数,其中又调用了updateContainer,这个updateContainer的作用大致就是为root添加一个Update,然后开启调度,记住这个hostRootFiber和updateQueue,后面要考哦。注意:我们在React提到的root对应的应该是FiberRootNode,而不是HostRootFiber。

typescript 复制代码
export function updateContainer(
	element: ReactElementType | null,
	root: FiberRootNode
) {
	const hostRootFiber = root.current;
    //<App/>对应的element
	const update = createUpdate<ReactElementType | null>(element);
	enqueueUpdate(
		hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
		update
	);
	scheduleUpdateOnFiber(hostRootFiber);
	return element;
}

调度阶段

​ 在调度阶段我们会执行下列几项操作:

  • 向上遍历到root,(这是mount阶段,所以hostRootFiber.stateNode即可,但在update中可能发生在任意一个fiber上)
  • 调用renderRoot
  • 初始化wip树
  • beginWork阶段
  • completeWork阶段
  • commitWork阶段
  • commit阶段

在这我们探讨FC的mount阶段,如果想了解其他类型的mount阶段可以关注后续发的文章。

向上遍历

typescript 复制代码
function markUpdateFromFiberToRoot(fiber: FiberNode) {
	let node = fiber;
	let parent = node.return;
	while (parent !== null) {
		node = parent;
		parent = node.return;
	}
    //hostRootFiber.tag = HostRoot
	if (node.tag === HostRoot) {
		return node.stateNode;
	}
	return null;
}

renderRoot函数

typescript 复制代码
function renderRoot(root: FiberRootNode) {
	//初始化
	prepareFreshStack(root);

	do {
		try {
			workLoop();
			break;
		} catch (e) {
			if (__DEV__) {
				console.warn('workLoop发生错误', e);
			}
		}
	} while (true);
	const finishedWork = root.current.alternate;
	root.finishedWork = finishedWork;

	commitRoot(root);
}

初始化wip树

typescript 复制代码
function prepareFreshStack(root: FiberRootNode) {
	workInprogress = createWorkInProgress(root.current, {});
}
//createWorkInProgress
export const createWorkInProgress = (
	current: FiberNode,
	pendingProps: Props
): FiberNode => {
	let wip = current.alternate;
	if (wip === null) {
		//mount
		wip = new FiberNode(current.tag, pendingProps, current.key);
        //其他代码
    }
    //其他代码
}

beginWork

​ beginWork的作用大致可以表达为:根据子的current fiberNode 和 reactElement,生成子对应的fiberNode,并且打上相对应的flags,然后返回子fiberNode,由于是mount阶段,只会有Placement Flag(插入)。

​ 进入beginWork后根据不同的fiber.tag进入不同的流程:

typescript 复制代码
export const beginWork = (wip: FiberNode) => {
	//与React Element比较,生成FiberNode,然后再返回子FiberNode
	switch (wip.tag) {
		case HostRoot:
			return updateHostRoot(wip);

		case HostComponent:
			return updateHostComponent(wip);

		case HostText:
			return null;

		case FunctionComponent:
			return updateFunctionComponent(wip);
		default:
			if (__DEV__) {
				console.warn('workloop未实现的类型', wip);
			}
			break;
	}
	return null;
};
hostRootFiber beginWork------首屏渲染优化

​ 第一个进入的当然是hostRootFiber,本来应该不在这提的,但是由于其中包含一个关于react首屏渲染的优化,所以还是在这里提一下,如果你已经了解了,那么可以直接跳到下一个fiberNode的beginWork中。

typescript 复制代码
function updateHostRoot(wip: FiberNode) {
	const baseState = wip.memoizedState;
	const updateQueue = wip.updateQueue as UpdateQueue<Element>;
	const pending = updateQueue.shared.pending;
	updateQueue.shared.pending = null;
	//memoizedState为传递进来的React Element,<APP/>组件
	const { memoizedState } = processUpdateQueue(baseState, pending);
	wip.memoizedState = memoizedState;

	const nextChildren = wip.memoizedState;
	reconcileChildren(wip, nextChildren);
	return wip.child;
}

​ 在updateContainer函数中我们已经为hostRootFiber创建了一个update,这个update就是来帮助我们创建App组件对应的fiberNode的,消费完这个update后就能获得到children的信息。还记得在上面让记住的hostRootFiber嘛,为什么我们要在未进行调度的时候就创建hostRootFiber并在它的updateQueue上添加一个update,这个就reconcileChildren函数有关了,别着急继续往下面看:

typescript 复制代码
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
	const current = wip.alternate;
	if (current !== null) {
		//update
		wip.child = reconcileChildFibers(wip, current?.child, children);
	} else {
		//mount
		wip.child = mountChildFibers(wip, null, children);
	}
}
typescript 复制代码
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

​ 这两个函数都是由ChildReconciler函数返回的,唯一的不同点在于shouldTrackEffects:是否追踪副作用这个参数的不同,区分进入不同树的依据就是wip.alternate是否存在这个fiber.alternate就是current树上的fiberNode,两棵树的fiberNode通过alternate连接,即wip.alternate = currentFiberNode,currentFiberNode.alternate = wip。这就是我们在调度之前创建hostRootFiber的原因,这样就能只让hostRootFiber进入reconcileChildFibers函数中。因为只有hostRootFiber.alternate !== null。

​ 那么区分hostRootFiber和其他fiber有什么用呢?还记得为什么要提hostRootFiber的beginWork流程吗?对,首屏渲染。因为是mount阶段不像后续的update阶段只需更新部分fiberNode的状态,而在这我们需要创建fiberNode,并且将它们一个一个插在父fiberNode中去,如果首屏渲染的组件很多那么就要消费很多Placement flag,所以为了解决首屏渲染的速度问题,我们可以先构建一棵离屏的DOM树,在都构建完成之后,一并插入到hostRootFiber中去,这时我们仅仅需要消费一个Placement flag。到这是不是很清晰了,我们需要打上Flag的fiberNode正是App组件所对应的fiberNode中,下面来看一下具体的实现:

typescript 复制代码
	function placeSingleChild(fiber: FiberNode) {
		if (shouldTrackEffects && fiber.alternate === null) {
			fiber.flags |= Placement;
		}
		return fiber;
	}

因为我们在调度之前只创建了hostRootFiber和fiberRootNode,所以App对应的current树下的fiberNode是不存在的,所以会打上Placement。

​ 完整的ChildReconciler函数,有兴趣的可以看一下,有什么不明白的可以提问:

typescript 复制代码
function ChildReconciler(shouldTrackEffects: boolean) {
	function reconcileSingleElement(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		element: ReactElementType
	) {
		//根据ReactElement创建一个fiber然后返回
		const fiber = createFiberFromElement(element);
		fiber.return = returnFiber;
		return fiber;
	}

	function reconcileSingeTextNode(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		content: string | number
	) {
		const fiber = new FiberNode(HostText, { content }, null);
		fiber.return = returnFiber;
		return fiber;
	}

	//插入单一的节点
	function placeSingleChild(fiber: FiberNode) {
		if (shouldTrackEffects && fiber.alternate === null) {
			fiber.flags |= Placement;
		}
		return fiber;
	}

	return function reconcileChildFibers(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		newChild?: ReactElementType
	) {
		//判断当前fiber的类型
		if (typeof newChild === 'object' && newChild !== null) {
			switch (newChild.$$typeof) {
				case REACT_ELEMENT_TYPE:
					return placeSingleChild(
						reconcileSingleElement(returnFiber, currentFiber, newChild)
					);

				default:
					if (__DEV__) {
						console.warn('未实现的reconcile类型', newChild);
					}
			}
		}
		//HostText
		if (typeof newChild === 'string' || typeof newChild === 'number') {
			return placeSingleChild(
				reconcileSingeTextNode(returnFiber, currentFiber, newChild)
			);
		}
		if (__DEV__) {
			console.warn('未实现的reconcile类型', newChild);
		}
		return null;
	};
    //其他代码
}
FC beginWork

​ 接下来就要进入我们的FC beginWork了,接下来我们要进入这个函数updateFunctionComponent。这时候想想FC的结构,jsx放在return中,那么我们要创建子fiberNode肯定要拿到其return出来的结果就是nextChildren并处理。那么是不是拿到这个函数并且执行是不是就可以了,这个函数就该fiber对应的type上:

拿到这个函数是不是就可以开始执行了,传入相对应的props,函数开始执行,碰到useState。在开始后续的步骤时先提几个概念,

typescript 复制代码
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

这个 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED就是前面提到的内部数据共享层,其中包括了一个currentDispatcher,这个就相当于上面那张图中的当前可使用的hook集合。currentDispatcher的信息:

typescript 复制代码
const currentDispatcher: { current: Dispatcher | null } = {
	current: null
};

我们在使用react的hook时都知道从react这个包中引入,那么在react包中暴露出去的hook:

typescript 复制代码
export const useState: Dispatcher['useState'] = (initialState) => {
	//获取当前上下文中所以的hook,并从中拿到useState
	const dispatcher = resolveDispatcher();
	return dispatcher.useState(initialState);
};

export const resolveDispatcher = (): Dispatcher => {
    const dispatcher = currentDispatcher.current;

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

    return dispatcher;
};

先导致了解一下这些概念,没理解为什么要这么做也没关系,后续会再捋一遍。

​ 但是我们的调度在react-reconciler中,所以这个数据共享层的作用就体现出来了,我们可以在react-reconciler中使用这个东西了。

回到我们的updateFunctionComponent函数,其内部通过调用renderWithHooks函数获得其nextChildren,看一下nextChildren函数:

typescript 复制代码
let currentlyRenderingFiber: FiberNode | null = null;//当前真正调度的wip
let workInProgressHook: Hook | null = null;//当前正在执行的hook

export function renderWithHooks(wip: FiberNode) {
	//赋值
	currentlyRenderingFiber = wip;

	wip.memoizedState = null;

	const current = wip.alternate;

	if (current !== null) {
		//update
	} else {
		//mount
		//mount时的hook
		currentDispatcher.current = HooksDispatcherOnMount;
	}
	//函数式组件的函数保存在该对应fiber的type上
	const Component = wip.type;
	const props = wip.pendingProps;
	const children = Component(props);

	//重置
	currentlyRenderingFiber = null;

	return children;
}

这个流程相信大家应该都不陌生了,当前传进来的是FC对应的fiberNode,wip.alternate为null,这时候将 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED中的currentDispatcher.current进行赋值,HooksDispatcherOnMount就对应mount时的hook集合,我们继续在下面实现这个current

typescript 复制代码
const HooksDispatcherOnMount: Dispatcher = {
	useState: mountState
};

这样是不是就能理解了react的hook为什么能感知上下文了吧。

捋一遍逻辑

​ 当我们在FC中使用hook时,实际导入的是react包下的hook,但是我们并没有在那里实现,而是将它指向__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.currentDispatcher.current,我们的实现是在这个FC进入beginWork时,更具体来说是进入renderWithHooks,我们在这个函数中给currentDispatcher.current赋值,所以当解析到hook时,找到我们在react包中暴露出去的hook,而那个hook又指向的是当前currentDispatcher.current下的hook,也就是mountState,所以最后执行的是mountState。还有就是当我们在函数式组件外使用hook时,发现当前的currentlyRenderingFiber是没有赋值的(进入函数式组件才会赋值),这时候就会报hook只能在FC中使用的错误,当当前的currentDispatcher.current是有值时我们即可将currentDispatcher.current指向一个全是报错的地方,调用任何一个hook都抛出一个错误,这就是hook中不能使用hook的实现。

​ 继续往下走就是执行useState,这个没什么说的,直接上代码:

typescript 复制代码
function mountState<State>(
	initialState: (() => State) | State
): [State, Dispatch<State>] {
	//找到当前useState对应的hook数据
	const hook = mountWorkInProgresHook();//获取当前的hook数据,创建hook,形成单向链表

	let memoizedState = null;
	if (initialState instanceof Function) {
		memoizedState = initialState();
	} else {
		memoizedState = initialState;
	}
	const queue = createUpdateQueue<State>();
	hook.updateQueue = queue;
	hook.memoizedState = memoizedState;

	// @ts-ignore
	const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
	queue.dipatch = dispatch;

	return [memoizedState, dispatch];
}

function dispatchSetState<State>(
	fiber: FiberNode,
	updateQueue: UpdateQueue<State>,
	action: Action<State>
) {
	const update = createUpdate(action);
	enqueueUpdate(updateQueue, update);
	scheduleUpdateOnFiber(fiber);
}

在这里值得一提的是:

typescript 复制代码
function App() {
	const [num] = useState(0);
	return (
		<div>
			<sapn>{num}</sapn>
		</div>
	);
}

你觉得这个div和span哪个先进入jsx方法,也就是createElement方法,答案是span,有兴趣的可以打一个断点去看看。

接下来就要进入return出的nextChildren的beginWork,一直到HostText,然后再进行completeWork。

completeWork

​ 这个阶段就是根据beginWork生成的fiberNode,自下而上的去创建宿主中的实例,在浏览器中就是element,这个使用的是浏览器提供的方法来创建,创建完成之后插入到其父element中去。在这个期间还会进行flags冒泡,什么是flags冒泡呢?在beginWork中我们为部分fiberNode打上了flag,我们将这个子孙的flag保存在父fiberNode中。有人可能就会问,又进行递归会不会太消耗性能?别忘了,我们现在处在completeWork下,是从下到上的,刚好能够将flags带上去,flags冒泡函数:

typescript 复制代码
function bubbleProperties(wip: FiberNode) {
	let subtreeFlags = NoFlags;
	let child = wip.child;

	while (child !== null) {
		subtreeFlags |= child.subtreeFlags;
		subtreeFlags |= child.flags;

		child.return = wip;
		child = child.sibling;
	}
	wip.subtreeFlags = subtreeFlags;
}

这个 subtreeFlags保存的就是当前fiberNode子孙fiberNode所包含的flags(不包含自己的flags)。

​ 当completeWork结束后,一颗离屏的DOM树就构建好了,flags也冒泡到了hostRootFiber。

commit阶段

​ commit阶段其实和这个mount时期的FC已经没有很大的关系了,commit阶段主要就是向下遍历找到flags并消费,在这里也就是Placement,这样调度的流程就结束了。

总结

​ 没啥总结的,连续敲了近3个小时,如果有错误请各位大佬指点,如果觉得写得还行的点点赞哦,感谢!!!

本文章中出现的所有代码皆是我自己实现的react(未实现完)中拷贝出来的,能通过官方的测试样例请放心,项目地址:github.com/samllbin/My...

相关推荐
疯狂的沙粒17 分钟前
Vue 前端大屏做多端屏幕适配时,如何让其自动适配多种不同尺寸的屏幕?
前端·javascript·vue.js
范小多21 分钟前
24小时学会Python Visual code +Python Playwright通过谷歌浏览器取控件元素(连载、十一)
服务器·前端·python
ooolmf21 分钟前
matlab2024读取温度01
java·前端·javascript
打工人小夏23 分钟前
前端vue3项目使用nprogress动画组件,实现页面加载动画
前端
一颗宁檬不酸24 分钟前
前端农业商城中产品产地溯源功能的实现
前端
李少兄32 分钟前
深入理解前端中的透视(Perspective)
前端·css
江公望41 分钟前
HTML5 History 模式 5分钟讲清楚
前端·html·html5
云和数据.ChenGuang1 小时前
Zabbix Web 界面安装时**无法自动创建配置文件 `zabbix.conf.php`** 的问题
前端·zabbix·运维技术·数据库运维工程师·运维教程
码界奇点1 小时前
Java Web学习 第15篇jQuery万字长文详解从入门到实战解锁前端交互新境界
java·前端·学习·jquery
前端老曹1 小时前
vue3 三级路由无法缓存的终极解决方案
前端·javascript·vue.js·vue