#性能优化-白屏度量 阿里是怎么监控前端白屏的?

本文详细解释了React中的ErrorBoundary组件如何监控和处理子组件渲染错误,展示了从代码到页面展示的React渲染流程,以及错误边界在异常捕获中的作用,确保页面不会完全空白。
摘要由CSDN通过智能技术生成

Error Boundaries


我们可以称之为错误边界,错误边界是什么?它其实就是一个生命周期,用来监听当前组件的 children 渲染过程中的错误,并可以返回一个 降级的 UI 来渲染:

class ErrorBoundary extends React.Component {

constructor(props) {

super(props);

this.state = { hasError: false };

}

static getDerivedStateFromError(error) {

// 更新 state 使下一次渲染能够显示降级后的 UI

return { hasError: true };

}

componentDidCatch(error, errorInfo) {

// 我们可以将错误日志上报给服务器

logErrorToMyService(error, errorInfo);

}

render() {

if (this.state.hasError) {

// 我们可以自定义降级后的 UI 并渲染

return 

Something went wrong.

;

}

return this.props.children;

}

}

一个有责任心的开发一定不会放任错误的发生。错误边界可以包在任何位置并提供降级 UI,也就是说,一旦开发者’有责任心’ 页面就不会全白,这也是我之前说的方案一与之天然冲突且其他方案不稳定的情况。那么,在这同时我们上报异常信息,这里上报的异常一定会导致我们定义的白屏,这一推导是 100% 正确的。

100% 这个词或许不够负责,接下来我们来看看为什么我说这一推导是 100% 准确的:

React 渲染流程

我们来简单回顾下从代码到展现页面上 React 做了什么。我大致将其分为几个阶段:render => 任务调度 => 任务循环 => 提交 => 展示 我们举一个简单的例子来展示其整个过程(任务调度不再本次讨论范围故不展示):

const App = ({ children }) => (

<>

hello

{ children }

</>

);

const Child = () => 

I’m child

const a = ReactDOM.render(

,

document.getElementById(‘root’)

);

准备

首先浏览器是不认识我们的 jsx 语法的,所以我们通过 babel 编译大概能得到下面的代码:

var App = function App(_ref2) {

var children = _ref2.children;

return React.createElement(“p”, null, “hello”), children);

};

var Child = function Child() {

return React.createElement(“p”, null, “I’m child”);

};

ReactDOM.render(React.createElement(App, null, React.createElement(Child, null)), document.getElementById(‘root’));

babel 插件将所有的 jsx 都转成了 createElement 方法,执行它会得到一个描述对象 ReactElement 大概长这样子:

{

$$typeof: Symbol(react.element),

key: null,

props: {}, // createElement 第二个参数 注意 children 也在这里,children 也会是一个 ReactElement 或 数组

type: ‘h1’ // createElement 的第一个参数,可能是原生的节点字符串,也可能是一个组件对象(Function、Class…)

}

所有的节点包括原生的 <a></a> 、 <p></p> 都会创建一个 FiberNode ,他的结构大概长这样:

FiberNode = {

elementType: null, // 传入 createElement 的第一个参数

key: null,

type: HostRoot, // 节点类型(根节点、函数组件、类组件等等)

return: null, // 父 FiberNode

child: null, // 第一个子 FiberNode

sibling: null, // 下一个兄弟 FiberNode

flag: null, // 状态标记

}

你可以把它理解为 Virtual Dom 只不过多了许多调度的东西。最开始我们会为根节点创建一个 FiberNodeRoot 如果有且仅有一个 ReactDOM.render 那么他就是唯一的根,当前有且仅有一个 FiberNode 树。

我只保留了一些渲染过程中重要的字段,其他还有很多用于调度、判断的字段我这边就不放出来了,有兴趣自行了解

render

现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render 。这里我们有个全局 workInProgress 对象标志当前处理的 FiberNode

  1. 首先我们为根节点初始化一个 FiberNodeRoot ,他的结构就如上面所示,并将 workInProgress= FiberNodeRoot

  2. 接下来我们执行 ReactDOM.render 方法的第一个参数,我们得到一个 ReactElement :

ReactElement = {

$$typeof: Symbol(react.element),

key: null,

props: {

children: {

$$typeof: Symbol(react.element),

key: null,

props: {},

ref: null,

type: ƒ Child(),

}

}

ref: null,

type: f App()

}

该结构描述了 <App><Child /></App>

  1. 我们为 ReactElement 生成一个 FiberNode 并把 return 指向父 FiberNode ,最开始是我们的根节点,并将 workInProgress = FiberNode

{

elementType: f App(), // type 就是 App 函数

key: null,

type: FunctionComponent, // 函数组件类型

return: FiberNodeRoot, // 我们的根节点

child: null,

sibling: null,

flags: null

}

  1. 只要workInProgress 存在我们就要处理其指向的 FiberNode 。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行 App(props) 方法,这里有两种情况

  2. 该组件 return 一个单一节点,也就是返回一个 ReactElement 对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点 CurrentFiberNode.child = ChildFiberNode 并将子节点的 return 指向当前节点 ChildFiberNode.return = CurrentFiberNode

  3. 该组件 return 多个节点(数组或者 Fragment ),此时我们会得到一个 ChildiFberNode 的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点 CurrentFiberNode.child = ChildFiberNodeList[0] ,同时每个子节点的 sibling 指向其下一个子节点(如果有) ChildFiberNode[i].sibling = ChildFiberNode[i + 1] ,每个子节点的 return 都指向当前节点 ChildFiberNode[i].return = CurrentFiberNode

如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement

  1. 重复步骤直到处理完全部节点 workInProgress 为空。

最终我们能大概得到这样一个 FiberNode 树:

FiberNodeRoot = {

elementType: null,

type: HostRoot,

return: null,

child: FiberNode,

sibling: null,

flags: Placement, // 待布局状态

}

FiberNode {

elementType: f App(),

type: FunctionComponent,

return: FiberNodeRoot,

child: FiberNode

,

sibling: null,

flags: Placement // 待布局状态

}

FiberNode

 {

elementType: ‘p’,

type: HostComponent,

return: FiberNode,

sibling: FiberNode,

child: null,

flags: Placement // 待布局状态

}

FiberNode {

elementType: f Child(),

type: FunctionComponent,

return: FiberNode,

child: null,

flags: Placement // 待布局状态

}

提交阶段

提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。

那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理

错误边界流程

刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:

const App = ({ children }) => (

<>

hello

{ children }

</>

);

const Child = () => 

I’m child {a.a}

const a = ReactDOM.render(

,

document.getElementById(‘root’)

);

执行步骤 4 的函数体是包裹在 try...catch 内的如果捕获到了异常则会走异常的流程:

do {

try {

workLoopSync(); // 上述 步骤 4

break;

} catch (thrownValue) {

handleError(root, thrownValue);

}

} while (true);

执行步骤 4 时我们调用 Child 方法由于我们加了个不存在的表达式 {a.a} 此时会抛出异常进入我们的 handleError 流程此时我们处理的目标是 FiberNode<Child> ,我们来看看 handleError :

function handleError(root, thrownValue): void {

let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点

throwException(

root, // 我们的根 FiberNode

erroredWork.return, // 父节点

erroredWork,

thrownValue, // 异常内容

);

completeUnitOfWork(erroredWork);

}

function throwException(

root: FiberRoot,

returnFiber: Fiber,

sourceFiber: Fiber,

value: mixed,

) {

// The source fiber did not complete.

sourceFiber.flags |= Incomplete;

let workInProgress = returnFiber;

do {

switch (workInProgress.tag) {

case HostRoot: {

workInProgress.flags |= ShouldCapture;

return;

}

case ClassComponent:

// Capture and retry

const ctor = workInProgress.type;

const instance = workInProgress.stateNode;

if (

(workInProgress.flags & DidCapture) === NoFlags &&

(typeof ctor.getDerivedStateFromError === ‘function’ ||

(instance !== null &&

typeof instance.componentDidCatch === ‘function’ &&

!isAlreadyFailedLegacyErrorBoundary(instance)))

) {

workInProgress.flags |= ShouldCapture;

return;

}

break;

default:

break;

}

workInProgress = workInProgress.return;

} while (workInProgress !== null);

}

代码过长截取一部分 先看 throwException 方法,核心两件事:

  1. 将当前也就是出问题的节点状态标志为未完成 FiberNode.flags = Incomplete

  2. 从父节点开始冒泡,向上寻找有能力处理异常( ClassComponent )且的确处理了异常的(声明了 getDerivedStateFromError 或 componentDidCatch 生命周期)节点,如果有,则将那个节点标志为待捕获 workInProgress.flags |= ShouldCapture ,如果没有则是根节点。

completeUnitOfWork 方法也类似,从父节点开始冒泡,找到 ShouldCapture 标记的节点,如果有就标记为已捕获 DidCapture ,如果没找到,则一路把所有的节点都标记为 Incomplete 直到根节点,并把 workInProgress 指向当前捕获的节点。

之后从当前捕获的节点(也有可能没捕获是根节点)开始重新走流程,由于其状态 react 只会渲染其降级 UI,如果有 sibling 节点则会继续走下面的流程。我们看看上述例子最终得到的 FiberNode 树:

FiberNodeRoot = {

elementType: null,

type: HostRoot,

return: null,

child: FiberNode,

sibling: null,

flags: Placement, // 待布局状态

}

FiberNode {

elementType: f App(),

type: FunctionComponent,

return: FiberNodeRoot,

child: FiberNode

,

sibling: null,

flags: Placement // 待布局状态

}

FiberNode

 {

elementType: ‘p’,

type: HostComponent,

return: FiberNode,

sibling: FiberNode,

child: null,

flags: Placement // 待布局状态

}

FiberNode {

elementType: f ErrorBoundary(),

type: ClassComponent,

return: FiberNode,

child: null,

flags: DidCapture // 已捕获状态

}

FiberNode

 {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

最后

前端校招精编面试解析大全点击这里即可获取完整版pdf查看

3年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-is5r5isX-1711643779418)]

[外链图片转存中…(img-v2lqVReL-1711643779418)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-PU27Q7cB-1711643779418)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

最后

前端校招精编面试解析大全点击这里即可获取完整版pdf查看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值