react16之前的问题
react16之前dom元素的更新采用递归遍历的方式来对比子节点。一旦进入到递归遍历,整个过程将不能被打断,如果dom树的层次比较深,整个对比过程将耗时较长。而js的运行和dom的渲染又是互斥的,所以很容易造成卡顿。
Fiber
fiber是react16采用的一种新的节点对比更新方法,是为了解决react16之前的问题而产生的。
核心思想
- 任务拆分,将任务才分成一个个小的任务
- 在浏览器空闲时间执行任务,避免长时间占用主线程
- 使用循环模拟递归,因为循环是可以中断的
实现思路
在 Fiber 方案中,为了实现任务的中断再继续,DOM比对算法被分成了两部分:
- 构建fiber,这个过程可以中断
- 提交Commit,不可中断
初始渲染的过程:virtualDom --> fiber --> fiber[] --> Dom
Dom更新操作:newFiber vs oldFiber --> 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 比对时使用
}
hostRoot:根节点
hostComponent:非根节点
classComponent:类组件
functionComponent:函数组件
React Fiber的实现
首先了解一下浏览器空闲时间
浏览器空闲时间
页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时, 用户会感觉到卡顿。s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间。
RequestIdleCallback
requestIdleCallback接收一个函数作为参数,将在浏览器的空闲时间执行函数。如果在执行函数的过程中,有更高优先级的任务需要执行,则立即停止执行函数,优先执行高级别的任务。
requestIdleCallback(function(deadline){
// deadline.timeRemaining()获取浏览器空闲时间,返回一个数字,单位为ms
})
搭建模拟项目的结构
按下图创建项目的目录结构
依赖介绍
依赖项 | 描述 |
---|---|
webpack | 模块打包工具 |
webpack-cli | 打包命令行工具 |
webpack-node-externals | 打包服务器端模块时剔除 node_modules 文件夹中的模块 |
@babel/core | JavaScript 代码转换工具 |
@babel/preset-env | babel 预置,转换高级 JavaScript 语法 |
@babel/preset-react | babel 预置,转换 JSX 语法 |
babel-loader | webpack 中的 babel 工具加载器 |
nodemon | 监控服务端文件变化,重启应用 |
npm-run-all | 命令行工具,可以同时执行多个命令 |
express | 基于 node 平台的 web 开发框架 |
使用命令安装依赖:
npm install webpack webpack-cli webpack-node-externals @babel/core @babel/preset-env @babel/preset-react babel-loader nodemon npm-run-all -D
npm install express
配置webpack以及babel
// webpack.config.server.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
target: 'node',
mode: 'development',
entry: './server.js',
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'build')
},
module: {
rules: [
{
test: /\.js$/,
exculde: /node_modules/,
use: 'babel-loader',
}
]
},
externals: [nodeExternals()]
}
// webpack.config.client.js
const path = require('path')
module.exports = {
target: 'web',
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exculde: /node_modules/,
use: 'babel-loader',
}
]
}
}
// babel.config.json
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
使用express开发一个简易的Web服务器
// server.js
const express = require('express')
const app = express()
// 静态资源处理
app.use(express.static('dist'))
// 定义模板
const template = `
<html>
<head>
<title>React Fiber</title>
<meta charset="utf-8" />
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
`
// 接收请求,并返回响应
app.get('*', (req, res) => {
res.send(template)
})
// 监听8888端口启动服务
app.listen(8888, () => console.log('server is running...'))
配置启动命令
"scripts": {
"start": "npm-run-all --parallel dev:*",
"dev:server-complie": "webpack --config webpack.config.server.js --watch",
"dev:server": "nodemon ./build/server.js",
"dev:client-compile": "webpack --config webpack.config.client.js --watch"
}
fiber模拟实现
将jsx转化为virtualDom
首先需要创建一个createElement方法来创建virtualDom,并在react/index.js中导出。babel在转换jsx时会调用createElement方法将jsx编译为virtualDom对象。createElement的创建可以参考React15 核心原理模拟实现。
初次渲染
初次渲染流程:
-
调用render方法,向任务队列中添加一个任务,并指定在浏览器空闲时执行任务。
export const render = (vdom, container) => { // 向任务队列中添加一项任务 taskQueue.push({ dom: container, props: { children: vdom } }) // 在浏览器空闲时执行任务 requestIdleCallback(preformTask) }
-
调用preformTask函数启动任务执行,并添加任务中断后,浏览器空闲时继续执行任务的处理。
// 指定在浏览器空闲时执行任务,以及任务中断后的继续执行 const preformTask = deadline => { workLoop(deadline) // 任务中断之后,浏览器空闲时继续执行任务 if (subTask && !taskQueue.isEmpty()) { requestIdleCallback(performTask) } }
-
调用workLoop开始执行任务。
const workLoop = deadline =><