异步调度实现

大家好,我是万维读客的讲师曹欢欢。上节我们介绍了如何渲染fiber树,本节我们优化一下异步中断调用实现方式。

任务调度

前面我们学习到fiber架构的异步渲染是这样:

|浏览器事件a|render阶段1|浏览器事件b|render阶段2|render阶段3|---commit阶段---|浏览器事件c|

PS: 
- render阶段1、render阶段2、render阶段3 每一步都只做部分的更新操作,操作完就让出主进程可以响应浏览器事件;整体看render阶段是可以中断的;但是单个的render子阶段是不能中断的。
- commit阶段是同步的,不可中断;

为了能够让出执行资源,每次指向render子阶段时,都会判断当前浏览器有没有空闲资源。有的话继续执行,如果需要执行更要优先级别的任务,那么就让出执行权。

在react中的源码对应的是workLoopConcurrent函数:

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

上面的shouldYield函数就是来判断是否中断的。React源码中是自己实现了一个浏览器任务优先级管理算法,我们这里可以使用requestIdleCallback来模拟实现。

requestIdleCallback是在浏览器空闲时执行回调函数,可以查看MDN:https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback

requestIdleCallback

window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

//xxx ms后还没被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。
requestIdleCallback(callback, {timeout: xxx }) 
-- callback: 一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。

IdleDeadline:接口是在调用 Window.requestIdleCallback() 时创建的闲置回调的输入参数的数据类型。它提供了 timeRemaining() 方法,用来判断用户代理预计还剩余多少闲置时间;以及 didTimeout (en-US) 属性,用来判断当前的回调函数是否因超时而被执行。
-- didTimeout
-- timeRemaining函数

修改我们上节写的render函数,修改成requestIdleCallback:

// setTimeout(this.workloop.bind(this));
window.requestIdleCallback(this.workloop.bind(this), { timeout: 500 });

这时候,查看我们控制台测试用例会报错,

 FAIL  treact03/jsx.test.jsx > async render > render in async
TypeError: window.requestIdleCallback is not a function
 ❯ TreactRoot.render treact03/Treact.jsx:104:16
    102|         workInProgress = this._internalRoot.current.alternate;
    103|         // setTimeout(this.workloop.bind(this));
    104|         window.requestIdleCallback(this.workloop.bind(this), { timeout: 100 });
       |                ^
    105|     }
    106| }
 ❯ treact03/jsx.test.jsx:45:14

 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  16:33:59
   Duration  21ms

测试环境没有requestIdleCallback方法,需要在环境中注入,可以修改代码如下:

import {describe, it, expect} from 'vitest';
import * as Treact from './Treact';
import { vi } from 'vitest'

// 定义 requestIdleCallback 的兼容处理,不执行则用setTimeout模拟实现
window.requestIdleCallback = window.requestIdleCallback || function(handler) {
    // 闭包,创建的时候记录一下时间
    let startTime = Date.now();
    return setTimeout(function() {
        handler({
            didTimeout: false,
            timeRemaining: function() {
                // 理论上系统给你空闲的时间会低于50ms,所以你的任务最好不要超过50ms,否则还是会卡顿
                return Math.max(0, 50.0 - (Date.now() - startTime));
            }
        });
    }, 1);
};
// 取消任务
window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
    clearTimeout(id);
};

vi.stubGlobal('requestIdleCallback', window.requestIdleCallback)
vi.stubGlobal('cancelIdleCallback', window.cancelIdleCallback)
...

然后可以看到测试用例正常了。这里面留个问题,上面兼容实现requestIdleCallback为什么timeRemaining里面的计算时间用50ms来减去执行时间?

为了后面代码执行方便,我们可以把requestIdleCallback直接放到treact代码中,测试用就不用再注入了。

修改workloop

除了修改render之外,我们也可以修改下workloop代码,去掉while循环,改成用requestIdleCallback调用。代码如下:

    workloop() {
        if (workInProgress) {
            workInProgress = this.performUnitOfWork(workInProgress);
            window.requestIdleCallback(this.workloop.bind(this), { timeout: 100 });
        }
    }

然后可以看到测试用例正常。

异步执行函数act

我们前面在写测试用例时候,用的是wait函数来延迟的。其实react中提供了一个act的异步调用方法,可以参考文档:https://zh-hans.legacy.reactjs.org/docs/testing-recipes.html#act

act(() => {
  // 渲染组件
});
// 进行断言

act是react-dom/test-utils中提供的测试能力,在react 18中提供了act的api实现,这里异步的写法如下,返回当前状态的promise。

await act(() => {
  // 渲染组件
});
// 进行断言

修改测试用例,采用act api,代码如下:

it('render with act', async () => {
    const ele = (
        <div className="container">
            <span>hello</span>
            <a>w3cdoc.com</a>
        </div>);
    const container = document.createElement('div');
    const root = Treact.createRoot(container);
    await Treact.act(() => {
        root.render(ele);
        // console.log(root);
        expect(container.innerHTML).toBe('');
    });
    console.log('container.innerHTML', container.innerHTML);
    expect(container.innerHTML).toBe('<div class="container"><span>hello</span><a>w3cdoc.com</a></div>');
})

然后我们来实现act,我们可以用workInProgress是否为null,来判断是否渲染完成。代码如下:

export function act(callback) {
    callback();
    return new Promise((resolve, reject) => {
        function detect(){
            if(workInProgress){
                window.requestIdleCallback(detect);
            }else{
                resolve(true);
            }
        }
        detect();
    });

}

然后查看测试用例执行通过。

参考

  1. requestIdleCallback:https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
  2. act文档:https://zh-hans.legacy.reactjs.org/docs/testing-recipes.html#act
  3. React 源码中 act 的使用案例:https://github.com/facebook/react/blob/17806594cc28284fe195f918e8d77de3516848ec/packages/react/src/__tests__/ReactStrictMode-test.internal.js#L87-L108
  4. global.IS_REACT_ACT_ENVIRONMENT:https://github.com/reactwg/react-18/discussions/102


请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部