前言
同步和异步的最大区别在于:
- 同步会阻塞,上面的代码执行完成后下面的代码才会执行
- 异步不会阻塞,上面的代码执行未完成下面的代码就已经开始执行了
我们知道 JS 是单线程语言,所以如果存在运行时间比较长的任务(如:定时器、DOM事件、异步请求等),如果把这些任务视为同步任务,那将会造成很严重的“阻塞现象”,所以浏览器把这些任务归为异步任务,JS 引擎线程遇到这些异步任务时,会通知其他线程(如:定时器线程、事件处理线程、异步请求线程等)去做,最后只需要把最终结果反馈 JS 引擎线程就行
好,那这个通知 和 反馈 怎么做呢?
于是就有了JavaScript的异步编程方式:Promise、Generator、async / await
我们来看看这些方式是怎么完成这项工作的:
Promise
Promise 是什么?
在控制台打印出 Promise ,结果可以看到是 [native code] ,说明是官方封装的函数,代码是本地的
在控制台输出 Promise.prototype ,结果可以看到有 then 、catch 、finally 等方法
综上,我们把 Promise 当做是构造函数,使用 new 关键字创建 Promise 的一个最简单的实例对象 p:
var p = new Promise((resolve,reject)=>{
if(1) resolve("异步任务完成")
else reject("异步任务失败")
})
复制代码
在控制台输出实例对象 p
我们在实例对象 p 的原型__proto__
属性里面找到了 Promise.prototype
,所以我们才能让实例对象 p 使用 then / catch / finally 等方法
Promise 的通知与反馈过程
通知
var p = new Promise((resolve,reject)=>{
if(1) resolve("异步任务完成")
else reject("异步任务失败")
})
复制代码
以上就是 Promise 的通知过程,Promise 里面是一个箭头函数,传入参数是 resolve 和 reject,{ }里面运行的代码是 resolve() 和 reject() 两个调用其他函数的操作。官方规定:写在前面的参数表示“任务完成”,写在第二个参数表示“任务失败”。所以由此可见:这两个参数的名称是可以自定的,只不过我们为了更加语义化才采用 resolve 和 reject。
状态
- 我们在控制台输出实例对象 p,显示的是一个 Promise 对象,对象的属性里发现除了原型还有
[[PromiseState]]
和[[PromiseResult]]
两个属性,顾名思义分别表示状态
和结果
,其中的状态的值是fulfilled
,而结果
的值就是我们刚刚 Promise 里面箭头函数里写的 resolve() 括号的实参"异步任务完成"
2. 我们修改一下 Promise 函数,控制其任务失败:
var p = new Promise((resolve,reject)=>{
if(0) resolve("异步任务完成")
else reject("异步任务失败")
})
复制代码
3. 其中[[PromiseResult]]
就是我们刚刚 Promise 里面箭头函数里写的 reject() 括号的实参"异步任务失败"
,而[[PromiseState]]
变成了rejected
。
这说明了:我们的通知过程中 Promise 所做的背后工作(原理)就是:执行 Promise 里面的箭头函数时会把该实例对象 p (一个Promise对象)里的
[[PromiseState]]
属性的值进行改变:
- 若是执行第一个参数(resolve)对应的函数(resolve())的话,就把这个属性的值改为
fulfilled
;(源码就是这个道理)- 若是执行第二个参数(reject)对应的函数(reject())的话,就把这个属性的值改为
rejected
- 那这个属性原来的值是什么,我们用 setTimeout() 作一下延时,才能看得到:
- 可以看到这个属性原来的值是
pending
。
反馈
- 我们对实例对象 p 使用其继承过来的 then 方法:(针对的是任务完成的例子)
可以看到调用 p.then() 后还是返回一个 Promise 对象,并且这个对象的状态
变为fulfilled
,结果
变为undefined
(因为我没有调用其他函数并传参)
- 我们针对任务失败的例子使用 then() 方法:
- 可以发现状态还是 rejected,也没有打印 res,说明 p.then() 并没有执行,我们在对这个返回的 Promise 对象使用 catch() 方法:
9. 可以看到调用 p.then().catch() 后还是返回一个 Promise 对象,并且这个对象的状态
变为fulfilled
,结果
变为undefined
,还把 err 给打印出来了
- 这就是实例对象 p 可以采用链式调用的本质原因,也证明了为什么任务失败只会执行catch()方法,而任务完成执行了then()方法后还会继续执行链上下一个then()方法,一直走到catch()不执行退出的原因
- then() 方法的入口钥匙是
[[PromiseState]]
属性的值是fulfilled
;- catch() 方法的入口钥匙是
[[PromiseState]]
属性的值是rejected
。(源码就是这个道理)
(任务完成的 p)
(任务失败的 p)
总结
- 使用 Promise 解决异步问题的步骤:
- 利用构造函数 Promise 创建实例对象;
- 在 构造函数里面写个箭头函数,传入两个参数,一个代表任务完成,一个代表任务失败;
- 在箭头函数里执行异步任务,执行完成后调用以这两个为名称的函数,可把执行结果做实参传过去;
- Promise()里的箭头函数的异步任务执行完毕后会返回一个 Promise 对象,这个对象已经赋值给了实例对象 p;p 可以使用从 Promise.prototype 继承过来的 then() / catch() 等方法
- p 执行这些方法前会看
[[PromiseState]]
这个属性的值看是否进入当前方法,进去之后出来[[PromiseState]]
和[[PromiseResult]]
两个属性的值都可能发生改变; - 每次执行完后都还是返回一个 Promise 对象
明白了这些本质之后,对于 Promise 的源码的基本框架就大概知道是什么了(虽然现在还可能很难看懂hhh),以及后面的各种用法、面对各种场景也都比较有把握了。
上面说了 Promise 的本质性的东西,接下来我们研究一下 Promise 的源码,是如何实现“通知” 和 “反馈” 的,想直接跳过的同学 点击这里
Promise((resolve,reject)=>{}) 的实现原理
- Promise 内部是如何获知成功与否的,并且里面返回出表示结果的 Promise 对象的?
Promise.then().catch() 的实现原理
- 为什么可以链式
- 为什么成功就会被then方法捕捉到;二是,为什么任何错误都被catch捕捉到
Promise().then().catch() 处理多个串行异步任务
比如有个需求 —— 想要获取一个地级市拥有多少个辖区:
- 我们现在得先调用【获取当地省份】的接口
- 拿到省份id后才能够调用【获取地级市】的接口
- 拿到对应地级市的id后才能调用【获取地级市拥有多少个辖区】的接口
其伪代码如下:
ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1') 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2') 的结果
复制代码
按照之前回调函数的写法就是:
ajax('url_1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
复制代码
先不说它的可读性差,如果中间某一环节出现错误,我们都很难找出来,修改起来也很麻烦。所以我们用 Promise 来解决这个问题,好处就是 Promise 实现了将多个异步任务串行执行写成同步任务执行顺序
let promise = fn('url_1',data1)
promise.then(data2 => { // 注意这里 promise 不要加括号,它是执行 new Promise(...) 后返回的一个对象
fn('url_2',data2)
}).then(data3 => {
fn('url_3',data3)
}).then(res =>{
console.log(res) // 串行完毕可以得到你想要的结果了
}).catch(err => {
console.log(err)
})
function fn(url,data){
return new Promise((resovle,reject)=>{
ajax(url,data).success(function(res){
resolve(res)
})
})
}
复制代码
这样代码可读性就好很多了,清晰明了。(其实后面还有更加简洁的方法)
说完串行了,那么并行怎么办? 当有多个异步事件,之间并无联系而且没有先后顺序,只需要全部完成就可以开始工作了。
串行会把每一个异步事件的等待时间进行一个相加,明显会对完成进行一个阻塞。那么并行的话该怎么确定全部完成呢?
Promise.all 与 Promise.race 的妙用
Promise.all([promise1,promise2,promise3,…])
- 接收一个数组,数组的每一项都是一个 promise 对象
- 当数组中所有的 promise 的状态都达到 resolved 的时候,Promise.all的状态就会变成 resolved
- 如果其中有一个 promise 的状态变成了 rejected,那么 Promise.all 的状态就会变成 rejected
- 调用then方法时的结果成功的时候是回调函数的参数也是一个数组,按顺序保存着每一个promise对象resolve执行时的值
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3])
promiseAll.then(res=>{
console.log(res); // [1,2,3]
}).catch(err => {
console.log(err);
})
复制代码
控制台 5s
后输出结果:(执行时间最长的 promise 完成才算完成)
顺序是[1,2,3],证明与哪个 promise 的状态先变成 resolved 无关
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseAll = Promise.all([promise1,promise2,promise3]);
promiseAll.then(res => {
console.log(res);
}).catch(err => {
console.log("任务" + err + "失败了") // 1 说明是 promise1 里的异步任务执行失败了
})
复制代码
控制台 4s
后输出结果:(其中一个 promise 一失败就退出)
Promise.all() 的实现原理:
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let index = 0
let result = [] // 存放异步任务执行成功的结果
if (promises.length === 0) {
resolve(result)
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
processValue(i, data) // 拿取任务完成的结果
}, (err) => {
reject(err) // 一旦发现错误,立马抛出
return
})
}
function processValue(i, data) {
result[i] = data // 把任务完成的结果存入数组中
if (++index === promise.length) {
// 每成功一次计数器就会加1,直到所有都成功的时候会与values长度一致,则认定为都成功了
resolve(result)
}
}
}
})
}
复制代码
总结:
- 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验,一旦发现错误就抛出
- 如果当前遍历的 promise 对象的结果是成功的,则放入一个 results 数组
- 当 results 数组的长度和原来的 promises 数组长度一致时返回 results 数组
Promise.race([promise1,promise2,promise3,…]) 竞速模式
- 接受一个每一项都是promise的数组。
- 但是与all不同的是,第一个promise对象状态变成resolved时自身的状态变成了resolved,第一个promise变成rejected自身状态就会变成rejected。第一个变成resolved的promsie的值就会被使用
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},4000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},3000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},5000)
});
let promiseRace = Promise.race([promise1,promise2,promise3])
promiseRace.then(res => {
console.log(res); // 打印出2 因为 promise2 最先完成,其余的就忽略了
}).catch(err => {
console.log("任务" + err + "失败了");
})
复制代码
但是在控制台输出 promise1 和 promise3 都显示“任务完成”的状态,也就是说“竞赛”输了的选手也还是会完成这场比赛
Promise.race() 的实现原理
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
let index = 0
let result = [] // 存放异步任务执行成功的结果
if (promises.length === 0) {
resolve(result)
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data) // 其中一个任务一有任务完成的结果,立马抛出
}, (err) => {
reject(err) // 一旦发现错误,立马抛出
return
})
}
function processValue(i, data) {
result[i] = data // 把任务完成的结果存入数组中
if (++index === promise.length) {
// 每成功一次计数器就会加1,直到所有都成功的时候会与values长度一致,则认定为都成功了
resolve(result)
}
}
}
})
}
复制代码
总结:
- 对数组中的每一个 promise 对象调用 then()方法和 catch()方法进行结果检验
- 一旦发现其中一个 promise 对象是任务完成的状态,立马抛出
- 一旦发现其中一个 promise 对象是任务失败的状态,立马抛出
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。那么,有没有更好的写法呢?
Generator
Generator 是什么
Generator(生成器)是ES6标准引入的新的数据类型。一个 generator 看上去像一个函数,但可以返回多次
function* fn(max) {
yield ...;
return;
}
复制代码
generator 生成器长得像函数,与函数不同的是用function*
定义的
yield 关键字
关键字 yield,有点像 return,yield 和 return 的区别在于:
- return 只在函数调用之后返回值,return 语句之后不允许你执行任何其他操作
- yield 相当于暂停函数执行而返回一次值,下次调用 next() 时,它将执行到下一个 yield 语句那里
return 返回的值
在控制台输出 return 的值(如果没有return就执行到函数结束)会得到一个 generator 对象,其属性[[GeneratorState]]
的值是suspended
表示“暂停状态”
Generator.prototype.next() 方法
在控制台输出 generator 对象的原型会看到有 next、return、throw等方法,我们重点看一下 next 方法:
使用 g.next() 方法后会得到一个对象 {value: 1,done: false}
,此时输出 g,可观察到其状态还是suspended
,当执行第三次 g.next() 后将会得到 {value: 1,done: false}
对象,此时输出 g 的状态为 “closed”。告知外界该 generator 对象已经执行完毕
返回的 value 就是 yield 的返回值,done 表示这个 generator 是否已经执行结束了。如果done为true,则value 就是 return的返回值。当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。
Generator.prototype.next() 方法 传参
next() 方法也可以传参
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
复制代码
第一次 g.next() 输出 { value: 3, done: false } 的原因是 gen(1) 传入参数 1,然后 yield 返回 1 + 2,所以得到 value 值为 3,而第二次 g.next() 传入参数 2,相当于把整个 yield x + 2 换成 2,所以 return 返回 y 的值为 2,执行结束,done 值为 true
Generator.prototype.return() 方法
return()方法,可以返回给定的值,并且终止遍历 Generator 函数
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
复制代码
Generator.prototype.throw() 方法
Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
复制代码
相当于把整个 yield x + 2
语句换成 throw("出错了")
,然后 try…catch…语句捕获到了,于是输出 e
Generator 的使用
- 输出斐波那契数列的每一项
以一个著名的斐波那契数列为例子引入:
0 1 1 2 3 5 8 13 21 34 ...
复制代码
要编写一个产生斐波那契数列的函数,可以这么写:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}
// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
复制代码
普通函数只能返回一次,所以必须返回一个Array。但有时我们需要每次拿其中一个返回值,比如做一个与用户交互的进度条
啊之类的,但普通函数只能返回一次,所以不能完成这项需求,但是,如果换成 generator,就可以一次返回一个数,不断返回多次:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
复制代码
直接调用试试:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
复制代码
直接调用一个 generator 和调用函数不一样,fib(5)仅仅是创建了一个 generator 对象,这个对象可以想象成隐藏了结果的数组,需要我们一次次的去触发它,它才会按顺序地一次次的冒出数组元素
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
f.next(); // {value: undefined, done: true}
复制代码
next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false}
,然后“暂停”【所以相比每次执行普通函数去获取斐波那契数列的第n项的性能要好很多】
或者直接用for … of循环迭代generator对象,这种方式不需要我们每次去 next() 和判断 done:
for (var x of fib(5)) {
console.log(x); // 依次输出0, 1, 1, 2, 3
}
复制代码
- 解决回调地狱:(还是上面那个 求一个地级市有多少个辖区 的例子)
ajax('url_1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('url_3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
复制代码
有了generator,用AJAX时可以这么写:
try {
r1 = yield ajax('url_1', data1);
r2 = yield ajax('url_2', data2); // 可以使用ajax('first')的结果,也就是 r1
r3 = yield ajax('url_3', data3); // 可以使用ajax('second')的结果,也就是 r2
success(r3);
}
catch (err) {
handle(err);
}
复制代码
这里使用 try…catch… 语句,所以在 try{ } 区域内不用对结果 r1、r2、r3 进行错误处理,哪个地方出错了自然会被 catch 语句捕获到,并且输出错误参数 err
- 更高级的应用是在 Lazy-loading
(未完成)
Generator
Generator 的实现原理
Generator 的实现原理:我关于它的实现原理的简单理解就是,有点像把函数的结果存到一个对象(数组)里面,然后返回,然后我们在对这个对象(数组)一个一个地遍历,得到每一次我们想想要的结果。只不过区别在于:使用 Generator 代替函数不会造成空间上的浪费,因为是在执行 Generator.next() 的时候才会去运行 Generator 函数
Yield
Generator 函数内部对代码的运行处理方式是:
- 第一次是从 函数起点 到 第一个yield 语句;
- 最后一次是从 最后一个 yield 语句 到 函数终点 或 return;
- 其余的每一次都是从 yield 语句 执行到 下一个yield 语句 后停下。
所以这就好比把一个函数分割成很多很多子块函数,每一块子函数的起点都是 yield 语句,终点是下一个 yield 语句(第一块和最后一块除外),自然就能起到“暂停”的效果【从而实现“同步阻塞”的效果】,而且每一小块函数的返回值都会存在一个叫“迭代器”的容器里面
最终,Generator 函数执行结束后会返回一个迭代器
Generator.next() —— 迭代器模式
定义:迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示
简单理解就是:在不暴露对象的内部表示的情况下,能够遍历出所有元素
下面我们就来实现一个简单的迭代器:
// 在数据获取的时候没有选择深拷贝内容,
// 对于引用类型进行处理会有问题
// 这里只是演示简化了一点
function Iterdtor(arr){
let data = [];
if(!Array.isArray(arr)){
data = [arr];
}else{
data = arr;
}
let length = data.length;
let index = 0;
// 迭代器的核心next
// 当调用next的时候会开始输出内部对象的下一项
this.next = function(){
let result = {};
result.value = data[index];
result.done = (index === length - 1 ? true : false;)
if(index !== length){
index++;
return result;
}
// 当内容已经没有了的时候返回一个字符串提示
return 'data is all done'
};
}
let arr = [1,2,3,4];
// 生成一个迭代器对象。
let iterdtor = new Iterdtor(arr);
复制代码
控制台输出:
async 和 await
async 函数是 Generator 函数的语法糖。使用
async
关键字代替 Generator 函数的星号*
,await
关键字代替yield
相较于Generator函数,async函数改进了以下四点:
- 内置执行器 Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。
- 更好的语义 async 和 await,比起 * 和 yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 更广的适用性 co 模块约定,yield 命令后面只能是
Thunk 函数
或Promise 对象
,而async 函数的await 命令后面,可以是Promise对象
和原始类型的值
。 - 返回值 async 函数的返回值是Promise对象,这比Generator 函数的返回值是 Iterator对象方便多了。你可以用 then 方法指定下一步的操作。
async 和 await 的使用
- 普通函数用 function 关键字开头
- async 函数就用 async function 关键字开头
- await 就是
等待
后面那一项的完成,如果后面那一项没完成绝不会往下走
还是上面那个 求一个地级市有多少个辖区 的例子:
ajax('url_1',data1);
ajax('url_2',data2); // 执行之前需要拿到 ajax('url_1',data1) 的结果
ajax('url_3',data3); // 执行之前需要拿到 ajax('url_2',data2) 的结果
复制代码
使用 async / await:
function fn(url,data) {
return new Promise((resolve, reject) => {
ajax(url, data, function (err, res) {
if (err) reject(err)
resolve(res)
})
})
}
async function asyncFn() {
let p1 = await fn('url_1',data1)
let p2 = await fn('url_2',data2)
let p3 = await fn('url_3',data3)
return p3
}
asyncFn().then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
复制代码
注意事项
- 凡是在前面添加了
async 的函数
在执行后都会自动返回一个 Promise 对象 await
必须在 async 函数里使用,不能单独使用await 后面如果跟的是
Promise 对象,则会等待该对象里面的函数执行完成;如果跟的不是 Promise 对象则会执行其后面的代码 / 等于后面的值
// await 后面是 Promise 对象
let promiseDemo = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('success')
console.log(3)
},0)
})
async function test() {
let result = await promiseDemo // 3
return result
}
// await 后面不是 Promise 对象
let promiseDemo = fn(() => {
console.log(3)
})
async function test() {
let result = await promiseDemo // 3
return result
}
复制代码
async 和 await 的执行顺序
这道题跟 async / await 的应用有关,还和 JavaScript系列 — event loop 事件轮询 有关
console.log(1) // 1
let promiseDemo = new Promise((resolve, reject) => {
console.log(2) // 1 2
setTimeout(() => {
let random = Math.random()
if (random >= 0) {
resolve('success')
console.log(3) // 1 2 4 6 3
} else {
reject('failed')
console.log(3)
}
}, 1000)
})
async function test() {
console.log(4) // 1 2 4
let result = await promiseDemo // 注意这里不用加括号,它是执行 new Promise(...) 后返回的一个对象
return result
}
test().then(result => {
console.log(5) // 1 2 4 6 3 5
}).catch((result) => {
console.log(5)
})
console.log(6) // 1 2 4 6
复制代码
值得注意的是:
- 我们在最开始 new Promise 的时候就已经执行 Promise 里面的箭头函数的代码了
- 而 async 函数 test() 只有在 test().then().catch() 的时候才被调用,test() 函数里面的代码才开始被执行
解析:
- 1 2 4 6 是同步任务,由主线程来办,所以在前半部分输出
- 进入 async 函数 test() 里面后面执行到
await promiseDemo
语句,就会等待定时器完成计时,输出 3 - 然后会返回一个 Promise 对象并赋值给变量 result,async 函数再把这个 Promise 对象返回到函数体外
- 函数体外对这个 Promise 对象使用 then() 方法和 catch() 方法,从而输出 5