Node.js从入门到放弃(六)
前言
这是该系列文章的第六篇,主要介绍异步和事件循环
初识异步
异步相对于同步而言,直观的体现就是异步不会阻塞后续代码执行,而同步会阻塞。
看起来异步比同步好一些,但异步操作不像同步代码那样符合预期,无论是编写还是执行。
异步初体验
setTimeout(()=>{
console.log(1)
},1000)
console.log(2)
按照我们的预期,代码从上到下执行,遇到延时器,等1s后输出1,然后再输出2。
而实际上,2立刻被输出,1s后1再被输出。
你可能觉得是因为延迟一秒导致的,其实你把延迟时间设置为0,也是2先输出。
回调函数
Node中基本所有的API都是异步的,如果你想获取到异步操作的结果,直接像同步代码那样return,外部是拿不到的。
异步操作不会阻塞后续代码运行,这意味着异步结果还没有获取到,外部取值的代码就执行了,自然不是预期的return的结果。
- 这种写法是同步思维,实际上,这个return的结果外部根本拿不到。
setTimeout(() => {
return 10
}, 1000)
- 传递一个回调函数进来才可以拿到异步操作的结果,这就是异步思维
function fn(callback) {
setTimeout(() => {
callback(10)
}, 1000)
}
function callback(data) {
console.log(data)
}
fn(callback)
回调地狱
刚提到获取异步处理结果用回调函数可以实现,所以你应该写过或看到过这样的代码,。
由于下一次请求依赖上一次请求结果,需要获取到上部操作数据才能进行下一次请求。
一阵疯狂嵌套… 如果嵌套一两层也许你还能捋清楚,十层呢?灾难性的。
axios.post(url1).then(res1 => {
axios.post(url2,{data:res1.data}).then(res2 => {
axios.post(url3,{data:res2.data}).then(res3 => {
...
})
})
})
事件循环
对异步有一定了解后,要透过现象看本质,js的单线程背后,究竟是如何调度各个程序运行的呢?事件循环又是什么?
小小测试
setTimeout(() => {
console.log('0')
},0);
new Promise(resolve => {
console.log('1');
resolve();
console.log('2');
}).then(() => {
console.log('3')
});
console.log('4');
- 你觉得输出什么?0,1,2,3,4?不妨去控制台或node中去试试
- 答案是1,2,4,3,0
- 如果你完全清晰,好的,后边不用看了,你很棒棒
- 如果你一脸懵逼或似懂非懂,请继续阅读,保持你的好奇心
任务划分和调度流程
js代码执行大致可划分为两种:同步任务(常规script标签或js文件中的代码段,promise的resolve 等),异步任务(setTimeout,promise的then函数等),由任务执行栈来控制调度,具体调度流程如下:
- 任务执行栈对当前任务进行分而治之,同步任务交给主线程执行,异步任务先去事件表中注册登记一下
- 主线程将当前任务执行完毕后,再去事件队列中检查是否存在待调用函数
- 已经在事件表注册的异步任务完成后,会将回调函数放入事件队列
- 空闲的主线程读取事件队列中的回调函数,执行
- 上述过程会不断重复,直到所有任务被执行完毕,这就是事件循环
输出解释
说了调度流程,接下来一对一的解释一下示例代码的输出顺序和成因, 首先,先分一下同步任务和异步任务
- 同步
new Promise(resolve => {
console.log('1');
resolve();
console.log('2');
})
console.log("4")
- 异步
setTimeout(() => {
console.log('0')
},0);
//promise的then函数
then(()=>{
console.log('3')
})
- 同步代码交给主线程执行,由上至下
- 遇到异步任务setTimeout ,promise只注册任务表,放行
- 异步任务完成,将回调函数放入事件队列
- 主线程读取事件队列中的回调函数,执行
看了上边的解释,也许你会感到疑惑,先同步执行,输出1,2,4
然后回调函数依次取出被主线程执行,那不应该是1,2,4,0,3吗?
- 其实,除了单纯的同步任务和异步任务,还有更精细的宏任务(整体js代码段,定时器,延时器等),和微任务(promise的then,process.nextTick)
- 任务队列可再细分微任务队列和宏任务队列,主线程读取微任务队列的优先级高于宏任务队列,所以promise的then函数执行优先级比延时器高,输出1,2,4,3,0
特殊的延时器
- 延时器从注册就开始计时,计时结束将回调函数放入事件队列
- 延时器延迟时间设为0的意思是,主线程空闲直接调用,而不需要额外等待
- 根据html标准,延迟时间最低4毫秒,0只是近似0而已
- 主线程阻塞(如10W次循环)会导致延时器的计时不精准
主线程死循环会导致异步任务无法执行,处于阻塞状态
- 这行代码,不会输出1的,卡死
while(true){setTimeout(()=>{console.log(1)})}
综合案例
猜猜下面的代码输出顺序是什么?
console.log('1');
setTimeout(() => {
console.log('2');
process.nextTick(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})
process.nextTick(() => {
console.log('6');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
})
setTimeout(() => {
console.log('9');
process.nextTick(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
console.log('12');
}).then(() => {
console.log('13')
})
})
输出分析