前言
- JavaScript是单线程语言,一个时刻只能
执行一段代码。打个比方:去银行办理业务(小银行,只有一个柜台),一个柜台的队员一次只能处理一个人的业务; - 同时JavaScript语言又存在异步任务,上面的代码还没执行完成,下面的代码就
已经开始执行了。打个比方:就是说一个人还没完全办理完业务时,另外其他人已经准备办理业务了,但是必须得等前面的人的业务办理完成了才能上; - 如果此时出现一个情况:有一段代码
执行时间特别长(ajax请求后台数据),就好比有一个人的业务的处理需要的时间特别长(或者业务特别多),那么后面的人越积越多,就容易造成“拥塞”的现象,而且引发混乱的结果; - 如何提高效率呢:① 现实中我们会另外派多几个人专门去处理那些业务较多、办理时间较长的人,处理完成后直接在总台消去这个人的号就行。在JavaScript中对应的就是
宏任务和微任务;而业务少的、办理时间短的人就按序排队在柜台办理就行,在JavaScript中对应的就是主线程的同步任务; - 如何处理混乱呢:现实中我们是采用“取号排队”来解决这个问题的,万事讲究个“先来后到”嘛。在JavaScript中也是一样,采用了“
先进先出”的任务队列模式 —— event loop;
在 ① 中提到多派几个“人”,其实对应到浏览器中就是多建几个独立的线程。浏览器是多进程的,其中的渲染进程是核心,这个进程是多线程的:包括GUI线程、JS引擎线程、定时器线程、事件触发线程、异步请求线程。其中后三个可以理解为是多派出的那几个“人”
执行上下文栈
我们通过例子来说明 执行上下文栈
function fn1(){
console.log(2)
console.log(3)
}
function fn2(){
console.log(1)
fn1()
console.log(4)
}
fn2();
执行栈 执行栈 执行栈 执行栈 执行栈
| | | | | | | | | |
| | | console.log(1) | | fn1() | | | | console.log(4) |
| fn2() | | fn2() | | fn2() | | fn2() | | fn2() |
------------------ ------------------ ------------------ ------------------ ------------------
1. fn2()入栈
2. console.log(1)入栈执行后出栈
3. fn1()入栈
4. fn1()里的代码全部执行完后出栈
5. console.log(4)入栈执行后出栈
6. 最终栈空,表示`主线程的同步任务`执行完毕
同步任务和异步任务
我们知道浏览器的渲染进程包括五个线程:(1)GUI渲染线程;(2)JS引擎线程;(3)事件触发线程;(4)定时器线程;(5)异步的http网络请求线程
而 JS 是单线程语言,所以如果存在运行时间比较长的任务(如:定时器、DOM事件、异步请求等),如果把这些任务视为同步任务,那将会造成很严重的“阻塞现象”,所以浏览器把这些任务归为异步任务,交给其他线程(如:定时器线程、事件处理线程、异步请求线程等)去做,最后只需要把最终结果告诉 JS 引擎线程就行
- 同步任务就好比是日常生活的刷牙、洗脸、吃饭
- 异步任务就好比是日常家居里的烧水炉、洗衣机,我们在刷牙、洗脸、吃饭的时候它们也可以同时在工作,而我们只要它们工作完的最终结果
- 同步任务是指在
主线程上执行的任务,同步任务会在调用栈中按照顺序等待主线程一个一个的执行,同一时刻只能执行一个任务,当前任务完成了才继续下个任务。
打个比方:上学时候起床,刷牙、洗脸、吃早饭,你不可能边刷牙边吃早饭吧,肯定得刷完牙后再去吃早饭
同步任务包括:
- DOM 元素渲染
- DOM 节点操作
- 异步任务是指主线程以外,进入
任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
打个比方:起床后你需要煮水备用,这时你可以开启炉子烧水后去吃早饭,不用傻傻的做在那等水烧开后才去吃早饭
异步任务包括:
- 各种DOM 事件(如:onclick、onsubmit...等等)
- setTimeout、setInterval 定时器
- Ajax
- Promise
- async
- 两者关联:主线程外的线程会在异步任务有了结果后,将注册的
回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行
打个比方:你吃完早饭后,炉子发出滴滴声,你就知道水烧开了,你就可以用水壶把水装起来了
异步任务又分为:宏任务和微任务
通俗理解:
去银行办理业务,得排队取号,念到号的人上前去办理业务。由于办理业务需要一些时间,所以对于柜员来说,前来办理业务的客户就是柜员的宏任务,由于当前柜台只有一个,所以必须等当前办理业务的客户是业务完成后才能让下一个人上。
一个宏任务在执行的过程中,是可以添加一些微任务的。就好比:如果这位客户除了想办理存款这个主业务外,还想购买一些理财产品、办一张信用卡,这些就属于微任务。而下一个客户必须得等上一个客户的全部业务办完才能这样上。也就是说,一个宏任务的执行可能会伴随着微任务的诞生。而我们需要把上一个宏任务所产生的所有微任务全部执行完,才会去执行下一个宏任务。
- 常见的宏任务:
setTimeout、setInterval、各种DOM 事件(如:onclick、onsubmit等)、Ajax - 常见的微任务:
Promise.then.catch、async、process.nextTick、MutationObserver
为什么要多弄一个微任务的队列呢,参考 JS为什么要区分微任务和宏任务?得出这样一个猜测:
可能是为了在下一个宏任务到来之前要(插队)提前做一些必要的任务。比如宏任务队列里面有多个定时器执行后的结果,此时宏任务队列输出一个值了可能就不需要后面的定时器去执行了,所以在上一个宏任务执行的过程中用Pormise...try...引出微任务去执行clearTimeout()清除后面的定时器。
宏任务和微任务的根本区别
- 宏任务是
浏览器规定的;(我的理解是就像定时器线程是浏览器的其中一个线程一样) - 微任务是
ES6语法规定的;(我的理解是因为JS代码里存在Promise和async)
浏览器中的 event loop
event loop大体由三个部分组成:执行栈、宏任务队列、微任务队列。这三个部分在event loop中非常重要
event loop 的运行机制
- 所有
同步任务都在JS线程上执行,形成一个执行栈 - JS线程之外,还存在定时器线程、事件处理线程、异步请求线程,它们负责处理
异步任务,只要异步任务有了结果,就会在宏任务队列或微任务队列中放置一个执行(任务完成)后的结果 - 一旦执行栈中的所有同步任务执行完毕,系统就会先执行微任务队列里的任务,然后执行宏任务队列里的一个宏任务,期间有可能会引发出新的微任务,所以会再次指向微任务队列里的任务,然后再回来执行下一个宏任务。这就有个 Loop 的过程。
我们先看 只有宏任务 的情况,再看 有宏任务和微任务 的情况
只有宏任务
function fn(){
console.log(1)
setTimeout(function(){
console.log(3)
},0)
console.log(2)
}
fn();
执行栈 执行栈 执行栈 执行栈 执行栈
| | | | | | | | | |
| | | console.log(1) | | console.log(2) | | | | |
| fn() | | fn() | | fn() | | fn() | | |
----------------- ------------------ ----------------- ----------------- -----------------
宏任务队列 宏任务队列 宏任务队列 宏任务队列 宏任务队列
------------------ ------------------ ------------------ ------------------ ------------------
`定时器正在计时` console.log(3)
------------------ ------------------ ------------------ ------------------ ------------------
1. fn()入栈
2. console.log(1)入栈执行后出栈
3. 遇到`setTimeout`,主线程将任务下发给定时器线程。该线程完成计时后把结果放在`宏任务队列`里面
4. console.log(2)入栈执行后出栈
5. 栈空,表示主线程的同步任务执行完毕,开始执行宏任务队列里的`任务结果`
6. console.log(3)出队,入栈执行后出栈
这就是setTimeout()有时会出现“延时”的原因
再看一个经典例子(这道题涉及了异步、作用域、闭包)
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
} // 输出: 3 3 3
这里输出3 3 3的原因是:3次循环里依次创建了3个定时器,各个定时器各自计时,3个执行结果按序排在宏任务队列里,等到执行栈空时依次执行console.log(对应的i),因为退出循环时i=3,而且由于var和let之间有无块级作用域的区别:
{
var i = 3
}
{
console.log(i) // 会被影响到
}
{
let i = 3
}
{
console.log(i) // 不会被影响到
}
导致到最后执行宏任务队列里的任务时i的值会不会被影响到,显然用var声明的变量会影响到后面的输出
既有宏任务,又有微任务
let p = new Promise(function(resolve){
console.log(1)
resolve()
})
function fn(){
p.then(funtion(){
console.log(4)
})
console.log(2)
setTimeout(function(){
console.log(5)
},0)
console.log(3)
}
fn();
执行栈 执行栈 执行栈 执行栈 执行栈
| | | | | | | | | |
| | | console.log(2) | | console.log(3) | | | | |
| console.log(1) | | fn() | | fn() | | fn() | | |
------------------ ------------------ ------------------ ------------------ ------------------
宏任务队列 宏任务队列 宏任务队列 宏任务队列 宏任务队列
------------------ ------------------ ------------------ ------------------ ------------------
`定时器线程在执行中` console.log(5)
------------------ ------------------ ------------------ ------------------ ------------------
微任务队列 微任务队列 微任务队列 微任务队列 微任务队列
------------------ ------------------ ------------------ ------------------ ------------------
`外部线程执行then` console.log(4)
------------------ ------------------ ------------------ ------------------ ------------------
1. 创建一个Promise对象p,其中执行了console.log(1)(入栈执行后出栈),然后Promise状态变为resolved
2. 进入fn(),首先遇到Promise对象的then()方法,将其放入微任务队列执行(执行结果是console。log(4))
3. console.log(2)入栈执行后出栈
4. 遇到setTimeout,将其放入宏任务队列执行(执行结果是console。log(5))
5. console.log(3)入栈执行后出栈,
6. 栈空,先将微任务队列的所有执行结果按序出队后,入栈执行后出栈
7. 再将宏任务队列里的一个宏任务的执行结果按序出队后,入栈执行后出栈。`此时宏任务可能又会产生微任务`
8. 所以执行一个宏任务后要回去检查一下有无微任务,有则要将微任务队列的全部结果执行完才继续处理下一个宏任务
【所以才会有一个 `loop` 的过程】
event loop 什么时候开始触发?
- 当同步代码执行完成,call stack清空之后
- 执行当前的微任务
- 再
尝试 DOM 渲染 - 最后触发
event loop 机制
为什么微任务比宏任务执行的早?
- 微任务:DOM渲染前触发,如Promise
- 宏任务:DOM渲染后触发,如setTimeOut
思考题巩固
例子 1
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);
1. 首先new Promise执行,resolve(1)表示创建的promise对象的`状态变为resolved`
2. Promise.resolve()相当于创建了一个promise对象,then里面的`匿名回调函数进入微任务队列`
此时的微任务队列是[() => console.log(2)]
3. 输出 4
4. new Promise的then函数里面的`匿名回调函数进入微任务队列`
此时的微任务队列是[() => console.log(2), t => console.log(t)]
5. 输出 3
所以,最后输出的顺序是 4 3 2 1
例子 2
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(()=> {
console.log('setTimeout');
}, 100);
async1();
var p = new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
看到 async / await 不必紧张,这里有个秘诀:
- async 表示函数里有异步操作,await 之前的代码该怎么执行怎么执行,await 右侧表达式照常执行,后面的代码被阻塞掉,等待 await 的返回
- 返回是非 promise 对象时,执行后面的代码;
- 返回的是 promise 对象时,等promise对象resolved时再执行。所以可以理解成后面的代码放到了promise.then 里面 对这块不是很熟的同学可以看看这篇文章:JavaScript系列 -- Promise、Generator、async及await
- 输出 script start
- 浏览器会在 100 ms 之后把setTimeout里面的匿名回调函数丢进宏任务队列 (请记得丢进任务队列里的是回调函数,函数!)
宏任务队列:[() => console.log('setTimeout')]
- 输出 async1 start
- 输出 async2
- 要输出 async1 end 代码被丢进微任务队列
微任务队列:[() => console.log('async1 end')]
- 输出promise1
- promise 对象状态变为 resolved
- promise.then 里的匿名函数进入微任务队列,此时的微任务队列为['async1 end', 'promise2']
微任务队列:[() => console.log('async1 end'),() => console.log('promise2')]
- 输出 script end
- 执行栈空
- 输出 async1 end
- 输出 promise2
- 微任务队列为空
- 输出 setTimeout
值得注意的是:
- 我们在最开始 new Promise 的时候就已经执行 Promise 里面的箭头函数的代码了
- 而 async 函数 test() 只有在 test().then().catch() 的时候才被调用,test() 函数里面的代码才开始被执行
- await 后面如果跟的是 Promise 对象,则会等待该对象里面的函数执行完成;如果跟的不是 Promise 对象则会直接执行其后面的代码 / 直接等于后面的值,不会将其加入微任务队列
例子 3(new Promise 里面是同步任务,得走到 then() 才把里面的代码是放入微任务队列)
setTimeout(function(){
console.log('timeout');
})
new Promise(function(resolve){
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function(){
console.log('then');
})
console.log('global')
console.log('timeout1')放入宏任务队列console.log('promise1')先执行(new 里面的同步任务),所以打印出 1. promise1console.log('promise2')执行,打印出 2. promise2console.log('then1'),then()里面的都放入微任务队列console.log('global1')为同步任务,所以打印 3. global- 执行栈空,微任务队列出队,打印 4. then,此时微任务队列空
- 宏任务队列出队,打印 5.timeout,此时宏任务队列为空
例子 4(setTimeout 里面是一个整体,整块任务放入宏任务队列)
console.log("start")
setTimeout(()=>{
console.log("timer1")
Promise.resolve().then(function(){
console.log("promise1")
})
console.log("timer2")
},0)
setTimeout(()=>{
console.log("timer3")
Promise.resolve().then(function(){
console.log("promise2")
})
},0)
console.log("end")
把每一个 setTimeout 里面的所有代码看成是一块整体:
console.log("timer1")
Promise.resolve().then(function(){
console.log("promise1")
})
console.log("timer2")
所以在这一块整体里面的执行顺序为:1. timer1;2. timer2;3.promise1;
同理在另一个定时器里面的所有代码的执行顺序为:1.timer3;2.promise2
然后两个定时器里面的整体任务都放在宏任务队列里面
关于输出顺序,有规律可循:
- promise.then
里面、await下面的任务都是微任务,settimeout里面是宏任务- 先执行所有微任务再执行一个宏任务,再执行所有微任务,... ,有个 loop 过程
了解 event loop 机制后,那我们怎么实现异步编程呢?
JavaScript系列 -- Promise、Generator、async及await