1
头图
要理解JS中的异步、同步,需要先了解JS代码的执行过程和Event Loop。

JavaScript代码的执行过程

程序需要执行的操作都会被放入Call Stack(A LIFO (Last In, First Out) Stack),先进后出的数据结构。

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()

当这段代码执行时,foo()会首先被调用。在foo()内部先调用了bar(),然后调用了baz()。在这个时刻Call Stack 是下面的样子:
图例

执行每一步操作都有新的操作压入栈顶,执行完毕就从栈顶弹出,直到整个栈变为Empty。

Event Loop 起到的作用就是每次迭代都回去检查Call Stack中是否有待执行的指令,然后去执行它。

JavaScript中的异步、同步

同步执行代码

大多数的情况下,JavaScript是以同步的形式执行代码:

let log = console.log;

let a = 5;
let b = 50;

let a1 = function() { return 5 }
let b1 = function() { return 50 }

log(a1())
log(a2())

let a2 = function(num) { return 5*num }
let b2 = function() { return 50 }

log(a2(b2))
// 打印出:
// 5
// 50
// 250

异步执行的代码

setTimeout, callbacks, Promise, fetch, ajax, filesystem interaction, database calls, DOM event listener

上边这些情况代码将是异步执行。

原因是代码执行到这些方法时,是不确定对应的操作多久可以执行完毕,所以会继续向下执行。

考虑下面的情形:

let a3 = 100;
setTimeout(function() { a3++ }, 0);
log(a3)
setTimeout(function() { log(a3) }, 0);
// 打印出
// 100
// 101

同步形式的代码被放入 Call Stack 中执行,异步代码放到了一个队列中(Message Queue)。Event Loop 会优先处理 Call Stack 中的任务,当 Call Stack 为空,就会把 Message Queue 中的任务拿出来执行。

ES6 Job Queue

ECMAScript 2015 引入了 Job Queue 的概念,被 Promises 使用。它会使异步方法的结果尽快执行,而不是放到 Call Stack 的最后执行。

Promise是异步的一个非常好的实现:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('should be right after baz, before bar')
  ).then(resolve => console.log(resolve))
  baz()
}

foo()

// foo
// baz
// should be right after baz, before bar
// bar

可以看到Promise会先于setTimeout执行。

看一下这段代码,你判断一下它的执行顺序是怎么样的?

console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);
// 1
// 7
// 3
// 5
// 2
// 6
// 4

Nodejs Process.NextTick() 和 SetImmediate()

Process.NextTick() 会在事件循环的一个周期的最后执行,通过这个方法可以实现,把一个异步方法尽快执行而不是放入异步队列中。

Process.NextTick 回调函数会被添加到 Process.NextTick 队列,Promise.Then() 会被添加到 Promises 微任务队列(Microtask Queue),SetTimeout, SetImmediate 会被添加到宏任务队列(Macrotask Queue)。

SetTimeout() 延时0ms的异步与 SetImmediate()很相似,它们都是在下一次事件循环执行。

事件循环会先执行 Process.NextTick 队列,然后执行 Promises 微任务队列,然后是 宏任务队列。

console.log('script start');

// 异步
Promise.resolve().then(function() {
  console.log('promise');
}).then(function() {
  console.log('promise-then');
});

// 异步
setImmediate(function() {
    console.log('setImmediate')
})

// 异步
setTimeout(function() {
    console.log('setTimeout 0')
}, 0)

// 异步
setTimeout(function() {
    return new Promise(resolve => {
        console.log('setTimeout-delay 100ms promise')
        resolve()
    }).then(res => {
        console.log('setTimeout-delay 100ms promise.then')
    })
}, 100)

process.nextTick(function() {
    console.log('process.nextTick')
})

console.log('script end');

/*
script start
script end
process.nextTick
promise
promise-then
setTimeout 0
setImmediate
setTimeout-delay 100ms promise
setTimeout-delay 100ms promise.then
*/

参考链接

https://nodejs.dev/en/learn/t...

文章首发于 IICOOM-个人博客|技术博客 《JavaScript中的异步、同步》


来了老弟
508 声望31 粉丝

纸上得来终觉浅,绝知此事要躬行