这是我参与更文挑战的第20天,活动详情查看: 更文挑战
大家好我是小村儿,在上节我们结束了TinyReact的实现,接下来我们学习React16中用到的被称为Fiber算法,我们会花几个篇章搞明白什么是Fiber算法!!!
开发配置环境
我们进行Fiber学习之前,我们首先要搭建一个开发环境。
1. 开发项目文件夹结构
| 文件/文件夹 | 描述 |
|---|---|
| src | 存储原文件(我们接下来的Fiber代码都卸载这里面) |
| dist | 存储客户端代码打包文件(src经过编译后的代码存储在这里) |
| build | 存储服务端代码打包文件 |
| server.js | 存储服务器端代码 |
| webpack.config.server.js | 服务端webpack配置文件 |
| webpack.config.client.js | 客户端webpack配置文件 |
| babel.config.json | babel配置文件 |
| package.json | 项目工程文件 |
package.json 由命令行输入:
npm init -y
生成
2. 安装项目第三方依赖
| 依赖项 | 描述 |
|---|---|
| webpack | 模块打包工具 |
| webpack-cli | 打包命令( 有这个才能在命令行执行webpack命令。) |
| webpack-node-externals | 打包服务器端模块时删除node_modules文件夹中的模块 |
| @babel/preset-env | babel预置,转换高级JavaScript语法 |
| @babel/preset-react | babel预置,转化JSX语法 |
| babel-loader | webpack中的babel工具加载器 |
| nodemon | 监控服务端文件变化,重启应用 |
| npm-run-all | 命令行工具,可以同时执行多个命令 |
| express | 基于node平台的web开发框架 |
安装命令:
// 开发依赖
npm i webpack webpack-cli webpack-node-externals @babel/preset-env @babel/preset-react babel-loader nodemon npm-run-all -D
// 项目依赖
npm i express
3. 开启一个服务端
我们使用express开启一个服务端,监听端口为3000
import express from "express"
const app = express()
app.use(express.static("dist"))
const template = `
<html>
<head>
<title>React Fiber</title>
</head>
<body>
<div id="root">
</div>
</body>
</html>
`
app.get("*", (req, res) => {
res.send(template)
})
app.listen(3000, () => console.log("server is running"))
这个服务端代码还是不能运行的,还需要babel对齐进行转化,执行webpack打包之后的代码。所以接下来我们应该需要对babel和webpack进行配置
4. 配置好babel 和 webpack
- 配置babel,将"@babel/preset-env", "@babel/preset-react"引入,为我们es6+代码转化成浏览器,或express能够兼容的代码
//babel.config.json
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
预设es6+代码 和 react代码的转化配置
- 设置服务端和客户端webpack打包配置 思路:
服务端目标代码为node代码,且为开发环境,打包入口是server.js,输出文件在build文件夹中,path配置的时候需要一个指定路径所以使用path这个模块进行指定 path.resolve(__dirname, "build").然后指定打包的名字为'server.js'.还需要配置打包规则,我们打包是js,则就设置js打包配置,使用的工具是babel-loader.最后配置externals这个配置告诉我们不要去打包node_modules下的模块。
代码:
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
target: "node",
mode: "development",
entry: "./server.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "server.js"
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}]
},
externals: [nodeExternals()]
}
思路:
浏览器端配置和服务端的差不多,我们先拷贝过来进行修改,target改为web,输入文件为src/index.js,输出位置为dist文件夹下,模块配置不变,但是在客户端不需要nodeExternals,我们将其去掉
代码:
const path = require('path')
module.exports = {
target: "web",
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devtool: "source-map",
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}]
}
}
我们在配置客户端的同时,补全src的index.js文件。在基础配置都设置好之后,我们最后在package.json中配置好启动服务上的相关shell命令
"main": "babel.config.json",
"scripts": {
"start": "npm-run-all --parallel dev:*", // 使用npm-run-all批量执行命令
"dev:server-compile": "webpack --config webpack.config.server.js --watch",// 执行服务端的webapck命令
"dev:server": "nodemon ./build/server.js", //当服务端文件发生变化,重启服务端命令
"dev:client-compile": "webpack --config webpack.config.client.js --watch" // 执行客户端的webpack命令
},
5. 校验开发环境配置
在src/index.js添加测试代码,在服务端模板代码中引入客户端打包后的代码文件bundle.js检查配置是否成功.
// server.js
const template = `
<html>
···
<div id="root">
</div>
<script src="bundle.js"></script> // 引入bundle.js
···
</html>
`
//src/index.js
console.log('form client js')
验证:
成功输出客户端代码。配置成功!!!!哈哈哈哈
认识requestIdleCallback
1. Fiber核心API功能介绍
利用浏览器的空余时间执行任务,如果有更高优先级的任务要执行时,当前的任务可以被终止,优先执行高级别的任务
使用场景:
现在我们有一个计算任务要执行,这个计算任务需要花费比较长的时间,执行任务过程当中,浏览器主线程会被一直占用,主线程被占用的时候浏览器是卡住的,并不能够去执行其他的任务。如果在这个时候用户想要操作这个页面,”向下滚动查看页面其他内容“,此时浏览器是不能响应用户当前操作的。给用户的感觉就是页面卡死了,就会造成非常差的体验。怎么去解决这件事情呢,正好我们可以将这个计算任务放入requestIdleCallback回调函数中,利用浏览器空闲时间执行他,当用户操作页面时,就是优先级高的任务执行了,此事计算任务就会被终止,用户操作就被浏览器响应,用户就不会感觉到页面卡顿了,当高优先级的任务执行完成之后将继续执行requestIdleCallback里面的计算任务。就解决了计算任务时间长占用主线程导致卡顿的问题。
API功能使用
requestIdleCallback接收一个参数,为回调函数,这个回调函数接收一个参数,就是浏览器的空闲时间(deadline),我们可以根据这个空闲时间deadline去判断执行那个计算任务。
requestIdleCallback(function(dealine) {
// deadline.timeRemaining() 获取浏览器的空闲时间
})
2. 浏览器空余时间
我们上面反复提到浏览器空闲时间,那浏览器空闲时间到底是什么呢?
页面时一帧一帧绘制出来的,当每秒绘制的帧数达到60时,页面时流畅的,小于这个值,用户会感觉到卡顿;1s 60帧,每一帧分到的时间是 1000/60≈16ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间;
如果任务在剩余的时间内内没有完成则会停止任务执行,继续优先执行主任务,也就是说requestIdleCallback总是利用浏览器的空余时间执行任务
3. API功能体验
光想总是体感不会那么真切,我们敲代码直接使用requestIdleCallback,来感受下真实效果
- 思路:
页面中有两个按钮和一个DIV,点击第一个按钮执行一项昂贵的计算,使其长期占用主线程,当计算任务执行的时候去点击第二个按钮更改页面中DIV的背景颜色。我们知道如果主线程长期被占用,浏览器是不会响应用户操作的,也就是div背景颜色是不能得到更改的。
使用requestIdleCallback就可以完美解决这个卡顿的问题。
// html
<div id="box"></div>
<button id="btn1">执行计算任务</button>
<button id="btn2">更改背景颜色</button>
// style
<style>
#box{
padding: 20px;
background: palegoldenrod;
}
</style>
// js
<script>
var box = document.getElementById("box")
var btn1 = document.getElementById("btn1")
var btn2 = document.getElementById("btn2")
var number = 99999
var value = 0
function calc() {
while (number > 0) {
value = Math.random() < 0.5 ? Math.random() : Math.random();
console.log(value)
number--
}
}
btn1.onclick = function () {
calc()
}
btn2.onclick = function () {
box.style.background = "green"
}
</script>
效果:
使用上面代码会长时间卡顿之后,再响应变色。算是已经很卡了
优化:
当点击第一个按钮的时候我们把昂贵的计算任务放到requestIdleCallback这个函数的回调函数里面去执行。
注意: 如果高级任务执行,循环被终止,我们等高级任务被执行完,应该再次调用requestIdleCallback(calc)执行循环
function calc(deadline) {
// 当空闲时间大于1ms的时候执行该循环
while (number > 0 && deadline.timeRemaining() > 1) {
value = Math.random() < 0.5 ? Math.random() : Math.random();
console.log(value)
number--
}
// 在这里应该再次执行计算任务
requestIdleCallback(calc)
}
btn1.onclick = function () {
requestIdleCallback(calc)
}
效果: 这样就可以点击第二个按钮立马给盒子换上了颜色
旧版Stack算法问题
以前DOM比对的算法名字叫做Stack。在React16之前的版本比对更新VirtualDOM的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件见数量庞大,主线程被长期占用,知道整棵VirtualDOM树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿,非常的影响用户体验。
核心问题: 递归无法中断,执行重任务耗时长。JavaScript又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿,用户体验差。
Fiber 算法
在React16当中官方对React代码进行了大量的重写,其中Fiber就是很重要的一部分,什么是React & Fiber呢?其实Fiber就是一种DOM比对的新的算法,Fiber就是这种算法的名字。
1. Fiber解决方案
- 利用浏览器空闲时间执行任务,拒绝长时间占用主线程
- 使用requestIdleCallback利用浏览器空闲时间,virtualDOM的比对不会占用主线程,如果有高优先级的任务要执行就会暂时中止VirtualDOM比对的过程,先去执行高优先级的任务,高优先级任务执行完成之后,在又开始执行VirtualDOM比对的任务,这样的话就不会出现页面卡顿的现象了。
- 放弃递归只采用循环,因为循环可以被中断
- 由于递归需要一层一层进入,一层一层退出,这个过程不能间断,如果要实现VirtualDOM比对任务可以被终止,就必须放弃递归,采用循环来完成VirtualDOM比对的过程,因为循环是可以终止的。只要将循环的终止时的条件保存下来,下一次任务再次开启的时候,循环就可以在前一次循环终止的时刻继续往后执行。
- 任务拆分, 将任务拆分成一个个小任务
- 拆分成一个个小任务,任务的单元就比较小,这样的话即使任务没有执行完就被终止了,重新执行任务的代价就会小很多,所以我们要做任务的拆分,将一个个大的任务拆分成一个个小任务执行。是怎么进行拆分的呢?
以前我们将整个一个VirtualDOM的比对看成一个任务,现在我们将树种每一个节点的比对看成一个任务,这样一个大的任务就拆分成一个个小任务了。
- 拆分成一个个小任务,任务的单元就比较小,这样的话即使任务没有执行完就被终止了,重新执行任务的代价就会小很多,所以我们要做任务的拆分,将一个个大的任务拆分成一个个小任务执行。是怎么进行拆分的呢?
为什么新的React VirtualDOM比对(diff)算法叫做Fiber呢?Fiber翻译过来就叫做<纤维>,表示限制任务执行的颗粒度很细了,像纤维一样。
2. 实现思路
在Fiber方案中,为了实现任务的终止再继续,DOM比对算法被拆分了两个部分:第一部分就是VirtualDOM的比对(也称为构建Fiber),第二部分就是真实DOM的更新(也称为提交Commit)。其中VirtualDOM的比对过程是可以终止的,真实DOM的更新更新时不可以被终止的。
- 构建Fiber
在使用React编写用户界面的时候我们还是使用js语法,babel会将JSX转回为React.createElement方法的调用,React.createElement被调用会返回virtualDOM,接下来就可以执行第一个阶段,第一阶段就是去构建Fiber对象,我们要采用循环的方式,从这个VirtualDOM对象当中找到内部的VirtualDOM对象,我们要为每个内部的VirtualDOM对象构建Fiber对象,Fiber对象也是JavaScript对象,是从VitualDOM对象演化而来,Fiber对象中除了有type, props,children属性以外,还存储了更多的关于节点的信息。其中 有一个很重要的信息呢就是记录当前节点要执行的操作,比如是想删除这个节点还是像更新这个节点,还是新增这个节点。当所有fiber对象构建完之后,还要将这些fiber对象存储在一个数组中。接下来就进行第二阶段操作
DOM初始渲染:virtualDOM->Fiber->Fiber[]->DOM
- 提交Commit
在循环FIber操作中,比对newFiber和oldFiber,更新节点操作类型,根据Fiber对象当中存储节点要实现操作的类型,将这个操作应用真实DOM对象当中。
DOM更新操作:newFiber vs oldFiber -> Fiber[] -> DOM
注意: 在执行第二阶段的时候,所有节点的Fiber对象,存储在一个数组中,原本的DOM节点不论是谁的子级,父级,兄弟关系,现在都变成了数组的第n个元素。也就是说DOM与DOM节点的关系被抹平了。但是在执行第二阶段的时候,我们需要构建出完整的DOM树,才能渲染到页面当中。也就是我们必须要知道谁是谁的自己谁是谁的父级谁是谁的兄弟,这样的我们才能准确的构建出我们DOM树。
3. Fiber对象
所以在根据上面所说的问题我们不止要存储当前fiber的对象需要的更新操作(effectTag),我们还要存储当前节点的子集(child)存储当前节点的父级(parent),存储当前节点的同级(sibling),以便我们在循环Fiber数组的时候,方便知道他们之间的关系,从而构建出一个完整DOM节点树。知道这些我们就可以写一个大概的Fiber对象了
{
type 节点类型(元素,文本,组件)(具体的类型)
props 加点属性
stateNode 节点DOM对象 | 组件实例对象
tag 节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
effects 数组,存储需要更改的fiber对象
effectTag 当前Fiber要被执行的操作(新增,删除,修改)
parent 当前Fiber的下一个父级Fiber
child 当前Fiber的下一个子级Fiber
sibling 当前Fiber的下一个兄弟Fiber
alternate Fiber 备份 fiber比对时使用
}
总结
以上,就是今天学习的内容。最开始我们先将我们的以后学习的代码开发环境进行配置,webpack的服务端和浏览器端的配置,server.js中启动一个express服务端任务,在使用babel和webpack打包出浏览器和服务器端可执行代码。在服务端webpack结合babel打包出node可执行代码,在客户端结合webapck和babel将前端reactJSX和js代码打包构建成浏览器可执行代码,完成开发环境基本搭建。
然后我们再对Fiber算法核心API requestIdleCallback有一个基本的认知,可以让一段js代码在代码空闲时间执行,并且使用一段代码进行真实感受,requestIdleCallback可以解决当有一段重任务执行时阻塞主线程导致页面卡顿的问题
我们学习了旧版Stack算法的问题,采用的是递归VirtualDOM比对,由于递归不可中断,当我们页面中有很多组件的时候,回导致页面出现卡顿。只有放弃递归比对使用requestIdleCallbackApi才能解决此问题 这就是Fiber算法要解决的问题:
- 利用浏览器空闲时间执行任务,拒绝长时间占用主线程
- 放弃递归只采用循环,因为循环可以被中断
- 任务拆分, 将任务拆分成一个个小任务
这是fiber的解决方案,通过解决方案和思路最后设计出了fiber对象的基本模型:
{
type 节点类型(元素,文本,组件)(具体的类型)
props 加点属性
stateNode 节点DOM对象 | 组件实例对象
tag 节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
effects 数组,存储需要更改的fiber对象
effectTag 当前Fiber要被执行的操作(新增,删除,修改)
parent 当前Fiber的下一个父级Fiber
child 当前Fiber的下一个子级Fiber
sibling 当前Fiber的下一个兄弟Fiber
alternate Fiber 备份 fiber比对时使用
}
今天希望大家对我们要做的事情的开发基本配置有个基本认知,并且需要知道以前Stack算法的问题,和Fiber的基本原理和思想,核心APIrequestIdleCallback的体感认知,接下来就是Fiber算法的实现,敬请期待!!!
如果看到了这里,希望能点赞,关注,评论(指出你的疑问),谢谢
源码https://github.com/zelixag/Fiber/tree/main
参考教程:
拉钩教程