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

写在前面

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

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

github.com/kongyich/ti...

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

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

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

深入react源码!react事件模型是如何实现的❓

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

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

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

期待点赞!😁😁

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

一. 基本概念

在实现之前,先来看一下useEffect​的基本使用,useEffect​是react中非常重要,也是最常用的几个hook之一,它的出现可以使函数组件拥有类组件上的一些功能,比如生命周期函数,弥补了函数式组件没有生命周期的缺陷。并且提供了为某些依赖项增加副作用函数,当依赖项发生改变,触发副作用函数。

基本使用:

ts 复制代码
useEffect(()=>{ 
    return destory
}, deps)

参数:

  • callbackuseEffect 的第一个入参,最终返回 destory,它会在下一次 callback 执行之前调用,其作用是清除上次的 callback 产生的副作用;
  • deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖改变,执行上一次的 callback 返回的 destory 和新的 effect 第一个参数 callback

挂载和卸载阶段触发

js 复制代码
useEffect(() => {
   console.log("挂载阶段");

   return () => {
      console.log("卸载阶段");
   };
}, []);

如果依赖项传入一个空数组,那么useEffect​分别会在挂载阶段和组件卸载阶段触发,这个效果可以替代类组件中的生命周期。

依赖项变化触发

js 复制代码
  useEffect(() => {
    console.log("num改变执行");
  }, [num]);

num​这个依赖项发生变化时,会执行副作用函数回调。

前面我们已经实现过useState​这个hook,在动手实现useEffect​之前先了解一下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​流程以及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​ 保存 hook​ 信息,每一个 hook​ 执行都会产生一个 hook​ 对象,hook​ 对象中,保存着当前 hook​的信息,不同 hook​保存的形式不同。每一个 hook​ 通过 next​ 链表建立起关系。

useEffect的hook链表结构

useEffect​的hook链表对象与useState​有一些差异,具体体现在useState​的hook对象的memoizedState​属性保存的是具体的值,而useEffect​保存了一个对象:

对象中保存与useEffect​相关的一些信息:create​(依赖函数),destroy​(卸载函数),deps​(依赖项),next​(下一个useEffect​ hook对象)。其中next​属性也是一个链表指针,将该fiber​下的所有useEffect​ hook对象连接在一起。

也就是说如果一个函数组件类型的fiber​节点中存在许多不同类型的hook,那么这个fiber​节点的memoizedState​属性将会是这样的:

四. useEffect

在介绍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 复制代码
export const useEffect = (create, deps) => {
	const dispatcher = resolveDispatcher();
	return dispatcher.useEffect(create, deps);
};

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 = {
	useEffect: mountEffect
};

const HookDispatcherOnUpdate = {
	useEffect: updateEffect
};

flags

为了在fiber​节点中标记副作用,增加PassiveEffect​代表具有effect​副作用的fiber​节点。

Passive​标记useEffect​对象,HookHasEffect​表示当前effect​本次更新存在副作用。

js 复制代码
// effect对象的标记
export const Passive = 0b0010;
export const HookHasEffect = 0b0001;

// fiber节点的标记
export const PassiveEffect = 0b0001000;

五. mount

在初始化阶段,useEffect​需要开始初始化相关的数据结构:

  • 创建hook对象并连接到原有的hook链表中
  • 创建effect对象保存到hook对象中的memoizedState属性
  • 构建effect链表,并保存到fiber.updateQueue.lastEffect属性中

js 复制代码
function mountEffect(create, deps) {
	// 创建hook对象
	const hook = mountWorkInProgresHook();
	const nextDeps = deps === undefined ? null : deps;
	// 为fiber标记flag
	currentlyRenderingFiber.flags |= PassiveEffect;
	// 生成effect对象,并保存effect链表
	hook.memoizedState = pushEffect(
		// 标记effect对象的flag
		Passive | HookHasEffect,
		create,
		undefined,
		nextDeps
	);
}

mount​阶段创建主要考虑当前执行的useEffect​函数是当前函数组件的第几个,如果是第一个需要初始化给fiber​节点的memoizedState​属性并赋值给全局变量workInProgressHook​。如果是后续的useEffect​,直接使用全局变量workInProgressHook​的next​绑定。

js 复制代码
function mountWorkInProgresHook() {
	// 定义hook对象
	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;
}

pushEffect​函数用来创建effect​对象,并且将同一个函数组件内的多个effect​对象连接成链表的形式,保存在函数组件的fiber​节点的updateQueue.lastEffect​中,在lastEffect​中保存最后一个effect​对象,但是由于effect​链表为环形链表,所以也就相当于将整个effect​链表保存在了lastEffect​属性。

js 复制代码
function pushEffect(
	hookFlags,
	create,
	destroy,
	deps
) {
	const effect = {
		tag: hookFlags,
		create,
		destroy,
		deps,
		next: null
	};
	const fiber = currentlyRenderingFiber;
	const updateQueue = fiber.updateQueue;
	// 如果无更新队列updateQueue?
	// 1. 创建fiber节点的更新队列
	// 2. 创建lastEffect属性保存effect链表
	if (updateQueue === null) {
		const updateQueue = createFCUpdateQueue();
		fiber.updateQueue = updateQueue;
		effect.next = effect;
		updateQueue.lastEffect = effect;
	} else {
		// 插入effect
		const lastEffect = updateQueue.lastEffect;
		// lastEffect是否存在?
		if (lastEffect === null) {
			effect.next = effect;
			updateQueue.lastEffect = effect;
		} else {
			// 拼接链表,环形
			const firstEffect = lastEffect.next;
			lastEffect.next = effect;
			effect.next = firstEffect;
			updateQueue.lastEffect = effect;
		}
	}
	return effect;
}

如果当前fiber​节点(函数组件类型的fiber​节点)没有updateQueue​属性,那么先创建updateQueue​属性,与普通的fiber​节点的更新队列略有不同,函数组件类型的fiber​节点多个一个lastEffect​保存effect​链表:

js 复制代码
function createFCUpdateQueue<State>() {
	const updateQueue = createUpdateQueue();
	// 创建lastEffect属性
	updateQueue.lastEffect = null;
	return updateQueue;
}

// 创建updateQueue
export const createUpdateQueue = () => {
	return {
		shared: {
			pending: null
		},
		dispatch: null
	};
};

如果是链表的第一项,为了创建环形链表,需要将next​指针指向自身,也就是整个链表的头尾节点都是同一个:

js 复制代码
effect.next = effect;

六. update

update​阶段主要是对比两棵fiber​树中对应的hook​对象和effect​对象是否发生了变化。

  • hook对象更新前后有无变化?检查是否能够复用
  • effect对象更新前后有无变化?检查是否能够复用

effect​对象是否可以复用的判断依据是根据比较依赖项(浅比较),确定新的effect​对象是否加入HookHasEffect​这个flag​标记,如果依赖项没有变化,则不加入副作用标记。

destroy​属性保存上次副作用函数的执行结果,直接在effect​对象中获取。

js 复制代码
function updateEffect(create, deps) {
	// 复用hook对象,构建新的hook链表
	const hook = updateWorkInProgresHook();
	const nextDeps = deps === undefined ? null : deps;
	let destroy;
	// currentHook 全局变量,原fiber树对应的hook对象
	if (currentHook !== null) {
		// 获取原effect对象,destroy卸载函数是上次执行create时保存的执行结果
		const prevEffect = currentHook.memoizedState;
		// 获取卸载函数
		destroy = prevEffect.destroy;

		if (nextDeps !== null) {
			// 浅比较依赖
			const prevDeps = prevEffect.deps;
			// 依赖项发生变化?
			if (areHookInputsEqual(nextDeps, prevDeps)) {
				hook.memoizedState = pushEffect(Passive, create, destroy, nextDeps);
				return;
			}
		}
		// 浅比较 不相等
		currentlyRenderingFiber.flags |= PassiveEffect;
		hook.memoizedState = pushEffect(
			Passive | HookHasEffect,
			create,
			destroy,
			nextDeps
		);
	}
}

对比依赖项:

js 复制代码
function areHookInputsEqual(nextDeps, prevDeps) {
	if (prevDeps === null || nextDeps === null) {
		return false;
	}
	for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
		// 	使用Object.is浅比较
		if (Object.is(prevDeps[i], nextDeps[i])) {
			continue;
		}
		return false;
	}
	return true;
}

‍ ​updateWorkInProgresHook​函数复用hook​对象。值得注意的是,如果在执行更新时获取某一个useEffect​旧的hook​对象为空,则说明本次更新比原来多了一个hook​,

这也是hook​不能用在条件判断中的原因,因为条件判断可能会存在动态逻辑,在更新时会导致hook​对象无法对应。

js 复制代码
function updateWorkInProgresHook() {
	let nextCurrentHook;

	if (currentHook === null) {
		// 这是这个函数组件 update时的第一个hook
		const current = currentlyRenderingFiber?.alternate;
		if (current !== null) {
			nextCurrentHook = current?.memoizedState;
		} else {
			// mount
			nextCurrentHook = null;
		}
	} else {
		// 这个函数组件 update时 后续的hook
		nextCurrentHook = currentHook.next;
	}
	// 本次有多余hook
	if (nextCurrentHook === null) {
		throw new Error(
			`组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行时多`
		);
	}

	currentHook = nextCurrentHook;
	// 复用hook对象
	const newHook = {
		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 {
		// 后续的hook 使用next连接
		workInProgressHook.next = newHook;
		workInProgressHook = newHook;
	}
	return workInProgressHook;
}

七. useEffect工作流程

commit​阶段开始调度副作用,收集回调,在此之前我们要在根节点的属性中新增两个集合,用于收集unmout​时需要执行的回调和update​时需要执行的回调。

diff 复制代码
export class FiberRootNode {

	// ...

	pendingPassiveEffects: PendingPassiveEffects;
	constructor(container, hostRootFiber) {
		// ...

++		this.pendingPassiveEffects = {
++			unmount: [],
++			update: []
++		};
	}
}

1. 调度副作用

在进入commit​阶段开始后,也就是进入commitRoot​函数,判断根节点中是否存在PassiveMask​这个标记,代表需要调度执行保存在根节点的副作用函数,然后以普通优先级调度一个任务:

由于调度副作用是异步执行,所以虽然任务会先开始调度,但是收集回调的逻辑会先执行。

js 复制代码
let rootDoesHasPassiveEffects = false;

function commitRoot(root) {

	// ...

	if (
		(finishedWork.flags & PassiveMask) !== NoFlags ||
		(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
	) {
		if (!rootDoesHasPassiveEffects) {
			rootDoesHasPassiveEffects = true;
			// 调度副作用
			scheduleCallback(NormalPriority, () => {
				// 执行副作用
				flushPassiveEffects(root.pendingPassiveEffects);
				return;
			});
		}
	}

	// ...

	commitMutationEffects(finishedWork, root);
}

本次更新的任何create​回调都必须在所有上一次更新的destroy​回调执行完后再执行。

整体执行流程包括:

  1. 遍历effect链表
  2. 首先触发所有unmount effect,且对于某个fiber,如果触发了unmount destroy,本次更新不会再触发update create
  3. 触发所有上次更新的destroy
  4. 触发所有这次更新的create

mount​、update​时的区别:

  • mount时:标记PassiveEffect
  • update时:deps变化时标记PassiveEffect
js 复制代码
function flushPassiveEffects(pendingPassiveEffects) {
	// 执行unmount集合
	pendingPassiveEffects.unmount.forEach((effect) => {
		commitHookEffectListUnmount(Passive, effect);
	});
	pendingPassiveEffects.unmount = [];
	// 执行update集合 
	pendingPassiveEffects.update.forEach((effect) => {
		commitHookEffectListDestroy(Passive | HookHasEffect, effect);
	});
	// 执行update集合 
	pendingPassiveEffects.update.forEach((effect) => {
		commitHookEffectListCreate(Passive | HookHasEffect, effect);
	});
	pendingPassiveEffects.update = [];
	flushSyncCallbacks();
}

useEffect​执行时,关于副作用回调和销毁函数会涉及这么几种情况:函数组件被销毁,此时应该执行所有的销毁函数,由于此时整个函数组件已经被销毁,后续也不应该再执行副作用函数,所以移除unmount​列表中所有effect​对象的HookHasEffect​标记。

js 复制代码
// 任务执行
function commitHookEffectList(flags, lastEffect, callback) {
	let effect = lastEffect.next
	// 循环执行effect链表
	do {
		if ((effect.tag & flags) === flags) {
			callback(effect);
		}
		effect = effect.next
	} while (effect !== lastEffect.next);
}

export function commitHookEffectListUnmount(flags, lastEffect) {
	commitHookEffectList(flags, lastEffect, (effect) => {
		const destroy = effect.destroy;
		if (typeof destroy === 'function') {
			destroy();
		}
		// 移除HookHasEffect标记,后面不会再执行副作用函数
		effect.tag &= ~HookHasEffect;
	});
}

更新任务先触发所有上次更新的destroy​函数,执行完后,触发所有这次更新的create​函数。本次更新产生的返回值将作为新的effect​对象的destroy​。

js 复制代码
export function commitHookEffectListDestroy(flags, lastEffect) {
	commitHookEffectList(flags, lastEffect, (effect) => {
		const destroy = effect.destroy;
		if (typeof destroy === 'function') {
			destroy();
		}
	});
}

export function commitHookEffectListCreate(flags, lastEffect) {
	commitHookEffectList(flags, lastEffect, (effect) => {
		const create = effect.create;
		if (typeof create === 'function') {
			// 将执行结果保存为destroy
			effect.destroy = create();
		}
	});
}

2. 收集回调

关于收集回调的时机,在进入commit​流程后,首先还要从上到下,从下到上的回溯一遍fiber​树,目的是根据fiber​上的不同标记开始对fiber​节点中保存的dom​进行操作,比如:新增,更新,删除等。(详细参考前面初始化急更新的文章)

之所以会选择在commit​阶段的回溯过程中收集,我们先来看一个例子:

js 复制代码
export default function App() {
  const [num, updateNum] = useState(0);
  useEffect(() => {
    console.log("App mount");
  }, []);

  useEffect(() => {
    console.log("num change create", num);
    return () => {
      console.log("num change destroy", num);
    };
  }, [num]);

  return (
    <div onClick={() => updateNum(num + 1)}>
      {num === 0 ? <Child /> : "noop"}
    </div>
  );
}

function Child() {
  useEffect(() => {
    console.log("Child mount");
    return () => console.log("Child unmount");
  }, []);

  return "i am child";
}

在这个例子中父子组件同时定义了useEffect​,那么它的执行顺序是什么样子的呢?

根据mount​阶段的执行结果可知,在初始化时,先执行了子组件Child​中useEffect​函数的初始化,同样副作用函数也会先执行,所以在收集副作用函数时,子组件的effect​副作用函数必要要先被收集。在不同层级的函数组件中遵循先子后父的执行顺序,如果是同一层级则按顺序执行。

按照useEffect​的执行顺序,我们在向上回溯的过程中收集effect​。

js 复制代码
const commitMutaitonEffectsOnFiber = (
	finishedWork,
	root
) => {
	const flags = finishedWork.flags;

	if ((flags & Placement) !== NoFlags) {
		// ...
	}
	if ((flags & Update) !== NoFlags) {
		// ...
	}
	if ((flags & ChildDeletion) !== NoFlags) {
		// ...
	}
	if ((flags & PassiveEffect) !== NoFlags) {
		// 收集更新回调
		commitPassiveEffect(finishedWork, root, 'update');
		// 收集回调后,移除副作用标记
		finishedWork.flags &= ~PassiveEffect;
	}
};

收集的过程比较简单,获取fiber​节点中的updateQueue.lastEffect​属性,根据type​保存在根节点的pendingPassiveEffects​中。

js 复制代码
function commitPassiveEffect(fiber, root, type) {
	// update unmount
	if (
		fiber.tag !== FunctionComponent ||
		(type === 'update' && (fiber.flags & PassiveEffect) === NoFlags)
	) {
		return;
	}
	const updateQueue = fiber.updateQueue
	if (updateQueue !== null) {
		if (updateQueue.lastEffect === null && __DEV__) {
			console.error('当FC存在PassiveEffect flag时,不应该不存在effect');
		}
		root.pendingPassiveEffects[type].push(updateQueue.lastEffect);
	}
}

unmount​列表收集过程在节点删除的流程中进行。

js 复制代码
function commitDeletion(childToDelete, root) {
	const rootChildrenToDelete = [];

	// 递归子树
	commitNestedComponent(childToDelete, (unmountFiber) => {
		switch (unmountFiber.tag) {
			case HostComponent:
				// ...
				return;
			case HostText:
				// ...
				return;
			case FunctionComponent:
				// 收集卸载函数
				commitPassiveEffect(unmountFiber, root, 'unmount');
				return;
			default:
				if (__DEV__) {
					console.warn('未处理的unmount类型', unmountFiber);
				}
		}
	});
}

当一个节点被删除或卸载时,不仅仅要删除此节点本身的各种副作用,还需要对所有子节点进行卸载。commitNestedComponent​会针对待删除节点的每个子fiber​节点执行回调函数。收集待删除节点的所有子节点的卸载函数。

‍ ‍ ‍写在最后

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

相关推荐
@大迁世界7 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路16 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug19 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213821 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中43 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星1 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全