客户端异步实现
我们经常在写客户端js的时候用到异步操作,实现异步基本上包含
setTimeout setInterval ajax
Node端异步实现
node中实现异步包含
setImmediate process.nextTick
定时器用法差不多
看下边的例子:
setTimeout(()=>{ console.log(1); }); setImmediate(()=>{ console.log(2); }); process.nextTick(()=>{ console.log(3); }); Promise.resolve().then(()=>{ console.log(4); }); (()=>console.log(5))();
输出结果如下:
5 3 4 1 2
下边来分析一下结果
同步和异步任务
同步任务要比异步任务更早执行,因此首先输出5
本轮循环和次轮循环
异步任务分为两种:
追加在本轮循环的异步任务 追加在次轮循环的异步任务
本轮循环任务要优先于次轮循环任务
Node规定,process.nextTick、Promise的回调函数,追加在本轮循环任务中,即同步任务完成后就开始执行。而setTimeout、setInterval、setImmediate的回调函数追加在次轮循环任务中。因此
// 下面两行,次轮循环执行 setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); // 下面两行,本轮循环执行 process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4));
process.nextTick()
process.nextTick任务在本轮循环中执行,而且是所有异步任务里执行最快的。如果希望快速执行异步任务就使用此函数。
微任务
Promise对象的回调函数会进入异步任务里边的“微任务”队列,微任务队列追加在process.nextTick队列后边,也属于本轮循环。
注意:只有前一个队列全部清空后才会执行下一个队列。
看下边的例子:
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 1 // 3 // 2 // 4
全部process.nextTick的回调函数会早于Promise
事件循环的概念
js只有一个主线程,事件循环是在主线程上完成的。
Node开始执行脚本时,先对事件循环进行初始化,但这时还没有执行事件循环,先进行一下操作。
同步任务 发出异步请求 规划定时器生效时间 执行本轮循环任务等等
上边事情都干完之后才进行事件循环。
事件循环的六个阶段
事件循环是无限次的执行,只有当异步任务回调函数队列都清空了,才会停止执行。
每一轮循环分为以下六个阶段
timers I/O callbacks idle,prepare poll check close callbacks
每个阶段都有一个先进先出的队列,只有该队列都清空或者该执行的回调函数都执行了,时间循环才进入下一个阶段。
(1) timers
这是一个定时器阶段,处理setTimeout,setInterval的回调函数,进入这个阶段,主线程会检查一下当前时间,是否满足定时器条件。如果满足就执行回调函数,否则调到下一个阶段。
(2) I/O callbacks
除了一下操作的回调函数,其他的回调函数都在这个阶段执行。
setTimeout()和setInterval()的回调函数 setImmediate()的回调函数 关于关闭请求的回调函数,例如socket.on(“close”,…)
本文中例子执行的是读取文件后的回调
(3) idle, prepare
该阶段只供 libuv 内部调用,这里可以忽略。
(4) Poll
这个阶段是轮询时间,用于等待未返回的I/O事件,比如服务器的回应,文件的读取,用户鼠标移动等等。
这个阶段的时间比较长,如果没有其他异步要处理(比如到期的定时器),会一直停留在这个阶段,等待I/O的响应。
(5) check
该阶段执行setImmediate()的回调函数。
(6) close callbacks
该阶段执行关闭请求的回调函数,比如socket.on(‘close’, …)。
事件循环的示例
分析一个官方案例:
const fs = require('fs'); const timeoutScheduled = Date.now(); // 异步任务一:100ms 后执行的定时器 setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms`); }, 100); // 异步任务二:文件读取后,有一个 200ms 的回调函数 fs.readFile('test.js', () => { const startCallback = Date.now(); while (Date.now() - startCallback < 200) { // 什么也不做 } });
第一轮事件循环,没有到期的定时器,也没有可以进行的I/O回调操作,所以会键入Poll阶段,等待内核返回文件读取结果,由于文件过小,读取时间不会超过100ms,所以在定时器到期之前Poll阶段就会返回结果。继续往下执行。
第二轮事件循环,依然没有到期的定期器,因此会执行I/O的回调函数,进入I/O的callbacks阶段,执行fs.readFile的回调函数,这个函数需要200ms,也就是执行到一半,定时器就会到期,但是必须等到这个回调执行完之后才会离开这个阶段。
第三轮事件循环,已经有了到期的定时器,所以会在timers阶段执行定时器,最后输出大约200多毫秒。
setTimeout 和 setImmediate
由于setTimeout在timers阶段,setImmediate在check阶段,所以setTimeout会比setImmediate提前执行。
setTimeout(() => console.log(1)); setImmediate(() => console.log(2));
上边代码应该先输出1后输出2,但是结果却是不确定的。有时会先输出2。
这是因为setTimeout的第二个参数默认为0,但是Node做不到0毫秒,最少也需要1ms,官方文档规定,setTimeout第二个参数范围在1~2147483647毫秒之间。
实际执行的时候,进入循环后,有可能到了1ms,也有可能没到1ms,取决于当前的系统状况。如果没到1ms那么timers阶段就会被跳过,进入check阶段,先执行setImmediate回调。
一下代码肯定是先输出2再输出1。
const fs = require('fs'); fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });