likes
comments
collection
share

React HOOK re-render 开销大吗?

作者站长头像
站长
· 阅读数 47

How do I implement getDerivedStateFromProps?

While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.

Here, we store the previous value of the row prop in a state variable so that we can compare:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

这是一个官方文档的 例子,大概意思就是说利用react的re-render机制,通过hook实现Class中的getDerivedStateFromProps,在描述的最后一句话中提到了 React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.       翻译过来的意思为 “React将在退出第一次渲染后立即重新运行状态更新的组件,这样不会太昂贵。”,这里 我有大概两个疑问 :

  1. 什么时候会出现re-render的情况。
  2. 为什么认为这样的开销是不会太昂贵的

首先 我们知道React 是采用双缓存机制的,一个应用同时存在两颗虚拟DOM树,通常一颗为current 树,和workInprogress 树,(当然为什么 我这么叫这两个树其实是因为他的源码里面的变量是这么命名的),我们的页面其实就是基于current 树 生成的,同时在Update时期基于 current 通过diff算法 生成workInprogress 后,用workInprogress 替换之前的 current ,此时wokInporgress  就成了 current。

什么时候会出现re-render的情况

首先 我们来聊一聊在那种情况下会出现 hook re-render的情况。

目前我只知道一种出现的情况,也就是上面的React 官方给出的例子,其中 如果名字if(row !== prevRow) 这个逻辑 执行其中的 两个 dispatchSetState 方法的时候就会 发生 当前hook 的re-render机制,也就是官方所说的 渲染阶段更新。说了半天 到底什么是re-render 机制呢。

什么是hook的 re-render 机制

总结一句话而言就是,就是在 hook 执行阶段 调用 useState 产生的 dispatchSetState 方法导致 重新执行且只执行当前hook的过程。以下方代码为例

function Animal() {
 const [name,setName] = useSate('tiger')

 setName('cat')
 setName('dog')

 return <div>{name}</div>
}

首先 我们都知道 在hook中可以通过 useState()   方法返回的 对应的 dispatchSetState (useState  返回数组的第二个) 方法来触发更新,然而每次更新都要从头到脚 更新 整棵树,不是类似Vue那种有目标型,而对于 渲染阶段的更新 React 采用 re-render 来优化 ,也就是上面的 Animal 组件中 也就是函数,在执行过程中 调用了两次 setName() 方法改变状态,这种情况下这两次的 状态更新 导致重新执行了 Animal () 方法,而不会 导致整个应用 全部比较更新

我们通过源码来看一下。

对于hook的执行发生在 renderWithHooks 这个方法中,通过 精简代码大概如下。

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) 
{  renderLanes = nextRenderLanes;  
   currentlyRenderingFiber$1 = workInProgress;  //第一步 通过
  {
    if (current !== null && current.memoizedState !== null) {//第二部
      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
       ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
    }
  }
  var children = Component(props, secondArg); // 第三部 这里的Component 就对应上面例子的 Animal
   if (didScheduleRenderPhaseUpdateDuringThisPass) {//第四部
    var numberOfReRenders = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;
      numberOfReRenders += 1;
      children = Component(props, secondArg); //在渲染阶段 发生了 更新 所以 从新执行当前的function Component方法
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  ReactCurrentDispatcher$1.current = ContextOnlyDispatcher; //解绑
  renderLanes = NoLanes;
  currentlyRenderingFiber$1 = null;  //Function Component 执行完后 置空
  currentHook = null;
  workInProgressHook = null;
  return children;  //返回 Function Component 结果
}

大致讲一下上面代码在做什么

  1. 将workInProgress fiber 复制给currentlyRenderingFiber$1,即当前正在操作的fiber 。

  2. 通过一些条件判断现在应该调用 官方提供的 hooks的,mount方法还是update方法,在react源码中 两个时期同一个hook调用的是不同的方法,举个例子,useState mount时期调用 mountState ,update时期调用 updateState 实际上是调用的updateReducer 方法,其他hook 同理,这个就通过ReactCurrentDispatcher$1 的current 属性来表明 之后调用mount  方法还是update方法

  3. 执行 当前的函数组件,返回jsx  生成的对象 ,(执行完后也就相当于 ‘退出第一次渲染’)

  4. 通过 didScheduleRenderPhaseUpdateDuringThisPass 变量判断是不是在渲染阶段进行了更新,命中的话 通过一个do. while 循环 开始我们的re-render ,也就是从新执行 Function Component 函数。(这个do while 中做的也正是我们所谓的 re-render)

  5. 对一些变量进行初始化,同时这里将currentlyRenderingFiber$1 赋值为空

  6. 返回结果children

接下里看看 didScheduleRenderPhaseUpdateDuringThisPass 这个变量是在什么时候被复制为true的

之前我们就说过 ,hook改变状态通过 每个useState 返回的对应的 dispatchSetState 方法来实现。

function dispatchSetState(fiber, queue, action) {
    var lane = requestUpdateLane(fiber);//确立优先级的
    var update = { //生成update 对象
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null
  };
  if (isRenderPhaseUpdate(fiber)) { //判断 是否存在 渲染阶段更新的情况
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    enqueueUpdate$1(fiber, queue, update);
    var alternate = fiber.alternate;
    var eventTime = requestEventTime();
    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitionUpdate(root, queue, lane);
    }
  }
  markUpdateInDevTools(fiber, lane);
}

这里也大致讲一下做了什么

  1. 生成update 对象
  2. 通过 isRenderPhaseUpdate(fiber) 方法判断是否在渲染阶段发生了更新,然后调用enqueueRenderPhaseUpdate(queue, update)

 isRenderPhaseUpdate 方法中很简单,看一下代码

function isRenderPhaseUpdate(fiber) {
          var alternate = fiber.alternate;
         return fiber === currentlyRenderingFiber$1 ||
         alternate !== null &&
         alternate === currentlyRenderingFiber$1;}

这里的参数fiber 到底是个什么动西我们通过调用栈来看一下

function mountState(initialState) {
  var hook = mountWorkInProgressHook();
  var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}
//mountState 中dispatchSetState

mountState 中dispatchSetState 传入 currentlyRenderingFiber$1, 这个不陌生吧 当前执行操作的fiber

所以dispatchSetState(fiber, queue, action) 中fiber 就是currentlyRenderingFiber$1

同时在 dispatchSetState 中调用 isRenderPhaseUpdate(fiber) 这个fiber 也就是currentlyRenderingFiber$1 ,那么刚刚在isRenderPhaseUpdate 的判断逻辑中 第一个就成立了,

其中的alternate 属性使用与在 将current 树 和 workInprogress 树中的fiber节点进行映射链接一个指针

在 一般情况下,也就是 不存在渲染阶段更新的情况的话,isRenderPhaseUpdate 函数中 currentlyRenderingFiber1变量应该等于null,  因为在renderWithHooks里面对应的函数组件方法执行以及re−render执行完后对一些变量进行了初始化,也就是在这个时候currentlyRenderingFiber1变量应该 等于null ,   因为在renderWithHooks 里面 对应的函数组件方法执行以及 re-render 执行完后 对一些变量进行了初始化 ,也就是在这个时候 currentlyRenderingFiber1null,  renderWithHooksrerendercurrentlyRenderingFiber1 = null。

而 didScheduleRenderPhaseUpdateDuringThisPass 这个变量 是在enqueueRenderPhaseUpdate 方法中被赋值为true

function enqueueRenderPhaseUpdate(queue, update) {
   didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  var pending = queue.pending;
  if (pending === null) {
    update.next = update; //初始环转链表
  } else {
    update.next = pending.next;
    pending.next = update;  //环转链表 末尾追加 update
  }
  queue.pending = update;
}

 使用实例

这个有待补充,目前开发中还没正式使用这种方法过,但是大致的一个使用场景我的理解 比如:当前组件的某一个 props  值  改变会引起 state 的变化,同时state 变化后 需要立即 执行render .

总结 

react 的re-render机制开销 不是很大。。。。。。。

转载自:https://juejin.cn/post/7075298386454249479
评论
请登录