第一次写博客,文笔有点烂,也不知道要说些什么,各位见谅一下吧😋
1. js执行机制
1.1 小谈一下
js是一门单线程语言,执行程序就像在食堂吃饭排队,必须一个一个来。这样看上去充满秩序,但在面对像定时器这样需要等待处理的任务时就会卡住,这就好比你在做饭时需要煮饭洗菜炒菜,而你必须等煮完饭才能去洗菜炒菜,这期间你什么都干不了。这样相当浪费时间,因此js将任务分为了同步和异步两种类型。
1.2 同步
所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
举个小例子:你是一名CCNU的学生。有一天,你想改善伙食,但无奈捉襟见肘。于是你给你家人打了个电话,只要接通,你就有money拿,但对方没有接通。你不停地打电话,因为没接通就没有生活费,没有生活费就没有豪赤的。最后一天过去了,电话没打通,你只能吃6块钱的营养套餐😡。
由此可见,同步的好处在于能够马上得到返回的结果,但必须一个接一个的执行。一旦中间哪步少了或者出错,程序就会卡住,白白浪费时间。
1.3 异步
异步,与同步相对,当一个异步过程调用发出后,在没有得到结果之前,可以继续执行后续操作。
有一个小例子:你是一名CCNU的学生。有一天,你想改善伙食,但无奈捉襟见肘。于是你给你家人发了一条短信,告诉他们你没米了,让他们速速转钱。虽然他们当时在忙没看到,但到了饭点,你还是收到了转账。最后,你happy地吃上了8块钱地营养套餐😋。
不难看出,异步虽然返回结果慢一些,但它能够在等待的同时执行后续操作,避免了时间的浪费。
在执行过程中,js会先执行同步代码,待其全部完毕后,再执行异步代码。
看上去好像很对,实际上不对。正如人与人不能一概而论,异步任务之间亦有差别。有些异步任务需要连贯执行,不能分开。
想象一下,你在超市买了很多东西,结果在结账时只接了一件,就被别人插队了,还要你重新去排,这不红温😡?为了解决这个问题,异步任务又被分为了宏任务和微任务。
1.4 宏任务
很好理解,宏任务指的就是不需要连贯执行的任务,例如setTimeout。
它就像你的寒假作业,你可以做一天,躺一天,只要能做完就行(虽然我一般不做)。
1.5 微任务
相对的,微任务指的就是需要连贯执行的任务,例如Promise。
它就像你的期末考试,不能考一天,躺一天,你必须连着把它考完,而且期考都在寒假作业前结束。
因此,在js实际执行过程中,js会先执行同步代码,遇到异步宏任务则将其放入宏任务队列中,遇到异步微任务则将其放入微任务队列中,此时二者不分优先级。待所有同步代码执行完,再将异步微任务从队列中调入主线程执行。待微任务执行完后,再将异步宏任务从队列中调入主线程执行。
2. Promise
刚才好像提到一下Promise,不知道该说点什么,就讲一下Promise吧😊
2.1 回调地狱
在这之前,先讲一下回调函数。当一个函数被当做一个实参传入到另一个函数,并且这个函数在外部函数内被调用,这个被用来完成某些任务的函数,就是回调函数。当一个回调函数嵌套一个回调函数就形成了一个嵌套结构,而嵌套多了就形成了回调地狱
1 | function(我是1){ |
非常好回调,使我的大脑旋转。
为了不让程序员在面对这串代码时一脸懵逼,ES6提出了一种新的解决方案,那就是promise。
2.2 Promise的参数和状态
Promise诞生的目的就是更优雅地书写异步任务 (优雅永不过时)。它是一个构造函数,因此在调用时可以实例化。它接收函数类型的参数,这个参数又接受两个参数,一个是resolve,一个是reject,分别接受成功和失败时的回调。在Promise的内部又有三种状态(state),分别是pending(原始态),fulfilled(成功态)和rejected(失败态)。其中状态只能由pending转为fulfilled或rejected,fulfilled和rejected既不能变回pending,也不能相互转换。
举个例子:new Promise是薛定谔的猫,猫活着就是resolve,寄了就是reject。其中pending是既活又寄的叠加态,打开容器后,猫既可以活着变为fulfilled,也可以寄了变为rejected,但是猫的状态已经确定,你不能用两级反转再把猫变回“寄-活叠加态”,也不能用死者苏生从墓地复活猫或是把哈基米变成哈寄米😡。
2.3 Promise的实例方法
Promise实例对象有成功的结果(value)和失败的结果(reason),如果外部代码想要访问这个成功或失败的值,就需要用到Promise的实例方法了。
2.3.1 then
then是Promise的一个实例方法,它能接受最多两个参数—onFulfilled和onRejected,通过它们我们可以拿到成功或失败时的结果,同时返回一个新的Promise实例对象。因为又会返回一个实例对象,因此then支持链式调用。
1 | let promise = new Promise((resolve,reject) => { |
可以看到then的执行依赖于上一步的结果,如果其中任意一步出错就会使整个程序出错。
一般来说,我们在执行程序时都会希望他是成功的,因此一步一个reject不仅写起来麻烦,而且看起来也比较麻烦。为了省略reject,我们就用到了Promise的另一种方法。
2.3.2 catch
catch用于在Promise失败或抛出错误时执行某些操作,它只接受一个失败的回调函数作为参数,并返回一个Promise对象。与then相同,catch同样支持链式调用。
1 | let promise = new Promise((resolve,reject) => { |
到这里,我们已经可以用then处理正确的结果,用catch处理错误或异常的结果。但实际上,有些操作无论结果正确或是失败都得执行。为此,我们又用到了finally方法。
2.3.3 finally
finally会在Promise状态决定时返回一个新的Promise对象,而不在意当前Promise的状态如何。
1 | let promise = new Promise((resolve,reject) => { |
总结一下这三位
1 | let promise = new Promise((resolve,reject) => { |
2.4 Promise静态方法
2.4.1 Promise.all
Promise.all接受一个 Promise 可迭代对象作为输入(一般是数组),并返回一个 新的promise对象。当所有输入的 Promise 都被成功时,返回的 Promise 也将被成功(空的也行),并返回一个包含所有成功结果的数组;如果输入的任意一个 Promise 失败,则返回的 Promise 也将失败,并带有第一个失败的原因。
1 | const promise1 = Promise.resolve(6) |
2.4.2 Promise.allSettled
Promise.allSettled接受一个 Promise 可迭代对象作为输入(一般是数组),无论输入的Promise对象结果如何,都会返回一个 新的promise对象,并带有一个包含所有结果的对象数组(包括状态)。
1 | const promise1 = Promise.resolve(`芜`) |
2.4.3 Promise.any
Promise.all接受一个 Promise 可迭代对象作为输入(一般是数组),并返回一个 新的promise对象。当输入的 Promise 任意一个成功时,返回的 Promise 也将被成功(空的也行),并返回第一个成功的返回值;如果输入的所有 Promise 失败,则返回的 Promise 也将失败,并带有一个失败的原因。
1 | const promise1 = Promise.reject(0); |
2.4.4Promise.race
Promise.race接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。这个返回的 promise的状态 会随着第一个 promise 的状态的决定而决定。
1 | function writeHomework(time,action){ |
2.4.5 Promise.reject
Promise.reject会返回一个失败的 Promise 对象,同时将值作为失败的原因。
1 | const p = Promise.reject(`错了`) |
2.4.6 Promise.resolve
Promise.resolve会将给定的值转换为一个Promise对象。如果该值本身就是一个Promise,就将该Promise返回;如果该值是一个thenable对象(有用then方法的对象),Promise.resolve将调用其then方法及其两个回调函数。
1 | const promise1 = Promise.resolve(`芜湖`) |
小结
Promise.all 相当于宝可梦中成功集齐八个道馆徽章才能挑战联盟,少一个即为失败。
Promise.allSettled 相当于一颗苹果树,不管好坏,只要结苹果就会把结果返回。
Promise.any 相当于你期考时写对一点沾边的内容都算你有分,但你一点不对就全错。
Promise.race 相当于一场竞赛,第一个到终点的决定结果。
Promise.reject 感觉像教材上学过的亚里士多德的名言,返回的全是错的.
Promise.resolve 。。。我不知道啊,返回“主 === 6”算了
3.async和await
书接上回,Promise名义上解决的回调地狱的问题,但实际在代码中依然充斥着大量的then函数。因此,async和await便诞生了,它们存在的意义便是让代码更加简洁。
3.1async
async是一个通过异步执行并隐式返回Promise作为结果的函数。async的使用很简单,只需要在函数前加上async关键字来表示函数是异步的。而异步函数的调用和普通函数相同,加括号调用就行。
1 | async function yibu(){ |
由上可知,async返回的Promise对象带有state和result,原因是调用异步函数时,内部会调用Promise.resolve()方法将它转化为一个Promise对象返回;如果异步函数内部抛出错误,内部就会调用Promise.reject()返回一个Promise对象。
1 | async function yibu2(){ |
如果想要获取async函数的结果,就可以调用then或catch。
1 | async function yibu3(){ |
可以看到,then能够获取async的结果并放入异步队列,且同步代码先于异步代码执行。
3.2await
根据MDN的定义,await 用于等待一个 Promise 成功并获取它成功之后的值。用人话说,await只能放入async函数中,并在之后接上一个能够返回Promise对象的表达式。它等待Promise对象执行完毕,并返回结果。事实上,await可以用于任意表达式,所以await后面可以接普通函数,只是不常用而已。
1 | function what(){ |
事实上,await在等待一个Promise对象时,会阻塞函数后面的代码,等待Promise对象resolve/reject,然后得到resolve/reject的值,作为await表达式的结果;如果等到了一个非Promise对象,那么它会隐式地返回一个resolve的Promise对象。
3.3它们有毛线用呢
还是那句老话,async和await能够使代码更简洁。
举个例子:
1 | function wuhu(name){ |
你可能没看出来,那再举个例子:
1 | function wuhu(name){ |
显然,第二种写法更加抽象,当then使用足够多时,你绝对不会想看下去,所以await在处理then链式调用方面还是很有用的。
3.2(补)阻塞
再来仔细说一下阻塞。当遇到await时,会阻塞函数内部(不是全部)处于它后面的代码,再去执行函数外部的同步代码;当外部的同步代码全部执行完成,再回到改函数执行剩余的代码。当然,微任务队列的代码会优先处理。
1 | function who(value){ |
讲一下执行过程:
调用after函数,里面遇到了await,代码就暂停到这,不再继续执行,等待后面的who(’369:’)执行完毕,4秒后返回了结果值赋值给promise1并打印,暂停结束,代码继续执行console.log语句。
而且,js引擎在等待Promise.resolve时,并没有真正的停止工作,它还可以处理其他一些事情。比如在after()函数后console.log()一下,我们会发现外部的console.log()语句会先执行。
再来一个
1 | async function first(){ |
再讲一下执行过程:
首先执行first(),打印出”我先来的”。
然后遇到await second(),在second()中遇到第二定时器,将其加入宏任务队列,在执行同步代码打印”再到我”。
因为seconde()阻塞了后面代码的执行,所以后面代码加入微任务队列。再到第三定时器,将其加入宏任务队列,之后打印“我在第三”。
因为同步代码执行完成,先执行微任务队列,打印“我最后”,再将第一定时器加入宏任务队列。
最后执行宏任务队列,按照先进先出的原则依次打印。
补充
1 | const first = () => (new Promise((resolve, reject) => { |
Author: John Doe
Link: https://159357254680.github.io/2024/11/20/%E8%BF%99%E6%98%AF%E6%A0%87%E9%A2%98/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.