React 原理系列之二:Hooks

318 阅读4分钟

一 引言

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>
}

alt 属性文本

四 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,会报错。

alt 属性文本

五 状态派发

我们继续完成上面的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的保存,更新,已经副作用如何执行。

源码地址gitee.com/leqee_896/r…