大话javascript 4期:事件循环(3)

9次阅读

共计 3210 个字符,预计需要花费 9 分钟才能阅读完成。

一、定时器
除了放置异步任务的事件,” 任务队列 ” 还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做 ” 定时器 ”(timer)功能,也就是定时执行的代码。
定时器功能主要由 setTimeout()和 setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论 setTimeout()。
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是 1,3,2,因为 setTimeout()将第二行推迟到 1000 毫秒之后执行。
如果将 setTimeout()的第二个参数设为 0,就表示当前代码执行完(执行栈清空)以后,立即执行(0 毫秒间隔)指定的回调函数。
setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是 2,1,因为只有在执行完第二行以后,系统才会去执行 ” 任务队列 ” 中的回调函数。
总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在 ” 任务队列 ” 的尾部添加一个事件,因此要等到同步任务和 ” 任务队列 ” 现有的事件都处理完,才会得到执行。
HTML5 标准规定了 setTimeout()的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。另外,对于那些 DOM 的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每 16 毫秒执行一次。这时使用 requestAnimationFrame()的效果要好于 setTimeout()。
需要注意的是,setTimeout()只是将事件插入了 ” 任务队列 ”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在 setTimeout()指定的时间执行。
二、Node.js 的 Event Loop
Node.js 也是单线程的 Event Loop,但是它的运行机制不同于浏览器环境。这里需要注意一下,node 新加了一个微任务 (process.nextTick) 和一个宏任务 (setImmediate) 简单的来说,就是 node 在处理一个执行队列的时候不管怎样都会先执行完当前队列,然后再清空微任务队列,再去执行下一个队列。
请看下面的示意图(作者 @BusyRich)。

根据上图,Node.js 的运行机制如下。
(1)V8 引擎解析 JavaScript 脚本。
(2)解析后的代码,调用 Node API。
(3)libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
(4)V8 引擎再将结果返回给用户。
除了 setTimeout 和 setInterval 这两个方法,Node.js 还提供了另外两个与 ” 任务队列 ” 有关的方法:process.nextTick 和 setImmediate。它们可以帮助我们加深对 ” 任务队列 ” 的理解。
process.nextTick 方法可以在当前 ” 执行栈 ” 的尾部 —- 下一次 Event Loop(主线程读取 ” 任务队列 ”)之前 —- 触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate 方法则是在当前 ” 任务队列 ” 的尾部添加事件,也就是说,它指定的任务总是在下一次 Event Loop 时执行,这与 setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log(‘TIMEOUT FIRED’);
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于 process.nextTick 方法指定的回调函数,总是在当前 ” 执行栈 ” 的尾部触发,所以不仅函数 A 比 setTimeout 指定的回调函数 timeout 先执行,而且函数 B 也比 timeout 先执行。这说明,如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前 ” 执行栈 ” 执行。
现在,再看 setImmediate。
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log(‘TIMEOUT FIRED’);
}, 0);

上面代码中,setImmediate 与 setTimeout(fn,0)各自添加了一个回调函数 A 和 timeout,都是在下一次 Event Loop 触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是 1 –TIMEOUT FIRED–2,也可能是 TIMEOUT FIRED–1–2。
令人困惑的是,Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
console.log(‘TIMEOUT FIRED’);
}, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate 和 setTimeout 被封装在一个 setImmediate 里面,它的运行结果总是 1 –TIMEOUT FIRED–2,这时函数 A 一定在 timeout 前面触发。至于 2 排在 TIMEOUT FIRED 的后面(即函数 B 在 timeout 后面触发),是因为 setImmediate 总是将事件注册到下一轮 Event Loop,所以函数 A 和 timeout 是在同一轮 Loop 执行,而函数 B 在下一轮 Loop 执行。
我们由此得到了 process.nextTick 和 setImmediate 的一个重要区别:多个 process.nextTick 语句总是在当前 ” 执行栈 ” 一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。事实上,这正是 Node.js 10.0 版添加 setImmediate 方法的原因,否则像下面这样的递归调用 process.nextTick,将会没完没了,主线程根本不会去读取 ” 事件队列 ”!
process.nextTick(function foo() {
process.nextTick(foo);
});

事实上,现在要是你写出递归的 process.nextTick,Node.js 会抛出一个警告,要求你改成 setImmediate。
另外,由于 process.nextTick 指定的回调函数是在本次 ” 事件循环 ” 触发,而 setImmediate 指定的是在下次 ” 事件循环 ” 触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查 ” 任务队列 ”)。
最后注意一下,微任务中 process.nextTick 比 promise.then 快

正文完
 0