“纤维”

157 阅读3分钟

Fiber

1. 基础知识

1.1 屏幕刷新率

大部分浏览器刷新率是60桢/秒
页面是一帧一帧绘制出来,每秒达到60桢时,页面才会流畅,用户不会觉得卡顿
所以每桢的绘制时间是 1/60 = 16.66毫秒,1000ms/60桢 = 16ms,每帧执行时间是16毫秒,也就是说每帧的工作时间如果<=16ms时候,页面是流畅的,>16ms页面是卡顿的

1.2 桢

前面说到如果浏览器刷新率是60帧/秒,则每帧的绘制时间是16.66ms
每帧的开头包含样式计算,布局,绘制等
如果某个任务执行时间过长,浏览器则会卡顿,推迟渲染

image.png

image.png

1.3 requestAnimationFrame

由浏览器专门为动画提供的API
上一节一帧图可以看到requestAnimationFrame回调函数是在绘制之前,
下面通过一个简单的例子看下用法

function Request() {
  const divRef = useRef<any>();
  let start = 0;

  function play() {
    divRef.current.style.width = 0;
    start = Date.now();
    requestAnimationFrame(progress)
  }

  function progress() {
    divRef.current.style.width = divRef.current.offsetWidth + 1 + 'px';
    divRef.current.innerHTML = divRef.current.offsetWidth + '%';
    if(divRef.current.offsetWidth < 100) {
      const current = Date.now();
      console.log(current - start); // 每次打印都是16ms左右
      start = current;
      requestAnimationFrame(progress)
    }
  }

  return(
    <div>
      <div className="box" ref={divRef}></div>
      <button onClick={() => play()}>开始</button>
    </div>
  )
}

1.4 requestIdleCallback

  1. 什么是requestIdleCallback?

  2. window.requestIdleCallback() 方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

  3. 正常帧任务完成后没超过16 ms,说明时间有富余,此时就会行 requestIdleCallback 里注册的任务

  4. 浏览器先执行优先级高的任务,执行完如果在16.6ms之内还没用完,还有时间,requestIdleCallback就会向浏览器申请时间碎片执行自己的任务,执行完之后再把控制权交给浏览器 如果浏览器没有耗时任务,就会最多有50ms的时间来执行,这就是第三次剩余时间还有49ms的原因

  5. 目前只有Chrome支持 image.png

  6. 下面看一个具体的例子理解一下

   <script>
    function sleep(d) {
        for (var t = Date.now(); Date.now() - t <= d;);
    }
    const works = [
        () => {
            console.log("第1个任务开始");
            sleep(20); //sleep(20);
            console.log("第1个任务结束");
        },
        () => {
            console.log("第2个任务开始");
            sleep(20); //sleep(20);
            console.log("第2个任务结束");
        },
        () => {
            console.log("第3个任务开始");
            sleep(20); //sleep(20);
            console.log("第3个任务结束");
        },
    ];
    requestIdleCallback(workLoop, {
        timeout: 1000 // 表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
    });
  // callback:回调即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
  // didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
  // timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
    function workLoop(deadline) {
        console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
        // didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
        // timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
        // console.log("本帧剩余时间", parseInt(deadline.timeRemaining()));
        while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
            performUnitOfWork();
        }
        if (works.length > 0) {
            console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
            requestIdleCallback(workLoop);
        }
    }
    function performUnitOfWork() {
        works.shift()();
    }
</script>  

image.png 如果sleep(0),则剩余时间完全可以执行所有任务

image.png

1.5 MessageChannel

  • MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
  • MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
  • MessageChannel是一个宏任务
  • React利用 MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作之后执行

下面看一个具体的例子:

  <script>
        const channel = new MessageChannel()
        let pendingCallback;
        let startTime;
        let timeoutTime;
        let perFrameTime = (1000 / 60);
        let timeRemaining = () => perFrameTime - (Date.now() - startTime);
        channel.port2.onmessage = () => {
            if (pendingCallback) {
                pendingCallback({ didTimeout: Date.now() > timeoutTime, timeRemaining });
            }
        }
        window.requestIdleCallback = (callback, options) => {
            timeoutTime = Date.now() + options.timeout;
            requestAnimationFrame(() => {
                startTime = Date.now();
                pendingCallback = callback;
                channel.port1.postMessage('hello');
            })
        }

        function sleep(d) {
            for (var t = Date.now(); Date.now() - t <= d;);
        }
        const works = [
            () => {
                console.log("第1个任务开始");
                sleep(30);//sleep(20);
                console.log("第1个任务结束");
            },
            () => {
                console.log("第2个任务开始");
                sleep(30);//sleep(20);
                console.log("第2个任务结束");
            },
            () => {
                console.log("第3个任务开始");
                sleep(30);//sleep(20);
                console.log("第3个任务结束");
            },
        ];

        requestIdleCallback(workLoop, { timeout: 60 * 1000 });
        function workLoop(deadline) {
            console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
            while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
                performUnitOfWork();
            }
            if (works.length > 0) {
                console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
                requestIdleCallback(workLoop, { timeout: 60 * 1000 });
            }
        }
        function performUnitOfWork() {
            works.shift()();
        }
    </script>

2.Fiber

2.1 什么是Fiber

  • React.createElement返回是一个虚拟dom,虚拟dom就是以js对象的方式描述dom的样子
  • Fiber是一个执行单元,也是一种数据结构
  • 我们可以通过某些调度策略合理分配CPU资源,提供用户相应程度
  • 通过fiber架构,可以让render(Reconciliation协调)阶段变成可中断,适时的让出CPU执行权,让浏览器更快的和用户交互
  • 每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)
  • 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,effect list;例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断
  • effect list 和完成顺序是一样的 // return 代表父节点

2.2 为什么要用fiber重构,原来的方式有什么问题?

1. 原来遍历虚拟dom是递归,深度优先遍历树结构,执行栈会越来越深,
2. 执行过程不能中断

2.3 Fiber的执行过程

image.png

image.png

  • Render(Reconciliation 协调) 遍历规则
    1. 下一个节点: 先儿子,后弟弟,再叔叔 绿色(遍历顺序),蓝色(完成顺序)
    1. 自己所有字节点完成后自己完成

image.png image.png

先儿子,A1, B1, C1,C1 没有儿子节点,所以C1完成,再C2,没有儿子,C2完成,B1子节点都完成,所以B1完成,再B2开始,B2没有儿子,没有叔叔,所以B2,完成,最后A1完成

2.4 complete 难点之一

A1text B1text C1text c1div c2text c2div b1div b2text b2div A1div divroot

在完成的时候要收集有副作用的fiber,然后组成effect list 每个fiber有两个属性 firstEffect指向第一个有副作用的子fiber lastEffect 指向最后一个有副作用子Fiber

IMG_0625.HEIC.JPG

2.5 commitWork

render结束后开始根据副作用nextEffect开始commit, 顺序就是完成的顺序

   commitWork(currentFiber);
   currentFiber = currentFiber.nextEffect;

如果不是类组件,并且是PLACEMENT(插入),currentFiber.return.stateNode.appendChild(nextFiber.stateNode)