浅析React

297 阅读6分钟

设计理念

react的设计理念:践行快速响应

制约快速响应的因素是:CPU的瓶颈与IO的瓶颈

那么react是如何解决这两个瓶颈的呢?

CPU的瓶颈

处理器速度不足以处理和传输数据 就会发生 CPU 瓶颈。

对应场景:当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。

解决CPU瓶颈的关键就是:并发更新替换同步更新

IO瓶颈

网络延迟是前端无法解决的,那么我们就要从体验方面尽量减少用户对网络延迟的感知

React的思路是什么:17.reactjs.org/docs/concur…

为此,React实现了SuspenseuseDeferredValue

Suspense

Suspense可以在组件请求数据时展示一个pending状态。请求成功后渲染数据。

本质上讲Suspense内的组件子树比组件树的其他部分拥有更低的优先级。

useDeferredValue

useDeferredValue返回一个延迟响应的值,该值可能“延后”的最长时间为timeoutMs。

在useDeferredValue内部会调用useState并触发一次更新。这次更新的优先级很低,所以当前如果有正在进行中的更新,不会受useDeferredValue产生的更新影响。所以useDeferredValue能够返回延迟的值。

当超过timeoutMs后useDeferredValue产生的更新还没进行(由于优先级太低一直被打断),则会再触发一次高优先级更新。

架构

新版react的架构可以分为三个阶段:

schedule 调度阶段

调度任务的优先级,高优任务优先进入Reconciler

render 协调阶段

执行reconcile算法找出变化的组件,打上flags,当所有组件都完成Reconciler的阶段,才进入下一阶段,此阶段时会在内存中构建一颗新的workInProgress Fiber树,不过在构建时会尝试复用current Fiber树中已有的Fiber节点内的属性

commit 渲染阶段

负责将变化的组件渲染到页面上,渲染完毕后,workInProgress Fiber 树变为current Fiber 树。

Fiber的双缓存架构

在首屏渲染时,会根据组件返回的jsx依次创建Fiber节点并连接在一起生成Fiber树,触发更新后,这会开启一次新的render阶段并构建一颗新的workInProgress Fiber树,但是构建新的workInProgress Fiber树时可以复用current Fiber树的数据,这个决定是否复用的过程就是Diff算法,workInProgress Fiber树在完成构建后进入commit阶段渲染到页面上,渲染完毕后,workInProgress Fiber树变为current Fiber树。

既然我们每次更新都会创建一颗新的Fiber树进行对比,那么react是怎样提升性能的呢;

首先,我们在创建一颗新树的时候会尝试复用原来树的数据--diff算法;

我们在更新的时候会查看当前state与上一次的是否相同--eagerState;

可以使用一些性能优化相关API来进入bailout阶段

Diffing算法

官网介绍:zh-hans.reactjs.org/docs/reconc…

为了降低算法复杂度,React的diff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定

经过官网的介绍呢,我们就知道了key对于diff算法的作用,以及它是怎么样去提高树的转换效率的,当我们用key指明了节点前后对应关系后,原来存在Key的DOM节点可以复用(此处是先判断key是否相同再判断fiber的type是否相同,如果key不同或者type不同,那么将fiber都标记为删除。

优化策略

eagerState

dispatchAction函数中, 在调用scheduleUpdateOnFiber之前, 针对update对象做了性能优化.

  1. queue.pending中只包含当前update时, 即当前update是queue.pending中的第一个update
  2. 直接调用queue.lastRenderedReducer,计算出update之后的 state, 记为eagerState
  3. 如果eagerState与currentState相同, 则直接退出, 不用发起调度更新.
  4. 已经被挂载到queue.pending上的update会在下一次render时再次合并.

bailout

bailout 是 React reconciler 源码内部 render阶段的一个性能优化路径。

React创建Fiber树时,每个组件对应的fiber都是通过如下两个逻辑之一创建的:

  • render。即调用render函数,根据返回的JSX创建新的fiber。
  • bailout。即满足一定条件时,React判断该组件在更新前后没有发生变化,则复用该组件在上一次更新的fiber作为本次更新的fiber。

可以看到,当命中bailout逻辑时,是不会调用render函数的。

当组件对应的 fiber 命中 bailout 策略后 ,该fiber及其子孙fiber都不会再执行reconcile操作(直观的表现为:不会触发组件render方法)

那么什么情况下会进入bailout呢?让我们看一下以下的代码块,并判断一下在点击setCount时console.log("child render!")是否会执行;

import React from "react";

function Son() {
  console.log("child render!");
  return <div>Son</div>;
}

function Parent(props) {
  const [count, setCount] = React.useState(0);

  return (
    <div
      onClick={() => {
        setCount(count + 1);
      }}
    >
      count:{count}
      <Son />
    </div>
  );
}

export default function App() {
  return (
    <Parent />
  );
import React from "react";

function Son() {
  console.log("child render!");
  return <div>Son</div>;
}

function Parent(props) {
  const [count, setCount] = React.useState(0);

  return (
    <div
      onClick={() => {
        setCount(count + 1);
      }}
    >
      count:{count}
      {props.children}
    </div>
  );
}

export default function App() {
  return (
    <Parent>
      <Son />
    </Parent>
  );
}

此时我们可以看到对比第一个组件,第二个组件的console并没有打印,可以看到组件二此时就进入到了bailout阶段。

当同时满足以下四个条件时,会进入到bailout阶段:

workInProgress.type===current.type

当前fiber树的type与更新前是否一致

oldProps === newProps

即本次更新的props与上次更新的props进行全等比较。

render的返回结果实际是一个包含了props属性的对象,如果本次更新是React.createElement执行的结果,那么就是一个全新的props引用,所以oldProps!==newProps,这就是demo里为什么第一个例子中的son组件会render的原因。

当我们使用memo时,那么在判断是进入render还是bailout时,只是会对props的每个属性进行浅比较,这也就表明了为什么useCallback是要和memo一起使用的原因,单独使用并不会进入到bailout阶段进而提升性能

context

context的value没有变化

!includesSomeLane(renderLanes, updateLanes)

当前的fiber是否存在上次更新,如果不存在或本身无法触发更新,则进入bailout;

如果存在更新并且更新的优先级和本次fiber树的调度的优先级一致,则进入render逻辑

实现

参考:pomb.us/build-your-…