https://mp.weixin.qq.com/s/uYd72aUb9wvUcjICFLREgg
Fiber之前的react
看一个例子
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import render from './vdom'
let element= (
<div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
)
render(element, document.getElementById('root'))
// ReactDOM.render(
// element,
// document.getElementById('root')
// );
以上jsx被babel编译之后就是如下结构:
// 经过babel编译后的虚拟dom
let element = {
"type": "div",
"props":{
"id": "A1",
"children": [
{
"type": "div",
"props": {
"id": "B1",
"children":[
{
"type": "div",
"props": {
"id": "C1"
}
},
{
"type": "div",
"props": {
"id": "C2"
}
}
]
}
},
{
"type": "div",
"props": {
"id": "B2"
}
}
]
}
}
虚拟dom经过render 方法调用将其渲染到页面上
function render(element, parentDom){
// 创建DOM 元素
let dom = document.createElement(element.type); // div
// 处理属性
Object.keys(element.props).filter( key => key !== 'children').forEach(key => {
dom[key] = element.props[key]; // dom.id = 'A1'
})
//递归处理子元素
if(Array.isArray(element.props.children)){
// 把子虚拟dom变成真实dom插入父dom里
element.props.children.forEach(child => render(child, dom))
}
parentDom.appendChild(dom)
}
export default render;
可以看到,react 对子元素的处理是递归调用render 方法渲染,但是 js 是单线程,UI 渲染和 js 执行互斥的,如果节点特别多,层级特别深,递归调用就不会停,调用栈会特别深,会阻塞浏览器的主进程。用户操作得不到响应,造成页面卡顿。
帧
每一帧处理完优先级较高的任务后,还有空闲时间就去执行用户代码,也就是 render 方法。
什么是Fiber
React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
其中每个任务更新单元为React Element对应的Fiber节点。
- fiber是一个调度算法,是为了解决如果有很多组件需要更新的时候,diff一口气执行完,造成主线程阻塞的问题,fiber会对diff策略进行更细粒度的控制,如果有涉及到用户输入这种高优先级的任务的时候,暂时停止diff,优先执行该任务,等到该任务结束后,再进行diff,而后commit渲染页面,
- 通过调度策略,合理分配CPU资源,从而提高用户的响应速度。
- 让自己的协调任务变成可被中断的,适时地让出CPU执行权,可以让浏览器响应用户交互。
Fiber是一个执行单元
以前的渲染是递归不能暂停,现在可以将一个渲染任务拆成一个个小任务单元,在浏览器空闲时间执行,
react 请求调度,每一帧浏览器首先处理优先级较高的任务,如果处理完还有空闲时间,就执行react 任务。Fiber 是一种数据结构
React 目前做法是使用链表,每个虚拟节点内部表示为一个Fiber
每个父节点只指向它的大儿子,二儿子由他的兄弟节点指向,每个子节点都 return 到父节点requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
每一帧的时间大概都是 16ms。
requestIdleCallback
window.requestIdleCallback()方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
MessageChannel
Fiber执行阶段
每次渲染有两个阶段:Reconciliation(协调render 阶段)和Commit(提交阶段)
- 协调阶段:可以认为是Diff阶段(但其实还包括创建fiber树、创建dom等),这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等,这些变更React称之为副作用(Effect)
- 提交阶段:将上一个阶段计算出来的需要处理的副作用一次性执行了。这个阶段必须同步执行,不能被打断。
render 阶段
render阶段的结果是生成一个部分节点标记了side effects(把不能在 render 阶段完成的一些 work 称之为副作用)的fiber节点树,side effects描述了在下一个commit阶段需要完成的工作。
render阶段可以异步执行。 React可以根据可用时间来处理一个或多个fiber节点,然后停止已完成的工作,并让出调度权来处理某些事件。然后它从它停止的地方继续。但有时候,它可能需要丢弃完成的工作并再次从头。由于在render阶段执行的工作不会导致任何用户可见的更改(如DOM更新),因此这些暂停是不会有问题的。相反,在接下来的commit阶段始终是同步的,这是因为在此阶段执行的工作,将会生成用户可见的变化,例如, DOM更新,这就是React需要一次完成它们的原因。
- 从顶点开始遍历
- 如果有第一个儿子,先遍历第一个儿子
- 如果没有第一个儿子,标志着此节点遍历完成
- 如果有弟弟就遍历弟弟
- 如果没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔节点遍历叔叔节点
- 没有父节点遍历结束
- 先儿子,后弟弟,再叔叔,辈分越小越优先
- 什么时候一个节点遍历完成?没有子节点,或者所有子节点都遍历完成
- 没父节点了就表示全部遍历完成了。
开始顺序
完成顺序
import {element} from './vdom'
let container = document.getElementById('root')
const PLACEMENT = 'PLACEMENT';
// 根
let workingInProgressRoot = {
stateNode: container, // 此fiber 对应的DOM节点
props:{ // fiber 属性,
children: [element] // 虚拟dom
},
// child, // 儿子
// return, // 完成指向父亲
// sibling // 兄弟
};
// 下一个工作单元
let nextUnitOfWork = workingInProgressRoot;
function workLoop(deadline){
// 存在下一个执行单元就去执行,并返回下下一个执行单元
while(nextUnitOfWork && deadline.timeRemaining()>0){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
// 如果没有下一个工作单元了就要提交了
if(!nextUnitOfWork){
commitRoot()
}
}
// 提交插入
function commitRoot(){
let currentFiber = workingInProgressRoot.firstEffect; // C1
while(currentFiber){
console.log('commitRoot', commitRoot.props.id)
if(currentFiber.effectTag === 'PLACEMENT'){
currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
}
currentFiber = currentFiber.nextEffect;
}
workingInProgressRoot = null;
}
/**
* beginWork 1.创建此fiber 的真实DOM,通过虚拟dom创建fiber树结构
* @param {*} workingInProgressFiber 正在执行的工作单元
*/
function performUnitOfWork(workingInProgressFiber){
beginWork(workingInProgressFiber)
// 有儿子下一个工作单元就处理儿子
if(workingInProgressFiber.child){
return workingInProgressFiber.child
}
while(workingInProgressFiber){
// 如果没有儿子,当前节点结完成了
completeUnitOfWork(workingInProgressFiber);
// 没有儿子下一个工作单元就处理弟弟
if(workingInProgressFiber.sibling){
return workingInProgressFiber.sibling
}
// 没有儿子没有弟弟就处理叔叔,首先指向父亲,循环找父亲的兄弟
workingInProgressFiber = workingInProgressFiber.return
}
}
/**
* 开始:创建真实DOM,并没有挂载,创建fiber子树结构
*/
function beginWork(workingInProgressFiber){
console.log('workingInProgressFiber', workingInProgressFiber.props.id)
if(!workingInProgressFiber.stateNode){ // 不存在真实dom就创建
workingInProgressFiber.stateNode = document.createElement(workingInProgressFiber.type)
for(let key in workingInProgressFiber.props){
// 处理属性
if(key !== 'children') workingInProgressFiber.stateNode[key] = workingInProgressFiber.props[key]
}
}
// 创建子fiber
let previousFiber;
if(Array.isArray(workingInProgressFiber.props.children)){
workingInProgressFiber.props.children.forEach((child, index) =>{
let childFiber = {
type: child.type, // DOM节点类型
props: child.props,
return: workingInProgressFiber, // 要指向的父亲
effectTag: 'PLACEMENT', // 这个fiber对应的DOM节点需要被插入到页面中去, 父DOM中去
nextEffect: null , // 下一个有副作用的节点
}
// 大儿子指向
if(index === 0) {
// child 属性 指向大儿子
workingInProgressFiber.child = childFiber;
}else{
// sibling 属性指向其他
previousFiber.sibling = childFiber
}
previousFiber = childFiber
})
}
}
/**
* 完成:在 completeUnitOfWork 方法中构建 effect-list 链表,该 effect list 在下一个 commit 阶段非常重要
* @param {*} workingInProgressFiber
*/
function completeUnitOfWork(workingInProgressFiber){
console.log('workingInProgressFiber', workingInProgressFiber.props.id)
let returnFiber = workingInProgressFiber.return //最后一个workingInProgressFiber是C1,它的父亲是 B1
if(returnFiber){
if(!returnFiber.firstEffect){
returnFiber.firstEffect = workingInProgressFiber.firstEffect
}
if(workingInProgressFiber.lastEffect){
if(returnFiber.lastEffect){
returnFiber.lastEffect.nextEffect = workingInProgressFiber.firstEffect
}
returnFiber.lastEffect = workingInProgressFiber.lastEffect
}
// 再把自己挂上去
if(workingInProgressFiber.effectTag){
if(returnFiber.lastEffect){
returnFiber.lastEffect.nextEffect = workingInProgressFiber;
}else{
returnFiber.firstEffect = workingInProgressFiber;
}
returnFiber.lastEffect = workingInProgressFiber;
}
}
}
// 告诉浏览器在空闲的时候执行workloop
requestIdleCallback(workLoop)
开始顺序和完成顺序
effects list 是render阶段运行的结果。render阶段的重点是确定需要插入,更新或删除哪些节点,以及哪些组件需要调用其生命周期方法,其最终生成了effects list,也正是在提交阶段迭代的节点集。
commit 阶段
commit 阶段是 React 更新真实 DOM 并调用 pre-commit phase 和 commit phase 生命周期方法的地方。与 render 阶段不同,commit 阶段的执行始终是同步的,它将依赖上一个 render 阶段构建的 effect list 链表来完成。
https://juejin.cn/post/6859528127010471949#heading-14