目录
进程和线程
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
浏览器是多进程的
浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
JavaScript是单线程,同一个时间只能做一件事,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
因此为了解决异步的问题js用事件循环执行异步代码(https://segmentfault.com/a/1190000015806981)
理解Event Loop
Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
JavaScript代码的具体流程:
- 调用栈Stack中执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
event loop就是看如果调用栈为空则从消息队列中取任务到调用栈中执行:
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。(注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行);microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出依次宏队列macrotask queue中位于队首的任务,放入Stack中执行;执行完毕后,调用栈Stack为空;
规则
- 按先后顺序执行完所有同步任务;
- 再执行异步任务,先按先后顺序执行微任务,再按先后顺序执行宏任务:
- macro-task: script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering.
- micro-task: process.nextTick, Promise(原生),Object.observe,MutationObserver
(一个简便常用的记法:Promise > set…)
async和await搭配相当于Promise,async的函数内要有await才会实行异步,await紧跟的任务执行完,再执行所有同步任务,然后才到await下一行的任务(其实就是await下一行都是异步任务,所有肯定是先执行所有同步任务)
例题
例题1 顺丰面试题
(function test() {
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(null)
}).then(v => {
console.log(3)
})
console.log(4);
})()
- 同步任务按顺序先输出2和4
- 异步任务先输出微任务promise.then的3,再宏任务setTimeout的1
例题2 比较全面的检测
setTimeout(function() {
console.log("s1")
}, 0);
setTimeout(function() {
console.log("s2")
}, 1000);
new Promise(function(resolve){
console.log("p1");
resolve();
console.log("p2");
}).then(function(){
console.log("p3");
});
console.log("w1");
async function test1() {
console.log("a1");
await test2();
console.log("a2");
}
async function test2() {
console.log("a3");
}
test1();
console.log("w2")
- 同步任务按顺序,输出p1 p2 w1 a1 a3 w2
- 异步任务,先微任务按顺序,输出p3 a2,后宏任务,输出s1 s2
参考解析
例题3 来自《深入浅出Node.js》
//加入两个nextTick的回调函数
process.nextTick(function () {
console.log('n1');
});
process.nextTick(function () {
console.log('n2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
console.log('s1');
// 进入下次循环
process.nextTick(function () {
console.log('n3');
});
});
setImmediate(function () {
console.log('s2');
});
console.log('w');
- 同步任务按顺序,输出 w
- 异步任务,先微任务输出 n1 n2,再宏任务输出 s1 s2,最后输出 n3
在新版的Node中,process.nextTick执行完后,会循环遍历setImmediate,将setImmediate都执行完毕后再跳出循环。所以两个setImmediate执行完后队列里只剩下第一个setImmediate里的process.nextTick。最后输出”n3”。(这里暂时还没弄懂为啥最后才到嵌在宏任务中的微任务)
例题4 Promise的异步
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});
- 执行第一个异步任务输出1,返回2
- 因为是resolve函数,所以跳过catch,执行第二个then,输出2
例题4 setTimeout的时间变化
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});
const start = Date.now();
console.log("w1")
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});
- 先执行同步任务,Promise的executor函数,内部是异步任务setTimeout,先挂起;
start赋值为当前时间;
再输出w1 - 执行异步任务,虽然Promise.then是微任务优先于setTimeout,但是没有调用它就不会执行,所以先执行setTimeout,5s后输出“开始”;
再调用resolve,执行第一个then,Date.now()是5s后的时间,输出success 5001;
执行第二个then,输出success 5001
例题5 宏任务和微任务互相穿插
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9);
})
console.log(10);
- 先同步任务,输出 1 4 10
- 异步任务,
先执行微任务队列,Promise.then输出5 6 7,此时里面的setTimeout是宏任务,挂到宏任务队列尾部;
再执行宏任务队列,第一个setTimeout输出2,里面的Promise是微任务,挂到微任务队列后直接执行,输出3;第二个setTimeout输出9;最后一个是Promise中的setTimeout,输出8