你不知道的函数式组件在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...

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js