JS 是单线程执行的。当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
那么JS是单线程,为什么会有异步呢?
众所周知,单线程执行就要排队,自上而下执行,可某一行解析时间过长,那么下面的代码就会被阻塞。
所以JS需要有异步,通过Event Loop来实现异步处理,来看一个经典的问题
console.log('1')
setTimeout(() => {
console.log('2')
},0)
console.log('3')
// 打印 1, 3, 2
这里说明 setTimeout 并不是立即执行,而是延迟了一段时间后再执行,在JS里这类代码,我们称之为异步代码(其实还是一个同步的过程,后面会解释)
1、执行栈概念
执行JS代码的时候,就是把一个一个的函数放进执行栈中,先进后出的概念,执行完毕就从栈中弹出。
function a() {
const name = 'Moxuanya';
return b(name);
};
function b(str) {
return str;
};
console.log(a());
放入执行栈的顺序:console.log(a()) → a() → b()
弹出执行栈的顺序:b() → a() → console.log(a())
注意:栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题。
2、Event Loop(事件循环机制)
上面已经说到执行JS其实就是往执行栈中放入函数,遇到异步的时候怎么办呢?
当遇到异步的代码时,会被挂起等待,并加入到Task队列中(microtask 和 macrotask,后面会深入讲到),当执行栈为空时,Event Loop就会把Task队列中的代码加入到执行栈中执行。所以JS的异步本质上,还是同步。
digui(n) {
if (n < 2) {
return n;
}
return this.digui(n - 2) + this.digui(n - 1);
},
text() {
console.log('s1', Date.now() / 1000);
setTimeout(() => {
console.log('s2', Date.now() / 1000);
}, 1000);
this.digui(39); // 一个需要大约3秒执行完毕
console.log('s3', Date.now() / 1000);
this.digui(39); // 一个需要大约3秒执行完毕
console.log('s4', Date.now() / 1000);
},
// s1 1563873581.294
// s3 1563873584.106
// s4 1563873586.687
// s2 1563873586.69
函数执行,遇到setTimeout 放入Task 队列中,执行栈的任务全部执行完毕大约6S 后打印出了s2。
重点来了!!!不管挂载到Task 队列等待的代码具体方法是什么,一定会等待执行栈中的主任务执行完毕
3、任务队列的深层原理:microtask(微任务)和 macrotask(宏任务)
这是我更改过的一道经典题,能答出正确打印顺序吗
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout1')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
new Promise(resolve => {
resolve()
})
.then(function() {
console.log('promise2')
})
setTimeout(function() {
console.log('setTimeout2')
}, 0)
console.log('script end')
// script start -> async2 end -> Promise -> script end -> async1 end -> promise1 -> promise2 -> setTimeout1 -> setTimeout2
常见的微任务和宏任务:
微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。
用我的理解解答一下,script start → async2 end → Promise → script end,这些任务是主任务,直接放到执行栈中执行,其他的任务则被挂载到Task 队列中,Task 队列区分是宏任务还是微任务,分别放入microtask队列和 macrotask队列,→ async1 end(这一步需要注意,在Chrome7.0以上await 提前了,当作一个new Promise处理),→ promise1→ promise2 执行微任务队列,→ setTimeout1→ setTimeout2,执行宏任务队列。那么往后延申,再执行宏任务的时候,又会有一些任务被挂起,
写到最后,我的结论先执行一遍宏任务队列(主任务),遇到异步被挂起,宏任务执行完毕,立马执行之前挂起的微任务,等到微任务执行完毕,又往执行栈中放入之前被挂起的宏任务,此时又遇到异步,则又会被挂起,如此循环。博主学艺不精,还望海涵。