一、fiber机理

1、浏览器卡顿原因

  • 浏览器每秒刷新60次
  • 如果浏览器每秒绘制的帧数大于60,那相当于浏览器刷新一次,页面就会发生变化,说明不卡顿
  • 但是如果每秒绘制的帧数小于60,那有可能浏览器连续刷新两次,页面一直处于某一帧,说明页面卡顿了
  • js的执行,如果占用空闲时间太长(超过了空闲时间),那就导致一直不能绘制,导致每秒绘制的帧数减少,造成卡顿

    2、如何解决

  • 我们知道每秒绘制打于60帧,页面就不会卡顿;
  • 也就每帧的时间只要低于有1s/60也就是16.66ms,页面就流畅;
  • 由于每一帧只要保持16.66ms页面就达到流畅,所以浏览器执行帧的周期也是16.66ms, 看下图
    图片.png
    浏览器每一帧开始都是优先执行顺序是:
    (1)input事件
    (2)定时器(js setTimeout/setInterval)
    (3)滚动、媒体查询、resize
    (4)requestAnimationFrame
    (5)layout(布局)
    (6)paint(绘制)
    (7)idle(空闲时间才执行JS)

因为js是在idle阶段执行,如果在这个阶段js占用时间过长,那就会造成页面一直卡在这一帧,无法进行下一次渲染

react 15 之前

渲染是通过遍历虚拟dom,不断创建dom,并append到父节点,如果dom节点很多,且中间不能停止,将耗费较长时间,容易造成页面卡顿

/**
 * react 15之前
 * 问题:js是单线程, UI渲染和js执行是互斥的
 * dom层级特别深
 */
export function render(element: any, parentDom: any) {
    const dom = document.createElement(element.type);
    Object.keys(element.props)
        .filter((key: any) => key !== 'children')
        .forEach((key: any) => {
            dom[key] = element.props[key];
        });
    if (Array.isArray(element.props.children)) {
        element.props.children.forEach((ele: any) => {
            render(ele, dom);
        });
    }
    parentDom.appendChild(dom);
}

3、fiber

fiber可以将分割执行单元,暂停执行,等到空闲再执行, fiber利用requestIdleCallback回调函数,只在浏览器空闲的时候去执行dom操作,如果超过空闲时间,就将控制权交回浏览器去进行渲染操作
image

另外fiber有自己的协调机制

  • 在V16版本之前 协调机制Stack reconciler, V16版本发布Fiber 架构后是 Fiber reconciler
  • 原来的React更新任务是采用递归形式,那么现在如果任务想中断, 在递归中是很难处理, 所以React改成了大循环模式,修改了生命周期也是因为任务可中断

    React Fiber 每个工作单元运行时有6种优先级:

  • synchronous 与之前的Stack reconciler操作一样,同步执行
  • task 在next tick之前执行
  • animation 下一帧之前执行
  • high 在不久的将来立即执行
  • low 稍微延迟(100-200ms)执行也没关系
  • offscreen 下一次render时或scroll时才执行

    4、requestAnimationFrame使用

    requestAnimationFrame:每帧都会执行,在页面不阻塞情况下时间间隔为16.66ms

    // dom
    <div style="background:blue;width:0;height: 20px;"></div>
    <button>开始</button>
    <script>
      let div = document.querySelector('div');
      let button = document.querySelector('button');
      let startTime = 0;
    
      function progress(raftime) {
          console.log('raftime:',raftime);
          div.style.width = div.offsetWidth + 1 + 'px';
          div.innerHTML = Math.floor(div.offsetWidth/3) + '%';
          if (div.offsetWidth < 300) {
              console.log(Date.now() - startTime + 'ms'); // 打印progress被调用的时间点
              requestAnimationFrame(progress); // 时间差16ms 可以传参DOMHighResTimeStamp表示执行的时间点=performance.now()
          // progress()  // 时间差只有1ms
          }
      }
    
      button.onclick = function () {
          div.style.width = 0;
          startTime = Date.now();
          // 浏览器会在每一帧渲染前执行这个方法
          requestAnimationFrame(progress);  // 宏任务
          // progress()
      };
    </script>

    直接调用progress和requestAnimationFrame(progress)效果对比:
    image
    image

    5、requestIdleCallback

    requestIdleCallback:协调处理,只有在浏览器空闲的时候才会执行这样防止了js占用阻塞渲染
    以下栗子:处理耗时的多个任务

    function sleep(duration) {
          let start = Date.now();
          while (duration + start > Date.now()) { }
      }
    
      const works = [
          () => {
              console.log('A1开始');
              sleep(20);
              console.log('A1结束');
          },
          () => {
              console.log('A2开始');
              sleep(20);
              console.log('A2结束');
          },
          () => {
              console.log('A3开始');
              sleep(10);
              console.log('A3结束');
          }
      ];
    
      function performUnitOfWork() {
          works.shift()();
      }
    
      function workLoop(deadline) { // 本帧的剩余时间
          console.log(deadline);
          console.log(deadline.timeRemaining());
          // 如果有剩余时间,或者已经过期了就执行deadline.didTimeout
          while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
              performUnitOfWork();
          }
          if (works.length > 0) {
              console.log(`剩余:${deadline.timeRemaining()}, 等待下次requestIdleCallback`);
              requestIdleCallback(workLoop, {timeout: 1000}); // 可以传参 如过过了1000还没执行,不管忙不忙都要执行
          }
      }
    
      // 空闲时间执行  只有chrome支持 react 用MessageChannel模拟了requestIdleCallback 并且将回调延迟到绘制结束执行
      requestIdleCallback(workLoop, {timeout: 1000});// 宏任务

    6、fiber中的requestIdleCallback

    由于requestIdleCallback回调函数只有chrome浏览器支持,react中试通过MessageChannel模拟requestIdleCallback的

    // messageChannel
    let channel = new MessageChannel();
    let port1 = channel.port1;
    let port2 = channel.port2;
    
    let activeFrameTime = 1000 / 60;
    let frameDeadline; // 这一帧的截止时间
    let pendingCallback;
    let timeRemaining = () => frameDeadline - performance.now();
    // 会在绘制之后执行,此时可以看是否空闲
    port2.onmessage = function (e) {
      console.log('port2收到port1的数据:', e.data);
      let currentTime = performance.now();
      let didTimeout = currentTime > frameDeadline;
    
      if (didTimeout || timeRemaining() >0) {
          !!pendingCallback && pendingCallback({didTimeout,timeRemaining})
      }
    };
    
    window.requestIdleCallback = (callback, option) => {
      // performance.timing.navigationStart + performance.now() = Date.now()
      requestAnimationFrame((rafTime) => {
          // 每一帧的开始时间+16.6=每一帧的截止时间
          frameDeadline = rafTime + activeFrameTime;
          pendingCallback = callback;
          // 发消息后相当于添加了一个宏任务
          port1.postMessage('hello');
      });
    };

    二、Fiber如何渲染

    1、大框架

    let container = document.getElementById('root');
    // 根节点fiber
    let inProgressFiberRoot = {
      stateNode: container,
      props: {children: [element]}
      // child,
      // sibling,
      // return
    };
    
    // fiber任务单元
    let nextUnitOfWork = inProgressFiberRoot;
    
    function workLoop(deadline) {
      while (nextUnitOfWork && deadline.timeRemaining() > 0) {
          // 这一步构建fiber链条
          nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      }
      if (!nextUnitOfWork) {
          // 链条结束就开始渲染
          commitRoot();
      }
    }
    // 渲染
    function commitRoot() {
      let currentFiber = inProgressFiberRoot.firstEffect;
      while (currentFiber) {
          if (currentFiber.effectTag === 'PLACEMENT') {
              currentFiber.return.stateNode.appendChild(currentFiber.stateNode);
          }
          currentFiber = currentFiber.nextEffect;
      }
      inProgressFiberRoot = null;
    }
    
    requestIdleCallback(workLoop);

    2、performUnitOfWork如何构建链条

    2.1 首先要创建真实dom,创建fiber树,如下图

    图片.png

  • 目的就是根据虚拟dom创建真实的dom,并挂载到stateNode中
  • 方式就是爸爸找儿子,儿子找弟弟,弟弟找叔叔的步骤,把所有节点遍历到
  • 代码实现:

    function beginWork(inProcessFiber) {
      console.log('干活了:', inProcessFiber.props.id);
      // 创建了真实dom并挂载到stateNode
      if (!inProcessFiber.stateNode) {
          inProcessFiber.stateNode = document.createElement(inProcessFiber.type);
          for (let key in inProcessFiber.props) {
              if (key !== 'children') {
                  inProcessFiber.stateNode[key] = inProcessFiber.props[key];
              }
          }
      }
    
      let previousFilber;
      Array.isArray(inProcessFiber.props.children) && inProcessFiber.props.children.forEach((child, index) => {
          let childFiber = {
              type: child.type,
              props: child.props,
              return: inProcessFiber, // 字节点的return当然是当前节点了
              effectTag: 'PLACEMENT', // 表示这个节点将要被插入
              nextEffect: null // 下一个有副作用的节点
          };
          if (index === 0) {
              // 先儿子
              inProcessFiber.child = childFiber;
          } else {
              // 再兄弟
              previousFilber.sibling = childFiber;
          }
          // 上一个节点,供下次用
          previousFilber = childFiber;
      });
    }

    2.2 上一节只是创建了第一层子元素的dom,这一节要通过儿子找兄弟,兄弟找叔叔的方式继续遍历

    fiber数是尊巡深度优先策略,先遍历儿子,然后弟弟,然后叔叔(叔叔是通过return到爸爸,让爸爸找兄弟)
    图片.png

function performUnitOfWork(inProcessFiber) {
    // 1创建真实dom, 2创建fiber树
    beginWork(inProcessFiber);  // 
    if (inProcessFiber.child) {
        // 有儿子找儿子
        return inProcessFiber.child;
    }
    while (inProcessFiber) {
        // 没有儿子,就完成了
        completeInProcessFiber(inProcessFiber);
        // 继续遍历弟弟
        if (inProcessFiber.sibling) {
            return inProcessFiber.sibling;
        }
        // 没有弟弟 就先找到父亲,让父亲帮助找叔叔
        inProcessFiber = inProcessFiber.return;
    }
}

2.3 当自己没儿子的时候,自己就完成了,就可以构建副作用链条了

图片.png

function completeInProcessFiber(inProcessFiber) {
    console.log('完成了:', inProcessFiber.props.id);
    // 构建副作用链条 effectTag不为null的节点
    let returnFiber = inProcessFiber.return;
    if (returnFiber) {
        if (returnFiber.firstEffect) {
            returnFiber.firstEffect = inProcessFiber.firstEffect;
        }
        if (inProcessFiber.lastEffect) {
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = inProcessFiber.firstEffect;
            }

            returnFiber.lastEffect = inProcessFiber.lastEffect;
        }
        // 挂自己
        if (inProcessFiber.effectTag) {
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = inProcessFiber;
            } else {
                returnFiber.firstEffect = inProcessFiber;
            }

            returnFiber.lastEffect = inProcessFiber;
        }
    }
}

参考

浅谈React Fiber

系列

重学react——slot
重学react——state和生命周期
重学react——redux
重学react——hooks以及原理
重学react——context/reducer
重学react——router
重学react——高阶组件
build your own react
React——fiber


lihaixing
463 声望719 粉丝

前端就爱瞎折腾