React中requestIdleCallback的polyfill实现

2019年12月25日Web前端

最近发现了requestIdleCallback函数,也听说React源码中也适用了requestIdleCallback函数,夜来看看他在此中的作用。

实际在React源码(16.12.0)中,并没有查到该方法,查到的也只是test中用setTimeout模拟了requestIdleCallback方法。

requestIdleCallback

首先来看下这个函数,该方法会在DOM渲染空闲时期执行任务,所以是一个执行抵优先级任务的方法。

requestIdleCallback(function(deadline) {
    console.log(`剩余空闲时间:${ deadline.timeRemaining() }`);
    console.log(`是否是超时执行:${ deadline.didTimeout }`);
});

deadline.timeRemaining()输出了每帧剩余的渲染时间,如果没有剩余时间,此回调不会在当前帧中执行,留到下次执行,此时就超时了,didTimeout即为true。但是为啥在React中没有直接使用,而是polyfill了呢?

chrome文档上,可以看到原生提供的requestIdleCallback方法的timeRemaining()最大返回是50ms,也就是20fps,达不到页面流畅度的要求,并且该API兼容性也比较差。

SchedulerHostConfig.default.js

该文件位于react/packages/scheduler/src/forks下,他实现了对requestIdleCallback的polyfill。

他针对DOM下和非DOM环境下实现了两套方案,我们先来看下该文件暴露出去的方法。

export let requestHostCallback; // requestIdleCallback的polyfill方法
export let cancelHostCallback;  // 用于取消requestHostCallback
export let requestHostTimeout;
export let cancelHostTimeout;   // 用于取消requestHostTimeout
export let shouldYieldToHost;
export let requestPaint;
export let getCurrentTime;      // 获取当前触发事件
export let forceFrameRate;      // 设置渲染的fps

非DOM环境

getCurrentTime

initialTime记录了一开始的触发时间戳,getCurrentTime函数则返回当前时间戳减去初始时间戳。

const initialTime = Date.now();
getCurrentTime = function() {
    return Date.now() - initialTime;
};

requestHostCallback

const _flushCallback = function() {
    if (_callback !== null) {
        // 无函数时不执行
        try {
            const currentTime = getCurrentTime();
            const hasRemainingTime = true;
            _callback(hasRemainingTime, currentTime);
            _callback = null;
        } catch (e) {
            setTimeout(_flushCallback, 0);
            throw e;
        }
    }
};
requestHostCallback = function(cb) {
    if (_callback !== null) {
        // 利用setTimeout的第三个参数
        setTimeout(requestHostCallback, 0, cb);
    } else {
        _callback = cb;
        setTimeout(_flushCallback, 0);
    }
};

requestHostCallback是基于setTimeout实现的,主要调用位于flushCallback函数体内,当存在callback时,便会触发_callback,传入true表示未超时,仍有空闲时间,currentTime则表示当前触发的时间戳。

requestHostCallback中,若存在_callback时,表示先前还有任务未完成,便利用setTimeout的第三个参数延后执行任务。

注:有关setTimeout的第三个参数可以见:setTimeout第三个参数

cancelHostCallback

cancelHostCallback函数是用来取消requestHostCallback定时的。将函数设置置空,即可。

cancelHostCallback = function() {
    _callback = null;   // 置空,不执行
};

requestHostTimeout和cancelHostTimeout

requestHostTimeout = function(cb, ms) {
    _timeoutID = setTimeout(cb, ms);
};
cancelHostTimeout = function() {
    clearTimeout(_timeoutID);
};

shouldYieldToHost

表示触发是否超时,即当前帧是否过期。非DOM环境下一直都是false。

requestPaint 和 forceFrameRate

非DOM下无实质用处,为空函数。

DOM环境下

if (typeof console !== 'undefined') {
    const requestAnimationFrame = window.requestAnimationFrame;
    const cancelAnimationFrame = window.cancelAnimationFrame;
    // ....
}

首先是对requestAnimationFramecancelAnimationFrame两方法做了是否有的判断,并做了error的提示,实际上,在本文件下是没有使用这两个方法的,注释是说以防未来可能有使用。(看了先前版本的代码,是有基于requestAnimationFrame实现的。)

getCurrentTime

在DOM环境下,优先使用performance下的now方法,不支持的情况下,使用Date方式。

if (
    typeof performance === 'object' &&
    typeof performance.now === 'function'
) {
    getCurrentTime = () => performance.now();
} else {
    const initialTime = Date.now();
    getCurrentTime = () => Date.now() - initialTime;
}

forceFrameRate

该方法用于设置渲染的fps。

forceFrameRate = function(fps) {
    if (fps < 0 || fps > 125) {
        console.error(
            'forceFrameRate takes a positive int between 0 and 125, ' +
            'forcing framerates higher than 125 fps is not unsupported',
        );
        return;
    }
    if (fps > 0) {
        yieldInterval = Math.floor(1000 / fps);
    } else {
        yieldInterval = 5;
    }
};

fps仅支持0-125fps的,超出的做error提示。yieldInterval就是每帧时间。

requestHostCallback

在非DOM下,requestHostCallback使用的是基于setTimeout实现的,而在DOM下是基于postMessage和onmessage(MessageChannel)这个发布订阅模式做的。

postMessage

先看下requestHostCallback函数主体,

requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
        isMessageLoopRunning = true;
        port.postMessage(null);
    }
};

将回调函数存储起来,触发postMessage发送消息。isMessageLoopRunning表示是否在postMessage处理中,即是否在时间处理中。

onmessage

postMessage后,触发performWorkUntilDeadline函数,

channel.port1.onmessage = performWorkUntilDeadline;

const performWorkUntilDeadline = () => {
    if (scheduledHostCallback !== null) {
        // 有回调任务
        const currentTime = getCurrentTime();
        deadline = currentTime + yieldInterval; // 设置deadline
        const hasTimeRemaining = true;  // 任务有剩余时间
        try {
            const hasMoreWork = scheduledHostCallback(
                hasTimeRemaining,
                currentTime,
            );
            if (!hasMoreWork) {
                isMessageLoopRunning = false;
                scheduledHostCallback = null;
            } else {
                port.postMessage(null);
            }
        } catch (error) {
            port.postMessage(null);
            throw error;
        }
    } else {
        isMessageLoopRunning = false;
    }

    needsPaint = false;
};

yieldInterval是根据fps算出的每帧时间,超过这个时间就会影响帧数,到达不了想要的结果,所以设置了个deadline,即最终期限时间。

hasTimeRemaining表示是否有剩余处理时间,即未到达deadline

当执行完scheduledHostCallback后,还有任务时,继续触发postMessage。没有的话,清空掉回调函数。

cancelHostCallback

置空回调,这样在performWorkUntilDeadline中就不会执行对应的函数了,相当于取消了requestHostCallback。

requestHostTimeout

requestHostTimeout = function(callback, ms) {
    taskTimeoutID = setTimeout(() => {
        callback(getCurrentTime());
    }, ms);
};

基于setTimeout,向回调传入当前时间作为参数。

cancelHostTimeout

requestHostTimeout的取消方法,即clearTimeout。

shouldYieldToHost和requestPaint

isInputPending的内容,可以看下这篇文章(is-input-pending)[https://wicg.github.io/is-input-pending/],目前这只是个提议,还没有DOM是支持的。

而在SchedulerFeatureFlags.js中enableIsInputPending也是被定义为false,故函数定义位于else中。

shouldYieldToHost = function() {
    return getCurrentTime() >= deadline;
};
requestPaint = function() {};

shouldYieldToHost根据当前时间是否大于最后的预期时间。requestPaint和在非DOM下一样,都是未实现的空函数。