一. 往期文章推荐
强烈推荐阅读第一篇文章手写mini React,理解React渲染原理,有助于理解本文章内容
1.1 React原理系列总结
二. useLayoutEffect
方法介绍
useLayoutEffect
方法接收两个参数,第一个是执行函数create
,第二个是依赖deps
,在首次渲染时会执行一次create
方法,在下次渲染时会比对deps
值是否变更,如果有会再次执行create
方法。
我们可以在create
方法里返回一个函数destroy
,destroy
方法会在deps
值发生变化或组件卸载时执行。
与useEffect
差异点是执行时机不同:useEffect
是页面完成渲染之后执行,useLayoutEffect
是页面完成渲染之前执行。
三. 实现useLayoutEffect
3.1 定义Hook
对象原型
每次调用React Hook
方法都会生成一个Hook
对象,多个Hook
对象之间通过next
指针进行索引,构成单链表数据结构。
function Hook() {
this.memoizedState = null // 记录hook数据
this.next = null // 记录下一个Hook对象
this.queue = [] // 收集更新state方法
}
3.2 修改FiberNode
对象原型
新增updateQueue
属性用于收集useEffect
、useLayoutEffect
和useInsertionEffect
数据,例如入参create
方法,依赖deps
和调用create
返回的destroy
方法。
function FiberNode() {
this.updateQueue = null
}
3.3 定义函数组件方法调用装饰器
在构建虚拟DOM
树阶段,每次调用函数组件方法(例如App Compoent Function
)时会执行renderWithHooks
方法,记录新FiberNode
节点,在调用React Hook
方法时会用到。
// 记录新FiberNode节点
let currentlyRenderingFiber = null
// 记录旧FiberNode节点的Hook链表节点
let currentHook = null
// 记录新FiberNode节点的Hook链表节点
let workInProgressHook = null
/**
* @param {*} current 旧FiberNode节点
* @param {*} workInProgress 新FiberNode节点
* @param {*} Component 函数组件方法
* @param {*} props 函数组件方法入参属性
*/
export function renderWithHooks(current, workInProgress, Component, props) {
// 记录新FiberNode节点
currentlyRenderingFiber = workInProgress
// 将FiberNode节点的updateQueue属性赋值为null,重新收集useEffect、useLayoutEffect、useInsertionEffect数据
workInProgress.updateQueue = null
// 调用组件方法获取child ReactElement
const children = Component(props)
currentlyRenderingFiber = null
currentHook = null
workInProgressHook = null
return children
}
3.4 定义HookFlags
枚举值
通过HookFlags
区分effect
类型,HookInsertion
代表useInsertionEffect
,HookLayout
代表useLayoutEffect
,HookPassive
代表useEffect
export const HookHasEffect = 1 // effect通用类型
export const HookInsertion = 2 // 对应useInsertionEffect
export const HookLayout = 4 // 对应useLayoutEffect
export const HookPassive = 8 // 对应useEffect
3.5 首次调用useLayoutEffect
方法
当首次执行组件方法调用useLayoutEffect
方法时,执行mountLayoutEffect
方法逻辑
- 将
FiberNode
节点的flags
属性赋值为Update
- 创建
Hook
对象,构建Hook
链表 - 创建
effect
对象,tag
属性即effect
类型,create
即入参执行函数,deps
即入参依赖,destroy
属性时调用create
返回的方法。将effect
对象赋值给FiberNode
节点的updateQueue
属性和Hook
对象的memoizedState
属性
function mountWorkInProgressHook() {
const hook = new Hook()
// 构建Hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
/**
* @param {*} tag HookFlags类型
* @param {*} create 入参执行函数
* @param {*} deps 入参依赖
* @param {*} destroy 入参执行函数返回值
*/
function pushEffect(tag, create, deps, destroy = null) {
const effect = { tag, create, deps, destroy }
if (currentlyRenderingFiber.updateQueue === null)
currentlyRenderingFiber.updateQueue = []
const queue = currentlyRenderingFiber.updateQueue
queue.push(effect)
return effect
}
/**
* @param {*} fiberFlags FiberNode节点副作用
* @param {*} hookFlags Effect类型
* @param {*} create 入参执行函数
* @param {*} deps 入参依赖
*/
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook()
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps)
}
function mountLayoutEffect(create, deps) {
mountEffectImpl(UpdateEffect, HookLayout, create, deps)
}
3.6 调用useLayoutEffect create
方法
在更新DOM
阶段,递归遍历FiberNode
节点,判断flags
属性值是否有Update
,如果有则遍历该节点updateQueue
属性值,调用effect
对象的create
方法
// 调用effect create方法,获取destroy方法
function commitHookEffectListMount(finishWork, hookFlags) {
const queue = finishWork.updateQueue
queue.forEach((effect) => {
if ((effect.tag & hookFlags) === hookFlags) {
effect.destroy = effect.create()
}
})
}
function recursivelyTraverseLayoutEffects(finishWork) {
if (finishWork.subtreeFlags & (Ref | Update)) {
let child = finishWork.child
while (child !== null) {
commitLayoutEffectOnFiber(child)
child = child.sibling
}
}
}
export function commitLayoutEffectOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraverseLayoutEffects(finishWork)
if (finishWork.flags & Update) {
// 调用useLayoutEffect的create方法
commitHookEffectListMount(finishWork, HookLayout | HookHasEffect)
}
break
}
default: {
recursivelyTraverseLayoutEffects(finishWork)
break
}
}
}
3.7 更新调用useLayoutEffect
方法
- 创建
Hook
对象,复制旧Hook
对象属性值,构建Hook
链表 - 比对新旧
effect
对象的dpes
属性值是否相同,相同则不需要将FiberNode
节点的flags
属性值赋值为Update
,即在更新DOM
阶段不会执行effect create
方法,如果不相同则需要将flags
属性赋值为Update
- 创建新的
effect
对象赋值给FiberNode
节点的updateQueue
属性和Hook
对象的memoizedState
属性
function updateWorkInProgressHook() {
if (currentHook === null) {
currentHook = currentlyRenderingFiber.alternate.memoizedState
} else {
currentHook = currentHook.next
}
const hook = new Hook()
hook.memoizedState = currentHook.memoizedState
hook.queue = currentHook.queue
// 构建hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
// 通过Object.is方法比对deps属性值是否变更
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < nextDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}
/**
* @param {*} fiberFlags FiberNode节点副作用
* @param {*} hookFlags Effect类型
* @param {*} create 入参执行函数
* @param {*} deps 入参依赖
*/
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
// 创建Hook对象,构建Hook单链表
const hook = updateWorkInProgressHook()
// 获取旧Effect对象
const effect = hook.memoizedState
// 判断新旧Effect deps是否相同
if (deps !== null && areHookInputsEqual(deps, effect.deps)) {
hook.memoizedState = pushEffect(hookFlags, create, deps, effect.destroy)
return
}
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps, effect.destroy)
}
function updateLayoutEffect(create, deps) {
updateEffectImpl(UpdateEffect, HookLayout, create, deps)
}
3.8 更新调用useLayoutEffect destory
方法
-
递归遍历
FiberNode
节点,判断flags
属性值是否有Update
,如果有则遍历该节点updateQueue
属性中的effect
对象,调用effect destroy
方法 -
如果
FiberNode
节点的deletions
属性不为空,说明有child FiberNode
节点被删除,则递归遍历child FiberNode
节点,调用对应effect destroy
方法
// 调用effect destroy方法
function commitHookEffectListUnmount(finishWork, hookFlags) {
const queue = finishWork.updateQueue
if (queue !== null) {
queue.forEach((effect) => {
if ((effect.tag & hookFlags) === hookFlags && effect.destroy) {
const destroy = effect.destroy
effect.destroy = null
destroy()
}
})
}
}
function recursivelyTraverseDeletionEffects(finishWork) {
let child = finishWork.child
while (child !== null) {
commitDeletionEffectsOnFiber(child)
child = child.sibling
}
}
function commitDeletionEffectsOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent:
commitHookEffectListUnmount(finishWork, HookLayout)
recursivelyTraverseDeletionEffects(finishWork)
break
default:
recursivelyTraverseDeletionEffects(finishWork)
break
}
}
function recursivelyTraverseMutationEffects(finishWork) {
if (finishWork.deletions !== null) {
// 递归遍历要删除的child FiberNode节点,调用effect destroy方法
finishWork.deletions.forEach((fiber) => {
commitDeletionEffectsOnFiber(finishWork, fiber)
})
}
// 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
let child = finishWork.child
while (child !== null) {
commitMutationEffectsOnFiber(child)
child = child.sibling
}
}
}
export function commitMutationEffectsOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraverseMutationEffects(finishWork)
if (finishWork.flags & Update) {
// 调用useLayoutEffect destroy方法
commitHookEffectListUnmount(finishWork, HookLayout | HookHasEffect)
}
break
}
default:
recursivelyTraverseMutationEffects(finishWork)
break
}
}
3.9 定义useLayoutEffect
方法
如果新节点不存在旧FiberNode
节点,说明是首次调用函数组件方法,则调用mountLayoutEffect
方法,否则调用updateLayoutEffect
方法
function useLayoutEffect(create, deps = null) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
mountLayoutEffect(create, deps)
} else {
updateLayoutEffect(create, deps)
}
}