react- Fiber

`1. 开发环境配置

1.1 文件夹结构
文件 / 文件夹描述
src存储源文件
dist存储客户端代码打包文件
build存储服务端代码打包文件
server.js存储服务器端代码
webpack.config.server.js服务端 webpack 配置文件
webpack.config.client.js客户端 webpack 配置文件
babel.config.jsonbabel 配置文件
package.json项目工程文件

创建 package.json 文件:npm init -y

1.2 安装项目依赖

开发依赖: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模块打包工具
webpack-cli打包命令
webpack-node-externals打包服务器端模块时剔除 node_modules 文件夹中的模块
@babel/coreJavaScript 代码转换工具
@babel/preset-envbabel 预置,转换高级 JavaScript 语法
@babel/preset-reactbabel 预置,转换 JSX 语法
babel-loaderwebpack 中的 babel 工具加载器
nodemon监控服务端文件变化,重启应用
npm-run-all命令行工具,可以同时执行多个命令
express基于 node 平台的 web 开发框架
1.3 环境配置
1.3.1 创建 web 服务器
// server.js
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>
			<script src="bundle.js"></script>
    </body>
  </html>
`
app.get("*", (req, res) => {
  res.send(template)
})
app.listen(3000, () => console.log("server is running"))
1.3.2 服务端 webpack 配置
// 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$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  externals: [nodeExternals()]
}
1.3.3 babel 配置

babel.config.json

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
1.3.4 客户端 webpack 配置
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$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  }
}
1.3.5 启动命令
"scripts": {
  "start": "npm-run-all --parallel dev:*",
  "dev:server-compile": "webpack --config webpack.config.server.js --watch",
  "dev:server": "nodemon ./build/server.js",
  "dev:client-compile": "webpack --config webpack.config.client.js --watch"
},

2. requestIdleCallback

2.1 核心 API 功能介绍

requestIdleCallback作用:利用浏览器的空余时间执行任务,如果有更高优先级的任务要执行时,当前执行的任务可以被终止,优先执行高级别任务。

requestIdleCallback作用:利用浏览器的空余时间执行任务,也就是说:如果浏览器有空余时间,就去执行requestIdleCallback中的任务,如果浏览器没有空余时间,这个正在执行的任务是可以随时被终止的。

它的应用场景是:举个例子:现在我们有个计算任务要去执行,这个计算任务要花费比较长的时间去执行,在执行任务的过程中,浏览器的主线程会一直被占用,在主线程被占用的过程中,浏览器是卡在那里的,并不能去执行其他的任务,如果此时用户想要去操作这个页面,比如:向下滚动页面去看其他的内容,其实此时浏览器是不能响应用户的操作的,给用户的感觉就是这个页面卡死了,这个体验是非常糟糕的。怎么样才能去解决这个问题呢?我们可以将这项计算任务注册到requestIdleCallback中,利用浏览器的空余时间去执行它,当用户操作这个页面时,就是优先级高的任务要执行,此时计算任务会被终止,优先响应用户的操作,这样用户就不会感觉到页面的卡顿了。当高优先级的任务要执行后,经继续执行计算任务。这样这个问题就被解决了。

它的使用
requestIdleCallback是window对象下面的一个方法,因此可以直接使用它。在调用requestIdleCallback的时候,需要传递一个参数,这个参数是一个函数,就是说:浏览器有空余时间就去执行这个函数,也就是说我们的任务要被写在这个函数中。

deadline:形参。形参有一个方法,timeRemaining,可以获取浏览器的空余时间。根据 【获取浏览器的空余时间的多少来决定要不要执行这个任务。】返回的时间是毫秒。

requestIdleCallback(function(deadline) {
  // deadline.timeRemaining() 获取浏览器的空余时间
})

浏览器空余时间到底是什么?
这个页面是一帧一帧绘制出来的,如果每一秒能够绘制60帧,那么用户看到的页面就是流畅的,如果绘制不到60帧,那么用户会感觉到卡顿。1s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms。如果每一帧执行的时间小于16ms,就说明浏览器有空余时间。那么这个时候是可以执行requestIdleCallback中的任务。如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说 requestIdleCallback 总是利用浏览器的空余时间执行任务

2.2 浏览器空余时间

页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时, 用户会感觉到卡顿

1s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间

如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说 requestIdleCallback 总是利用浏览器的空余时间执行任务

2.3 API 功能体验

页面中有两个按钮和一个DIV,点击第一个按钮执行一项昂贵的计算,使其长期占用主线程,当计算任务执行的时候去点击第二个按钮更改页面中 DIV 的背景颜色。

使用 requestIdleCallback 就可以完美解决这个卡顿问题。

<div class="playground" id="play">playground</div>
<button id="work">start work</button>
<button id="interaction">handle some user interaction</button>
<style>
  .playground {
    background: palevioletred;
    padding: 20px;
    margin-bottom: 10px;
  }
</style>
var play = document.getElementById("play")
var workBtn = document.getElementById("work")
var interactionBtn = document.getElementById("interaction")
var iterationCount = 100000000
var value = 0

var expensiveCalculation = function (IdleDeadline) {
  while (iterationCount > 0 && IdleDeadline.timeRemaining() > 1) {
    value =
      Math.random() < 0.5 ? value + Math.random() : value + Math.random()
    iterationCount = iterationCount - 1
  }
  requestIdleCallback(expensiveCalculation)
}

workBtn.addEventListener("click", function () {
  requestIdleCallback(expensiveCalculation)
})

interactionBtn.addEventListener("click", function () {
  play.style.background = "palegreen"
})

3 Fiber

3.1 问题

旧版本中react存在的问题和新版本的解决方案:

旧版本中react存在的问题:
什么是fiber呢?其实fiber就是dom比对的新的一种算法。fiber就是这种算法的名字,以前dom对比的算法名字叫做stack。
为什么react官方要重写react比对算法呢?这是因为react16之前的版本比对更新虚拟dom的过程采用循环加递归的方式实现的,这种比对方式有一个问题,就是递归需要一层一层的进入,一层一层的退出,过程不能中断,如果过程中组件的数量非常庞大,那么主线程就会被长期占用,直到整颗虚拟dom树更新完成之后,主线程才能被释放,主线程才能去执行其他的任务,就会导致一些用户交互,例如:动画,任务无法立即得到执行,页面就会卡顿,非常影响用户的体验。

就是因为递归利用的是javascirp的自身执行栈,所以旧版虚拟dom比对的算法叫做stack(堆栈)。

React 16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。

总结:核心问题:递归无法中断,长期占用主线程,JavaScript 又是单线程,无法同时执行其他任务,导致在绘制页面的过程中不能执行其他的任务,比如:元素动画,用户交互等任务,必须要延后,页面就会产生卡顿,用户体验差。

3.2 解决方案

react16中的解决方案:

  1. 用了requestIdleCallback这个api。利用浏览器空余时间来完成这个比对的过程,也就是说:虚拟dom的比对不会长期占用主线程了,如果有高优先级的任务要执行,就会暂时终止虚拟dom比对的过程,先去执行高优先级的任务要。高优先级的任务执行完成,再去执行虚拟dom比对的任务。这样的话页面就不会卡顿了。
  2. 由于采用了递归递,一层一层的进入,一层一层的退出,过程不能中断。那么就必须放弃递归,只采用循环来执行比对的过程,循环可以终止。只要将循环的条件保存下来,下一次任务就可以继续从中断的地方执行。
  3. 如果任务要实现终止再继续,那么任务的单元必须要小,这样的话,即使任务没有执行完就被终止了,重新执行任务的代价就会小很多,所以我们要做任务的拆分,将一个大的任务,拆分成一个个小的任务来执行。以前将整颗树的比对看成是一个任务,现在将每个节点看成一个任务,这样话,一个大的任务就会被拆成一个个小的任务了。为什么新版比对的过程叫做fiber?fiber中文翻译:纤维。意思就是:执行的任务颗粒度变细腻了,像一个个纤维一样。

解决方案总结:

  1. 利用浏览器空闲时间执行任务,拒绝长时间占用主线程
  2. 放弃递归只采用循环,因为循环可以被中断
  3. 任务拆分,将任务拆分成一个个的小任务
3.3 实现思路

Fiber具体实现:

在Fiber中为了实现任务的终止再继续,它将dom比对的过程拆分成2个部分:1.虚拟dom的比对 2.真实dom的更新。其中虚拟dom的比对是可以终止的。真实dom的更新是不能终止的。

它的具体过程是这样的:

在使用react编写用户界面的时候,我们仍然使用jxs语法,babel会将jsx语法转换成Raect.createElement()方法的调用,Raect.createElement()方法在调用后会返回虚拟dom对象,接下来就可以执行第一个阶段了,第一个阶段就是构建fiber对象,我们要采用循环的方式,从这个虚拟dom对象中,找到每一个内部的虚拟dom对象,我们要为每一个虚拟dom对象构建fiber对象,fiber对象也是javascript对象,它是从虚拟dom对象演化来的。Fiber 对象中存储了更多的信息。其中一个重要的信息是:对当前节点的操作,例如 (新增, 删除, 修改)。当所有的fiber对象构建完成之后,将fiber存储在一个数组中。接下来就可以执行第2个阶段的操作了。就是循环fiber数组。在循环的过程中,根据fiber对象中存储的【当前节点要操作的类型将这个操作应用在真实的dom对象当中】。这就是fiber算法的一个流程。

总结:
如果是初始化渲染:通过虚拟dom对象,构建Fiber对象,再将Fiber对象存储在一个数组中,然后将Fiber对象要执行的操作应用在真实dom对象中。
如果是状态更新:我们就重新构建所有的Fiber对象,再获取到旧的Fiber对象进行比对,然后形成Fiber数组,再将Fiber对象应用到真实dom对象中。

在执行第2阶段的时候,所有节点的Fiber对象都被存储在一个数组中,原本的dom节点,不论以前是是父级,是子级。现在都变成了数组中的第n个元素,这相当于dom节点的关系被抹平了。但是在第2阶段我们需要构建完整的dom节点树,才能将dom元素追加到页面中。也就是我们需要每个元素之间的关系例如:谁是谁的父级,谁是谁的子级,同级,这样我们才能准确的构建dom节点树。因此在Fiber对象中还需要存储当前节点的父级,当前节点的子级,当前节点的同级,方便我们在循环Fiber数组的时候可以知道他们之间的关系。从而构建dom节点树。

  1. 构建 Fiber (可中断)
  2. 提交 Commit (不可中断)

DOM 初始渲染: virtualDOM -> Fiber -> Fiber[] -> DOM

DOM 更新操作: newFiber vs oldFiber -> Fiber[] -> DOM

3.4 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 比对时使用
}

在这里插入图片描述

3.5接下来实现Fiber算法:
创建任务队列并添加任务
在这里插入代码片
import React from './react'

const jsx = (<div>hello world</div>)

实现任务的调度逻辑
构建根节点Fiber对象
构建子节点Fiber对象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值