异步解决方案

同步与异步

同步是指发起一个请求时,如果未得到请求结果,代码逻辑将会等待,直到结果出来才会继续执行之后的代码。

异步是指当发起一个请求时,不会等待请求结果,直接继续执行后面的代码。请求结果的处理逻辑,会添加一个监听,等到反馈结果出来后,在回调函数中处理对应的逻辑。

使用 Promise 模拟一个发起请求的函数,该函数在 1s 之后返回数值 30。

1
2
3
4
5
6
7
8
9
function fn() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(30);
}, 1000);
});
}

fn().then((res) => console.log(res)); // 输出 30

在该函数的基础上,我们可以使用 async/await 来模拟同步的效果。

1
2
3
4
5
6
7
8
9
10
var foo = async function () {
var res = await fn();
console.log(res);
console.log('next code');
};
foo();

// 输出结果
// 30
// next code

而异步效果则会有不同的输出

1
2
3
4
5
6
7
8
9
10
var foo = function () {
fn().then((res) => {
console.log(res);
});
console.log('next code');
};

// 输出结果
// next code
// 30

ajax

ajax 是网页与服务端进行数据交互的一种技术。
我们可以通过服务端提供的接口,利用 ajax 向服务端请求需要的数据

整个过程的简单实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var url = 'http: www.demo.com/user/info';

var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function () {
if (XHR.readyState == 4 && XHR.state == 200) {
result = XHP.reponse;
console.log(result);
}
};

这看起来没什么麻烦的,但是这时候,如果我们还需要做另一个 ajax 请求,这个请求的参数是从上一个 ajax 请求中获取的,那么我们就不得不如下这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var url = "http: www.demo.com/user/info"

var result;

var XHR = new XMLHttpRequest()
XHR.open('GET', url, true)
XHR.send()

XHR.onreadystatechange = function () {
if (XHR.readyState == 4 && XHR.state == 200) {
result = XHP.reponse
console.log(result)

// 伪代码2
var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}

当出现第三个(甚至更多)仍然依赖上一个请求的时候,代码就变成了一场灾难。

我们需要不停的嵌套回调函数。这样的灾难,我们称之为 回调地狱。

**Promise **可以帮助我们解决这个问题。

Promise

我们知道,如果要确保代码在谁之后执行,可以利用函数调用栈,将想要执行的代码放入回调函数中。

1
2
3
4
5
6
7
8
9
10
11
function want() {
console.log('这是你想要执行的代码');
}

function fn(want) {
console.log('这里表示执行了一大堆其他代码');

// 其他代码执行完后,最后执行回调函数
want && want();
}
fn(want);

或者可以利用任务队列

1
2
3
4
5
6
7
8
9
10
11
function want() {
console.log('这是你想要执行的代码');
}

function fn(want) {
// 根据事件循环的机制,我们就不用非得将代码放在最后面了,由你自由选择
want && setTimeout(want, 0);

console.log('这里表示执行了一大堆其他代码');
}
fn(want);

与 setTimeout 类似, Promise 也可以认为是一种任务分发器,它将任务分发到 PromiseJobs 执行队列中。通常的用法是,我们发起一个请求,然后等待并处理请求结果。

简单用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tag = true;
var p = new Promise(function (resolve, reject) {
if (tag) {
resolve();
} else {
reject();
}
});

p.then(function (result) {
console.log(result);
}).catch(function (err) {
console.log(err);
});

定义

Promise 是一个对象,保存着未来某一时刻才会执行的事件。

我们常使用 Promise 来解决反馈结果需要等待的场景。

例如

  • 前端向服务端发送一个接口请求。请求结果不会马上返回,而是需要等待一段时间。
  • 加载图片,需要等待一段时间
  • 弹窗中,等待用户点击确认或者取消

基础知识

创建 Promise 实例

1
const p = new Promise();

Promise 函数中的第一个参数为一个回调函数,我们可以称之为 executor 。通常情况下,在这个函数中,我们将会执行发起请求操作,并修改结果的状态值。

1
2
3
4
5
6
7
8
const p = new Promise((resolve, reject) => {
if (true) {
resolve();
}
if (false) {
reject();
}
});

状态

状态有三种

  1. pending:等待结果状态
  2. fulfilled:已出结果,结果符合预期完成状态
  3. rejected:已出结果,结果未符合预期完成状态

promise 表达的就是从发起请求开始,从没有结果 padding 到有结果 fulfilled/rejected 的一个过程。

在 executor 函数中,我们可以分别使用 resolve 与 reject 将状态修改为对应的 fulfilled 与 rejected.

resolve/reject 是 executor 函数的两个参数。他们能够将请求结果的具体数据传递出去。

  1. Promise 实例拥有 then 方法,用来处理请求结果变为 fulfilled 状态时的逻辑。then的第一个参数也是一个回调函数,该函数的参数则是 resolve 传递出来的数据。第二个参数用来处理 rejected 状态时的逻辑。
  2. Promise 实例拥有 catch方法,用来处理请求结果变为 rejected 时的逻辑。catch的第一个参数也是一个回调函数,该函数的参数则是 reject 传递出来的数据。

基本使用

写个例子感受一下 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
function fn(num) {
return new Promise(function (resolve, reject) {
// 模拟一个请求,2s 后得到结果
setTimeout(function () {
if (typeof num == 'number') {
resolve(num);
} else {
var err = num + ' is not a number';
reject(err);
}
}, 2000);
});
}

fn('abc')
.then(function (resp) {
console.log(resp);
})
.catch(function (err) {
console.log(err);
});

// 注意观察该语句的执行顺序
console.log('next code');

then方法可以接收两个参数,第一个参数用来接收 fulfilled 状态的逻辑,第二个参数用来处理 rejected 状态的逻辑。

1
2
3
4
5
6
7
8
fn('abc').then(
function (resp) {
console.log(resp);
},
function (err) {
console.log(err);
},
);

因此 catch 方法其实与下面的写法等价。

1
2
3
fn('abc').then(null, function (err) {
console.log(er);
});

then方法返回的仍然是一个 Promise 实例对象,因此 then 方法可以链式调用,通过在内部 return 的方式,能够将数据持续往后传递。

封装 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
var url = 'http://www.demo.com/user/info';

// 封装一个get请求的方法
function getJSON(url) {
return new Promise(function (resolve, reject) {
// 利用ajax发送一个请求
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

// 等待结果
XHR.onreadystatechange = function () {
if (XHR.readyState == 4) {
if (XHR.status == 200) {
try {
var response = JSON.parse(XHR.responseText);
// 得到正确的结果修改状态并将数据传递出去
resolve(response);
} catch (e) {
reject(e);
}
} else {
// 得到错误结果并抛出错误
reject(new Error(XHR.statusText));
}
}
};
});
}

// 封装好之后,使用就很简单了
getJSON(url).then(function (resp) {
console.log(resp);
// 之后就是处理数据的具体逻辑
});

Promise.all

当有一个 ajax 请求,它的参数需要另外 2 个甚至更多请求都有了结果之后才能确定,那么这个时候,就需要 Promise.all 来帮助我们应该这个场景。

1
var p = Promise.all([p1, p2, p3]);

Promise.all 接收一个由 Promise 对象组成的数组作为参数,当 Promise 对象状态都变成 fulfilled 的时候,才会去调用 then 方法。

如果其中一个 Promise 对象状态变成 rejected,那么 p 的状态就会变成 rejected,第一个被 reject 的实例的返回值会传递给回调函数。

如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all() 的 catch() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p5 = new Promise(function (resolve) {
resolve('hello');
});

const p6 = new Promise(function (resolve, reject) {
throw new Error('报错了');
}).catch((e) => e);

Promise.all([p5, p6])
.then((res) => console.log(res))
.catch((e) => console.log(e));

// 输出结果
// ["hello", Error: 报错了]

Promise.race

与 Promise.all 相似的是,Promise.race 也是接收一个 Promise 对象组成的数组作为参数,不同的是,只要当数组中的一个 Promise 状态变为 fulfilled 或者 rejected 时,就可以调用 .then 方法了。

1
var p = Promise.race([p1, p2, p3]);

Promise.race 可以理解为 Promise 实例赛跑,哪个实例有了状态就返回哪个,通常用于处理规定时间内请求超时的情况。

如果 5 秒之内请求无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Promise.race([
new Promise((resolve, reject) => {
// 模拟请求,10秒后返回数据
setTimeout(resolve, 10000, 'my name is a');
}),
new Promise(function (resolve, reject) {
// 处理请求超时
setTimeout(reject, 5000, new Error('超时了'));
}),
])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log('err: ', err);
alert(err);
});

封装

如何封装与使用息息相关。

加载图片

封装一个加载图片的函数,Promise 的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function imageLoad(url) {
const img = new Image();
img.src = url;

return new Promise(function (resolve, reject) {
img.onload = function () {
resolve('图片加载成功');
};

img.onerror = function () {
reject('图片加载失败');
};
});
}

然后我们就可以使用 imageLoad 来执行图片加载完成之后的逻辑。

1
2
3
4
5
6
7
imageLoad('xxx.png')
.then((res) => {
alert(res);
})
.catch((err) => {
alert(err);
});

封装的核心关键是: Promise 的最终目的是,为了执行 then 中的回调函数,我们称它为 then_cb

所以在封装的时候,我们就应该思考如何在 Promise 内部,让 then_cb执行。

简易版 MyPromise

显而易见,Promise 包含原型方法 then,构造函数需要传递回调函数 executor,该回调函数包含两个参数,resolve 与 reject 。

根据这些特点,我们得出:

1
2
3
4
5
6
7
8
9
10
11
12
class MyPromise {
construcotr (executor) {
executor(this._resolve.bind(this), this._reject.bind(this)
}

_resolve (value) {}

_reject () {}

then (then_cb) {}
}

目的是为了调用 then_cb ,通过封装加载图片可以发现,调用 resolve 时,then_cb 才会执行,所以可以得出结论, then_cb 的执行需要被 resolve 触发

我们可以通过保存 then_cb 引用的方式来解决。所以代码就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyPromise {
constructor(executor) {
this.thenCallback = null;

executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
this.thenCallback(value);
}

_reject(value) {}

then(then_cb) {
this.thenCallback = then_cb;
}
}

同理,再解决 catch 回调函数的执行问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyPromise {
constructor(executor) {
this.thenCallback = null;
this.rejectCallback = null;
executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
this.thenCallback(value);
}

_reject(value) {
this.rejectCallback(value);
}

then(then_cb, onRejected) {
this.thenCallback = then_cb;
this.rejectCallback = onRejected;
}

catch(onRejected) {
this.then(null, onRejected);
}
}

如果不追求别的特性,我们的 Promise 对象就已经封装好了,并且可以使用了。

1
2
3
4
5
6
7
8
9
10
11
12
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
// resolve('123')
reject('some err');
}, 1000);
});

// p.then(res => {
// console.log(res);
// })

p.catch((err) => [console.log('err', err)]);

再模拟将 then_cb 放入队列中执行,简单调整如下:

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
class MyPromise {
constructor(executor) {
this.thenCallback = null;
this.rejectCallback = null;
executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
// this.thenCallback(value)
setTimeout(() => {
this.thenCallback(value);
}, 0);
}

_reject(value) {
// this.rejectCallback(value)
setTimeout(() => {
this.rejectCallback(value);
}, 0);
}

then(then_cb, onRejected) {
this.thenCallback = then_cb;
this.rejectCallback = onRejected;
}

catch(onRejected) {
this.then(null, onRejected);
}
}

加入队列机制后,就可以在 executor 中直接执行 resolve ,否则会报错 this.thenCallback is not a function

1
2
3
4
5
6
7
const p = new MyPromise((resolve, reject) => {
resolve('123');
});

p.then((res) => {
console.log(res);
});

Promise.all

Promise.all 返回的是一个数组,我们要将参数 array 内的每个 Promise 的执行结果放在一个数组 result 里,并且 result 数组成员的顺序要与传入时的 array 成员顺序保持一一对应。

实现 Promise.all 的重点在于对 all 参数内 Promise 实例全部执行完毕时机的判断。

因为 Promise 是异步的,我们不能保证 Promise 实例完成的时机与数组顺序一样。也就是说我们不能使用数组的 length 属性来表达 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
const delay = (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, Math.random() * 1000);
});
};

Promise._all = (array) => {
return new Promise((resolve, reject) => {
let count = 0;
const result = [];
for (let i = 0, len = array.length; i < len; i++) {
array[i].then((data) => {
result[i] = data;
count++;
// 因为 array[i] 的执行是异步的,所以这种判断是错误的
// 如果 i===2 的 promise 先执行完毕,result[2] 导致 result.length === 3
if (result.length === array.length) {
resolve(result);
}
}, reject);
}
});
};

const p1 = delay(1);
const p2 = delay(2);
const p3 = delay(3);
Promise._all([delay(2), delay(1), delay(3)]).then(
(res) => {
console.log('res: ', res); // [2, 1, 3]
},
(err) => {
console.log('err: ', err);
},
);

上面的代码,如果最后一个 Promise 先执行完毕,赋值时 result[2] = data,那么 result.length等于 3, 满足 result.length === array.length的判断条件,就会提前执行 resolve 。

所以,我们可以参考垃圾回收机制的引用计数法,在内部添加一个计数变量 count 来判断是否所有 Promise 都已执行完毕。当 count 等于传入的 Promise 数组长度时,表示所有 Promise 都有了结果,然后我们再执行 resolve 将结果传递出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise._all = (array) => {
return new Promise((resolve, reject) => {
let count = 0;
const result = [];
for (let i = 0, len = array.length; i < len; i++) {
array[i].then((data) => {
result[i] = data;
count++;
// 因为array[i]的执行是异步的,所以这种判断是错误的
// 如果i===2的promise先执行完毕,result[2]导致result.length === 3
// if (result.length === array.length) {
// resolve(result)
// }
if (count === array.length) {
resolve(result);
}
}, reject);
}
});
};

async/await

异步问题除了使用 Promise 来解决之外,还可以使用 ES7 中新增的语法 async/await 来搞定。

在函数声明前面,加上关键字 async,这就是 async 的具体使用了。

1
2
3
4
5
6
7
8
async function fn() {
return 30;
}

// 或者
const fn = async () => {
return 30;
};

然后我们查看一下 fn 的运行结果

1
2
3
4
5
6
7
8
console.log(fn());

// result
Promise = {
__proto__: Promise,
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: 30,
};

发现 fn 函数运行返回的是一个标准的 Promise 对象。也就是说 async 其实就是 Promise 的一个语法糖,目的是为了让写法更加简单。于是,我们可以使用 Promise 的相关语法来处理后续的逻辑

1
2
3
fn().then((res) => {
console.log(res); // 30
});

await 含义为等待,意思就是需要等待 await 后面函数运行完了,并且有了返回结果,才能继续执行下面的代码。这正是同步的效果。

需要注意的是,await 关键字只能在 async 函数中使用。并且 await 后面的函数运行后必须返回一个 Promise 对象才能实现同步的效果。

当我们使用一个变量去接收 await 的返回值时,该返回值为 Promise 中 resolve 传递出来的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 30);
});
}

const foo = async () => {
const t = await fn();
console.log('t', t);
console.log('next code');
};

foo();

// 输出结果
// t: 30
// next code

从例子中我们可以看出,在 async 函数中,遇到 await 时,就会等待 await 后面的函数运行完毕,而不会直接执行 next code。

如果我们直接使用 then 方法,就不得不把后续的逻辑写在 then 方法中。

1
2
3
4
5
6
7
const foo = () => {
return fn().then((t) => {
console.log('t: ', t);
console.log('next doce');
});
};
foo();

很明显,如果使用 async/await 的话,代码会更简洁,逻辑也更清晰。

错误处理

  • 在 Promise 中,我们使用 .catch 方法来捕获错误
  • 而使用 async 时,我们使用 try/catch 来捕获错误
    • 如果有多个 await ,只会捕获到第一个错误
      :::info
      tyr/catch 除了可以捕获使用 async 时 Promise 抛出的错误,还可以捕获 throw 关键字抛出的错误。
      :::

在 Promise 中,我们使用 .catch 方法来捕获错误,而不是使用 .then 的第二个参数。因为:

  1. 更接近同步的语法(try/catch)
  2. then 中的错误也会被 catch 捕获

使用 then 的第二个参数,并不能捕获到第一个参数内部抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p3 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(123);
}, 1000);
});
p3.then(
(result) => {
console.log(result);
throw new Error('is err');
},
(error) => {
console.log('error', error);
},
);

// 输出结果
// 123
// Uncaught (in promise) Error: is err

使用 catch ,则可以捕获到上一个 then 内部抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p3 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(123);
}, 1000);
});
p3.then((result) => {
console.log(result);
throw new Error('is err');
}).catch((error) => {
console.log('error', error);
});

// 输出结果
// 123
// error Error: is err

而使用 async 时,我们使用 try/catch 来捕获错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error');
}, 1000);
});
}

const foo = async () => {
try {
await fn();
} catch (e) {
console.log('e: ', e);
}
};
foo();

如果有多个 await ,只会捕获到第一个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn1');
}, 1000);
});
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn2');
}, 1000);
});
}

const foo = async () => {
try {
await fn1();
await fn2();
} catch (e) {
console.log('e: ', e); // some error fn1
}
};
foo();

Generator 函数

Generator 函数可以认为是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

1
2
3
4
5
6
7
8
function* gen(x) {
var y = yield x + 2;
return y;
}

var g = gen(1);
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }

Generator 函数名前要加*号,调用 Generator 函数,会返回一个指针对象(即遍历器)。调用指针对象的 next 方法,会移动内布指针,指向第一个遇到的 yield 语句。

也就是说,next 的作用是分段执行 Generator 函数。每次调用 next 方法,都会返回一个对象,表示当前阶段的信息。

value 属性是 yield 语句后面表达式的值,done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

错误处理

使用 try catch 捕获函数体外抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw'出错了');
// 出错了

async

从 Promise 对象,再到 Generator,异步解决方案每次都有所改进,很多人认为 async 函数是异步操作的终极解决方案。
async 函数就是 Generator 函数的语法糖。
async 函数就是将 Generator 函数的*号替换成 async,将 yield 替换成 await。

1
2
3
4
5
6
7
8
9
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
};

var asyncReadFile = async function () {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
};

async 函数对 Generator 函数的改进,体现在一下三点:

  1. 内置执行器。Generator 函数的执行必须依靠执行器。
  2. 更好的语义。async 和 await,比起星号和 yield,语义更清楚。
  3. 更广的适用性。await 命令后面可以跟 Promise 对象和原始类型的值(数值、字符串、布尔值),Generator 函数只能是 thunk 函数或 Promise 对象。

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×