promise及异步编程async await

Promise是ES6异步编程的一种解决方案(目前最先进的解决方案是async和await的搭配(ES8),但是它们是基于promise的),从语法上讲,Promise是一个对象或者说是构造函数,用来封装异步操作并可以获取其成功或失败的结果。

最重要也是最主要的一个场景就是ajax和axios请求。通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,但是我们下一步要执行的代码依赖于上一次请求返回值,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去。

promise的好处:

  • 防止出现回调地狱;
  • 提高代码的可读性;
  • 像同步操作那样去执行异步操作。

前置说明

ECMAScript 6 新增了正式的 Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制。

JavaScript 是单线程事件循环模型。异步行为是为了优化因计算量大而时间长的操作,只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

同步与异步

JS中同步任务会立即执行并放入调用栈中,而异步任务会被放入事件队列中,等待调用栈中的任务执行完毕后再被推入调用栈中执行。当异步任务被推入调用栈中执行时,它就变成了同步任务。这种机制被称为事件循环。

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值)。

例如

1
2
let x = 3; 
x = x + 4;

在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。等到最后一条指定执行完毕,存储在 x 的值就立即可以使用。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步代码不容易推断

例如,在定时回调中执行一次简单的数学计算:

1
2
let x = 3; 
setTimeout(() => x = x + 4, 1000);

这段程序虽与上面同步代码执行的任务一样,都是把两个数加在一起,但这一次执行线程不知道 x 值何时会改变,因为这取决于回调何时从消息队列出列并执行。

虽然这个例子对应的低级代码最终跟前面的例子没什么区别,但第二个指令块(加操作及赋值操作)是由系统计时器触发的,这会生成一个入队执行的中断。到底什么时候会触发这个中断,这对 JavaScript 运行时来说是一个黑盒。

异步编程主要包含以下几类,异步编程传统的解决方案是回调函数,这种传统的方式容易导致“回调地狱”,即函数多层嵌套,让代码难以阅读,维护困难,空间复杂度大大增加。

  • fs 文件操作

    1
    require('fs').readFile('./index.html', (err,data)=>{})
  • 数据库操作

  • AJAX

    1
    $.get('/server', (data)=>{})
  • 定时器

    1
    setTimeout(()=>{}, 2000);

Promise 对象

Promise属于ES6规范, 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。期约故意将异步行为封装起来,从而隔离外部的同步代码。

  1. 从语法上来说: Promise 是一个构造函数
  2. 从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值
1
2
const p=new Promise(()=>{})
console.log(p);

Promise 的状态及结果

Promise 构造函数: Promise (excutor)

  • executor 函数: 执行器 (resolve, reject) => {}
  • executor 会在 Promise 内部立即同步调用(即executor函数直接放入调用栈中,而不是消息队列,new Promise()执行时就会立即执行executor 函数),异步操作在执行器中执行
1
2
3
4
5
let p = new Promise((resolve, reject) => {
// 同步调用
console.log(111);
});
console.log(222);

期约是一个有状态的对象,Promise实例对象中的属性PromiseState存储着该Promise实例的状态,可能处于如下 3 种状态之一:

  • 待定(pending):期约的最初始状态,在待定状态下,期约可以落定(settled)为resolved状态或reject状态。无论落定为哪种状态都是不可逆的。只要从待定转换为解决或拒绝,期约的状态就不再改变。
    例如使用一个空函数对象来应付一下解释器:
1
2
let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending>

之所以说是应付解释器,是因为如果不提供执行器函数,就会抛出 SyntaxError。
无论 resolve()和 reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败,如下所示:

1
2
3
4
5
let p = new Promise((resolve, reject) => { 
resolve();
reject(); // 没有效果
});
setTimeout(console.log, 0, p); // Promise <resolved>
  • 解决(resolved,有时候也称为“兑现”,fulfilled):代表成功
  • 拒绝(rejected):代表失败
    无论变为成功还是失败, 都会有一个结果数据(这个数据存储在Promise实例对象的PromiseResult属性中),成功的结果数据一般称为 value, 失败的结果数据一般称为 reason

期约主要有两大用途:

  • 首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。“待定”表示尚未开始或者正在执行中。“解决”表示已经成功完成,而“拒绝”则表示没有成功完成
  • 在另外一些情况下,期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。
    为了支持这两种用例,每个期约只要状态切换为解决,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。无论是值还是理由,都是包含原始值或对象的不可修改的引用。二者都是可选的,而且默认值为 undefined。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。
    由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误
1
2
3
4
5
let p1 = new Promise((resolve, reject) => resolve()); 
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易

Promise方法

Promise.prototype.then()

Promise.prototype.then([onResolved|null],[onRejected|null])是为期约实例添加处理程序的主要方法。

参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。

返回值:一个新的Promise对象

可以指定多个回调,当promise对象变为相应的状态的时候就都会执行.

1
2
3
4
5
6
7
8
9
10
11
let p = new Promise((resolve, reject) => {
resolve('OK');
});
// 指定回调 - 1
p.then(value => {
console.log(value);
});
// 指定回调 - 2
p.then(value => {
alert(value);
});

问:then()返回的Promise对象的状态是如何决定的呢?

① 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常

1
2
3
4
5
6
7
8
9
10
11
let p = new Promise((resolve, reject) => {
resolve('ok');
});
// 执行 then 方法
let result = p.then(value => {
console.log(value);
// 1. 抛出错误
throw '出了问题';
}, reason => {
console.warn(reason);
});

② 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值

1
2
3
4
5
6
7
8
9
10
11
12
let p = new Promise((resolve, reject) => {
resolve('ok');
});
// 执行 then 方法
let result = p.then(value => {
console.log(value);
// 2. 返回结果是非 Promise 类型的对象
return 521;
}, reason => {
console.warn(reason);
});
console.log(result);

③ 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p = new Promise((resolve, reject) => {
resolve('ok');
});
// 执行 then 方法
let result = p.then(value => {
console.log(value);
// 3. 返回结果是 Promise 对象
return new Promise((resolve, reject) => {
// resolve('success');
reject('error');
});
}, reason => {
console.warn(reason);
});
console.log(result);

不管调用的是onResolved还是onRejected函数,返回的新promise对象主要由返回值决定(抛出错误的情况除外)

then()的链式调用

问:promise 如何串连多个操作任务?

(1) promise 的 then()返回一个新的 promise, 可以形成 then()的链式调用

(2) 通过 then 的链式调用串连多个同步/异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
});
const result=p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}).then(value => {
console.log(value);
}).then(value => {
console.log(value);
})
console.log(result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}) // Promise (resolved): 'success'

p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}).then(value => {
console.log(value);
}) // Promise (resolved): undefined 因为没有返回值也没有抛出错误,所以值是undefined, 因为undefined不是Promise对象,所以返回的Promise状态为resolved

p.then(value => {
return new Promise((resolve, reject) => {
resolve("success");
});
}).then(value => {
console.log(value);
}).then(value => {
console.log(value);
}) // Promise (resolved): undefined

中断then()链

方法:在then()的回调函数中返回一个 pendding 状态的 promise 对象。因为pendding状态的promise对象不会触发onResolved()或onRejected()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
});
p.then(value => {
console.log(111);
// 有且只有一个方式
return new Promise(() => {});//
}).then(value => {
console.log(222);
}).then(value => {
console.log(333);
}).catch(reason => {
console.warn(reason);
});

穿透:当没有指定相应Promise状态的回调函数时,就可以跳过执行该then()

练习:分析输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p = new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('OK');
reject('Err');
}, 10);
});
p.then(null,reason => {
console.log(111);
}).then(null,reason => {
console.log(222);
}).then(null,reason => {
console.log(333);
}).catch(reason => {
console.warn(reason);
});

分析:p.then(null,reason => { console.log(111);})的返回值是Promise (resolved):undefined, 后面没有对应的onResolved的回调函数,就跳过了执行,所以控制台只输出了111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
// reject('Err');
}, 10);
});
// console.log(p.then(value => {
// console.log(111);
// },reason=>{
// throw '失败啦!';
// })==p.then(value => {
// console.log(111);
// },reason=>{
// throw '失败啦!';
// }).then(value => {
// console.log(222);
// })); // false

console.log(p.then(value => {
console.log(111);
},reason=>{
throw '失败啦!';
}).then(value => {
console.log(222);
}).then(value => {
console.log(333);
}));

p.then(value => {
throw '失败啦!';
}).then(value => {
console.log(222);
}).then(value => {
console.log(333);
}).catch(reason => {
console.warn(reason);
});

p.then(value => {throw ‘失败啦!’;})返回Promise (rejected): ‘失败啦!’,后面没有对应的onRejected()回调函数,就跳过了执行,所以就直接执行catch()来捕获错误信息

Promise.prototype.catch()

Promise.prototype.catch(onRejected)

catch()基于then()做了一个单独的封装,只接收rejected状态的回调函数

给Promise对象设置回调函数的方法有Promise.prototype.then()和Promise.prototype.catch(),注意Promise.prototype.catch()只能指定错误的回调函数

Promise.resolve()

静态方法,不是实例方法

期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。

下面两个期约实例实际上是一样的:

1
2
let p1 = new Promise((resolve, reject) => resolve()); 
let p2 = Promise.resolve();

参数:成功的数据或 promise 对象

说明: 返回一个成功/失败的 promise 对象

  • 参数为非Promise对象,则返回一个成功的Promise对象,成功的结果为该参数
1
2
let p1 = Promise.resolve(521);
console.log(p1);

1
2
let p = Promise.resolve(new Error('foo')); 
setTimeout(console.log, 0, p); // Promise <resolved>: Error: foo
  • 参数为Promise对象时,则返回该Promise对象,对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p = Promise.resolve(7); 
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true
// 这个幂等性会保留传入期约的状态:
let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true

let p2 = Promise.resolve(new Promise((resolve, reject) => {
resolve('OK');
}));
console.log(p2);
p2.catch(reason => {
console.log(reason);
})

1
2
3
4
5
6
7
let p2 = Promise.resolve(new Promise((resolve, reject) => {
reject('Error');
}));
console.log(p2);
p2.catch(reason => {
console.log(reason);
})

Promise.reject()

与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。

下面的两个期约实例实际上是一样的:

1
2
let p1 = new Promise((resolve, reject) => reject()); 
let p2 = Promise.reject();

参数:失败的理由或 promise 对象

说明: 返回一个失败的 promise 对象

  • 参数为非Promise对象时,返回的失败Promise对象的理由就为该参数值
1
2
let p1 = Promise.reject(521);
console.log(p1);

  • 参数为Promise对象时,返回的失败Promise对象的理由就为该参数值(即失败的理由就是该Promise对象)
1
2
3
4
let p2 = Promise.reject(new Promise((resolve, reject) => {
resolve('OK');
}));
console.log(p2);

Promise.all()

参数: 包含 n 个 promise 的数组

说明: 返回一个新的 promise, 只有所有的 promise 都成功该promise才成功,成功结果为所有promise成功结果组成的数组。只要有一个promise失败了该promise就失败,失败理由为第一个失败的promise的失败理由

  • 当所有promise都成功时,返回一个新的成功的promise,成功结果为所有promise成功结果组成的数组
1
2
3
4
5
6
7
let p1 = new Promise((resolve, reject) => {
resolve('OK');
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');
const result = Promise.all([p1, p2, p3]);
console.log(result);

  • 当有失败的promise时该promise就失败,失败理由为第一个失败的promise的失败理由
1
2
3
4
5
6
7
let p1 = new Promise((resolve, reject) => {
resolve('OK');
})
let p2 = Promise.reject('Error1');
let p3 = Promise.reject('Error2')
const result = Promise.all([p1, p2, p3]);
console.log(result);

Promise.race()

通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。

参数: 包含 n 个 promise 的数组

说明: 返回一个新的 promise, 该promise等于第一个完成的 promise(即第一个确定状态的promise)

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('OK');
}, 1000);
})
let p2 = Promise.resolve('Success');
let p3 = Promise.resolve('Oh Yeah');
// 调用
const result = Promise.race([p1, p2, p3]);
console.log(result);

改变Promise对象状态的方式

  1. promise回调函数中改变
1
2
3
4
5
6
7
8
9
let p = new Promise((resolve, reject) => {
//1. resolve 函数
// resolve('ok'); // pending => fulfilled (resolved)
//2. reject 函数
// reject("error");// pending => rejected
//3. 抛出错误
// throw '出问题了';// pending => rejected
});
console.log(p);

  1. 调用实例方法Promise.resolve()或Promise.reject()

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

下面是一个抽奖示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<button id="btn">点击抽奖</button>
<script>
const btn=document.getElementById("btn")
btn.onclick=function(){
// 每次点击按钮创建一个Promise实例对象
const p =new Promise((resolve,reject)=>{
setTimeout(()=>{
let n =parseInt(Math.random()*101)//取值[1,100]的整数
if(n<30){
resolve(n)
}else{
reject(n)
}
},10)
})
p.then((value)=>{
alert("恭喜中奖!中奖数字为"+value);
},(reason)=>{
alert("再接再厉! 中奖数字为"+reason);
})
}
</script>
</body>

加载图片资源例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!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>
<div></div>
<script>
function loadImageAsync(url) {
var promise = new Promise(function (resolve, reject) {
const image = new Image();
image.onload = function () {
resolve(image);
};
image.onerror = function () {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
return promise;
}
loadImageAsync("http://iwenwiki.com/api/vue-data/vue-data-1.png")
.then(function(data){
console.log(data);
$("div").append(data)
},function(error){
$("div").html(error)
})
</script>
</body>
</html>

Promise对象_Ajax实操

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!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>
<script>
const getJSON = function (url) {
const promise = new Promise(function (resolve, reject) {
const handler = function () {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("http://iwenwiki.com/api/blueberrypai/getIndexBanner.php").then(function (json) {
console.log(json);
}, function (error) {
console.error('出错了', error);
});
</script>
</body>
</html>

util.promisefy()函数

传入一个遵循常见的错误优先的回调风格的函数(即以(err, value)=>{…}回调作为最后一个参数),并返回一个返回值为promise对象的函数版本

“错误优先”是指回调函数中,错误参数为回调函数的第一个参数,node.js环境中fs模块中的异步api大多是这种风格的
参数:函数

返回值:函数,返回的该函数的返回值是promise对象

将函数promise化的好处:函数promise化之后,函数的返回值就变成了promise对象,这样就可以调用promise的实例方法then()或catch(),利用它们的链式写法,就能避免回调函数多层嵌套

示例:调用util.promisefy()函数,将fs.readFile()函数promise化

1
2
3
4
5
6
7
8
9
10
// 引入 util 模块
const util = require('util');
// 引入 fs 模块
const fs = require('fs');
// promise化fs.readFile()函数
let mineReadFile = util.promisify(fs.readFile);
// promise化之后就可以调用promise实例的then()或catch()方法
mineReadFile('./resource/content.txt').then(value=>{
console.log(value.toString());
});

封装promisefy函数

在实际应用中,一个函数满足这几个条件,就可以被 promisify 化:

  • 该方法必须包含回调函数
  • 回调函数必须执行
  • 回到函数第一个参数代表 err 信息,第二个参数代表成功返回的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
// promisefy()返回一个函数,返回的该函数的返回值是Promise对象
// fn指要promise化的函数
const promisefy = (fn) => {
// ‘...args’表示剩余参数
return function(...args){
return new Promise((resolve,reject)=>{
fn(...args,(err,data)=>{
if(err) reject(err);
resolve(data)
})
})
}
}

封装Promise类实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// 在异步任务中不能抛出错误,即使是内置的Promise也捕获不到

class Promise {
constructor(executor){
// 设置默认的PromiseState和PromiseResult
// 因为Promise()构造函数是以实例的方式调用(new运算符),所以this指向实例
this.PromiseState='pendding'
this.PromiseResult=null
// 将callback对象定义到实例内部,用来存储后面通过then()或catch()指定的onResolved()和onRejected()函数,这是考虑到executor中通过异步任务改变promise实例状态和多个then()指定了多个onResolved()或onRejected()回调的情况
this.callbacks=[];
// 而resolve()和reject()是以函数形式调用的,this为window对象,所以这里用self存储指向实例的this
const self=this;
function resolve(data){
// 该判断为了避免状态重复改变
if(self.PromiseState!=='pendding') return;
self.PromiseState='fulfilled'//或'resolved'
self.PromiseResult=data
// 注意要在Promise实例状态改变并且数据赋值之后调用onResolved
// 注意在调用前要判断有没有定义onResolved()函数,所以就看callbacks数组第一个元素有没有onResolved属性
// if(self.callbacks[0].onResolved){
// 通过setTimeout将then中的回调设置成异步, 因为内置的Promise的实例方法then()中的回调是异步的

setTimeout(()=>{
self.callbacks.forEach(item => {
item.onResolved(data)
})
});
// }
};
function reject(data){
// 该判断为了避免状态重复改变
if(self.PromiseState!=='pendding') return;
self.PromiseState='rejected'
self.PromiseResult=data
// 注意要在Promise实例状态改变并且数据赋值之后调用onRejected
// 注意在调用前要判断有没有定义onRejected()函数,所以就看callbacks数组第一个元素有没有onRejected属性
// if(self.callbacks[0].onRejected){
// 通过setTimeout将then中的回调设置成异步, 因为内置的Promise的实例方法then()中的回调是异步的

setTimeout(()=>{
self.callbacks.forEach(item => {
item.onRejected(data)
})
})

// }
};
try{
// executor在传入时就能确定是个函数,所以这里不需要再定义executor, 只是调用即可
executor(resolve,reject)
}catch(e){
reject(e)
}
}

//添加 then 方法
then(onResolved, onRejected){
self=this
// 考虑异常穿透情况
if(typeof onRejected!=='function'){
onRejected=reason=>{
throw reason;
}
}
if(typeof onResolved!=='function'){
onResolved=value=>value // (value)=>{return value}的简写形式
}
//

return new Promise((resolve,reject)=>{
// 这个callback()函数得放到返回的Promise实例里,否则会报错,说resolve和reject未定义
function callback(type){
try{
// 因为callback()是以函数形式调用的,此时this指向window对象,所以此处参数不能为this.PromiseResult
let result=type(self.PromiseResult)
if(result instanceof Promise){
result.then(v=>{
resolve(v)
},e=>{
reject(e)
})
}else{// 抛出错误时result是undefined还是null? 不进入该分支应该
resolve(result)
}
}catch(e){//考虑抛出错误的情况
reject(e)
}
}
// 考虑executor中同步任务改变promise实例状态

if(this.PromiseState==='fulfilled'){
// 通过setTimeout将then中的回调设置成异步, 因为内置的Promise的实例方法then()中的回调是异步的
setTimeout(()=>{
callback(onResolved)
})
}

if(this.PromiseState==='rejected'){
// 通过setTimeout将then中的回调设置成异步, 因为内置的Promise的实例方法then()中的回调是异步的

setTimeout(()=>{
callback(onRejected)
})
}
//

// 考虑executor中异步任务改变promise实例状态
if(this.PromiseState==='pendding'){
this.callbacks.push({
onResolved:function(){
callback(onResolved)
},
onRejected:function(){
callback(onRejected)
}
})
}
})
}
// 定义Promise.prototype.catch()
catch(onRejected){
return this.then(undefined,onRejected)
}
// 定义Promise.resolve()静态方法,注意静态方法要加关键词static
static resolve(value){
return new Promise((resolve,reject)=>{
if(value instanceof Promise){
value.then(
v=>resolve(v),
e=>reject(e))
}else{
resolve(value)
}
})
}
// 定义Promise.reject()静态方法
static reject(reason){
return new Promise((resolve,reject)=>{
reject(reason)
})
}
// 定义Promise.all()静态方法
static all(promises){
let arr=[]
let count=0;
return new Promise((resolve,reject)=>{
for(let i in promises){
promises[i].then(v=>{
arr[i]=v;
count++;
if(count===promises.length){
resolve(arr)
}
},r=>{
reject(r)
})
}
// 为什么这个判断不能放在这
// if(count===promises.length){
// resolve(arr)
// }
})
}
// 定义Promise.race()静态方法
static race(promises){
return new Promise((resolve,reject)=>{
for(let i in promises){
promises[i].then(
v=>resolve(v),
r=>reject(r))
}
})
}
}

Async 函数

async 英文单词的意思是异步,虽然它是 ES8 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写法,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。异步函数的返回值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。

ES2017 标准引入了 async 函数,使得异步操作变得更加方便

async函数可以将异步操作变为同步操作

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定
    • 如果返回值是一个非Promise类型的数据,相当于执行了Promise.resolve(), 则返回的Promise实例状态为fulfilled
    • 如果返回值是一个Promise对象,相当于执行了Promise.resolve(返回值),则返回的Promise实例等效于该Promise对象
1
2
3
4
5
6
7
8
9
10
11
async function main(){
// return 521; // Promise<fulfilled>:521
return new Promise((resolve, reject) => {
// resolve('OK'); // Promise<fulfilled>:'OK'
reject('Error'); // Promise<rejected>:'Error'
});
// 3. 抛出异常
// throw "Oh NO"; // Promise<rejected>:"Oh NO"
}
let result = main();
console.log(result);

示例代码

1
2
3
4
5
6
7
8
function print(){
setTimeout(() =>{
console.log("定时器");
},1000)
console.log("Hello");
}

print()

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}

asyncPrint('hello world', 50);

异步应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function ajax(url){
return new Promise(function(resolve,reject){
$.getJSON(url,function(result){
resolve(result)
},function(error){
reject(error)
})
})
}

async function getInfo(){
let ids = await ajax("http://iwenwiki.com/api/generator/list.php")
let names = await ajax("http://iwenwiki.com/api/generator/id.php?id="+ids[0])
let infos = await ajax("http://iwenwiki.com/api/generator/name.php?name=" + names.name)
console.log(infos);
}

getInfo();

await 表达式

await 可以理解为 async wait 的简写,表示等待异步执行完成。

await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码

  1. await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
  2. 如果表达式是 promise 对象, await 返回的是 promise 成功的值, promise对象状态是rejected时,需要捕获错误来查看错误理由
  3. 如果表达式是其它值, 直接将此值作为 await 的返回值

注意

  1. await 必须写在 async 函数中, 但 async 函数中可以没有 await
  2. 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try…catch 捕获处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
return new Promise(function (resolve) {
setTimeout(function () {
// 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
resolve(v);
}, 2000);
});
}

// 一个用于正常输出内容的函数
function log() {
console.log("2.正在操作");
}

async function fn() {
console.log("1.开始");
await log();
let p1 = await p("3.异步请求");
console.log(p1);
console.log("4.结束");
}
fn();

根据页面效果,源代码解析如下:

  • fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。
  • 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。
  • 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
  • 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
  • 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 “4.结束”,在 async 函数中的执行顺序,如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function main(){
let p = new Promise((resolve, reject) => {
// resolve('OK');
reject('Error');
})
// 1. 右侧为promise的情况
// let res = await p;
// 2. 右侧为其他类型的数据, 但这种情况不常见
// let res2 = await 20;
// 3. 如果promise是失败的状态,需要捕获错误来查看错误理由
try{
let res3 = await p;
console.log(res3);
}catch(e){
console.log(e);
}
}
main();

async与await结合实践

1.txt

1
2
3
4
观书有感
-朱熹
半亩方塘一鉴开
天光云影共徘徊。

2.txt

1
2
问渠那得清如许?
为有源头活水来。

3.txt

1
2
3
 
---------
中华古诗词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const fs = require('fs')
const util= require('util')
// 将fs.readFile promise化,即将异步函数转换成同步的形式(只是转换形式,实质还是异步)
const myReadFile=util.promisify(fs.readFile)
// 传统的读多个文件,并将文件内容串联起来输出。缺点是会有多个回调嵌套
// fs.readFile('./resource/1.txt',(err,data1)=>{
// if(err) throw err;
// fs.readFile('./resource/2.txt',(err,data2)=>{
// if(err) throw err;
// fs.readFile('./resource/2.txt',(err,data3)=>{
// if(err) throw err;
// console.log(data1+data2+data3);
// })
// })
// })

// 结合使用async和await, 将异步任务以同步的形式调用,减少嵌套,结构更清晰
async function main(){
try{
const data1=await myReadFile('./resource/1.txt')
const data2=await myReadFile('./resource/2.txt')
const data3=await myReadFile('./resource/3.txt')
console.log(data1+data2+data3);
}catch(e){
console.log(e);
}
}
main()

async与await结合发送AJAX请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// axios是基于Promise封装的AJAX请求库
function sendAJAX(url){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open("GET", url);
xhr.send();
// 处理结果
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
// 判断成功
if(xhr.status >= 200 && xhr.status < 300){
// 成功的结果
resolve(xhr.response);
}else{
reject(xhr.status);
}
}
}
});
}

// 段子接口地址 https://api.apiopen.top/getJoke
let btn = document.querySelector('#btn');

btn.addEventListener('click',async function(){
// 获取段子信息
let duanzi = await sendAJAX('https://api.apiopen.top/getJoke');
console.log(duanzi);
});

本文标题:promise及异步编程async await

文章作者:LiJing

发布时间:2023年05月16日 - 17:55:57

最后更新:2023年06月03日 - 10:03:08

原始链接:https://blog-next.xiaojingge.com/posts/4156702810.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------------本文结束 感谢您的阅读-------------------