2024年最全大厂的前端白屏监控解决方案(1),字节大牛教你手撕前端学习

总结

前端资料汇总

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 框架原理真的深入某一部分具体的代码和实现方式时,要多注意到细节,不要只能写出一个框架。

  • 算法方面很薄弱的,最好多刷一刷,不然影响你的工资和成功率😯

  • 在投递简历之前,最好通过各种渠道找到公司内部的人,先提前了解业务,也可以帮助后期优秀 offer 的决策。

  • 要勇于说不,对于某些 offer 待遇不满意、业务不喜欢,应该相信自己,不要因为当下没有更好的 offer 而投降,一份工作短则一年长则 N 年,为了幸福生活要慎重选择!!!
    喜欢这篇文章文章的小伙伴们点赞+转发支持,你们的支持是我最大的动力!

饿了么的白屏监控方案,其原理是记录页面打开 4s 前后 html 长度变化,并将数据上传到饿了么自研的时序数据库。如果一个页面是稳定的,那么页面长度变化的分布应该呈现「幂次分布」曲线的形态,p10、p20 (排在文档前 10%、20%)等数据线应该是平稳的,在一定的区间内波动,如果页面出现异常,那么曲线一定会出现掉底的情况。

其他

其他都大同小样,其实调研了一圈下来发现无非就是两点

  1. 监控时机:调研下来常见的就三种:

  2. onerror

  3. mutation observer api

  4. 轮训

  5. DOM 检测:这个方案就很多了,除了上述的还可以:

  6. elementsFromPoint api 采样

  7. 图像识别

  8. 基于 DOM 的各种数据的各种算法识别

改变方向


几番尝试下来几乎没有我想要的,其主要原因是准确率 – 这些方案都不能保证我监听到的是白屏,单从理论的推导就说不通。他们都有一个共同点:监听的是’白屏’这个现象,从现象去推导本质虽然能成功,但是不够准确。所以我真正想要监听的是造成白屏的本质。

那么回到最开始,什么是白屏?他是如何造成的?是因为错误导致的浏览器无法渲染?不,在这个 spa 框架盛行的现在实际上的白屏是框架造成的,本质是由于错误导致框架不知道怎么渲染所以干脆就不渲染。由于我们团队 React 技术栈居多,我们来看看 React 官网的一段话:

React 认为把一个错误的 UI 保留比完全移除它更糟糕。我们不讨论这个看法的正确与否,至少我们知道了白屏的原因:渲染过程的异常且我们没有捕获异常并处理。

反观目前的主流框架:我们把 DOM 的操作托管给了框架,所以渲染的异常处理不同框架方法肯定不一样,这大概就是白屏监控难统一化产品化的原因。但大致方向肯定是一样的。

那么关于白屏我认为可以这么定义:异常导致的渲染失败

那么白屏的监控方案即:监控渲染异常。那么对于 React 而言,答案就是:Error Boundaries

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 {

最后

我可以将最近整理的前端面试题分享出来,其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器、数据结构与算法等等,还在持续整理更新中,希望大家都能找到心仪的工作。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分截图:

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值