一. 往期文章推荐
强烈推荐阅读第一篇文章手写mini React,理解React渲染原理,有助于理解本文章内容
1.1 React原理系列总结
二. useEffect
方法介绍
useEffect
方法接收两个参数,第一个是执行函数create
,第二个是依赖deps
,在首次渲染时会执行一次create
方法,在下次渲染时会比对deps
值是否变更,如果有会再次执行create
方法
我们可以在create
方法里返回一个函数destroy
,destroy
方法会在deps
值发生变化或组件卸载时执行。
例如下面这段代码,在首次渲染时控制台会输出HelloWorld Mount
、App Mount
,在点击click
按钮时将visible
设置为false
,会触发更新渲染,控制台会输出HelloWorld Unmount
function HelloWorld() {
useEffect(() => {
console.log('HelloWorld Mount')
return () => {
console.log('HelloWorld Unmount')
}
}, [])
return <h1>hello world</h1>
}
function App() {
const [visible, useVisible] = useState(true)
useEffect(() => {
console.log('App Mount')
}, [])
return (
<div>
<button onClick={() => setVisible(!visible)}>click</button>
{visible && <HelloWorld />}
</div>
)
}
三. 实现useEffect
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 // 记录useEffect数据
}
3.3 定义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.4 定义函数组件方法调用装饰器
在构建虚拟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.5 首次调用useEffect
方法
当首次执行组件方法调用useEffect
方法时,执行mountEffect
方法逻辑
- 将
FiberNode
节点的flags
属性赋值为Passive
- 创建
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 }
// 将effect添加到FiberNode节点updateQueue属性中,在更新DOM阶段执行
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) {
// 创建Hook对象,构建Hook单链表
const hook = mountWorkInProgressHook()
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps)
}
function mountEffect(create, deps) {
mountEffectImpl(PassiveEffect, HookPassive, create, deps)
}
3.6 调用useEffect create
方法
递归遍历FiberNode
节点,判断flags
属性值是否有Passive
,如果有则遍历该节点updateQueue
属性值,调用useEffect
的create
方法
// 遍历调用useEffect的create方法,获取destroy方法
function commitHookPassiveMountEffects(finishWork, hookFlags) {
const queue = finishWork.updateQueue
queue.forEach((effect) => {
if ((effect.tag & hookFlags) === hookFlags) {
effect.destroy = effect.create()
}
})
}
function recursivelyTraversePassiveMountEffects(finishWork) {
if (finishWork.subtreeFlags & Passive) {
let child = finishWork.child
while (child !== null) {
commitPassiveMountOnFiber(child)
child = child.sibling
}
}
}
function commitPassiveMountOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraversePassiveMountEffects(finishWork)
if (finishWork.flags & Passive) {
// 调用useEffect的create方法
commitHookPassiveMountEffects(finishWork, HookPassive | HookHasEffect)
}
break
}
default: {
recursivelyTraversePassiveMountEffects(finishWork)
break
}
}
}
3.7 更新调用useEffect
方法
- 创建
Hook
对象,复制旧Hook
对象属性值,构建Hook
链表 - 比对新旧
effect
对象的dpes
属性值是否相同,相同则不需要将FiberNode
节点的flags
属性值赋值为Passive
,即在更新DOM
阶段不会执行effect create
方法,如果不相同则需要将flags
属性赋值为Passive
- 创建新的
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
}
// 比对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 updateEffect(create, deps) {
updateEffectImpl(PassiveEffect, HookPassive, create, deps)
}
3.8 更新调用useEffect destory
方法
-
递归遍历
FiberNode
节点,判断flags
属性值是否有Passive
,如果有则遍历该节点updateQueue
属性中的effect
对象,调用effect destroy
方法 -
如果
FiberNode
节点的deletions
属性不为空,说明有child FiberNode
节点被删除,则递归遍历child FiberNode
节点,调用对应effect destroy
方法
// 遍历调用useEffect的destroy方法,将effect的destroy属性赋值为null
function commitHookPassiveUnmountEffects(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 recursivelyTraversePassiveUnmountEffects(finishWork) {
if (finishWork.deletions !== null) {
// 采用深度优先遍历算法
for (let i = 0; i < finishWork.deletions.length; i++) {
let fiber = finishWork.deletions[i]
while (true) {
let nextChild = fiber.child
commitHookEffectListUnmount(fiber, HookPassive)
while (nextChild !== null) {
fiber = nextChild
commitHookEffectListUnmount(fiber, HookPassive)
nextChild = nextChild.child
}
if (fiber.sibling !== null) {
nextChild = fiber.sibling
fiber.sibling = null
fiber = nextChild
} else {
if (fiber === finishWork.deletions[i]) break
fiber = fiber.return
fiber.child = null
}
}
}
}
if (finishWork.subtreeFlags & (Passive | ChildDeletion)) {
let child = finishWork.child
while (child !== null) {
commitPassiveUnmountOnFiber(child)
child = child.sibling
}
}
}
function commitPassiveUnmountOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraversePassiveUnmountEffects(finishWork)
if (finishWork.flags & Passive) {
commitHookPassiveUnmountEffects(finishWork, HookPassive | HookHasEffect)
}
break
}
default: {
recursivelyTraversePassiveUnmountEffects(finishWork)
break
}
}
}
3.9 定义useEffect
方法
如果新节点不存在旧FiberNode
节点,说明是首次调用函数组件方法,则调用mountEffect
方法,否则调用updateEffect
方法
function useEffect(create, deps = null) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
mountEffect(create, deps)
} else {
updateEffect(create, deps)
}
}