关于JS
这个标题下,我们只需要牢记一句话: JavaScript 是单线程语言
既然
JavaScript是单线程语言,那么就会存在一个问题,所有的代码都得一句一句的来执行。就像我们在食堂排队打饭,必须一个一个排队点菜结账。那些没有排到的,就得等着~
事件循环(Event Loop)
JavaScript有一个主线程main thread,和调用栈call-stack也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
JavaScript的Event Loop是伴随着整个源码文件生命周期的,只要当前JavaScript在运行中,内部的这个循环就会不断地循环下去,去寻找 队列queue里面能执行的 任务task。
任务队列(task queue)
task,就是任务的意思,我们这里理解为每一个语句就是一个任务console.log(1);
console.log(2);如上语句,其实就是就可以理解为两个任务
task。而 队列
queue呢,就是先入先出FIFO的队列!所以任务队列
Task Queue就是承载任务的队列。而JavaScript的Event Loop就是会不断地过来找这个队列queue,问有没有任务task可以运行运行。
同步任务(SyncTask)、异步任务(AsyncTask)
同步任务说白了就是主线程来执行的时候立即就能执行的代码,比如:
console.log('this is THE LAST TIME');
console.log('Nealyang');
代码在执行到上述 console 的时候,就会立即在控制台上打印相应结果。
而所谓的异步任务就是主线程执行到这个 task 的时候,“唉!你等会,我现在先不执行,等我 xxx 完了以后我再来等你执行” 注意上述我说的是等你来执行。
说白了,异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(()=>{
console.log(2)
});
console.log(1);
如上述代码,setTimeout 就是一个异步任务,主线程去执行的时候遇到 setTimeout 发现是一个异步任务,就先注册了一个异步的回调,
然后接着执行下面的语句console.log(1),等上面的异步任务等待的时间到了以后,在执行console.log(2)。具体的执行机制会在后面剖析。

主线程自上而下执行所有代码
同步任务直接进入到主线程被执行,而异步任务则进入到
Event Table并注册相对应的回调函数异步任务完成后,
Event Table会将这个函数移入Event Queue主线程任务执行完了以后,会从
Event Queue中读取任务,进入到主线程去执行。循环如上
上述动作不断循环,就是我们所说的事件循环(Event Loop)。
宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
宏任务(
MacroTask),所有的同步任务代码都是宏任务(MacroTask)(这么说其实不是很严谨,下面解释),setTimeout、setInterval、I/O、UI Rendering等都是宏任务。微任务(
MicroTask),为什么说上述不严谨我却还是强调所有的同步任务都是 宏任务(MacroTask)呢,因为我们仅仅需要记住几个 微任务(MicroTask)即可,排除法!别的都是 宏任务(MacroTask)。微任务(
MicroTask)包括:Process.nextTick、Promise.then catch finally(注意我不是说 Promise)、MutationObserver。
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

Promise和async中的立即执行
我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。
而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
async await
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到微任务microtask中,然后就会跳出整个async函数来执行后面的代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是微任务microtask。
关于async await
async函数其实是Geneator函数的语法糖。
1.async函数的返回值是Promise对象,可以用then方法指定下一步的操作(添加回调函数)。async函数可以看做多个异步操作,包装成一个Promise对象,await命令就是内部then命令的语法糖。
2.当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句。
3.async函数返回一个Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
相当于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
面试题一:
//请写出输出内容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
1、首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:

2、然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:

3、script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
遇到了await时,会将await的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log('async1 end')加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。

4、script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。

5、script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
6、第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。
面试题二:
在第一个变式中我将async2中的函数也变成了Promise函数,代码如下
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise3');
resolve();
}).then(function() {
console.log('promise4');
});
console.log('script end');
/*
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
*/
在第一次macrotask执行完之后,也就是输出script end之后,会去清理所有microtask。所以会相继输出promise2,async1 end ,promise4
面试题三:
在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:
async function async1() {
console.log('async1 start');
await async2();
//更改如下:
setTimeout(function() {
console.log('setTimeout1')
},0)
}
async function async2() {
//更改如下:
setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
*/
在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按3,2,1的顺序来输出。
面试题四:
async function a1 () {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2 () {
console.log('a2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
/*
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
*/
1、执行console.log('script start')
2、将setTimeout放入宏任务中(宏任务1)
3、将console.log('promise1')放入微任务(微任务1)
4、执行a1()依次输出a1 start、a2,将await返回的promise放入微任务中(微任务2)
5、输出promise2',将resolve('promise2.then')放入微任务中(微任务3)
6、输出'script end'
主线程结束
开始任务队列
1、执行微任务1 ,输出promise1
2、执行微任务2,输出a1 end
3、执行微任务3,输出'promise2.then',将console.log('promise3')放入微任务(微任务4)
4、执行微任务4,输出promise3
5、执行宏任务1,输出setTimeout
面试题五:
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
resolve 函数将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject 函数将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
而一旦状态改变,就不会再变。
所以 代码中的reject('error'); 不会有作用。
Promise 只能 resolve 一次,剩下的调用都会被忽略。
所以 第二次的 resolve('success2'); 也不会有作用。
面试题六:
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
})
promise.then(() => {
console.log(3);
})
console.log(4);
首先 Promise 新建后立即执行,所以会先输出 1,2,而 Promise.then() 内部的代码在 当次 事件循环的 结尾 立刻执行 ,所以会继续输出4,最后输出3。
面试题七:
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
第一轮事件循环
- 先执行宏任务,主script ,new Promise立即执行,输出【3】,
- 执行 p 这个new Promise 操作,输出【7】,
- 发现 setTimeout,将回调放入下一轮任务队列(Event Queue),p 的 then,姑且叫做 then1,放入微任务队列,发现 first 的 then,叫 then2,放入微任务队列。执行
console.log(4),输出【4】,宏任务执行结束。 - 再执行微任务,执行 then1,输出【1】,
- 执行 then2,输出【2】。
- 到此为止,第一轮事件循环结束。开始执行第二轮。
第二轮事件循环
- 先执行宏任务里面的,也就是 setTimeout 的回调,输出【5】。
resolve(6)不会生效,因为 p 这个 Promise 的状态一旦改变就不会在改变了。
面试题八:
async function a() {
console.log(1);
await console.log(2);
await console.log(3);
console.log(4);
}
a();
new Promise((resolve, reject) => {
console.log(5);
resolve(6);
}).then(res => {
console.log(res);
});
输出125364
如果两个await在一个async里面,那么第二个await需要等待第一个await执行完才执行,整个async函数会停止执行,而主线程去执行async函数之外的任务

本文深入解析JavaScript的单线程特性、事件循环机制、宏任务与微任务的区别及执行顺序,并通过具体示例帮助读者理解JavaScript的异步执行原理。
1万+





