一. 往期文章推荐
1.1 手写mini React,理解React渲染原理
1.2 手写React useState,理解useState原理
二. useEffect
方法介绍
useEffect
方法接收两个参数,第一个是执行函数create
,第二个是依赖deps
,在首次渲染时会执行一次create
方法,在下次渲染时会比对deps
值是否变更,如果有会再次执行create
方法
我们可以在create
方法里返回一个函数destroy
,destroy
方法会在deps
值发生变化或组件卸载时执行。
例如下面这段代码,在首次渲染时控制台会输出HelloWorld Mount
、App Mount
,在点击click
按钮时将visible
设置为false
,会触发更新渲染,控制台会输出HelloWorld Unmount
javascript
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
对象原型
每次调用useEffect
方法时都会创建一个hook
对象,多个hook
对象通过next
指针索引,构建单链表数据结构
javascript
function Hook() {
this.memoizedState = null // 记录hook数据
this.next = null // 记录下一个hook
this.queue = [] // 收集更新state方法
}
3.2 修改FiberNode
对象原型
新增updateQueue
属性,记录useEffect
数据,如执行方法create
,依赖deps
和调用create
方法返回的destroy
方法
javascript
function FiberNode() {
this.updateQueue = null // 记录useEffect数据
}
3.3 定义函数组件方法调用装饰器
当每次调用函数组件方法(例如App Compoent Function
)时会执行renderWithHooks
方法,记录新FiberNode
节点、旧FiberNode
节点的hook
链表节点,在调用useEffect
方法时会用到
javascript
// 记录新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
if (current !== null) {
// 记录旧FiberNode节点的hook链表
currentHook = current.memoizedState
}
workInProgress.updateQueue = null
// 调用组件方法获取child ReactElement
const children = Component(props)
currentlyRenderingFiber = null
currentHook = null
workInProgressHook = null
return children
}
3.4 首次调用useEffect
方法
当首次执行函数组件方法,调用useEffect
方法时会执行mountEffect
方法逻辑,创建hook
对象,将useEffect
的入参赋值给hook
的memoizedState
属性和FiberNode
节点的updateQueue
属性
javascript
function mountWorkInProgressHook() {
const hook = new Hook()
// 构建hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
function pushEffect(create, deps, destroy = null) {
const effect = { create, deps, destroy }
// 将effect添加到FiberNode节点updateQueue属性中,在更新DOM阶段执行
if (currentlyRenderingFiber.updateQueue === null)
currentlyRenderingFiber.updateQueue = []
const queue = currentlyRenderingFiber.updateQueue
queue.push(effect)
return effect
}
function mountEffect(create, deps) {
// 创建hook对象,构建hook单链表
const hook = mountWorkInProgressHook()
// 将当前FiberNode节点的flags赋值为Passive,flags属性表示副作用,例如更新,删除等,在更新DOM阶段会根据flags属性值执行对应的副作用逻辑
currentlyRenderingFiber.flags |= Passive
hook.memoizedState = pushEffect(create, deps)
}
3.5 调用useEffect create
方法
递归遍历FiberNode
节点,判断flags
属性值是否有Passive
,如果有则遍历该节点updateQueue
属性值,调用useEffect
的create
方法
javascript
// 遍历调用useEffect的create方法,获取destroy方法
function commitHookPassiveMountEffects(finishWork) {
const queue = finishWork.updateQueue
queue.forEach((effect) => {
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) {
// 调用useEffeact的create方法
commitHookPassiveMountEffects(finishWork)
}
break
}
default: {
recursivelyTraversePassiveMountEffects(finishWork)
break
}
}
}
3.6 更新调用useEffect
方法
当触发更新渲染重新调用useEffect
方法时,会比对deps
是否变更,如果没有变更则不需要将FiberNode
节点flags
属性赋值为Passive
,即在更新DOM
阶段不会执行useEffect create
方法,如果deps
变更则需要将flags
属性赋值为Passive
javascript
// 比对deps属性值是否变更
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < nextDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}
function updateEffect(create, deps) {
const hook = mountWorkInProgressHook()
// 获取旧useEffect入参数据
const effect = currentHook.memoizedState
if (deps !== null && areHookInputsEqual(deps, effect.deps)) {
hook.memoizedState = pushEffect(create, deps, effect.destroy)
return
}
currentlyRenderingFiber.flags |= Passive
hook.memoizedState = pushEffect(create, deps)
}
3.7 更新调用useEffect destory
方法
递归遍历FiberNode
节点,判断flags
属性值是否有Passive
,如果有则遍历该节点updateQueue
属性中的effect
,调用useEffect
的destroy
方法
如果FiberNode
节点的deletions
属性不为空,说明有child FiberNode
节点被删除,则递归遍历FiberNode
节点deletions
属性中的child FiberNode
节点,调用对应useEffect
的destroy
方法
javascript
// 遍历调用useEffect的destroy方法,将effect的destroy属性赋值为null
function commitHookPassiveUnmountEffects(finishWork) {
const queue = finishWork.updateQueue
if (queue !== null) {
queue.forEach((effect) => {
if (effect.destroy) {
const destroy = effect.destroy
effect.destroy = null
destroy()
}
})
}
}
function recursivelyTraversePassiveUnmountEffects(finishWork) {
if (finishWork.deletions !== null) {
// 采用深度优先遍历算法,优先执行分支叶子节点useEffect的destroy方法
for (let i = 0; i < finishWork.deletions.length; i++) {
let fiber = finishWork.deletions[i]
while (true) {
let nextChild = fiber.child
while (nextChild !== null) {
fiber = nextChild
nextChild = nextChild.child
}
commitHookPassiveUnmountEffects(fiber)
if (fiber.sibling !== null) {
nextChild = fiber.sibling
fiber.sibling = null
} 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)
}
break
}
default: {
recursivelyTraversePassiveUnmountEffects(finishWork)
break
}
}
}
3.8 定义useEffect
方法
如果新节点不存在旧FiberNode
节点,说明是首次调用函数组件方法,则调用mountEffect
方法,否则调用updateEffect
方法
javascript
function useEffect(create, deps = null) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
mountEffect(create, deps)
} else {
updateEffect(create, deps)
}
}