今天作了一道笔试题以为颇有意义分享给你们,题目以下:html
setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');复制代码
JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 没法进行多线程编程,可是 JS 当中却有着无处不在的异步概念 。要彻底理解异步,就须要了解 JS 的运行核心——事件循环(event loop)。
前端
首先来看一个小小的demoes6
console.log('start');
setTimeout(()=>{
console.log('A');
},1000);
console.log('end');
//start
//end
//A复制代码
js执行以后,程序输出 'start' 和 'end',在大约1s以后输出了 'A' 。那咱们就有疑问了?为何A不在end以前执行呢?
编程
这是由于 setTimeout 是一个异步的函数。意思也就是说当咱们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束以后,事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务事后便将任务压入执行栈(stack)当中,执行栈执行任务,输出 'A'。
promise
事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间前后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕以后,再执行下一个任务。执行栈则是一个相似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,若是不为空的话,事件队列便将第一个任务压入执行栈中运行。
浏览器
那么我将这个例子作一个小小的改动看一看:bash
console.log('start');
setTimeout(()=>{
console.log('A');
},0);
console.log('end');
//start
//end
//A复制代码
能够看出,咱们将settimeout第二个参数设置为0后,'A' 也老是会在 'end' 以后输出。因此究竟发生了什么?这是由于 setTimeout 的回调函数只是会被添加至事件队列,而不是当即执行。因为当前的任务没有执行结束,因此 setTimeout 任务不会执行,直到输出了 'end' 以后,当前任务执行完毕,执行栈为空,这时事件队列才会把 setTimeout 回调函数压入执行栈执行。
多线程
所谓Promise
,简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。从语法上说,Promise 是一个对象,从它能够获取异步操做的消息。Promise 提供统一的 API,各类异步操做均可以用一样的方法进行处理。
异步
写一个小demo看一下Promise的运行机制:函数
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved复制代码
上面代码中,Promise 新建后当即执行,因此首先输出的是Promise
。而后,then
方法指定的回调函数,将在当前脚本全部同步任务执行完才会执行,因此resolved
最后输出。
Macrotasks和Microtasks 都属于上述的异步任务中的一种,他们分别有以下API:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver
setTimeout的macrotask, 和 Promise的microtask 有哪些不一样,先来看下代码以下:
console.log(1);
setTimeout(function(){
console.log(2);
}, 0);
Promise.resolve().then(function(){
console.log(3);
}).then(function(){
console.log(4);
});
//1
//3
//4
//2复制代码
如上代码能够看到,Promise的函数代码的异步任务会优先于setTimeout的延时为0的任务先执行。
缘由是任务队列分为 macrotasks 和 microtasks, 而promise中的then方法的函数会被推入到microtasks队列中,而setTimeout函数会被推入到macrotasks
任务队列中,在每一次事件循环中,macrotask只会提取一个执行,而microtask会一直提取,直到microsoft队列为空为止。
也就是说若是某个microtask任务被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。
而事件循环每次只会入栈一个macrotask,主线程执行完成该任务后又会检查microtasks队列并完成里面的全部任务后再执行macrotask的任务。
setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');复制代码
一、首先 setTimeout A 被加入到事件队列中 ==> 此时macrotasks中有[‘A’];
二、obj.func()执行时,setTimeout B 被加入到事件队列中 ==> 此时macrotasks中有[‘A’,‘B’];
三、接着return一个Promise对象,Promise 新建后当即执行 执行console.log('C'); 控制台首次打印‘C’;
四、而后,then
方法指定的回调函数,被加入到microtasks队列,将在当前脚本全部同步任务执行完才会执行。 ==> 此时microtasks中有[‘D’];
五、而后继续执行当前脚本的同步任务,故控制台第二次输出‘E’;
六、此时全部同步任务执行完毕,如上所述先检查microtasks队列,完成其中全部任务,故控制台第三次输出‘D’;
七、最后再执行macrotask的任务,而且按照入队列的时间顺序,控制台第四次输出‘A’,控制台第五次输出‘B’。
分析与实际符合,NICE!
参考文章:www.cnblogs.com/tugenhua070…
还有阮老师的promise介绍:es6.ruanyifeng.com/?search=pro…
文章本人原创,转载请评论;
前端菜鸟对JavaScript的理解还有不少不足,若有错误欢迎你们指出来;
喜欢的点个赞把!