一 引言
Hooks的出现让函数组件拥有了类组件的能力,有自己的状态,可以处理副作用,解决逻辑复用难的问题。 我们可以带着如下几个问题去阅读本文:
- Hooks 如何把状态保存起来?保存的信息存在了哪里?
- React Hooks 为什么不能写在条件语句中?
- useEffect 添加依赖项发生变化,为什么 useEffect 回调函数 create 重新执行?
- useEffect 和 useLayoutEffect 有什么区别?
开始本文之前先说明一下,本文代码是延续 React原理系列之一:调和与Fiber,由于Hooks的实现离不开Fiber,所以对Fiber不熟悉的建议先阅读系列之一。
二 函数组件触发
函数组件的触发是在renderWithHooks方法中, 在系列之一的文章中我知道Fiber调和过程中遇到函数组件,会调用updateFunctionComponent更新Fiber,所以在updateFunctionComponent内部会调用renderWithHooks。
ReactFiberReconciler.js
export function updateFunctionComponent(wip) {
renderWithHooks(wip)
const children = wip.type(wip.props)
reconcileChildren(wip, children)
}
hooks.js
let currentlyRenderingFiber = null
let workInProgressHook = null
export function renderWithHooks(wip) {
currentlyRenderingFiber = wip
currentlyRenderingFiber.memorizedState = null
workInProgressHook = null
currentlyRenderingFiber.updateQueueOfEffect = []
currentlyRenderingFiber.updateQueueOfLayout = []
}
wip正在调和更新函数组件对应的 fiber 树
- 对于类组件用memorizedState保存state信息,对于函数组件用memorizedState保存hooks信息
- updateQueueOfEffect和updateQueueOfLayout分别保存useEffect和useLayoutEffect的副作用
- 通过currentlyRenderingFiber变量,在hooks内部可以读取当前的fiber信息
三 Hooks初始化
hooks初始化时需要把hooks和fiber建立关系,如何建立关系?我们通过实现useReducer来加以说明
hooks.js
export function useReducer(reducer, initState) {
const hook = updateWorkInProgressHook()
const dispatch = (action) => {}
return [hook.memorizedState, dispatch]
}
function updateWorkInProgressHook() {
let hook
let current = currentlyRenderingFiber.alternate
if (current) {
// 更新阶段
} else {
// 初次渲染
hook = { memorizedState: null, next: null }
if (workInProgressHook === null) { // 只有一个 hooks
currentlyRenderingFiber.memorizedState = workInProgressHook = hook
} else { // 有多个 hooks
workInProgressHook = workInProgressHook.next = hook
}
}
}
用函数组件对应fiber的memorizedState保存hooks信息,每一个hooks都的执行都会产生一个hooks对象,保存当前hooks的信息,hooks通过next链表建立关系。
假如一个函数组件如下:
export default function Index(){
const [ number,setNumber ] = React.useState(0) // 第一个hooks
const dom = React.useRef(null) // 第二个hooks
React.useEffect(()=>{ // 第三个hooks
console.log(dom.current)
},[])
return <div ref={dom} >
<div onClick={()=> setNumber(number + 1 ) } > { number } </div>
<div onClick={()=> setNum(num + 1) } > { num }</div>
</div>
}
四 Hooks更新
function updateWorkInProgressHook() {
let hook
let current = currentlyRenderingFiber.alternate
if (current) {
// 更新阶段
currentlyRenderingFiber.memorizedState = current.memorizedState
if (workInProgressHook === null) {
hook = workInProgressHook = current.memorizedState
} else {
hook = workInProgressHook = workInProgressHook.next
}
} else {
// 初次渲染
hook = { memorizedState: null, next: null }
if (workInProgressHook === null) { // 只有一个 hooks
currentlyRenderingFiber.memorizedState = workInProgressHook = hook
} else { // 有多个 hooks
workInProgressHook = workInProgressHook.next = hook
}
}
}
更新的时候首先会取出workInProgres.alternate里的hooks(上一次的hooks),赋值给当前fiber的memorizedState形成新的链表关系。 此时也就解释了上面的问题,hooks为什么不能在条件判断里,在更新过程中如果增加或删除了一个hooks,会出现上一次的hooks和当前hooks不一致的问题。
把hooks写在条件判断里的例子
export default function Index({ flag }){
let number, setNumber
if (flag) {
[ number,setNumber ] = React.useState(0) // 第一个hooks
}
const dom = React.useRef(null) // 第二个hooks
React.useEffect(()=>{ // 第三个hooks
console.log(dom.current)
},[])
return <div ref={dom} >
<div onClick={()=> setNumber(number + 1 ) } > { number } </div>
<div onClick={()=> setNum(num + 1) } > { num }</div>
</div>
}
如果初始化时flag=true,更新时flag=false,会出现上一次的hooks和当前执行的hooks不一致的,上一次第一个hooks是useState,更新时第一个hooks是useRef,会报错。
五 状态派发
我们继续完成上面的useReducer里触发更新的函数dispatch
export function useReducer(reducer, initState) {
const hook = updateWorkInProgressHook()
if (!currentlyRenderingFiber.alternate) {
hook.memorizedState = initState
}
const dispatch = (action) => {
hook.memorizedState = reducer(hook.memorizedState, action)
scheduleUpdateOnFiber(currentlyRenderingFiber) // 发起调度更新
}
return [hook.memorizedState, dispatch]
}
本文中的dispatch很简单就是获取最新的state,再次触发调度。
有了上面的useReducer,useState的实现就很简单了
export function useState(initState) {
return useReducer(null, initState)
}
useReducer的dispath里面要做相应的修改
export function useReducer(reducer, initState) {
const dispatch = (action) => {
hook.memorizedState = reducer ? reducer(hook.memorizedState, action) : isFn(action) ? action(hook.memorizedState) : action
scheduleUpdateOnFiber(currentlyRenderingFiber)
}
}
六 处理副作用
在React原理系列之一中我知道在 render 阶段没有进行真正的 DOM 元素的增加,删除,等到commit 阶段,统一处理这些副作用, 包括 DOM 元素增删改,执行一些生命周期等。hooks 中的 useEffect 和 useLayoutEffect 也是副作用,接下来以 effect 为例子, 看一下 React 是如何处理 useEffect 副作用的。
hooks.js
export function useEffect(create, deps) {
return updateEffectIml(create, deps)
}
function updateEffectIml(create, deps) {
const hook = updateWorkInProgressHook()
// currentHook 和当前hook对应的上一次的hook
if (currentHook) {
const prevEffect = currentHook.memorizedState
if (deps) {
const prevDeps = prevEffect.deps
if (areHookInputsEqual(deps, prevDeps)) { // 更新阶段判断deps是否发生变化
return
}
}
}
const effect = { create, deps }
hook.memorizedState = effect
currentlyRenderingFiber.updateQueueOfEffect.push(effect)
}
- 通过updateWorkInProgressHook产生一个hook,并和fiber建立关系
- 创建一个effect,并保存到当前 hooks 的 memoizedState 属性下
- 就是如果存在多个 effect,push到函数组件 fiber 的 updateQueueOfEffect 数组里,React源码里用的是链表,此处用数组简单处理
- 更新阶段判断 deps 项有没有发生变化,如果没有发生变化,不再往updateQueueOfEffect里添加副作用
- React 会用不同的 EffectTag 来标记不同的 effect,在commit阶段会区分处理 useEffect 和 useLayoutEffect 的副作用,如果是useLayoutEffect在DOM变更后会同步处理,如果是useEffect会异步处理
七 总结
本文讲解了Hooks的保存,更新,已经副作用如何执行。