首先来一段代码开篇
console.log(1);
setTimeout(function() {
console.log(2);
});
function fn() {
console.log(3);
setTimeout(function() {
console.log(4);
}, 2000);
}
new Promise(function(resolve, reject){
console.log(5);
resolve();
console.log(6);
}).then(function() {
console.log(7);
})
fn();
console.log(8);
思考一下, 能给出准确的输出顺序吗?
下面一步步的了解,最后看看这块代码怎么去执行的.
1. 进程,单线程与多线程
进程: 运行的程序就是一个进程,比如你正在运行的浏览器,它会有一个进程.
线程: 程序中独立运行的代码段.
一个进程由单个或多个线程组成,线程是负责执行代码的.
学过 JS 的想必都知道 JS 是单线程的,那么既然有单线程就有多线程,下面首先看看单线程与多线程的区别.
单线程 从头执行到尾,一行一行执行,如果其中一行代码报错,那么剩下代码将不再执行.同时容易代码阻塞.
多线程 代码运行的环境不同,各线程独立,互不影响,避免阻塞.
2. Event Loop(浏览器)
js 既然是单线程, 那么肯定是排队执行代码,那么怎么去排这个队,就是 Event Loop.虽然 JS 是单线程,但浏览器不是单线程.浏览器中分为以下几个线程:
js 线程
UI 线程
事件线程 (onclick,onchange,...)
定时器线程 (setTimeout, setInterval)
异步 http 线程 (ajax)
其中 JS 线程和 UI 线程相互互斥,也就是说,当 UI 线程在渲染的时候,JS 线程会挂起,等待 UI 线程完成,再执行 JS 线程
JS 会存在执行栈,从上至下执行 js 代码,当遇到异步 api 时, 列如上面所述的各种非 JS 线程的事件,那么会扔给对应的线程去处理,等处理完毕后,则把回调函数放入事件队列中,等待执行栈执行完毕,再去读取事件队列中的回调函数执行.
当一个函数执行,会产生一个新的执行栈,当执行完毕返回上一层执行栈, 直到回到全局执行栈
当一个函数调用自己,会产生一个新的执行栈.
整个过程,执行栈,读取事件队列就是 Event Loop
再来看看 promise, 如果对 promise 不是很了解的同学可以看看另一篇我写的文章 Promise 是个什么鬼?实现一个 Promise. Promise 在整个执行中是个特殊的存在,传入 Promise 的 fn 是在当前执行栈中的,会立即执行,但它的 then 方法是在执行栈之后,事件队列之前,当然这个和浏览器实现有关,大部分浏览器是微任务 (Microtask),也有浏览器放入了宏任务 (Macrotask),chorme 大哥是放入了微任务,其他纷纷效仿.那大家可能会问什么是微任务?什么是宏任务了?
宏任务 (Macrotask) 也就是上面所说的 事件队列 callback queue
微任务 (microtask) 是在执行栈和事件队列之间 在执行栈之后先清空在微任务中的任务,再去执行事件队列
3. Node Event Loop
Nodejs 是通过 V8 引擎去解析的,解析后的代码会去调用 node 提供的 api 执行,这些 API 由 libuv 这个库去分配线程执行,最后异步返回给 V8 引擎.
在 Node 中提供了 2 个方法和我们的执行队列有关
process.nextTick
把方法放入执行栈的底部,并不放入宏任务和微任务
cosnole.log(1);
process.nextTick(function(){
console.log(2);
});
new Promise(function() {
console.log(3);
}).then(function() {
console.log(4);
})
console.log(5);
因为 nextTick 是放入了执行栈的底部,那么会优先于 Promise 的 then 方法,故输出为 1 3 5 2 4
setImmediate
把方法放入宏任务的队列中去, 但有一个奇怪的事发生,看下面代码:
setImmediate(function() {
console,log(1);
});
setTimeout(function() {
console.log(2);
}, 0);
大家可以试试把代码多次执行,发现输出顺序不一定,他们都是放入了宏任务中,但在 node 文档中,setImmediate 总是排在 setTimeout 前面,但是在实际中确不一定,不知道是不是一个 bug.
4. 讲讲 setTimeout, setInterval
任务队列与定时器 上面讲到了定时器都是放入了宏任务.如果当前执行栈消耗时间已经大于我们设置的定时器时间,那么定时器的回调在宏任务里,并没有及时去调用,所有这个时间不是特别准确.
setTimeout(function(){
console.log(1);
}, 2000);
task();
假设 task 函数执行需要 5 秒钟,那么打印 1 需要在 5 秒之后再打印,task 占用了当前执行栈,要等执行栈执行完毕后再去读取微任务,等微任务完成,这个时候才会去读取宏任务里面的 setTimeout 回调函数执行.setInterval 同理,例如每 3 秒放入宏任务,也要等到执行栈的完成.
定时器自身 有时候为了延后执行代码会写:
setTimeout(function() {
console.log(1);
},0);
但是根据标准这个时候最低是 4 毫秒,即便现在执行栈已经完成.0 是不成立的.写 0 浏览器为默认为最低毫秒数.
5. 回到开篇的代码
现在再回到上面的代码,有答案了吗?
// 非异步api,立即执行
console.log(1);
// 放入全局宏任务
setTimeout(function() {
console.log(2);
});
// 声明函数,但暂时未调用,不会立马形成执行栈
function fn() {
// 调用fn时立即执行
console.log(3);
// 放入当前fn执行栈宏任务
setTimeout(function() {
console.log(4);
}, 2000);
}
new Promise(function(resolve, reject){
// task任务立即执行
console.log(5);
resolve();
console.log(6);
}).then(function() {
// then方法放入微任务
console.log(7);
})
// 调用fn进入下个执行栈
fn();
// fn执行栈完成执行
console.log(8);
答案就是 1 5 6 3 8 7 2 4
来源: https://juejin.im/post/5a634b606fb9a01c982cab18