由于接触JavaScript断断续续也有挺长段时间,从以前做Web开发开始,到后来做移动端解除到ReactNative,NodeJS,对这门语言一直在边做边学,边学边深入,所以还是想记录一些自己的所学所得吧,也当做是自己的总结,因为JavaScript这门语言老实话确实不太好掌握,细节还是挺多的,而且一直在更新,咋们今天还是花点时间聊聊JavaScript里面的Promise,await ,微任务,宏任务的内容吧,为了我下面要写flutter的 Future ,事件队列,微任务的内容来做一个铺垫和对比,好的废话不多说了,我们开始吧:
让我们先从概念开始了解下这两个任务是什么:
首先JavaScript的任务分为宏任务(macrotask),微任务(microtask)两种
macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每一个task会从头到尾将这个任务执行完毕,不会执行其它,浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
也就是说,在当前task任务后,下一个task之前,在渲染之前,所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染,也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
分别很么样的场景会形成macrotask和microtask呢?
macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
microtask:Promise,process.nextTick等
OK了解了这两个概念以后,我们再来看一下事件队列的执行机制,如下图(借鉴拿来的图):
稍微解释下上面图的运行流程(宏任务,微任务的定义看上面):
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
了解了上面几个概念以后(如果没有理解的请仔细阅读),我们来几个例子来检验下“真理”,首先我们先看看promise的例子:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
resolve(true)
}).then(function() {
console.log('then');
})
console.log('console');
// promise
// console
// then
// setTimeou
首先会遇到setTimeout,将其放到下一个宏任务(相对于主任务的宏任务)event queue里面,然后回到 promise , new promise 会立即执行, then会分发到微任务,遇到 console 打印立即执行,整体宏任务执行完成,接下来判断是否有微任务,刚刚放到微任务里面的then,执行then打印,ok第一轮宏任务事件结束,进行第二轮宏任务,刚刚我们放在event queue 的setTimeout 函数进入到宏任务,立即执行
注意这里的 new promise 里面的同步代码可以理解为宏任务的代码所以可以立即执行(这里是要注意的)
为什么上面我们说的Promise调用then方法会分发到微任务呢,我们来看看下面这个例子:
function sleep(second) {
return new Promise((resolve, reject) => {
resolve(5);
})
}
console.log("主线程1")
var p = sleep(0);
Promise.resolve(12345).then(val => console.log(val));
p.then(val1 => console.log(val1));
console.log("主线程2")
//主线程1
//主线程2
//12345
//5
稍微解读一下,首先打印主线程宏任务的句子“主线程1”,然后进入到sleep方法里面返回的是一个fullfilled状态的Promise赋值给变量p,然后Promise打印12345进入微队列,p再调用then方法进入微队列打印5,按照微队列的执行顺序来说先加入微队列的先执行,所以p调用then是后加入微队列的所以最后打印,这也说明了Promise是调用then方法才加入到微队列的,而不是在sleep函数里面调用resolve方法的时候加入微队列的
上面还算是比较好理解的,好的,再来一个复杂的:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
分析下顺序:
首先先执行console.log(1) 然后将setTimeout放到宏任务event queue里面 记作 setTimeout 1 ,接着 看到 process.nextTick ,将其放到微任务里面 ,记作 process 1,
然后 看到new promise 立即执行输出7,将里面的then 放到 微任务里面 记作 then 2, 继续,遇到 setTimeout 放到宏任务里面记作 setTimeout 2 。目前输出的是:1,7,
OK, 接下来,开始判断是否有微任务,刚刚放入到微任务event queue的进入到主程序开始执行,process 1 , then 2 目前输出的是:6,8、
接下来,微任务的event queue 空了,进行下一轮事件,将刚刚放到宏任务的 setTimeout 1 进入到主线程遇到 console 立即执行, 遇到 process.nextTick 放到微任务 event queue 里面 记作 process1, 接着遇到 new Promise 立即执行, 将 then 放到event queue 里面 记作 then 2,OK,当前宏任务里的任务执行完了,判断是否有微任务,发现有 process1, then 2 两个微任务 , 一次执行 目前输出的是:2,4,3,5、
目前主线程里的任务都执行结束了,又开始第三轮事件循环,同上(字太多,省略。。。。) 目前输出的是:9,11,10,12、
好的,与Promise,setTimeout相关的宏任务,微任务大家应该掌握得差不多了,下面继续解说结合await的时候会是什么样子的呢?
我们先来复习下基础:
若 async 定义的函数有返回值,return 123;相当于Promise.resolve(123)
async function demo01() {
return 123;
}
demo01().then(val => {
console.log(val);// 123
});
没有声明式的 return则相当于执行了Promise.resolve();
async function demo01() {
console.log("withOut return")
}
demo01().then(val => {
console.log(val);// undefined
});
await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用,下面是有问题的调用:
function notAsyncFunc() {
await Math.random();
}
notAsyncFunc();//Uncaught SyntaxError: Unexpected identifier
await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。
如果 await 的是 promise对象,await 会暂停 async 函数内后面的代码,先执行 async 函数外的同步代码(注意,promise 内的同步代码会先执行),等着 Promise 对象 fulfilled(这点非常重要),然后把 resolve 的参数作为 await 表达式的运算结果返回后,再继续执行 async 函数内后面的代码,我个人理解这种情况下await函数就类似与Promise.then()函数作用,只有Promise状态变成了fulfilled的话then函数才会执行
下面是我的个人理解:
我的个人理解await后面的代码或者表达式只要遇到promise ,以后的代码可以看成是一个微队列来执行,类似await前面的代码可以看做是Promise的同步宏任务执行,遇到resolve 代码(async 函数中不指定返回值,则会默认返回resolve(undefined))就变成了一个微队列,此时微队列应该是在主宏队列后面执行的,所以await后面的代码会被“阻塞”(await以后的代码可以看成是一个微队列来执行,因为是微队列不会立马执行,转而执行主宏队列的任务),而await后面的代码类似于Promise后面的then()函数执行的代码,只有在Promise变成fulfilled状态才会继续执行
先来看一个例子:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sleep');
}, second);
})
}
async function awaitDemo() {
let result = await sleep(0);
console.log(result?result:0);
}
console.log("主线程1")
Promise.resolve(123).then(res => console.log(res))
awaitDemo();
console.log("主线程2")
//主线程1
//主线程2
//123
//sleep
执行顺序:
先打印”主线程1” ,然后遇到Promise.then()放到微队列任务里面,然后到await sleep(0); 进入到sleep函数里面,
然后里面遇到promise的setTimeout放到宏队列里面,await 会暂停 async 函数内后面的代码(等待Promise调用resolve方法再执行),先执行 async 函数外的同步代码打印”主线程2” ,
宏队列里面的主任务执行完了,然后执行微队列里面打印123,然后执行下一个宏队列里面的任务resolve('sleep');
然后promise状态变为fullfilled,await后面的代码开始执行打印sleep
如果你把setTimeout函数里面的resolve('sleep’);去掉的话,那么await函数会一直等待,后面的代码永远不会执行,大家可以试一下
我们在这个例子改变些代码再来观察下:
function sleep(second) {
return Promise.resolve(111);
}
async function awaitDemo() {
let result = await sleep(0);
console.log(result?result:0);
}
console.log("主线程1")
awaitDemo();
Promise.resolve(123).then(res => console.log(res))
console.log("主线程2")
//主线程1
//主线程2
//111
//123
改变的是sleep函数里面的内容,以及awaitDemo与后面代码的执顺序
我来说一下区别:执行sleep的时候遇到Promise.resolve(111); 进入微队列1,再执行Promise.resolve(123).then(res => console.log(res)) 在进入微队列2,所以主队列宏任务执行完毕后执行微队列按照顺序应该是执行微队列打印111,再打印222
再来一个小的变化:
function sleep(second) {
return Promise.resolve(111).then(res => res);
}
async function awaitDemo() {
let result = await sleep(0);
console.log(result?result:0);
}
console.log("主线程1")
awaitDemo();
Promise.resolve(123).then(res => console.log(res))
console.log("主线程2")
// 主线程1
//主线程2
//123
//111
这次的改变仅仅是sleep函数里面的内容,resolve以后在then函数里面返回了res值,
注意:then方法会返回一个新的Promise实例。因此可以采用链式写法,then方法中的回调函数(不管是成功的回调还是失败的回调)返回了一个参数(return xxx),那么这个then方法返回的新的promise的状态会变成fulfilled
为什么这次改变以后打印111会在打印222的后面呢,大家细想一下就知道了,resolve(111).then 进入了第一个微任务,这个微任务在执行的时候又返回一个promise(then(res => res)),这个又是一个微任务记做3,这个时候不会立马执行,等待微任务2执行完毕才能执行,所以打印的顺序又不同了,大家可以细细理解下这里
上面讲解了await等待的是一个Promise的情况,下面讲解如果等待的不是一个Promise会是怎么样呢?
如果 await 的不是一个 promise ,而是一个表达式。await 会暂停 async 函数内后面的代码执行,先执行 async 函数外的同步代码(注意,此时会先执行完 await 后面的表达式后再执行 async 函数外的同步代码)
我的个人理解这种情况下await后面的代码或者表达式只要遇到return ,或者运行结束的时候(函数中不指定返回值,则会默认return返回undefined)await以后的代码可以看成是一个微队列来执行,等待下一个微队列执行的时机再执行await后面的代码
再来看一个例子:
function sleep(second) {
return 100;
}
async function awaitDemo() {
let result = await sleep(0);
console.log(result? result:0);
}
console.log("主线程1")
setTimeout(() => {
console.log('sleep');
}, 0);
Promise.resolve(123).then(res => console.log(res))
awaitDemo();
console.log("主线程2")
//主线程1
//主线程2
//123
//100
//sleep
执行顺序是这样的:
先打印的是主任务的宏队列 ”主线程1” ,
然后遇到setTimeout函数放入下一个宏队列,
然后遇到 Promise.resolve(123).then 放入到改宏队列对应的微队列里面然后执行await sleep(0);
进入到sleep函数,函数里面return 100返回,这个时候return 100 这句话变成如下:
Promise.resolve(100);
await以后的代码可以看成是一个微队列来执行(这个是我个人比较深刻的理解,很多网络上面的文章其实都没说清楚这一块) ,await函数必须等待Promise返回fullfilled状态才会 往下执行(可以看上面的解释),所以接下来不会执行await后面的代码,微任务的代码不会立马执行,转而继续执行宏任务的代码
然后执行主任务宏队列的打印”主线程2” ,然后执行微队列任务打印”123” , 执行下一个微队列任务return 100 返回给await函数执行下面的打印“100”,
然后执行另一个宏任务打印“sleep”
好了上面的几个例子已经比较好的说明了《promise await 宏任务 微任务》这个主题的内容了,如果你还没明白的建议你多看几遍,有什么问题可以给我留言,这个只是为了给接下来flutter的内容做一个对比的铺垫,希望大家继续关注,谢谢···