https://mp.weixin.qq.com/s/uYd72aUb9wvUcjICFLREgg
Fiber之前的react
看一个例子
// index.jsimport 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编译后的虚拟domlet 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; // C1while(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]}}// 创建子fiberlet 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,它的父亲是 B1if(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;}}}// 告诉浏览器在空闲的时候执行worklooprequestIdleCallback(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
