Skip to content

Latest commit

 

History

History
384 lines (302 loc) · 12.7 KB

hook-detail.md

File metadata and controls

384 lines (302 loc) · 12.7 KB

源码解析二十五 各个hook的实现

我们在函数式组件中会调用各种hook,这些hook或多或少都会使用官方的api,如useState,useReducer,useEffect等,这些官方的api便于我们操作状态,处理副作用,做缓存,做优化等等

上节提到,我们全局有一个hook的注册机,每次调用这些api其实最终都会通过注册机的分发走到真正的处理函数上

在渲染逻辑跟更新逻辑中,注册机上挂载的hook都是不相同的,所以对于每一个hook api,都有渲染更新两个函数

useState

useState在渲染时调用的是mountState函数,在更新时调用的是updateState

const State = {
  basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
    return isFunction(action) ? (action as Function)(state) : action
  },

  mountState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
    if (isFunction(initialState)) {
      initialState = (initialState as Function)()
    }
    return Reducer.mountReducer(State.basicStateReducer, initialState)
  },

  updateState<S>(_initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
    return Reducer.updateReducer(State.basicStateReducer)
  },
}

可以看到,这两个函数只是在Reducer上又封装了一层,所以实现的关键还在reducer

useReducer

reducer中,也存在着mountReducerupdateReducer函数,先从mountReducer分析起

mountReducer

在渲染阶段,我们先通过mountWorkInProgressHook创建了一个新的Hook对象,在创建的过程中,会把其插入当前hook队列的末尾。

由于每个Hook对象都是一个独立的更新单元,它有着自己的stateupdateQueue,所以我们也会初始化掉这些变量,最后返回一个dispathAction的闭包

  function mountWorkInProgressHook(): Hook {
    const hook: Hook = {
      memoizedState: null,
      baseState: null,
      baseUpdate: null,
      queue: null,
      next: null,
   }

   if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook // 第一次
   } else {
    workInProgressHook = workInProgressHook.next = hook // 插入链表中
   }
   return workInProgressHook
 }

  mountReducer<S, I, A>(reducer: (s: S, a: A) => S, initialArg: I, init?: (i: I) => S): [S, Dispatch<A>] {
    const hook: Hook = mountWorkInProgressHook()

    let initialState: S = null

    if (init !== undefined) {
      initialState = init(initialArg)
    } else {
      initialState = initialArg as any
    }

    hook.memoizedState = hook.baseState = initialState
    const queue = hook.queue = {
      last: null,
      dispatch: null,
      eagerReducer: reducer,
      eagerState: initialState,
    }

    const dispatch: Dispatch<A> = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue)

    return [hook.memoizedState, dispatch]
  }
dispatchAction
function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
  const { alternate } = fiber
  ...
}

useStateuseReducer的第二个返回值都是dispatchAction的一个带参版,前两个参数已经在mountReducer中挂载上。这个函数接受一个action,我们将其封装成一个Update对象

    const currentTime = requestCurrentTime()
    const expirationTime = computeExpirationTimeForFiber(currentTime, fiber)
    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    }

之后,我们将其放到任务队列的队尾,这里的updateQueue不同于ClassComponent,采用的是环形链表

    const { last } = queue
    if (last === null) {
      update.next = update // 第一个update,创建环形链表
    } else {
      const first = last.next
      if (first !== null) {
        update.next = first
      }
      last.next = update
    }
    queue.last = update

react在这个函数的最后,做了一层优化。如在setTimeout等一些异步情况下触发了一个dispatch,由于在下一个事件循环,当前的fiber或许不在工作中,此时可以提前计算出State,减轻update时的负担

  // 当前工作队列为空,在进入render阶段前提前计算下一个state,update时可以根据eagerReducer直接返回eagerState
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      const { eagerReducer } = queue

      if (eagerReducer !== null) {
        const currentState: S = queue.eagerState
        const eagerState = eagerReducer(currentState, action)

        // 存储提前计算的结果,如果在更新阶段reducer没有发生变化,可以直接使用eager state,不需要重新调用eager reducer在调用一遍
        update.eagerReducer = eagerReducer
        update.eagerState = eagerState

        if (Object.is(eagerState, currentState)) {
          return
        }
      }
    }
updateReducer

mount阶段我们会生成一条Hook队列。放在FibermemoizedState上,当更新时,我们会依次取出当前队列头部的Hook,由于我们在编码时已经约束了hook的调用条件,所以取出时的顺序与我们mount插入时的顺序一定是一样的,调用updateWorkInProgressHook获取到当前的Hook对象

function updateWorkInProgressHook(): Hook {
  if (nextWorkInProgressHook) {
    workInProgressHook = nextWorkInProgressHook
    nextWorkInProgressHook = workInProgressHook.next

    currentHook = nextCurrentHook
    nextCurrentHook = currentHook !== null ? currentHook.next : null
  } else {
    currentHook = nextCurrentHook

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,
      next: null,
    }

    if (workInProgressHook === null) {
      workInProgressHook = firstWorkInProgressHook = newHook // 第一次
    } else {
      workInProgressHook = workInProgressHook.next = newHook // 插入链表中
    }
    nextCurrentHook = currentHook.next
  }
  return workInProgressHook
}

同样按照fiber的思路,update时统一使用workInProgressHook,如果没有workInProgressHook,会参照currentHook赋值一份Hook

  updateReducer<S, A>(reducer: (s: S, a: A) => S): [S, Dispatch<A>] {
    const hook = updateWorkInProgressHook()
    const { queue } = hook
    ...
  }

获取到hook后,整个updateReducer的部分就很简单,类似于ClassComponent

由于整个任务队列是个环形链表,所以先把环形链表解开

    let first: Update<S, A> = null
    if (baseUpdate !== null) {
      if (last !== null) { // 从一次停顿的地方开始,为了防止无限循环,需把环形链表解开
        last.next = null
      }
      first = baseUpdate.next
    } else {
      first = last !== null ? last.next : null
    }

随后,从队列头开始遍历,找到优先级大于当前更新优先级的update,依次调用它们,直到队列尾,这个有个关键,前面提到,我们在dispatchAction做了一层优化,提前进行了state的计算,所以,这里通过eagerReducer是否与传入的reducer相等来判断是否需要获取计算结果

   if (first !== null) {
      let newState: S = baseState
      let prevUpdate: Update<S, A> = baseUpdate
      let update: Update<S, A> = first

      let newBaseState: S = null
      let newBaseUpdate: Update<S, A> = null
      let didSkip: boolean = false

      do {
        const updateExpirationTime: ExpirationTime = update.expirationTime
        if (updateExpirationTime < renderExpirationTime) {
          if (!didSkip) { // 优先级低需要跳过,如果是第一个跳过的Update,需要记录下来
            didSkip = true
            newBaseState = newState
            newBaseUpdate = prevUpdate
          }

          if (updateExpirationTime > remainingExpirationTime) {
            remainingExpirationTime = updateExpirationTime
          }
        } else {
          if (update.eagerReducer === reducer) { // 直接使用提前计算的结果
            newState = update.eagerState
          } else {
            const { action } = update
            newState = reducer(newState, action)
          }
        }

        prevUpdate = update
        update = update.next
      } while (update !== null && update !== first)

      if (!didSkip) {
        newBaseState = newState
        newBaseUpdate = prevUpdate
      }

      if (!Object.is(newState, hook.memoizedState)) {
        markWorkInProgressReceivedUpdate()
      }

      hook.memoizedState = newState
      hook.baseUpdate = newBaseUpdate
      hook.baseState = newBaseState

      queue.eagerReducer = reducer
      queue.eagerState = newState
    }

    const { dispatch } = queue
    return [hook.memoizedState, dispatch]
  

最后,返回计算出的statedispatch

两个优化型hook useCallbackuseMemo

pureComponentmemo都采取的是浅比较,如果props的引用不同,都会触发重新渲染,所以react的基本优化法则,就是在传递Props引用类型时要提前定义,防止无关渲染

然而在FunctionComponent中,由于每次更新都会重新执行,所以在FunctionComponent中声明的一些函数,引用类型值每次渲染都会不同,带来很多无关渲染,增大性能损耗,所以,官方出了两个用来做优化的hookuseCallbackuseMemo

useCallback

它的实现非常简单,经由hook存储一份值,在update时拿出之前的值,进行比较,相等返回老的,不相等则返回新的

const Callback = {
  mountCallback<T>(callback: T, deps?: any[]): T {
    const hook = mountWorkInProgressHook()
    const nextDeps = deps === undefined ? null : deps
    hook.memoizedState = [callback, nextDeps]
    return callback
  },

  updateCallback<T>(callback: T, deps?: any[]): T {
    const hook = updateWorkInProgressHook()
    const nextDeps = deps === undefined ? null : deps
    const prevState = hook.memoizedState

    if (prevState !== null) {
      if (nextDeps !== null) {
        const prevDeps: any[] | null = prevState[1]
        if (areHookInputsEqual(nextDeps, prevDeps)) {
          return prevState[0]
        }
      }
    }

    hook.memoizedState = [callback, nextDeps]
    return callback
  },
}

mount时将需要优化的callbacknextDeps放到hookmemoizedState, 在update时,将当前的deps与之前的prevDeps作比较,如果相等,返回缓存的值,反之,则使用新的callback

function areHookInputsEqual(nextDeps: any[], prevDeps: any[] | null): boolean {
  if (prevDeps === null) {
    return false
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue
    }
    return false
  }
  return true
}

areHookInputsEqual这个函数依次遍历数组的每一项,进行浅比较

useMemo

useMemouseCallback的实现大致相同,唯一的区别是useCallback会对第一个参数进行整个缓存,而useMemo会执行第一个参数,对其生成的值进行缓存

const Memo = {
  mountMemo<T>(nextCreate: () => T, deps?: any[] | null): T {
    const hook = mountWorkInProgressHook()
    const nextDeps = deps === undefined ? null : deps
    const nextValue = nextCreate()
    hook.memoizedState = [nextValue, nextDeps]

    return nextValue
  },

  updateMemo<T>(nextCreate: () => T, deps?: any[] | null): T {
    const hook = updateWorkInProgressHook()
    const nextDeps = deps === undefined ? null : deps
    const prevState = hook.memoizedState
    if (prevState !== null) {
      if (nextDeps !== null) {
        const prevDeps: any[] | null = prevState[1]
        if (areHookInputsEqual(nextDeps, prevDeps)) {
          return prevState[0]
        }
      }
    }

    const nextValue = nextCreate()
    hook.memoizedState = [nextValue, nextDeps]
    return nextValue
  },
}

useRef

useRef相当于FunctionComponent里的一块引用区域,无论如何渲染更新,它的值都保持最新,它的实现可能是useXXX里面最简单的一个,保存一个引用,存到hook

const Ref = {
  mountRef<T>(initialValue: T): { current: T } {
    const hook = mountWorkInProgressHook()
    const ref = { current: initialValue }

    hook.memoizedState = ref
    return ref
  },

  updateRef<T>(_initialValue: T): { current: T } {
    const hook = updateWorkInProgressHook()
    return hook.memoizedState
  },
}