后端小白的Promise学习笔记

Promise

写在前面

本博文仅作为个人学习过程的记录,可能存在诸多错误,希望各位看官不吝赐教,支持错误所在,帮助小白成长!

一、认识Promise

Promise是什么?!

Promise是ES6提出的异步编程的一种新规范。

旧版本中完成异步任务都是通过简单的回调任务!

Promise解决什么问题?

由于JavaScript作为脚本语言,解释执行的特点所有的代码执行都是单线程,就导致有些操作需要借助异步操作来完成。而异步的任务的执行顺序难以控制。

例如下面的代码:(JavaScript中使用Node的内建模块fs操作文件就是异步任务):

const fs = require('fs')

fs.readFile('./1.txt', 'utf-8', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

fs.readFile('./2.txt', 'utf-8', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

fs.readFile('./3.txt', 'utf-8', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

fs.readFile('./4.txt', 'utf-8', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

代码并不会按照你所写代码的顺序执行,而是通过异步回调的方式执行,所以你就可能看到这样的结果:

image-20210524151708057

如果我们想要读取的顺序受我们控制,我们就只能在回调函数中嵌套另一个异步任务,就像这样:

const fs = require('fs')

fs.readFile('./1.txt', 'utf-8', (err, data) => {
    if (err) {
        throw err
    }
    console.log(data)
    fs.readFile('./2.txt', 'utf-8', (err, data) => {
        if (err) {
            throw err
        }
        console.log(data)
        fs.readFile('./3.txt', 'utf-8', (err, data) => {
            if (err) {
                throw err
            }
            console.log(data)
            fs.readFile('./4.txt', 'utf-8', (err, data) => {
                if (err) {
                    throw err
                }
                console.log(data)
            })
        })
    })
})

这样写的代码,输出的顺序永远是1,2,3,4,因为只有上一个文件的内容输出完成后才会启动下一个异步任务!

但是这个代码…,是不是看的头疼?!这就是**回调地狱!(callback hell)**有两个明显的缺陷:

  1. 难以阅读!
  2. 容易出错,且难以排查!

Promise的优势

  1. 允许链式调用,解决回调地狱问题
  2. 逻辑清晰,便于理解

二、Promise初上手

2.1、基本案例

我们先用一个最简单的例子,来演示一下Promise的使用:

就以上面读取文件为例:

首先我们创建一个Promise对象:

image-20210524154004453

哇擦,这个参数你一看,头皮发麻。
Promise的构造函数,需要一个函数executer(也即你的异步任务),而这个异步任务函数的参数是所两个函数,函数名分别为resolvereject(这是我推荐的一种命名规范!)

resolve函数:函数类型是(value) => void,先说一下,这个函数可以在你异步任务中调用,会将你的异步任务执行状态从待定(pending)转换为(fulfilled)成功!同时这个函数可以传入你异步任务的执行结果作为函数参数value

reject函数:函数类型的(reason) => void,与resolve函数相反,它会将你的异步任务执行状态从待定(pending)转为(rejected)失败!并且可以传入一个值作为你异步任务的出错的原因,然后在具体的reject函数实现中进行处理!

const fs = require('fs')

const promise = new Promise((resolve, reject) => {
  fs.readFile('./1.txt', 'utf-8', (err, data) => {
    if (err) {
      // 调用reject, 标识任务失败,同时传入err作为失败的reason
      reject(err)
    } else {
      // 调用resolve, 标识任务成功,传入data作为异步任务的执行结果
      resolve(data)
    }
  })
})

现在我们要知道几点:

  1. Promise的状态装换有两种:pending -> fulfilledpending -> rejected
  2. resolvereject是两个回调函数,分别会在调用时将你的异步任务状态修改为fulfilled、rejected。我们在异步任务中调用了它们,但是并没有为它们写具体的实现

目前我们的代码运行,没有任何动静

现在通过promise对象的then方法,对两种任务状态下的回调函数进行实现:

then方法的两个参数分别刚好与resolve、reject函数的类型相同:

image-20210524162118938

(并且then方法的返回值还是一个Promise对象,这是方便链式调用的独特设计!)

promise.then(
  // 任务转态为成功时,执行此函数, value为异步任务中调用resolve传入的执行结果
  (value) => {
    console.log(value)
  },
  // 任务状态为失败时,执行此函数,reason为你在异步任务中传入的失败原因reason
  (reason) => {
    console.log(reason)
  },
)

然后就可以开开心心测试代码啦~~

2.2、让小屁孩都能懂的解释

看完代码,不禁我都有疑问,为什么可以这样写?!这是个什么思路?!对于Java代码写的多的我,无法理解为什么!!

直到我看到了这篇文章:我以为我很懂Promise,直到我开始实现Promise/A+规范 - 知乎 (zhihu.com)

那么我们就从这项技术的命名开始说起,毕竟它的名字就能代表它的绝大部分思想的源泉!Promise,直译就是承诺!我们简称它为画饼。

那么我们创建Promise实例的过程也就是我们画饼的过程,这个具体的饼也就是Promise创建时我们传入的异步任务

我们画饼时就会强调后果:

  • 如果画出来了,各位就工资几何翻倍,别墅靠海。这也就是我们在“饼”中写下的resolve()
  • 如果饼没了,各位就天天加班,月月低保。这就是“饼”中的reject()

当然这都是在画饼,都还是虚构的,还没有真正实现,谁都不清楚“饼”的结果如何!即这个饼的状态还是pending状态,它有可能fulfilled,也有可能变成rejected!(这取决于这个“饼”内部的走向)

既然画了饼,那必然有人来接这个”饼“,如果饼实现了我就朝九晚五,房车不愁。如果饼没了打工仔我整日就以泪洗面。而这个接饼的过程就是我们常见的发布与订阅模式,而在Promise的概念中,订阅就是通过then方法来实现,所有的订阅者需要做好两方面的心理准备!这两手心理准备的实现就是在then方法的两个参数函数中体现。

除了一个饼可以多个人同时订阅外,一个饼还可以一层层往下传,也就是后面会学习的链式调用!比如老板先画了一个大饼给经理,然后经理又给部门组长画了一个饼,然后组长又给组员画了一个饼。这就是promise.then(..).then(..).then(..)的结果!

2.3、util.promisify

如果所有的异步任务,都需要我们进行手动包装为Promise版本,写起来还是比较麻烦的。于是Node中util模块中提供了一个函数promisify:传入一个遵循常见的错误优先的回调风格的函数(即以 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。

我们先来搞清楚什么叫遵循常见的错误优先的回调风格的函数

  1. 函数参数中有一个回调函数
  2. 回调函数是这个函数最后一个参数
  3. 回调函数的参数列表中错误优先,例如(err, value) => { ... }

很显然,我们刚才使用的fs.readFile()就满足这个条件!!

那现在我们利用这个工具,来将其进行封装:

const util = require('util')
const fs = require('fs')

// 封装返回一个Promise对象
const promiseReadFile = util.promisify(fs.readFile)

promiseReadFile('./1.txt', 'utf8').then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  },
)

三、Promise状态与对象值

在基础案例中,我们说过Promise对象的三个状态:

  • pending
  • resolved/fulfilled
  • rejected

我可不是胡编乱造,看代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
  </body>
  <script>
    const p1 = new Promise((resolve, reject) => {
      resolve()
    })
    console.log(p1)

    const p2 = new Promise((resolve, reject) => {
      reject()
    })
    console.log(p2)

    const p3 = new Promise((resolve, reject) => {})
    console.log(p3)
  </script>
</html>
image-20210524194630461

他们由Promise对象的属性PromiseState保存,我们通过JavaScript代码是无法获取到的,是JavaScript引擎内部使用的属性!

此外我们还要说的就是Promise的对象值,我们之前说过我们在异步任务中调用resolve()reject()函数是,都是可以传参的。而向这两个函数的传递的参数,将会被保存到Promise的另一个属性中:PromiseResult,上述代码中我们都没有进行传参所以都是undefined!

修改后的代码:

const p1 = new Promise((resolve, reject) => {
resolve('OK!')
})
console.log(p1)

const p2 = new Promise((resolve, reject) => {
reject('ERROR!')
})

运行结果:

image-20210524210542824

什么可以改变PromiseState和PromiseResult?!

只有resolve(),reject()函数可以修改PromiseState和PromiseResult。

(不过有种特殊的情况,若在异步任务中throw了错误,PromiseState将被置为rejected):

const p3 = new Promise((resolve, reject) => {
    throw 'ERROR'
})
console.log(p3)

image-20210524211234424

其效果与使用reject()相同。

四、Promise API

4.1、Promise构造函数

我们使用Promise构造函数创建promise对象的过程,我们称其为画饼的过程。过程中饼的状态都是pending

而画饼的结果有可能出现两种情况:fulfilledrejected我们创建Promise对象的过程中,内部的任务是同步执行的!
即你创建Promise对象的时间,就取决于你内部任务执行的时间!!

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        let number = Math.floor(Math.random() * 100)
        if (number < 50) {
            resolve(number)
        } else {
            reject(number)
        }
    }, 3000)
})

alert('Hello!')

promise.then(
    (value) => {
        console.log('nice! ' + value)
    },
    (reason) => {
        console.log('dammit! ' + reason)
    },
)

你猜这段代码怎么执行?!

promise-create

有点出乎意料,也就是我们创建Promise是异步的,并不影响我们普通代码的运行(不管那句alter放哪,它都会首先运行!!)而我们的发布者即我们创建的Promise,和其订阅者即使用了promise的then方法的,他们之间的运行过程是按顺序执行的!

订阅者必须等待发布者发布对应状态后,才能做出对应的动作!

并且这个响应动作,是异步的!

const promise = new Promise((resolve, reject) => {
    resolve(521)
})

promise.then(
    (value) => {
        console.log('nice! ' + value)
    },
    (reason) => {
        console.log('dammit! ' + reason)
    },
)

console.log('Hello')
image-20210524215402138

Promise的构造函数参数executor,此函数的函数体就是我们异步任务的主体内容,函数体中调用resolve()reject()方法时,就表示异步任务完成,并且Promise的State置为fulfilledrejected!此时“饼”的状态也就确定了!订阅者也就会按照饼的状态做出对应的响应!

4.2、订阅者动作:then、catch、finally

then

promise.then表示一个promise订阅者,方法有两个参数:

  1. onfulfilled
  2. onrejected

两个参数都是函数,函数参数分别为value、reason.

两个函数分别会在发布者所给“饼”的状态为fulfilledrejected时被执行!(相当于订阅者的两个动作!)
函数参数就是发布者发布饼时所传值即**PromiseResult**

then方法的两个参数都是可选的,如果作为订阅者只关心任务完成的状态,那么onrejected可以写null,反之亦然!

catch

与then相似,但是它只有一个参数:onrejected
意思也就是它只能对发布者任务状态为rejected时候做出响应。可以看做是then(null, (reason) => { .. })的简写!

finally

try-catch-finally的中finally的意义一样,finally(f)其效果与then(f, f)效果相同,即不管任务的状态是什么,只要调用resolve()reject()使得PromiseState被settled,就会做出响应!

要注意的是,因为finally无论状态如何都会调用处理函数,但是处理函数是没有参数的!所以我们可以将其作为一个状态信息传递者。

const promise = new Promise((resolve, reject) => {
    resolve(521)
})

promise
    .finally(() => console.log('Promise is ready!!'))
    .then(
    (value) => {
        console.log('nice! ' + value)
    },
    (reason) => {
        console.log('dammit! ' + reason)
    },
)

4.3、Promise.all()

函数用于同时执行多个任务,并返回结果。

函数的参数接收一个promise实例数组,返回值为一个新的promise对象。
仅当promise数组中所有的promiseState都为fulfilled时,返回值的promiseState为fulfilled。只要有一个rejected,那么最终结果的state也是rejected.

当结果的PromiseState为fulfilled时,返回的promise的PromiseResult是一个数组,数组中每个元素对应函数参数中promise的result。

但是如果PromiseState为rejected时,只会将第一个reject的数据加入PromiseResult,也只会抛出第一个错(因为只要遇到一个rejected,就可以决定Promise.all的最终State也为rejected!!)

全通过:

const promises = [
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(1), 3000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(2), 1000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(3), 2000)
    }),
]
const result = Promise.all(promises)
image-20210524233226143

一个或多个失败:

const promises = [
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(1), 3000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => reject(2), 1000) // reject!
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => reject(3), 2000) // reject!
    }),
]
const result = Promise.all(promises)
image-20210524233549419

第一个reject有效,后面即使reject了,也不能再改变了,因为返回值的promiseState已经确定了!

all的参数数组元素并不一定都要是promise对象,还可以是常规值!!这些常规值,会被直接加入Result中,并且不会影响all返回值promise的State

const promises = [
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(1), 3000)
    }),
    'Hello',
    2021,
]
const result = Promise.all(promises)
image-20210525105601690

4.4、Promise.allSettled

Promise.all在面对全部fulfilled时,可以返回所有Promise的Result数组,但是如果其中有一个rejected,就只能获取rejected的信息。而allSettled无论Promise的状态如何,只要State被Settled,都会将State和Result封装为一个对象放入一个数组并设置为返回值Promise对象的Result进行返回!我们就可以看到所有的Promise中任务的执行状态和结果了。

const promises = [
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1)
        }, 2000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2)
        }, 2000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(3)
        }, 2000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(4)
        }, 2000)
    }),
]
const result = Promise.allSettled(promises)
console.log(result)
image-20210525110555110

all()和allSettled(),当参数数组任意一个Promise的状态为pending时,返回值Promise对象的状态也为pending!!

4.5、Promise.race

race,竞赛。和它的名字一样,谁先跑到就算谁“赢”。传入一个Promise对象数组,可以视为多条赛道,那条赛道先执行完,对State进行修改,那么最终的返回值Promise对象的Result和State就固定了!

const promises = [
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1)
        }, 4000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2)
        }, 1000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(3)
        }, 3000)
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(4)
        }, 2000)
    }),
]
const result = Promise.race(promises)
console.log(result)
image-20210525113618475

四条赛道,第二条跑道抢先起跑,所以返回值Promise对象第一次Settled,就是被第二跑道。所以结果与第二个Promise的结果一致!

4.6、Promise.resolve()/reject()

在现代的代码中,很少需要使用 Promise.resolvePromise.reject 方法,因为 async/await 语法使它们变得有些过时了。

Promise.resolve(value)

创建一个State为fulfilled,Result为value的Promise对象!

等同于:

const promise = new Promise((resolve, reject) => {resolve(value)})

适用于将一个值封装为Promise对象,便于后续使用链式调用进行处理。

Promise.reject(reason)

创建一个State为rejected,Result为reason的Promise对象。

五、Promise重要注意点

5.1、改变状态和指定回调函数谁先发生

答案是:都有可能。

改变转态是指在executor中,使用resolve或者reject函数改变PromiseState。
指定回调函数是指使用then方法创建订阅,并指定对应转态的回调函数!(注意这里说的是指定!并不是执行!)

我们可以分为两个场景来解释:

场景一:

某手机厂商预进行产品发布会,于是众多爱好者提前订阅了发布会提醒,并且已经做好了买到与不买两种状态的动作,这个过程就相当于是指定回调函数!而此时发布会还没有进行,所以发布者的状态还没有发生变化。

这就是典型的指定回调函数先于状态改变

当发布会发布以后,订阅者会马上根据发布者的状态变化,做出响应!

在Promise的实现中,只要我们的executor中改变状态是通过一个异步任务完成的,那么就会先执行then指定回调函数。然后执行异步任务完成状态改变:

const promise = new Promise((resolve, reject) => {
    // 异步任务,改变状态
    setTimeout(() => {
        resolve('Hello World!')
    }, 2000)
})


promise.then(
	(value) => {
        // ...成功回调函数体
    },
    (reason) => {
        // ...失败回调函数体
    }
)

场景二:

还是发布会的例子,发布会开完了,讲的一塌糊涂,产品功能烂到了极点,是一个极其失败的产品发布会!改品牌的很多粉丝看到这条新闻失望至极,路人看到也秒转黑。

这是状态改变先于指定回调函数。

发布会的糟糕表现在前,没有提前关注发布会的的粉丝与路人没有心理准备,在看到发布会结果后做出对应的反应。

这在Promise的实现中,只需要将转态改变使用同步任务完成即可。

const promise = new Promise((resolve, reject) => {
    if (Math.random * 100 > 50) {
        resolve("OK")
    } else {
        reject("ERROR")
    }
})

5.2、then方法返回值

我们在最开始介绍Promise的特点的时候,提到了链式调用,并且我们已经确定了then方法的返回值也是一个Promise对象!也就可以实现饼传饼。可是返回的Promise的State与Result是如何确定的?!

我们分别从三种情况来说

返回一个Promise对象

const promise = new Promise((resolve, reject) => {
    resolve('Hello!')
})

let result = promise.then(
    (value) => {
        // 返回一个Promise对象
        return new Promise((resolve, reject) => {
            resolve('World')
        })
    },
    (reason) => {},
)

// 延迟输出
setTimeout(() => {
    console.log(result)
}, 2000)
image-20210525163617731

then方法会直接将Promise对象返回!如果使用then链式执行,那么下个订阅者就是此Promise的订阅者!

返回一个常规值

let result = promise.then(
    (value) => {
        // 直接返回一个常规值
        return 2021
    },
    (reason) => {},
)

这个常规值也会被包装为一个Promise对象,State为fulfilled,Result就是这个常规值本身!

image-20210525164032370

throw异常

let result = promise.then(
    (value) => {
        throw 'Error'
    },
    (reason) => {},
)

throw的内容将会被设置为Promise对象的Result,State为rejected

image-20210525164444902

5.3、串联任务

这里就是使用Promise来解决地狱回调的关键手段了!你可以先创建一个初始任务,然后通过then对后续任务进行串联。

就以最开始的文件读取任务为例:

const fs = require('fs')

const promise = new Promise((resolve, reject) => {
    fs.readFile('../callback-hell/1.txt', 'utf8', (err, data) => {
        if (err) {
            reject(err)
        } else {
            resolve(data)
        }
    })
})

promise
    .then((value) => {
    console.log(value)
    return new Promise((resolve, reject) => {
        fs.readFile('../callback-hell/2.txt', 'utf8', (err, data) => {
            if (err) {
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
})
    .then((value) => {
    console.log(value)
    return new Promise((resolve, reject) => {
        fs.readFile('../callback-hell/3.txt', 'utf8', (err, data) => {
            if (err) {
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
})
    .then((value) => {
    console.log(value)
    return new Promise((resolve, reject) => {
        fs.readFile('../callback-hell/4.txt', 'utf8', (err, data) => {
            if (err) {
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
})
    .then(console.log)

虽然比起之前的代码量更多了,但是其实大部分都是重复代码。这样代码看起来过程感更强烈,任务就像流水线一样在顺次进行。每一次then返回的promise对象的Result都会传递给后续的订阅者去处理。

思考:如果我在最后一行再加上一个then(console.log),你觉得最后一次输出的内容是什么呢?!

答案是:undefined,因为then方法的返回Promise的对象其中的Result是根据then方法的返回值来确定的,而console.log没有返回值,那么返回的Promise对象State为fulfilled,但是Result为undefined!!

5.4、异常穿透

上面的串联任务代码中间,我们并没有对异常错误的捕捉。如果每个then中间都还要对上一个任务的异常进行捕捉,并且如果动作都一样的,将产生更糟糕的代码冗余。其实我们可以使用.catch(),并将其放在任务的最后,就可以捕获到这条任务链中在执行过程中出现的错误了!!

// ....
.then(console.log)
.catch(console.warn)

现在我们误将2.txt和3.txt的路径写错,看看会输出什么:

image-20210525171311936

果然是可以捕获到错误的,但是也仅限第一个出现的错误!!

为什么正确的4.txt也没能执行呢!?

当我们在2.tx文件读取失败时,返回的是一个状态为reject的Promise对象,然而后面的订阅者都没有对rejected状态的回调函数,那么就只能将错误继续抛出,也就是说从第一个出错的位置开始,后学的的订阅者都会执行onrejected回调函数!!

如何让4.txt能够执行呢?!(即使前面出错了!)

每个订阅者都加上对rejected状态的回调函数! 并且在rejected回调函数中再把自己的任务复述一遍

.then(
    (value) => {
        console.log(value)
        return new Promise((resolve, reject) => {
            fs.readFile('../callback-hell/3.txt', 'utf8', (err, data) => {
                if (err) {
                    reject(err)
                } else {
                    resolve(data)
                }
            })
        })
    },
    (reason) => {
        console.log('3: 失败啦~')
        return new Promise((resolve, reject) => {
            fs.readFile('../callback-hell/3.txt', 'utf8', (err, data) => {
                if (err) {
                    reject(err)
                } else {
                    resolve(data)
                }
            })
        })
    },
)

就像这样,无论什么前面任务什么情况,我们都可以正确处理,并且不会影响自己的任务执行!当然这样写异常穿透也就不存在了。

5.5、中断Promise链

前面看到了,当使用串联任务时,如果每个订阅者的回调设置得当,且开启自己的任务后,并不会使得Promise链在遇到错误时就直接中断(因为这样任务执行的状态会在其订阅中进行处理,然后并不会向下传递了),它会一直执行到最后一个then()。

那么我们使用什么办法可以使得Promise链在执行时,直接中断不继续向下执行呢?!方法只有一个!返回一个State为pending的Promise!

const promise = new Promise((resolve, reject) => {
    resolve(1)
})

promise
    .then(
    (value) => {
        console.log(value)
        // 自己的任务
        return Promise.resolve(2)
    },
    (reason) => {
        console.warn(reason)
        // 自己的任务
        return Promise.resolve(2)
    },
)
    .then(
    (value) => {
        console.log(value)
        // 自己的任务
        return Promise.reject(3) // 这里使用reject模拟异常
    },
    (reason) => {
        console.warn(reason)
        // 自己的任务
        return Promise.resolve(3)
    },
)
    .then(
    (value) => {
        console.log(value)
        // 自己的任务
        return Promise.resolve(4)
    },
    (reason) => {
        console.warn(reason)
        // 自己的任务
        return Promise.resolve(4)
    },
)
    .then(console.log)

执行结果到异常位置并没有中断,并且一直执行到了最后:你用throw也不好使!

image-20210526002545004

如果你期望遇到异常就中断任务链,就只能在异常位置返回一个转态为pending的Promise对象!

try {
    throw 3 // 模拟异常
} catch (e) {
    console.warn(e)
    // 返回一个pending状态的Promise
    return new Promise(() => {})
}

这样,由于此任务状态没有改变,其订阅者也就无法进行相应的动作,Promise链也就自此中断!

image-20210526002957836

5.6、微任务队列

Promise的订阅处理程序,thencatchfinally都是异步的!

于是我们会看到这样一幕:

const promise = Promise.resolve(200)

promise.then(console.log)
console.log('Script finished')

代码的执行结果:

image-20210526100952122

也就是这些处理程序之外的代码会在所有处理程序之前执行!!

而发生这种情况的唯一解释,就是本节要接触的微任务队列,异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(ES8 术语)。

而我们的处理程序都属于异步任务,所以他们并不会直接执行,而是向加入到微任务队列中。如其规范中所说:

  • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
  • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。

JavaScript引擎会优先执行非队列之中任务的代码,最后按入队顺序顺次调用微任务队列中的异步任务!

如果你希望某些代码必须在处理程序之后执行,你可以使用then将其转换为异步的,然后被加入到微任务队列中。

const promise = Promise.resolve(200)

promise
    .then(console.log)
    .then(() => {console.log('Script finished')})

六、async与await

Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

6.1、async

用于修饰函数的关键字。使用async修饰的函数有以下特点:

  1. 返回值是一个Promise对象
  2. 返回的Promise对象的State与函数的运行结果有关,如果抛出异常State为rejected,否则为fulfilled
  3. 返回的Promise的Result,也与函数的运行结果相关,如果结果是常规值将直接用于设置Result。

测试使用:

async function sayHi() {
    return 'Hello World'
}

const promise = sayHi()
image-20210526103620218

这样我们的异步任务就不要使用new Promise来进行包裹了,直接使用async来修饰我们异步任务函数即可。

async经常会与await联合使用,来看看这个小家伙。

6.2、await

await通常用于修饰一个表达式,表达式可以是Promise对象,也可以是常规值!

  • 当是Promise对象时
    • 若对象State为fulfilled时,返回PromiseResult!
    • 若对象State为rejected时,直接抛出异常,异常内容为PromiseResult
  • 当为常规值时,直接返回

await只能在async修饰的函数中使用!!!

如果你敏锐度高,你可能已经闻到了一丝丝味道,两者的设计和使用都是这么巧妙~(await可以解析一个async函数的返回值)

我们先来测试使用一下await:

async function sayHi() {
    return await 200
}

const promise = sayHi()
image-20210526104617229

到这里,你看出什么端倪了吗?! 为什么说await和async是使用Promise的语法糖?!

6.3、结合使用

我们现在使用async和await来完成文件读取案例:

const fs = require('fs')
const util = require('util')
// 先将fs.readFile 转为Promise方法
const promiseReadFile = util.promisify(fs.readFile)

async function main() {
  try {
    // 将Promise化的文件读取方法的Result使用await进行转换
    let file1Content = await promiseReadFile('../callback-hell/1.txt', 'utf8')
    let file2Content = await promiseReadFile('../callback-hell/2.txt', 'utf8')
    let file3Content = await promiseReadFile('../callback-hell/3.txt', 'utf8')
    let file4Content = await promiseReadFile('../callback-hell/4.txt', 'utf8')
    console.log(file1Content, file2Content, file3Content, file4Content)
  } catch (e) {
    console.warn('Error' + e)
  }
}

main()

可以看到,我们在使用await和async时,是没有写任何回调函数的,就像写同步代码一样。await可以直接取到我们异步任务的执行结果值,我们拿来即用,代码十分清爽易懂。

当我们使用async和await时,我们基本不再需要使用then、catch,await帮我们的处理了等待,代替了then中指定回调函数来进行promise任务结果处理。使用await我们可以拿到Promise任务结果然后进行同步处理,这就省去了我们在then中写的俩回调!

promise.then(
    (value) => {
		console.log(value)
        // ...其他处理
    }
)

// 等效于

let value = await promise
console.log(value)
// .. 其他处理

并且使用常规的try...catch() 而不是.catch


以上就是Promise的入门使用的全部笔记。

学习参考:

Promise,async/await (javascript.info)

async function main() {
try {
// 将Promise化的文件读取方法的Result使用await进行转换
let file1Content = await promiseReadFile(’…/callback-hell/1.txt’, ‘utf8’)
let file2Content = await promiseReadFile(’…/callback-hell/2.txt’, ‘utf8’)
let file3Content = await promiseReadFile(’…/callback-hell/3.txt’, ‘utf8’)
let file4Content = await promiseReadFile(’…/callback-hell/4.txt’, ‘utf8’)
console.log(file1Content, file2Content, file3Content, file4Content)
} catch (e) {
console.warn(‘Error’ + e)
}
}

main()




可以看到,我们在使用await和async时,是没有写任何回调函数的,就像写同步代码一样。await可以直接取到我们异步任务的执行结果值,我们拿来即用,代码十分清爽易懂。

当我们使用async和await时,我们基本不再需要使用then、catch,`await`帮我们的处理了等待,代替了then中指定回调函数来进行promise任务结果处理。使用await我们可以拿到Promise任务结果然后进行同步处理,这就省去了我们在then中写的俩回调!

```javascript
promise.then(
    (value) => {
		console.log(value)
        // ...其他处理
    }
)

// 等效于

let value = await promise
console.log(value)
// .. 其他处理

并且使用常规的try...catch() 而不是.catch


以上就是Promise的入门使用的全部笔记。

学习参考:

Promise,async/await (javascript.info)

posted @ 2022-01-21 20:33  5akura  阅读(68)  评论(0编辑  收藏  举报