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

相关推荐
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport5 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg5 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全