要理解JS中的异步、同步,需要先了解JS代码的执行过程和Event Loop。
程序需要执行的操作都会被放入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是以同步的形式执行代码:
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 中的任务拿出来执行。
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
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/the-nodejs-event-loop/