React 原理(二)

21 阅读19分钟

Hooks

Hooks数据结构

const hooks = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null,
}

memoizedState,因为在 FiberNode 上面也有这么一个字段,与 Hook 上面的 memoizedState 存储的东西是不一样的

  • FiberNode.memoizedState上保存的是 hooks 链表里面的第一个链表
  • hook.memoizedState 保存的是某个 hook 自身的数据

不同类型的 hook,hook.memoizedState保存的内容也是不同的

  • useState:对于 const [state, updateState] = useState(initialState), memoizedState保存的是 state 的值
  • useReducer:对于 const [state, dispatch] = useReducer(reducer, {}), memoizedState保存的是 state 的值
  • useEffect:对于useEffect(callback, [...deps]), memoizedState保存的是 [callback, [...deps])]等数据
  • useRef 保存的是{current: initialValue}
  • useMemo 保存的是[callback, [...deps])]
  • useCallback 保存的是[callback, [...deps])]

有些 Hook 不需要memoizedState保存自身数据,比如 useContext

dispatcher

react 中针对 hook 有三种类型的 dispatcher

  • HooksDispatcherOnMount:负责初始化工作,让函数组件的一些初始化信息挂在到 Fiber 上面
const HooksDispatcherOnMount = {
  readContext,
  //...
  useCallback: mountCallback,
  useEffect: mountEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  // ...
}
  • HooksDiepatcherOnUpdate:函数组件更新的时候,会执行该对象所对应的方法。此时 Fiber 上面已经存储了函数组件的相关信息
const HooksDispatcherOnUpdate = {
  readContext,
  //...
  useCallback: updateCallback,
  useEffect: updateEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  // ...
}
  • ContextOnlyDispatcher:这个是和报错相关的,防止开发者在函数组件外部调用 Hook
const ContextOnlyDispatcher = {
  readContext,
  //...
  useCallback: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  // ...
}

当 FC 进入到 render 流程的时候,首先会判断是初次渲染还是更新

if(current != null && current.memoizedState !== null) {
  //update
  ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
} else {
  // mount
  ReactCurrentDispatcher.current = HooksDispatcherOnMount
}

判断了是 mount 还是 update 之后,会给 ReactCurrentDispatcher.current赋值为对应的 dispatcher,因为赋值了不同的上下文对象,因此就可以根据上下文对象调用不同的方法。

假设有嵌套的 hooks,此时的上下文对象就指向ContextOnlyDispatcher,最终执行的就是throwInvalidHookError抛出错误

Hooks 的执行流程

renderWithHooks

当 FC 进入到 render 阶段时,首先会被 renderWithHooks 函数处理执行, renderWithHooks 会被每次函数组件触发时( mount, update),该方法就会清空 workInProgress 的 memoizedState 以及 updateQueue,接下来判断该组件是初始化还是更新,为ReactCurrentDispatcher.current赋值不同的上下文对象,之后调用 Component 方法执行函数组件,组件里面所书写的 Hook 就会依次执行。

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
  renderLanes = nextRenderLanes
  currentlyRenderingFiber = workInProgress

  // 每一次执行函数组件之前,先清空状态(用于存放 hooks 列表)
  workInProgress.memoizedState = null
  workInProgress.updateQueue = null
  //...
  // 判断组件是初始化流程还是更新流程
  // 如果是初始化用 HooksDispatcherOnMount对象
  // 如果是更新用 HooksDispatcherOnUpdate对象
  // 初始化对应的上下文对象,不同上下文对象对应一组不同的方法
  ReactCurrentDispatcher.current = 
    current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate
  
  // 执行我们真正函数组件,所有 hooks 将依次执行
  let children = Component(props, secondArg)
  // ...

  // 判断环境
  finishRenderingHooks(current, workInProgress)
  return children
}

function finishRenderingHooks(current, workInProgress) {
  // 防止 hooks 在函数组件外部调用,如果调用直接报错
  ReactCurrentDispatcher.current = ContextOnlyDispatcher
  //...
}

接下来就会根据是 mount 还是 update 调用上下文里面所对应的方法

mount阶段

示例:

function App() {
  const [count, setCount] = useState(0)
  return <div onClick={() => {setCount(count + 1)}}>{count}</div>
}

mount 阶段调用的是 mountState

function mountState(initialState) {
  // 1,拿到 hook 对象
  const hook = mountWorkInProgressHook()
  if (typeof initialState === 'function') {
    initialState = initialState()
  }
  // 2,初始化 hook 的属性
  // 2.1 设置 hook.memoizedState 为 initialState/hook.baseState
  hook.memoizedState = hook.baseState = initialState
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderredReducer: basicStateReducer,
    lastRenderredState: initialState
  }
  // 2.2 设置 hook.queue
  hook.queue = queue
  // 2.3 设置 hook.dispatch
  const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue))
  // 3,返回 hook.memoizedState 和 dispatch
  return [hook.memoizedState, dispatch]
}

在执行 mountState 的时候,首先调用的 mountWorkInProgress,该方法的作用就是创建一个 hook 对象

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null, // Hook 自身维护的状态
    baseState: null,
    baseQueue: null,
    queue: null, // Hook 自身维护的更新队列
    next: null, // 下一个 hook
  }
  // 最终 hook 对象是要以链表的形式串联起来的,因此需要判断当前 hook 是否为第一个
  if (workInProgressHook === null) {
    // 如果当前组件的 Hook 链表为空,那么刚才新建的 Hook 就作为链表的头节点
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    // 如果当前的 Hook 链表不为空,那么刚才创建的 Hook 添加到 Hook 链表的末尾
    workInProgressHook = workInProgressHook.next = hook
  }
  return workInProgressHook
}

示例:

function App() {
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(1)
  const dom = useRef(null)
  useEffect(() => {
    console.log(dom.current)
  }, [])
  return <div ref={dom}>
    <div onClick={() => {setCount(count + 1)}}>{count}</div>
    <div onClick={() => {setNum(num + 1)}}>{num}</div>
  </div>
}

当上面的组件第一次初始化以后就会形成一个 hook 的链表

update阶段

更新阶段会执行 updateXXX 相关方法

  1. 获取 currentHook(旧链表指针)
  2. 定位 workInProgressHook(新链表指针)
  3. 复用/克隆逻辑
  4. 构建新链表
function updateWorkInProgressHook() {
  let nextCurrentHook;
  if (currentHook === null) {
    // 从 alternate 中获取当前的 fiber 对象
    const current = currentlyRenderingFiber.alternate
    if (current !== null) {
      // 拿到第一个 hook 对象
      nextCurrentHook = current.memoizedState
    } else {
      nextCurrentHook = null
    }
  } else {
    // 拿到下一个 hook 对象
    nextCurrentHook = currentHook.next
  }

  // 更新workInProgressHook的指向
  // 让 workInProgressHook 指向最新的 hook
  let nextWorkInProgressHook; // 下一个要工作的 hook
  if (workInProgressHook === null) {
    // 如果当前是第一个,直接从 fiber 上获取第一个 hook
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState
  } else {
    // 取链表的下一个 hook
    nextWorkInProgressHook = workInProgressHook.next
  }

  // nextWorkInProgressHook 指向的是当前要工作的 hook
  if (nextWorkInProgressHook !== null) {
    // 进行复用
    workInProgressHook = nextWorkInProgressHook
    nextWorkInProgressHook = workInProgressHook.next
    currentHook = nextCurrentHook
  } else {
    // 进行克隆
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate
      if (currentFiber !== null) {
        const newHook = {
          memoizedState: null,
          baseState: null,
          baseQueue: null,
          queue: null,
          next: null
        }
        nextCurrentHook = newHook
      } else {
        throw new Error('Rendered more hooks than during the previous render')
      }
    }
  }
  currentHook = nextCurrentHook
  const newHook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null
  }
  // 之后的操作和 mount 时候一样
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook
  } else {
    workInProgressHook = workInProgressHook.next = newHook
  }
  return workInProgressHook
}

如果nextWorkInProgressHook不为 null,那么就会复用之前的 hook, 这也是 hook 不能放在条件或者循环语句中的原因。因为更新过程中如果通过 if 条件增加或者删除了 hook,那么复用的时候就会产生当前 hook 的顺序和之前 hook 的顺序不一致的问题

例如下面

function App({showNumber}) {
  let number, setNumber
  showNumber && ([number, setNumber] = useState(0)) // 第一个
  const [count, setCount] = useState(0) // 第二个
  const dom = useRef(null) // 第三个
  useEffect(() => { // 第四个
    console.log(dom.current)
  }, [])
  return <div ref={dom}>
  </div>
}

假设第一次父组件传递过来的 showNumber 为 true,此时就会渲染第一个 hook,第二次渲染的时候,父组件传递的为 false,那么第一个 hook 就不会执行,逻辑就会变得如下表所示:

hooks 链表顺序第一次第二次
第一个 hookuseStateuseState
第二个 hookuseStateuseRef

此时在进行复用的时候就会报错

useState 和 useReducer

useState 的本质就是 useReducer的简化版,只是 useState 内部会有一个内置的 reducer

mount阶段

  1. 创建 Hook 节点
  2. 解析初始状态(支持函数形式)
  3. 初始化更新队列为环形链表结构
  4. 绑定 dispatch 方法到当前组件上下文

useReducer 的 mount 阶段

function mountReducer(reducer, initialArg, init) {
  const hook = mountWorkInProgressHook()
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg)
  } else {
    initialState = initialArg
  }

  hook.memoizedState = hook.baseState = initialState
  const queue = hook.queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState
  }
  hook.queue = queue
  const dispatch = (queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue))
  return [hook.memoizedState, dispatch]
}

useState 的 mount 阶段

function mountState(initialState) {
  // 1,创建 hook
  const hook = mountWorkInProgressHook()
  if (typeof initialState === 'function') {
    initialState = initialState()
  }
  // 2,初始化 hook 的属性
  // 2.1 设置 hook.memoizedState 为 initialState/hook.baseState
  hook.memoizedState = hook.baseState = initialState
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderredReducer: basicStateReducer,
    lastRenderredState: initialState
  }
  // 2.2 设置 hook.queue
  hook.queue = queue
  // 2.3 设置 hook.dispatch
  const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue))
  // 3,返回 hook.memoizedState 和 dispatch
  return [hook.memoizedState, dispatch]
}

basicStateReducer 对应的代码如下

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action
}

update阶段

从上面可以看出,dispatch就是dispatchAction绑定参数后返回的函数, 整体流程为:

  1. 触发 dispatchAction:生成 update 对象并加入队列。
  2. 调度渲染:标记组件需要更新,进入 React 渲染流程。
  3. 执行 updateReducer:遍历队列,调用 reducer 计算新状态。
  4. 提交新状态:更新组件状态并触发重新渲染。
function dispatchAction(fiber, queue, action) {
  const update: Update = {
    action, // 用户传递的 action(如 { type: 'INCREMENT' })
    next: null,
  };
  // 将 update 加入环状链表
  const pending = queue.pending;
  if (pending === null) {
    update.next = update; // 自环
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // 调度更新
  scheduleUpdateOnFiber(fiber);
}
  • 更新队列:update 对象会被加入 Hook 的 updateQueue 队列,形成一个环状链表。
  • 调度机制:scheduleUpdateOnFiber 触发 React 的渲染调度,最终进入 updateReducer 处理阶段。

在组件重新渲染时,React 会调用 updateReducer 处理队列中的更新:

在 update 阶段,useState 内部直接调用的就是 updateReducer,传入的 reducer 仍然是basicStateReducer

useState:

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState)
}

useReducer

function updateReducer(reducer, initialArg, init) {
  // 获取对应 hook
  const hook = updateWorkInProgressHook()
  // 拿到更新队列
  const queue = hook.queue
  queue.lastRenderedReducer = reducer
  // 省略根据 update 链表计算 state 的逻辑
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch]
}

批处理更新:多个 dispatch 调用会合并到同一队列,减少渲染次数(通过 React 的调度机制实现)。

优先级调度:React 18 的并发模式会根据更新优先级动态调整处理顺序

useEffect 和 useLayoutEffect

在 react 中用于定义有副作用的 hook 有三个

  • useEffect:回调函数会在 commit 阶段完成后异步执行,它不会阻塞视图渲染
  • useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作
  • useInsertionEffect:会在 commit 的 Mutation 子阶段同步执行,和useLayoutEffect的区别在于,useInsertionEffect在执行的时候是无法访问 DOM 的引用。

数据结构

对于这三个 Effect 相关的 hook,hook.memoizedState 共同使用一套数据结构

const effect = {
  tag, // effect 类型 passive|layout| insertion
  create, // effect 回调函数
  destory, // effect 销毁函数
  deps,// 依赖项
  next: null // 当前 FC 与其他 effect 形成环状链表
}

tag用来区分 effect 的类型

  • Passive:useEffect
  • Layout:useLayoutEffect
  • Insertion:useInsertion

create 和 destory 分别指代 effect 的回调函数以及销毁函数

deps 为依赖项

next:用于和当前组件的其他 effect 形成环状链表,链接方式是单项环状链表

function App() {
  useEffect(() => {
    console.log(1)
  })
  const [num1, setNum1] = useState(0)
  const [num2, setNum2] = useState(0)
  useEffect(() => {
    console.log(2)
  })
  useEffect(() => {
    console.log(3)
  })
  return <div>hello effect</div>
}

工作流程

整个工作流程可以分为三个阶段

  • 声明阶段
  • 调度阶段(useEffect 独有的)
  • 执行阶段

声明阶段

mount 阶段执行的是 mountEffectImpl

function mountEffectImpl(fiberFlages, hookFlags, create, deps) {
  // 生成 hook 对象
  const hook = mountWorkInProgressHook()
  // 保存依赖数组
  const nextDeps = deps === undefined ? null : deps
  // 修改当前fiber 的 flag
  currentlyRenderingFiber.flags |= fiberFlags
  // 将 pushEffect 返回的环形链表挂载到 hook.memoizedState 上
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps
  )
}

首先生成 hook 对象,拿到依赖,修改 fiber 的 flag,将之前的 effect 推入到环状链表,hook.memoizedState 指向该环状链表

update 阶段执行的是 updateEffectImpl

function updateEffectImpl(fiberFlages, hookFlags, create, deps) {
  // 生成 hook 对象
  const hook = mountWorkInProgressHook()
  // 保存依赖数组
  const nextDeps = deps === undefined ? null : deps
  // 初始化清除effect 函数
  let destroy = undefined
  // 如果依赖项不为空
  if (nextDeps !== null) {
    const prevDeps = prevEffect.deps
    // 两个依赖项进行比较
    if (areHookInputEqual(nextDeps, prevDeps)) {
      // 如果依赖的值相同,即依赖项没有变化,那么智慧给这个 effect 打上一个 HookPassive 标记
      // 然后在组件渲染完以后会跳过这个 effect 的执行
      hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps)
      return
    }
  }
  // 如果 deps发生变化,赋予 effectTag,在 commit 阶段就会再次执行 effect
  currentlyRenderingFiber.flags |= fiberFlags
  // 然后把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
  hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps)
}

在上面的代码中,首先从 updateWorkInProgress 方法中拿到 hook 对象,之后会从 hook.memoizedState 拿到所存储的 effect 对象,之后会用areHookInputEqual方法进行前后依赖项的比较,如果依赖项相同,就会在 effect 上打一个 tag,在组件渲染完以后会跳过这个 effect 执行

如果依赖项发生了变化,那么当前的 FiberNode 就会有一个 flags,在 commit 阶段统一执行该 effect,之后会推入新的 effect 到环状链表上面。

areHookInputEqual的作用是比较两个依赖项数组是否相同,采用的是浅比较。

function areHookInputEqual(nextDeps, prevDeps) {
  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
}

pushEffect作用是生成一个 effect 对象,推入到当前的环状链表

function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null
  }
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue

  // 创建单项环状链表
  if (componentUpdateQueue === null) {
    // 进入此 if 说明是第一个 effect
    // createFunctionComponentUpdateQueue 会返回一个对象,对象上有 lastEffect 属性
    componentUpdateQueue = createFunctionComponentUpdateQueue()
    currentlyRenderingFiber.lastEffect = effect.next = effect
    componentUpdateQueue.lastEffect = effect.next = effect
  } else {
    // 存在多个副作用,拿到之前的副作用
    const lastEffect = componentUpdateQueue.lastEffect
    if (lastEffect === null) {
      // 如果没有 就和上面的 if 一样
      currentlyRenderingFiber.lastEffect = effect.next = effect
    } else {
      // 如果之前有 effect 先储存到firstEffect
      const firstEffect = lastEffect.next
      // lastEffect 指向新的副作用对象
      lastEffect.next = firstEffect.next = effect
      // 新的副作用对象指向第一个副作用对象,形成环状链表
      effect.next = firstEffect
      currentlyRenderingFiber.lastEffect = effect
    }
  }
  return effect
}

update 的时候即使 effect deps 没有变化,也会创建对应的 effect,因为这样才能保证 effect 数量以及顺序是稳定的。

调度阶段(useEffect 独有的)

调度阶段时 useEffect 独有的,因为 useEffect 的回调函数会在 commit 阶段完成后异步执行。因此需要调度阶段。在 commit 阶段的三个子阶段开始之前,会执行如下的代码

if (
  (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
  (finishedWork.flags & PassiveMask) !== NoFlags
) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    pendingPassiveEffectsRemainingLanes = remainingLanes;
    // ...
    // scheduleCallback 来自于 Scheduler, 用于以某一优先级调度回调函数
    scheduleCallback(NormalSchedulerPriority, () => {
      // 执行 effect 回调函数的具体方法
      flushPassiveEffects();
      return null;
    });
  }
}

flushPassiveEffects 会去执行对应的 effects

function flushPassiveEffects(){
  if (rootWithPendingPassiveEffects !== null) {
    // 执行 effects
  }
  return false;
}

另外,由于调度阶段的存在,为了保证下一次的commit阶段执行前,上一次 commit 所调度的 Effect 都已经执行过了,因此会在 comit 阶段的入口处,也会执行 flushPassiveEffects,而且是一个循环执行。

function commitRootImpl(root, renderPriorityLevel) {
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
}

使用 do-while 循环,是为了保证上一轮调度的 effect 都执行过了

执行阶段

这三个 effect 相关的 hook 执行阶段,有两个相关的方法

  • commitHookEffectListUnmount:用于遍历 effect 链表,依次执行 effect和destory 方法
function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        // 从 effect 对象上面拿到 destroy 函数
        const destroy = effect.destroy;
        effect.destroy = undefined;
        // ...
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
  • commitHookEffectListMount:遍历 effect 链表,依次执行 create 方法,在声明阶段中,update 时会根据 deps 是否变化,打上不同的 tag,之后再执行阶段就会根据是否有 tag,来决定是否要执行该 effect
// 类型为 useInsertionEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Insertion | HasEffect, fiber);
// 类型为 useEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Passive | HasEffect, fiber);
// 类型为 useLayoutEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Layout | HasEffect, fiber);

由于commitHookEffectListUnmount 会先于commitHookEffectListMount执行,因此每次都是先执行 effect.destory 后才会执行 effect.create

useCallback

使用 useCallback 最终会得到一个缓存的函数,该缓存函数会在依赖项发生变化时再更新。

mount 阶段

在 mount 阶段执行的就是 mountCallback

function mountCallback(callback, deps) {
  const hook = mountWorkInProgressHook();
  // 依赖项
  const nextDeps = deps === undefined ? null : deps;
  // 把要缓存的函数和依赖数组存储到 hook 对象上
  hook.memoizedState = [callback, nextDeps];
  // 向外部返回缓存函数
  return callback;
}

在上面的代码中,首先调用mountWorkInProgressHook得到一个 hook 对象,在 hook 对象的memoizedState保存 callback 和依赖项,最终返回 callback 到外部

update 阶段

调用 updateCallback

function updateCallback(callback, deps) {
  // 拿到之前的 hook 对象
  const hook = updateWorkInProgressHook();
  // 新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  // 之前的值,也就是 [callback, nextDeps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1]; // 拿到之前的依赖项
      // 对比依赖项是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 相同返回 callback
        return prevState[0];
      }
    }
  }
  // 否则重新缓存
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

在组件更新阶段,首先拿到之前的 hook 对象,从之前的 hook 对象的 memoizedState 上面拿到之前的依赖项,和新传入的依赖项做对比,如果相同则返回之前缓存的 callback,否则就重新缓存,返回新的 callback

useMemo

使用 useMemo 缓存的是一个值。值会在依赖项发生变化的时候重新进行计算并缓存。

mount 阶段

mount 阶段调用的是 mountMemo

function mountMemo(nextCreate, deps) {
  // 创建 hook 对象
  const hook = mountWorkInProgressHook();
  // 存储依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // ...
  // 执行传入的函数,拿到返回值
  const nextValue = nextCreate();
  // 将函数返回值和依赖存储到 memoizedState 上面
  hook.memoizedState = [nextValue, nextDeps];
  // 返回函数计算得到的值
  return nextValue;
}

在 mount 阶段,首先会调用mountWorkInProgressHook得到 hook 对象,之后执行传入的函数,得到计算值,将计算值和依赖项存储到 hook 对象的 memoizedState 上,最后向外部返回计算得到的值。

update 阶段

updeate阶段调用的是 updateMemo

function updateMemo(nextCreate, deps) {
  // 获取之前的 hook 对象
  const hook = updateWorkInProgressHook();
  // 新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  // 获取之前的 memoizedState,也就是 [nextValue, nextDeps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      // 拿到之前的依赖项
      const prevDeps = prevState[1];
      // 比较和现在的依赖项是否相同
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果相同,则返回之前的值
        return prevState[0];
      }
    }
  }
  // 否则重新计算
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

首先仍然是从 updateWorkInProgressHook 上拿到之前的 hook 对象,从而获取到之前的依赖项,然后和新传入的依赖项木进行对比,如果依赖项没有变化,则返回之前的计算值,否则重新计算。

React 中的事件

在 React 中有一套自己的事件系统来描述 FiberTree 和 UI 之间的交互的。

对于 ReactDOM 宿主环境,这套事件系统由两个部分组成

  • 合成事件对象

SyntheticEvent(合成事件对象),是对浏览器原生事件的一层封装,兼容了主流的浏览器,同时拥有和浏览器原生事件相同的 API,例如 stopPropagation 和 preventDefault。合成事件对象存在的目的就是为了消除不同浏览器在事件层面上的差异。

  • 模拟实现事件传播机制

利用事件委托的原理,react 会基于 FiberTree 来实现了事件的捕获,目标,以及冒泡的过程,就类似于原生DOM 事件的传递过程,并且在自己实现的这一套事件传播机制中还加入了许多新的特性,比如:

  • 不同的事件对应了不同的优先级
  • 定制事件名
    • 比如在 React 中统一采用 onXXX 的驼峰写法来绑定事件,如 onClick
  • 定制事件行为
    • 例如 onChange 的默认行为与原生的 oninput 是相同的

React 事件系统需要考虑到很多边界情况,代码量是非常大的,可以通过写个简易版的事件系统,来学习 React 事件系统的原理。

假设我们现在有如下 jsx代码

import React from 'react';
import ReactDOM from 'react-dom';

const jsx = (
  <div onClick={e => console.log('click div')}>
    <h3>hello world</h3>
    <button onClick={e => {
      // e.stopPropagation();
      console.log('click button');
    }}>click me</button>
  </div>
)

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(jsx);

我们为外部的 div 和内部的 button 都绑定了 click 事件,如果打开e.stopPropagation()就会阻止事件冒泡,可以看出 React 内部的事件系统实现了“模拟版”事件传播机制,接下来写一套简易版事件系统,绑定事件的方式改为 bindXXX

SyntheticEvent

syntheticEvent 是指合成事件对象,在 React 中的 SyntheticEvent 会包含很多属性和方法,这里只模拟实现阻止事件冒泡。

/**
 * 合成事件对象
 * @class SyntheticEvent
 */
class SyntheticEvent {
  constructor(e) {
    // 保存原生事件对象
    this.nativeEvent = e;
  }
  // 提供一个和原生事件同名的阻止冒泡
  stopPropagation() {
    // 当调用 stopPropagation 时,设置一个标记
    this._stopPropagation = true;
    if (this.nativeEvent.stopPropagation) {
      // 如果原生事件对象有 stopPropagation 方法,调用来阻止冒泡
      this.nativeEvent.stopPropagation();
    }
  }
}

上面的代码中创建了一个SyntheticEvent类,可以用来创建合成事件对象,内部保存了原生的事件对象,还提供了一个和原生 DOM 事件同名的阻止冒泡方法

实现事件的传播机制

对于可以冒泡的事件,整个事件的传播机制实现步骤如下

  • 在根元素绑定“事件类型对应的事件回调”,所有子孙元素触发该事件时最终会委托给根元素的事件回调函数来进行处理
  • 寻找触发事件的 DOM 元素,找到对应的 FiberNode
  • 收集从当前的 FiberNode 到 HostRootFiber 之间所有注册了该事件的回调函数
  • 反向遍历并执行一遍收集的所有回调函数(模拟捕获阶段的实现)
  • 正向遍历并执行一遍收集的所有的回调函数(模拟冒泡阶段的实现)

通过 addEvent 来给根元素绑定事件,目的是使用事件委托

// 该方法用于给根元素绑定事件
export const addEvent = (container, type) => {
  container.addEventListener(type, e => {
    // 进行事件的派发
    dispatchEvent(e, type.toUpperCase());
  })
}

在入口处绑定事件

// 给根元素绑定事件, 使用我们自己的事件系统
addEvent(document.getElementById('root'), 'click');

在 addEvent 中,调用 dispatchEvent 进行事件派发

/**
 * 事件派发
 * @param {*} e 原生事件对象
 * @param {*} type 事件类型, 已经转为了大写,比如这里传递过来的是 CLICK
 */
const dispatchEvent = (e, type) => {
  // 实例化合成事件对象
  const se = new SyntheticEvent(e);
  // 获取触发事件的目标元素
  const ele = e.target;
  // 通过DOM 元素找到对应的 fiberNode
  let fiber;
  for (let prop in ele) {
    if (prop.toLocaleLowerCase().includes('fiber')) {
      fiber = ele[prop];
      break;
    }
  }
  // 找到对应的 fiberNode 之后,需要收集路径中,该事件类型所对应的所有的回调函数
  const paths = collectPaths(type, fiber)
  // 模拟捕获的实现
  triggerEventFlow(paths, type + 'Capture', se)
  // 模拟冒泡的实现, 首先需要判断是否阻止冒泡,如果没有,我们只需要将 paths 进行反向,在遍历执行一次即可
  if (!se._stopPropagation) {
    triggerEventFlow(paths.reverse(), type, se)
  }
}

dispatchEvent方法对应如下的步骤:

  • 实例化一个合成事件对象
  • 找到对应的 fiberNode
  • 收集从当前的 fiberNode 一直往上,所有该事件类型的回调函数
  • 模拟捕获的实现
  • 模拟冒泡的实现

收集路径中对应的事件处理函数

/**
 * 该方法用于收集路径中所有 type 类型的事件回调函数
 * @param {*} type 事件类型
 * @param {*} begin fiberNode
 * @returns
 * [
 * "CLICK": function() {...}},
 * "CLICK": function() {...}}
 * ]
 */
const collectPaths = (type, begin) => {
  const paths = []; // 存放收集到的所有的事件回调函数
  // 如果不是 HostRootFiber,就一直向上遍历
  while(begin.tag !== 3) {
    const {memoizedProps, tag} = begin
    // 如果tag 对应的值为 5 说明是 DOM 元素对应的 FiberNode
    if (tag === 5) {
      const eventName = 'bind' + type
      // 看当前的节点是否有绑定事件
      if (memoizedProps && Object.keys(memoizedProps).includes(eventName)) {
        // 如果进入了该 if 就说明当前这个节点绑定了对应类型的事件
        const pathNode = {}
        pathNode[type] = memoizedProps[eventName]
        paths.push(pathNode)
      }
      begin = begin.return
    }
  }
  return paths
}

实现的思路就是从当前的 fiberNode 一直向上遍历,直到 HostRootFiber,收集遍历过程中 fiberNode.memoizedProps属性所保存的对应事件处理函数

捕获和冒泡的实现

由于是从目标元素的 fiberNode 向上遍历的,因此收集到的顺序:

[目标元素事件回调,某个祖先元素的事件回调,某个更上层祖先元素]

因此要模拟捕获就要从后往前遍历并执行

在执行事件回调函数的时候,每一次执行要检验是否阻止冒泡

/**
 * @param {*} paths 收集到的事件回调函数的数组
 * @param {*} type 事件类型
 * @param {*} se 合成事件对象
 */
const triggerEventFlow = (paths, type, se) => {
  // 遍历数组然后执行回调函数即可
  // 模拟捕获阶段的实现,所以需要从后往前遍历数组并执行回调
  for (let i = paths.length - 1; i >= 0; i--) {
    const pathNode = paths[i];
    const callback = pathNode[type];
    if (callback) {
      callback.call(null, se);
    }
    if (se._stopPropagation) {
      // 说明在当前的事件回调中,开发者阻止了继续冒泡
      break;
    }
  }
}

如果是模拟冒泡阶段,只需要将 paths 反向并执行

  // 模拟冒泡的实现, 首先需要判断是否阻止冒泡,如果没有,我们只需要将 paths 进行反向,在遍历执行一次即可
  if (!se._stopPropagation) {
    triggerEventFlow(paths.reverse(), type, se)
  }