React Fiber源码笔记(一):概念-浏览器帧渲染React Fiber 架构理念前置知识:复习浏览器事件循环机制
前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正(免责声明) 博客链接:pionpill
这篇文章开始会逐步解析 React Fiber 架构的源代码,简单说一下本系列文章的结构:
- 概念章 (理念性文章)
- 浏览器帧渲染: 简单介绍一些基础的浏览器概念。
- Fiber架构: 简单介绍 Fiber 架构的理念。
- vDOM 章 (数据结构篇)
- JSX 解析: 介绍 JSX -> ReactElement 的过程,给下一篇打基础
- FiberNode: 介绍 FiberNode, FiberRootNode 两种数据结构及其构建过程,阐述几个重要的属性
- scheduler 章
- 优先级与准备阶段: scheduler 的一些基础概念以及 FiberNode 执行更新任务的准备阶段。
- 调度流程: scheduler 在时间片内处理异步任务的流程
- 过渡章
- 双缓存机制: Fiber 架构的核心机制,是后面所有文档的基础
- 冲刷副作用: react 副作用异步执行逻辑,这个逻辑在后续各个阶段都会被频繁提到
- render 章
- 准备阶段: 进入 render 核心阶段前的准备阶段
- beginWork: Fiber 树递阶段,开始从根节点到子节点进行 diff 比较,尝试替换 FiberNode
- completeWork: Fiber 树归阶段,更新 FiberNode
- commit 章
- 概述: commit 阶段整体逻辑
- DOM阶段: 执行 DOM 各阶段的钩子/API函数,触发被动副作用 (useState 导致的副作用)
如有错误或者表述不明的地方请联系本人。React 源代码较为难读,国内相关文章比较少,多个模块互相依赖。如果一遍看不懂,建议往后看几章,再回过来看会恍然大悟(我写的时候有这种感觉)。
这篇文章简述浏览器在帧时间内的操作逻辑,说明一些基本概念,方便后续解析 Fiber 框架。系列文章默认浏览器内核为 chromium,JS 引擎为 v8 引擎
浏览器多进程体系
现代浏览器基本都是多进程(process)架构,以 chrome 浏览器为例,它主要包含了以下几种类型的进程:
- 浏览器进程(browser process): 主进程,负责管理合协调其他进程,只有一个。
- 渲染进程(render process): 每个标签页,扩展页,
iframe
都有一个,负责解析和渲染网页内容,解析与执行 JS 代码,与用户交互。 - GPU 进程(GPU process): 负责处理与图形相关的任务,例如页面绘制,CSS 动画。
- 插件进程(plugin process): 运行诸如 flash,pdf 等插件。
- 存储进程(utility process): 负责处理存储相关的任务: Cookie, 缓存...
- 扩展进程(Extension Process):提供额外的功能和服务。
假设我们打开一个使用 chromium 内核的 Edge 浏览器(不打开 Chrome 的原因是 Chrome 浏览器所有进程都叫 Google Chrome),展开 Microsoft Edge 可能会有如下进程:
Microsoft Edge
|- 浏览器 // 浏览器进程
|- GPU 进程 // GPU 进程
|- 标签页: xxx // 渲染进程
|- 标签页: xxx // 渲染进程
|- 标签页: xxx // 渲染进程
|- 扩展: React devtools // 插件进程
|- 实用工具: Storage Service // 存储进程
这一系列文章只关注渲染进程,渲染进程负责处理 HTML,CSS,JS。每个渲染进程都会有一个 v8 引擎负责解析与执行 JavaScript 代码。渲染进程(基本)只和主进程与GPU进程通信。
渲染进程
这里的所有进程都可以是多线程(thread)的,渲染进程中常见的线程包括:
- GUI 渲染线程: 将 HTML 和 CSS 转换为可视化页面。
- JS 引擎线程: 执行与解析 JS 代码,顾名思义有一个 v8 引擎。
- 事件线程: 处理用户输入,定时器事件和异步操作的回调。
- 定时触发器线程: setInterval与setTimeout所在线程,由于 JS 引擎是单线程的,因此要单独开一个线程。
- 异步http请求线程: 处理 http 请求的。
- 工作线程: 执行 Web Workers,在后台执行复杂的计算任务,不影响JS线程。
这里浏览器引擎不同或者版本不同可能存在一定差异。下文只关注于 GUI 线程和 JS 线程
这些线程中有几个需要注意的地方:
- JS 线程是单线程的,并且一个渲染进程一定有一个 JS 线程(即使啥都不干)。由于 JS 线程是单线程的,可能存在任务阻塞问题,才将一些任务委派给了事件线程,定时触发器线程...
- GUI 线程和 JS 线程是互斥的。因为 GUI 线程会和 JS 线程都会操作 DOM。
简单说一下渲染线程,渲染线程可以被分为以下子阶段:
构建 DOM 树 -> 样式计算 -> 布局 -> 分层 -> 绘制 -> 分块 -> 光栅 -> 合成
layout | paint
由于浏览器不同,在很多文章中,还有一个主线程的概念。主线程是指 GUI 线程或者 JS 线程,因为这两个线程是互斥的,同一时间只能执行一个,而这两线程又承担了绝大多数任务,因此称某一时刻正在执行的 JS 线程或 GUI 线程为主线程。也有说 JS 引擎 GUI 渲染在一个主线程中调度;或者说 GUI 线程就是主线程,但是 GUI 线程是没有执行 JS 代码能力的,很奇怪。总而言之,我们记住渲染过程和 JS 解析执行操作互斥,主线程是一个可以包含两者功能的概念就行了。
事件循环
下文会涉及到的几个概念:
- 主线程: "进入主线程"表示会在一轮事件循环中执行;"不进入主线程"表示不在该轮事件循环中执行,一般是指放到容器中不处理。
- 任务队列: 一种数据结构,先进先出地存储异步任务。
JS 和 JS 引擎都是单线程的,为了防止多任务阻塞,产生了事件循环机制。事件循环过程中有两类任务:
- 同步任务: 进入主线程,排队执行的任务。
- 异步任务: 不进入主线程,进入异步任务队列。
异步任务又分为两种:
- 宏任务: 由浏览器,Node等宿主环境发起的任务。
- 定时器任务 setTimeout/setInterval,setImmediate...
- I/O
- UI 渲染
- requestAnimationFrame
- script 标签
- 微任务: 由 JS 引擎发起的任务
- promise 的回调函数,注意 Promise 本身是同步的。
- Object.observe / MutationObserver
- process.nextTick(node.js)
- Async/Await
遇到一串 JS 代码,事件循环的执行逻辑如下:
- 开始执行代码
- 找到同步任务,执行
- 找到微任务,加入到微任务队列
- 找到宏任务,加入到宏任务队列
- 同步任务执行完毕
- 依此取微任务队列中的所有任务执行
- 取下一个宏任务(将其变成同步代码),开始下一轮事件循环
如果将首次执行的同步代码看作一个宏任务转变来的,那么一次事件循环就是执行一个(先前的)宏任务,与所有的微任务。甚至可以这样理解: 宏任务会触发事件循环,微任务处理异步操作。
帧渲染
前面说了那么多,都是为了帧渲染做铺垫。
浏览器一般是 60fps,即一秒渲染 60 帧,一帧的渲染时间就是 16.6ms,如果刷新率有变化,这个时间也会发生对应的变化,我们将渲染一帧可用的时间称为帧时间。
帧时间内会干以下事:
我们一般将 layout 和 paint 两个过程合称为渲染过程
上面蓝色部分 JS 引擎会参与,其中两 rAF 和 idle 含义如下(并不是所有的浏览器都有这两个过程):
- rAF: 两个 API 最初的设计都是用于控制动画的,也可以用于在渲染过程前执行 JS 代码,相关链接: requestAnimationFrame,MutationObserver
- idle: 这里是帧时间的空闲阶段,如果某帧执行完了发现还有多余的时间,会在这里执行一些操作。
所以说,浏览器帧时间内,JS 引擎会先后执行:
- 取出上一个宏任务
- 执行同步代码与异步微任务
- 看看有没有 rAF 相关的回调,有就执行
- 如果有空闲时间,执行 idle
如果某个任务执行时间过长,在帧时间内无法完成,就会一直执行下去,直到完成该任务。这种执行时间超过帧时间的任务叫做长任务。例如某个任务执行了 70ms,那么就占用了 5 帧。本来 1 帧就会返回结果,现在却要 5 帧,就有 4 帧无法进行渲染操作,画面就会卡顿。
在浏览器 devtools 中有一个性能(performance)界面,可以录制一段时间内的操作,并显示堆栈图。
转载自:https://juejin.cn/post/7403552860456140826