卡住 setTimeout()s (Chrome)

Stuck setTimeout()s (Chrome)

我遇到这样一种情况,多个 setTimeout() 调用(典型长度:100 毫秒到 1 秒)处于活动状态并且本应关闭,但它们没有触发。 Chrome (Mac) 调试器配置文件在此(可能无限)期间显示“空闲”。配置文件显示没有任何事情发生。没有循环。无代码 运行。没有垃圾收集。没有 activity 任何东西。视口处于活动状态并具有焦点。在我等待(可能是无限时间)之后,当我“做其他事情”时,比如 mouseover 一些不相关的元素——就像 :hover 一样简单——僵局打破了,排队的 setTimeout() 全部火。

当我在这种“冻结”发生后在 setTimeout() 处理程序函数中设置断点时,它们会在僵局中断时按顺序命中,正如您所期望的那样。

不幸的是,复制路径很困难。到目前为止,创建更简单的测试用例只会使复制变得更加困难,或者最终无法复制。

大多数围绕setTimeout()“问题”的喋喋不休来自不了解jscript等单线程性质的人,因此没有帮助。让我重复一遍:计时器已排队,应该已经触发。正如探查器所证明的那样,浏览器处于空闲状态。计时器最终会触发,但只有在鼠标 activity 发生之后。这种行为对我来说似乎是非常错误的。如果浏览器空闲,并且队列中有事件,它们应该触发。

有人见过这样的行为吗?我是否偶然发现了一种锁定事件调度程序的方法?也许我错过了什么。

更新:无法在 Windows 7.

上复制

更新 2:在 Mac 重新启动 Chrome,无法再复制。因此,可能出现的最坏结果:无法回答为什么会发生、为什么会一直发生、为什么没有可靠地发生、为什么会消失以及为什么不会再发生。

我最近遇到了类似的问题,发现自版本 47 以来,chromium 用户已经决定在他们认为这是 'detrimental to the majority of users' 时不遵守 setTimeout。基本上他们已经弃用了 setTimeout API(像往常一样不问任何人)。
这是 bug 570845 人们发现这件事的地方。关于这个问题还有许多其他错误和讨论线程。

后备方案是使用 requestAnimationFrame 来模拟 setTimeout。
这是一个概念证明:

'use strict'

var PosfScheduler = ( function () {

    /*
     * Constructor.
     * Initiate the collection of timers and start the poller.
     */
    function PosfScheduler () {
        this.timers   = {};
        this.counter  = 0;

        this.poll = function () {
            var scheduler = this;
            var timers = scheduler.timers;
            var now = Date.now();

            for ( var timerId in timers ) {
                var timer = timers[timerId];
                if ( now - timer.submitDate >= timer.delay ) {
                    if ( timer.permanent === true ) {
                        timer.submitDate = now;
                    } else {
                        delete timers[timer.id];
                    }
                    timer.func.apply.bind( timer.func, timer.funcobj, timer.funcargs ).apply();
                }
            }

            requestAnimationFrame( scheduler.poll.bind(scheduler) );

        };

        this.poll();

    };

    /*
     * Adding a timer.
     * A timer can be
     *   - an interval (arg[0]: true) - a recurrent timeout
     *   - a simple timeout (arg[0]: false)
     */
    PosfScheduler.prototype.addTimer = function () {
        var id         = this.counter++;
        var permanent  = arguments[0] ;
        var func       = arguments[1] ;
        var delay      = arguments[2] ;
        var funcobj    = arguments[3] ;
        var funcargs   = Array.prototype.slice.call(arguments).slice(4);
        var submitDate = Date.now() ;

        var timer = {
                id:         id,
                permanent:  permanent,
                func:       func,
                delay:      delay,
                funcargs:   funcargs,
                submitDate: submitDate,
        }

        this.timers[id] = timer;

        return timer;
    };

    /*
     * Replacement for setTimeout
     * Similar signature:
     *                      setTimeout ( function, delay [obj,arg1...] )
     */
    PosfScheduler.prototype.setTimeout = function () {
        var args = Array.prototype.slice.call(arguments);
        return this.addTimer.apply.bind( this.addTimer, this, [false].concat(args) ).apply();

    };

    /*
     * Replacement for setInterval - Untested for now.
     * Signature:
     *                      setInterval ( function, delay [obj,arg1...] )
     */
    PosfScheduler.prototype.setInterval = function () {
        var args = Array.prototype.slice.call(arguments);
        return this.addTimer.apply.bind( this.addTimer, this, [true].concat(args) ).apply();
    };

    PosfScheduler.prototype.cancelTimeout = function ( timer ) {
        delete this.timers[timer.id];
    };

    /*
     * Don't want to leave all these schedulers hogging the javascript thread.
     */
    PosfScheduler.prototype.shutdown = function () {
        delete this;
    };

    return PosfScheduler;

})();

    var scheduler = new PosfScheduler();

    var initTime = Date.now();

    var timer1 = scheduler.setTimeout ( function ( init ) {
        console.log ('executing function1 (should appear) after ' + String ( Date.now() - init ) + 'ms!' );
    }, 200, null, initTime );


    var timer2 = scheduler.setTimeout ( function ( init ) {
        console.log ('executing function2 afte: ' + String ( Date.now() - init ) + 'ms!' );
    }, 300, null, initTime );

    var timer3 = scheduler.setTimeout ( function ( init ) {
        console.log ('executing function3 (should not appear) after ' + String ( Date.now() - init ) + 'ms!' );
    }, 1000, null, initTime );

    var timer4 = scheduler.setTimeout ( function ( init, sched, timer ) {
        console.log ('cancelling timer3  after ' + String ( Date.now() - init ) + 'ms!' );
        sched.cancelTimeout ( timer3 );
    }, 500, null, initTime, scheduler, timer3 );

    var timer5 = scheduler.setInterval ( function ( init, sched, timer ) {
        console.log ('periodic after ' + String ( Date.now() - init ) + 'ms!' );
    }, 400, null, initTime, scheduler, timer3 );

    var timer6 = scheduler.setTimeout ( function ( init, sched, timer ) {
        console.log ('cancelling periodic after ' + String ( Date.now() - init ) + 'ms!' );
        sched.cancelTimeout ( timer5 );
    }, 900, null, initTime, scheduler, timer5 );