关于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函数之外的任务