玖叶教程网

前端编程开发入门

2023.04.30 更新前端面试问题总结(7道题)

2023.04.26 - 2023.04.30 更新前端面试问题总结(7道题)
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues

目录:

  • 中级开发者相关问题【共计 1 道题】
    • 317.[React] react 是如何实现页面的快速响应?【热度: 696】【web框架】
  • 高级开发者相关问题【共计 5 道题】
    • 318.[React] React15 架构存在什么样的问题?【热度: 1,613】【web框架】
    • 319.[React] React16 是什么样的架构特点?【热度: 2,403】【web框架】
    • 322.[React] fiber 架构 的工作原理?【热度: 1,774】【web框架】
    • 323.[React] Fiber的含义与数据结构【热度: 1,778】【web框架】
    • 324.[React] render 阶段的执行过程【热度: 1,793】【web框架】
  • 资深开发者相关问题【共计 1 道题】
    • 320.[React] React Reconciler 为何要采用 fiber 架构?【热度: 1,794】【web框架】

中级开发者相关问题【共计 1 道题】

317.[React] react 是如何实现页面的快速响应?【热度: 696】【web框架】

关键词:react 快速响应实现、react 可中断更新、react IO瓶颈、react CPU瓶颈

react 是如何实现快速响应的?

我们日常使用App,浏览网页时,有两类场景会制约快速响应:

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

发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

这两类场景可以概括为:

  • CPU的瓶颈
  • IO的瓶颈

CPU的瓶颈

主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。

我们知道,JS可以操作DOM,GUI渲染线程与JS线程是互斥的。所以JS脚本执行和浏览器布局、绘制不能同时执行。

在每16.6ms时间内,需要完成如下工作: JS脚本执行 ----- 样式布局 ----- 样式绘制

当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。

比如我们可以通过一个循环, 渲染列表 3000 个组件, 那么这种渲染时间, 就肯定是远超过 16.6 ms 的, 页面就会感觉到卡顿。

如何解决这个问题呢?

答案是:在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件(可以看到,在源码中,预留的初始时间是5ms)。
源码位置: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L119

当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作。

这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)

所以,解决CPU瓶颈的关键是实现时间切片,而时间切片的关键是:将同步的更新变为可中断的异步更新。

IO的瓶颈

网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下,减少用户对网络延迟的感知?

简单点儿来说, 就是在点击页面跳转的是时候提前去加载下一个页面的内容。 或者在当前页面 hold .5s 左右时间, 利用这个时间去加载下一个页面的内容。
从而达到下一个页面的快速交互

React实现了 Suspense 功能及配套的 hook——useDeferredValue。

而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新。

高级开发者相关问题【共计 5 道题】

318.[React] React15 架构存在什么样的问题?【热度: 1,613】【web框架】

关键词:react15 架构、react 架构、react Reconciler、react 渲染器、react 协调器

React15 架构可以分为两层:

  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

Reconciler(协调器)

我们知道,在React中可以通过 this.setState、this.forceUpdate、ReactDOM.render 等API触发更新。

每当有更新发生时,Reconciler会做如下工作:

  • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  • 将虚拟DOM和上次更新时的虚拟DOM对比
  • 通过对比找出本次更新中变化的虚拟DOM
  • 通知Renderer将变化的虚拟DOM渲染到页面上

Renderer(渲染器)

由于React支持跨平台,所以不同平台有不同的Renderer。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM

除此之外,还有:

  • ReactNative 渲染器,渲染App原生组件
  • ReactTest 渲染器,渲染出纯Js对象用于测试
  • ReactArt 渲染器,渲染到Canvas, SVG 或 VML (IE8)

在每次更新发生时,Renderer接到 Reconciler 通知,将变化的组件渲染在当前宿主环境。

React15 架构的缺点

react15 是通过递归去更新组件的

在 Reconciler 中,mount的组件会调用 mountComponent (opens new window),update 的组件会调用 updateComponent (opens new window)。这两个方法都会递归更新子组件。

由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。

本质上说是因为 递归 的架构, 是不允许中断的, 因为 react 希望有更好的渲染性能,那么面对大规模 dom diff 更新渲染的时候, 就不能让每一递归时间超过 16 ms。
递归是做不到这个功能的。 所以只有重写 react15 架构。引入了 react16 fiber 架构。

319.[React] React16 是什么样的架构特点?【热度: 2,403】【web框架】

关键词:react16 架构、react Reconciler、react fiber、react 渲染器、react 协调器

React16架构可以分为三层:

Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
Reconciler(协调器)—— 负责找出变化的组件
Renderer(渲染器)—— 负责将变化的组件渲染到页面上
可以看到,相较于React15,React16中新增了Scheduler(调度器)。

Scheduler(调度器)

以浏览器是否有剩余时间作为任务中断的标准,那么需要一种机制,当浏览器有剩余时间时通知我们

其实部分浏览器已经实现了这个API,这就是 requestIdleCallback (opens new window)。但是由于以下因素,React放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的 requestIdleCallback 触发的频率会变得很低

基于以上原因,React实现了功能更完备的 requestIdleCallback polyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置。

Scheduler (opens new window) 是独立于React的库

Reconciler(协调器)

在 React15 中 Reconciler 是递归处理虚拟DOM的

在 React16 中更新工作从递归变成了可以中断的循环过程。每次循环都会调用 shouldYield 判断当前是否有剩余时间。

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

那么React16是如何解决中断更新时DOM渲染不完全的问题呢?

在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记;

全部标记可以见这里: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactSideEffectTags.js

整个Scheduler与 Reconciler 的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。

可以看这里 react16 对 Reconciler 的解释:https://zh-hans.legacy.reactjs.org/docs/codebase-overview.html#fiber-reconciler

Reconciler 内部采用了 Fiber 的架构。

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

参考资料

  • https://react.iamkasong.com/preparation/newConstructure.html#react16%E6%9E%B6%E6%9E%84

322.[React] fiber 架构 的工作原理?【热度: 1,774】【web框架】

关键词:react16 架构、react Reconciler、react fiber、react 协调器

双缓存Fiber树

如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题, 就有了图像处理中的双缓存技术

双缓存是一种技术,用于在图像处理中减少闪烁和图像模糊等视觉问题。在使用双缓存时,图像处理器会将图像绘制到一个“后台缓存”中,而不是直接绘制到屏幕上。一旦绘制完成,新的图像将与当前显示的图像交换,使得新图像无缝地显示在屏幕上,避免了闪烁和模糊的问题。因此,双缓存有助于提高图像处理的质量和可靠性,特别是在高速显示和实时处理应用中。

React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

React Fiber 的双缓存机制是一种优化技术,用于在 UI 更新过程中避免视觉问题,如闪烁、撕裂和卡顿等。React Double Buffer 在 React Fiber 内部实现了两个缓存区域:当前显示的缓存(Current Buffer)和等待显示的缓存(Work Buffer)。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

当应用程序状态发生更改,并需要更新 UI 时,React Fiber 首先在 Work Buffer 中执行所有渲染操作,以避免将中间状态呈现在屏幕上。一旦 Work Buffer 中的所有渲染操作完成,React Fiber 将当前缓存与工作缓存进行切换,即将 Work Buffer 设置为当前缓存,以此来更新屏幕上的 UI。

这样一来,React Fiber 就可以确保在任何时候,所有呈现在屏幕上的内容都是完整和稳定的。

mount与update 场景

当组件第一次被挂载时:

class MyComponent extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleClick = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        Click me: {this.state.count}
      </div>
    );
  }
}

ReactDOM.render(<MyComponent />, document.getElementById('root'));

当我们将 <MyComponent /> 挂载到页面上时,React Fiber 首先会在内存中创建一个空的 Fiber 树,然后根据组件的定义,为组件创建一个初始的“工作单元”(Work In Progress)。

在这个工作单元内部,React Fiber 会为状态和 props 建立初始的 Fiber 对象,并在之后的更新过程中使用这些 Fiber 对象来跟踪组件的状态和变化。这样可以确保任何时候都可以根据状态和 props 的变化来更新 UI,而不会出现任何问题。

接下来,React Fiber 开始在工作单元中执行所有的渲染操作,生成一棵虚拟 DOM 树,并将其添加到 Work Buffer 中。然后,React Fiber 会检查 Work Buffer 是否有更改,如果有更改,就将 Work Buffer 与 Current Buffer 进行对比,以查找差异并更新到 DOM 上。

这个初次渲染的过程不太会涉及到双缓存树,因为当前缓存是空的,所有的操作都是在 Work Buffer 中进行的。但是,一旦初次渲染完成,并且组件状态发生变化时,双缓存树就开始发挥作用了。

当我们通过点击按钮更新组件状态时,React Fiber 将启动一个新的渲染周期,并为更新创建一个新的工作单元。React Fiber 会在新的工作单元中更新状态、生成新的虚拟 DOM 树,并将其添加到 Work Buffer 中。

然后,React Fiber 会将 Work Buffer 与 Current Buffer 进行对比,找出差异并将其更新到 DOM 上。但是,由于双缓存树的存在,React Fiber 不会立即将 Work Buffer 切换到 Current Buffer,以避免将中间状态显示在屏幕上。

执行流程

好的,下面是 React Fiber 在页面初次更新时的工作过程的流程图:

  1. 应用程序启动,ReactDOM 调用 ReactDOM.render() 方法,并将组件渲染到 DOM 中,React Fiber 创建一个空的 Fiber 树。
  2. React Fiber 为组件创建初始的“工作单元”,并在其中创建状态和 props 的 Fiber 对象。
  3. React Fiber 执行组件的 render() 方法,生成虚拟 DOM 树并添加到工作单元中。
  4. React Fiber 将工作单元中的虚拟 DOM 树添加到 Work Buffer 中。
  5. React Fiber 检查 Work Buffer 是否有更改,如果有更改,则将其与 Current Buffer 进行对比,并将差异更新到 DOM 上。
  6. 由于这是初次渲染,Current Buffer 为空,所有更新操作都在 Work Buffer 中完成,然后将 Work Buffer 设置为 Current Buffer。
  7. React Fiber 在内存中保留 Fiber 树的副本,并用于后续的更新操作。此时,组件初次渲染流程结束。

323.[React] Fiber的含义与数据结构【热度: 1,778】【web框架】

关键词:react16 架构、react Reconciler、react fiber、react 协调器

Fiber的含义

  1. 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)。

Fiber的结构

总的属性如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

可以按三层含义将他们分类来看

作为架构

每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

举个例子,如下的组件结构:

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

对应的Fiber树结构:

作为静态的数据结构

作为一种静态的数据结构,保存了组件相关的信息:

// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;

作为动态的工作单元

作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息,我们会在后续的更新流程中使用到具体属性时再详细介绍

// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

324.[React] render 阶段的执行过程【热度: 1,793】【web框架】

关键词:react16 架构、react Reconciler、react fiber、react 协调器

render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。

workInProgress代表当前已创建的workInProgress fiber。

performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。

创建节点

首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 (opens new window)。

该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

在“归”阶段会调用completeWork (opens new window)处理Fiber节点。

当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。

如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

举例

代码如下:

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App/>, document.getElementById("root"));

对应的 fiber 树结构如下

render 阶段会依次执行

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

beginWork

源码链接: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3075

工作流程图:

beginWork的工作是传入当前Fiber节点,创建子Fiber节点,我们从传参来看看具体是如何做的。

传参

function beginWork(
  current: Fiber | null, // 当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  workInProgress: Fiber, // 当前组件对应的Fiber节点
  renderLanes: Lanes, // 优先级相关,在讲解Scheduler时再讲解
): Fiber | null {
  // ...省略函数体
}

beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    // ...省略
    case LazyComponent:
    // ...省略
    case FunctionComponent:
    // ...省略
    case ClassComponent:
    // ...省略
    case HostRoot:
    // ...省略
    case HostComponent:
    // ...省略
    case HostText:
    // ...省略
    // ...省略其他类型
  }
}

update时

满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)

if (current !== null) {
  const oldProps = current.memoizedProps;
  const newProps = workInProgress.pendingProps;

  if (
    oldProps !== newProps ||
    hasLegacyContextChanged() ||
    (__DEV__ ? workInProgress.type !== current.type : false)
  ) {
    didReceiveUpdate = true;
  } else if (!includesSomeLane(renderLanes, updateLanes)) {
    didReceiveUpdate = false;
    switch (workInProgress.tag) {
      // 省略处理
    }
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }
} else {
  didReceiveUpdate = false;
}
  1. oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够,会在讲解Scheduler时介绍

mount

当不满足优化路径时,我们就进入第二部分,新建子Fiber。

// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
  case IndeterminateComponent:
  // ...省略
  case LazyComponent:
  // ...省略
  case FunctionComponent:
  // ...省略
  case ClassComponent:
  // ...省略
  case HostRoot:
  // ...省略
  case HostComponent:
  // ...省略
  case HostText:
  // ...省略
  // ...省略其他类型
}

我们可以看到,根据fiber.tag不同,进入不同类型Fiber的创建逻辑。

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren (opens new window)方法。

reconcileChildren

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

从代码可以看出,和beginWork一样,他也是通过current === null ?区分mount与update。

不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值 (opens new window)
,并作为下次performUnitOfWork执行时workInProgress的传参

effectTag

我们知道,render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

通过二进制表示effectTag,可以方便的使用位操作为fiber.effectTag赋值多个effect。

那么,如果要通知Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:

  1. fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
  2. (fiber.effectTag & Placement) !== 0,即 Fiber节点存在Placement effectTag

我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?

针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement
effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。

为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

completeWork

流程图:

类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
    // ...省略
  }
  // ...省略
}

我们重点关注页面渲染所必须的 HostComponent(即原生DOM组件对应的Fiber节点),其他类型Fiber的处理留在具体功能实现时讲解。

处理 HostComponent

和beginWork一样,我们根据 current === null ?判断是mount还是update。

同时针对 HostComponent,判断 update 时我们还需要考虑 workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

case
HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  return null;
}

update 时

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClick、onChange 等回调函数的注册
  • 处理 style prop
  • 处理 DANGEROUSLY_SET_INNER_HTML prop
  • 处理 children prop

我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
}

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

workInProgress.updateQueue = (updatePayload: any);

其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。

mount 时

同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程
// mount的情况

// ...省略服务端渲染相关逻辑

const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?

原因就在于 completeWork中的appendAllChildren 方法。

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。

effectList

至此render阶段的绝大部分工作就完成了。

还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?

这显然是很低效的。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。

类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

这样,在commit阶段只需要遍历effectList就能执行所有effect了。

流程结尾

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

commitRoot(root);

资深开发者相关问题【共计 1 道题】

320.[React] React Reconciler 为何要采用 fiber 架构?【热度: 1,794】【web框架】

关键词:react16 架构、react Reconciler、react fiber、react 协调器

代数效应的实践

React中做的就是践行代数效应(Algebraic Effects)。

简单点儿来说就是: 用于将副作用从函数调用中分离。

举例子:
比如我们要获取用户的姓名做展示:

const resource = fetchProfileData();

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

代码如上, 但是 resource 是通过异步获取的。 这个时候代码就要改为下面这种形式

const resource = fetchProfileData();

async function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = await resource.user.read();
  return <h1>{user.name}</h1>;
}

但是 async/await 是具有传染性的。 这个穿践行就是副作用, 我们不希望有这样的副作用, 尽管里面有异步调用, 不希望这样的副作用传递给外部的函数, 只希望外部的函数是一个纯函数。

代数效应在React中的应用

在 react 代码中, 每一个函数式组件, 其实都是一个纯函数, 但是内部里面可能会有各种各样的副作用。 这些副作用就是我们使用的 hooks;

对于类似useState、useReducer、useRef这样的Hook,我们不需要关注FunctionComponent的state在Hook中是如何保存的,React会为我们处理。

我们只需要假设useState返回的是我们想要的state,并编写业务逻辑就行。

可以看官方的 Suspense demo, 可以是通过 Suspense 让内部直接可以同步的方式调用异步代码;
代码链接: https://codesandbox.io/s/frosty-hermann-bztrp?file=/src/index.js:152-160

import React, { Suspense } from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import { fetchProfileData } from "./fakeApi";

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense
      fallback={<h1>Loading profile...</h1>}
    >
      <ProfileDetails />
      <Suspense
        fallback={<h1>Loading posts...</h1>}
      >
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

const rootElement = document.getElementById(
  "root"
);
ReactDOM.createRoot(rootElement).render(
  <ProfilePage />
);

Generator 架构

从React15到React16,协调器(Reconciler)重构的一大目的是:将老的同步更新的架构变为异步可中断更新。

异步可中断更新可以理解为:更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。

其实,浏览器原生就支持类似的实现,这就是Generator。

但是Generator的一些缺陷使React团队放弃了他:

  • 类似async,Generator也是传染性的,使用了Generator则上下文的其他函数也需要作出改变。这样心智负担比较重。
  • Generator执行的中间状态是上下文关联的。

例如这样的例子:

function* doWork(A, B, C) {
  var x = doExpensiveWorkA(A);
  yield;
  var y = x + doExpensiveWorkB(B);
  yield;
  var z = y + doExpensiveWorkC(C);
  return z;
}

但是当我们考虑“高优先级任务插队”的情况,如果此时已经完成doExpensiveWorkA与doExpensiveWorkB计算出x与y。

此时B组件接收到一个高优更新,由于Generator执行的中间状态是上下文关联的,所以计算y时无法复用之前已经计算出的x,需要重新计算。

如果通过全局变量保存之前执行的中间状态,又会引入新的复杂度。

fiber 架构

他的中文翻译叫做纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。

在很多文章中将纤程理解为协程的一种实现。在JS中,协程的实现便是Generator。

所以,我们可以将纤程(Fiber)、协程(Generator)理解为代数效应思想在JS中的体现。

React Fiber可以理解为:

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

其中每个任务更新单元为React Element对应的Fiber节点。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言