关于JS事件队列的一些总结
2017-12-08 15:37:00

关于JS事件队列的一些总结

关于任务队列

其实之所以我们要去关心JS的任务队列,主要还是因为JS的单线程的特质决定。

为什么JavaScript是单线程?

本段来自阮老师的博客中对JS单线程的介绍。

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

任务队列的本质

  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  • 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

关于 setTimeOut、setImmediate、process.nextTick()的比较

setTimeout()

  • 将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
  • 当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
  • 浏览器端每次setTimeout 会有4ms的延迟,当连续执行多个setTimeout ,有可能会阻塞进程,造成性能问题。

setImmediate()

  • 事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和setTimeout(fn,0)的效果差不多。
  • 服务端node提供的方法。浏览器端最新的api也有类似实现:window.setImmediate,但支持的浏览器很少。

process.nextTick()

  • 插入到事件队列尾部,但在下次事件队列之前会执行。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。
  • 大致流程:当前”执行栈”的尾部–>下一次Event Loop(主线程读取”任务队列”)之前–>触发process指定的回调函数。
  • 服务器端node提供的办法。用此方法可以用于处于异步延迟的问题。
  • 可以理解为:此次不行,预约下次优先执行。

关于消除 setTimeout 延迟的实践:soon.js

why?

setTimeout 的介绍所言,浏览器端每次setTimeout 会有4ms的延迟,当连续执行多个setTimeout ,有可能会阻塞进程,造成性能问题。

soon.js就是关于这个问题的一个好的实践。但其实大多数情况我们不必为这4ms的延迟计较,除非你在一次执行中setTimeout的次数足够多。代码很短,可以用来学习下。

使用方法

可以参考示例

源码:

// See http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon
// 使用 soon.js 处理在浏览器端 settimeout(大量调用),4ms * n 的延迟问题


var soon = (function() {
    
        var fq = []; // 事件队列;
    
        function callQueue()
        {
            while(fq.length) // 执行队列中事件
            {
                var fe = fq[0];
                fe.f.apply(fe.m,fe.a) // 执行队列中事件
                fq.shift(); 
            }
        }
    
        // 异步执行队列事件,最大效率
        var cqYield = (function() {
    
                // 通过 MutationObserver 来监听 Dom 来执行回调,此法最快
                if(typeof MutationObserver !== "undefined")
                {
                    var dd = document.createElement("div");
                    var mo = new MutationObserver(callQueue);
                    mo.observe(dd, { attributes: true });
    
                    return function(fn) { dd.setAttribute("a",0); } // trigger callback to
                }
    
                // 如果支持 setImmediate ,采取此策略,其实 setImmediate 和 setTimeout(callQueue,0) 差不多
                if(typeof setImmediate !== "undefined")
                    return function() { setImmediate(callQueue) }
    
                // 没办法了,就用 setTimeOut 的办法
                return function() { setTimeout(callQueue,0) }
            })();
    
        return function(fn) {
                // 队列事件装载进一个数组
                fq.push({f:fn,a:[].slice.apply(arguments).splice(1),m:this});
    
                if(fq.length == 1) // 在添加第一个条目时,启动回调函数
                    cqYield();
            };
    
    })();

分析

其实,值得分析就是一个新的东西–MutationObserver

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

简而言之,就是这个东西比setTimeOutsetImmediate快,浏览器支持就用它就行了。

关于soon.js的更详细的介绍可以查看这篇文章。Speed up your Websites with a Faster setTimeout using soon()

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

参考博客: