欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-04-30 20:15:37
...

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

输出分析

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


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


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


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