Node.js从入门到放弃(六)

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')
    })
})

输出分析

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

发布了465 篇原创文章 · 获赞 812 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/qq_42813491/article/details/104086045