Fiber
主要功能
- 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新执行优先级低的任务
- 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
- dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber
是对 React
核心算法(即调和过程)的重写
从编码角度来看,Fiber
是 React
内部所定义的一种数据结构,它是 Fiber
树结构的节点单位,也就是 React 16
新架构下的虚拟DOM
一个 fiber
就是一个 JavaScript
对象,包含了元素的信息、该元素的更新操作队列、类型。
解决的问题
JavaScript
引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待
如果 JavaScript
线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿
如果组件较大,那么js
线程会一直执行,然后等到整棵VDOM
树计算完成后,才会交给渲染的线程;这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况
如何解决
Fiber
把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行;
即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element
对应的 Fiber
节点
实现的上述方式的是requestIdleCallback
方法
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
该实现过程是基于 Fiber
节点实现,作为静态的数据结构来说,每个 Fiber
节点对应一个 React element
,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。
作为动态的工作单元来说,每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作。
webworker 多线程
作用
为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。
在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
**特点:**Worker线程一旦新建成功就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。
**优点:**有利于随时响应主线程的通信。
缺点: 比较耗费资源,不应该过度使用,一旦使用完毕,就应该关闭。
使用注意点
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document
、window
、parent
这些对象。但是,Worker 线程可以navigator
对象和location
对象。
(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker 线程不能执行alert()
方法和confirm()
方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://
),它所加载的脚本,必须来自网络。
使用
1 主线程
主线程采用new
命令,调用Worker()
构造函数,新建一个 Worker 线程。
> var worker = new Worker('work.js');
Worker()
构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。
然后,主线程调用worker.postMessage()
方法,向 Worker 发消息。
> worker.postMessage('Hello World');
> worker.postMessage({method: 'echo', args: ['Work']});
worker.postMessage()
方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。
接着,主线程通过worker.onmessage
指定监听函数,接收子线程发回来的消息。
> worker.onmessage = function (event) {
> console.log('Received message ' + event.data);
> doSomething();
> }
>
> function doSomething() {
> // 执行任务
> worker.postMessage('Work done!');
> }
上面代码中,事件对象的data
属性可以获取 Worker 发来的数据。
Worker 完成任务以后,主线程就可以把它关掉。
> worker.terminate();
> ```
#### 2 Worker 线程
Worker 线程内部需要有一个监听函数,监听`message`事件。
```javascript
> self.addEventListener('message', function (e) {
> self.postMessage('You said: ' + e.data);
> }, false);
上面代码中,self
代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。
> // 写法一
> this.addEventListener('message', function (e) {
> this.postMessage('You said: ' + e.data);
> }, false);
>
> // 写法二
> addEventListener('message', function (e) {
> postMessage('You said: ' + e.data);
> }, false);
除了使用self.addEventListener()
指定监听函数,也可以使用self.onmessage
指定。监听函数的参数是一个事件对象,它的data
属性包含主线程发来的数据。self.postMessage()
方法用来向主线程发送消息。
根据主线程发来的数据,Worker 线程可以调用不同的方法,下面是一个例子。
> self.addEventListener('message', function (e) {
> var data = e.data;
> switch (data.cmd) {
> case 'start':
> self.postMessage('WORKER STARTED: ' + data.msg);
> break;
> case 'stop':
> self.postMessage('WORKER STOPPED: ' + data.msg);
> self.close(); // Terminates the worker.
> break;
> default:
> self.postMessage('Unknown command: ' + data.msg);
> };
> }, false);
上面代码中,self.close()
用于在 Worker 内部关闭自身。
3 Worker 加载脚本
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()
。
> importScripts('script1.js');
该方法可以同时加载多个脚本。
> importScripts('script1.js', 'script2.js');
4 错误处理
主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error
事件。
> worker.onerror(function (event) {
> console.log([
> 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
> ].join(''));
> });
>
> // 或者
> worker.addEventListener('error', function (event) {
> // ...
> });
Worker 内部也可以监听error
事件。
5 关闭 Worker
使用完毕,为了节省系统资源,必须关闭 Worker。
> // 主线程
> worker.terminate();
>
> // Worker 线程
> self.close();
数据通信
主线程与 Worker 之间通信时的内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。
主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
> // Transferable Objects 格式
> worker.postMessage(arrayBuffer, [arrayBuffer]);
>
> // 例子
> var ab = new ArrayBuffer(1);
> worker.postMessage(ab, [ab]);