React高级前端面试—-React Hooks

image.png
开始之前,先讲一下该文章能帮你解决哪些问题?

  • React Hooks是什么?

  • React Hooks是怎么实现的?(包含部分React源码)

    • react hooks和class component的差异
    • react hooks是怎么保存状态的
    • 为什么setState是”异步”的

关于Fiber如果有问题,可以阅读之前的文章

React高级前端面试—React Fiber

前言

该文章涉及的源码部分基于React v17.0.2

PS.文章以useState为例介绍React Hooks的流程,其他的hook也属于这个流程之内,但是会有自己的逻辑处理

React Hooks是什么?

在class组件中,每次都只会生成一个class实例,组件状态都被存储在实例中,之后的每次更新,只是调用了class的render方法,在class中的状态都不会丢失。但是如果是函数组件,每次render更新都需要重新执行这个函数,函数组件也就没有保存状态的能力。所以在之前我们都会把函数组件当做纯展示组件,通过props来更新页面。所以Hooks就是赋予函数组件保存数据状态和执行side effects的能力

既然函数每次render都是从新开始,React Hooks是怎么把上次的数据状态保存下来的呢?

React Hooks是怎么实现的?

虚拟dom自从react出现已经被提烂了,都知道,虚拟dom就是使用js数据来存储dom结构,每次的数据更新都是更新js的对象,通过把对象映射到dom上,来进行页面更新。在这里提出这个是因为需要做一个思维转换,所有的dom都是js中的一个数据结构,那所有的组件其实也是一个数据结构——在react中叫Fiber,函数组件也是一样,对应一个Fiber节点,如果把函数组件的状态存储到Fiber之上,每次执行函数时都从Fiber中取数据不就可以保存数据状态了么。其实所有组件本质上都是一个Fiber节点的数据结构,所有的数据都可以存储在fiber中。

hooks长什么样子?

我们直接快进到Hook的数据结构

reactFiberHooks.new.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
​
  if (workInProgressHook === null) {
    // This is the first hook in the list
    // 可以看到state存储在Fiber的memoizedState上
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 下次进入直接给next赋值
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
复制代码

在react源码中可以看到,每次useState,都新建一个hook,并把它挂载到当前的Hook.next,所以函数组件的所有的state都是以单链表的形式存储在fiber的memoizedState中,这也解释了为什么react官方有警告不能把hooks写在条件判断中。

hooks是怎么更新数据的?

还记得吗?react每次更新都会执行function。但是对于我们使用者来说,无论是初始化,还是update,在使用中都是执行的useState(),就像下面这样:

const [num,setNum] = use(0)
复制代码

其实在react内部,所有的初始化和更新都是两套代码

ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
复制代码

分别对应:mountState和updateState,初始化阶段,初始化hook,新建上述的hook单链表,更新阶段更新state数据

初始化阶段
function mountState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === "function") {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
    (dispatchAction.bind(null, currentlyRenderingFiber, queue): any));
  return [hook.memoizedState, dispatch];
}
复制代码

调用useState的初始化会执行mountState,可以看到返回值是[hook.memoizedState, dispatch],我们可以看到dispatch就是我们调用setNum更新数据的函数,可以看看他执行了什么(下方代码删除了很多无关代码)

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
) {
  const update: Update<S, A> = {
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  const alternate = fiber.alternate;
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // fiber调度更新机制
  scheduleUpdateOnFiber(fiber);
  }
}
复制代码

可以看到dispatchAction接收3个参数,fiber,queue和action,但是为什么我们setNum时候只传入了一个action,这是因为react已经通过dispatchAction.bind(null, currentlyRenderingFiber, queue)把当前的fiber和queue传入进去,所以当我们执行setNum时候传入action,会执行dispatchACtion,构建一个update队列,然后进行fiber的调度机制(和class 的setState使用的同一套机制)来触发更新,如果更新开始就会进入到下一次render,下一次render重新执行function,又会走到useState,这时候就会进入updateState的更新阶段。

更新阶段
function updateState<S>(
  initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
复制代码

从updateState可以看到,updateState本质上走的也是updateReducer(所以updateState只是updateReducer的一个简易版,无论从使用上来看,还是实现上来看,都是这样)。所以直接跳转到updateReducer(删除了调度等其他一些不相关的代码)

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current: Hook = (currentHook: any);
  // The last rebase update that is NOT part of the base state.
  let baseQueue = current.baseQueue;
  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
        //调用传入的reducer更新state
        const action = update.action;
        newState = reducer(newState, action);
        update = update.next;
    } while (update !== null && update !== first);
​
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
复制代码

可以看到在更新阶段,是把update的队列循环merge,计算新的state数据,这也是setNum为什么是”异步”的原因

总结

  • 一句话讲清楚Hooks实现原理:Hooks通过单链表形式将state存储在对应的Fiber结构中,每次更新按顺序读取执行。

  • setState是”异步”的,是因为react把状态存储在queue队列中,等待调度完成一起更新

  • 几乎所有的Hooks使用相关的问题及解答都可以在React官网FAQ 中看到,强烈建议阅读(可能会遇到面试中的问题,譬如usePrevious怎么实现,useMemo, useRef , etc…)

  • 感觉贴了太多的源码(尽管已经是简化了的),会影响阅读体验

  • 文章涉及的源码部分均可以在这里找到

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享