异步编程

te3jgK.png

什么是同步和异步?

你可能知道, JavaScript 语言 的执行环境是“单线程”

所谓“单线程”, 就是指一次只能完成一件任务, 如果有多个任务, 就必须排队, 前面一个任务完成, 再执行后面一个任务, 以此类推
例如现实生活中的排队

这种模式的好处是实现起来比较简单, 执行环境相对单纯, 坏处是只要有一个任务耗时很长, 后面的任务都必须排队等着, 会拖延整个程序的执行
常见的浏览器无响应(假死), 往往就是因为某一段 JavaScript 代码长时间运行(比如死循环), 导致整个页面卡在这个地方, 其他任务无法执行

为了解决这个问题, JavaScript 语言将任务的执行模式分成两种

  • 同步(Synchronous)
  • 异步(Asynchronous)

这里的 “同步”和“异步” 与我们现实中的同步、异步恰恰相反

例如:

  • 一边吃饭一边打电话, 我们认为这是同时进行(同步执行)的, 但在计算机中, 这种行为叫做异步执行
  • 吃饭的同时, 必须吃完饭才能打电话, 我们认为这是不能同时进行(异步执行)的, 但在计算机中, 这种行为我们叫做同步执行

至于为什么, 那你要问英文单词了, 例如 异步(Asynchronous) 翻译成中文是异步的, 但在计算机中, 表示的是我们认知的同时执行的

什么时候我们需要异步处理事件?

  • 一种很常见的场景自然就是网络请求了
  • 我们封装一个网络请求的函数, 因为不能立即拿到结果, 所以不能像简单的 3 + 4 = 7 一样立刻获得结果
  • 所以我们往往会传入另一个函数 (回调函数 callback), 在数据请求成功之后, 再将得到的数据以参数的形式传递给回调函数

JavaScript 和 Node.js 中的异步操作都会在最后执行, 例如 ajax、readFile、writeFile、setTimeout 等

获取异步操作的值只能使用回调函数的方式, 异步操作都是最后执行

回调函数

回调函数的方式获取异步操作内的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(a, b, callback) {
console.log(1)
setTimeout(function () {
callback(a + b)
}, 1000)
console.log(2)
}

sum(10, 20, function (res) {
console.log(res)
})

// log: 1 2 30

这种方式虽然看似没什么问题, 但是, 当网络请求非常复杂时, 就会出现回调地狱

ok, 我们用一个非常夸张的案例来说明

1
2
3
4
5
6
7
8
9
$.ajax('url1', function (data1) {
$.ajax(data1['url2'], function (data2) {
$.ajax(data2['url3'], function (data3) {
$.ajax(data3['url4'], function (data4) {
console.log(data4)
})
})
})
})
  • 我们需要通过一个 url1 向服务器请求一个数据 data1, data1 中又包含了下一个请求的 url2
  • 我们需要通过一个 url2 向服务器请求一个数据 data2, data2 中又包含了下一个请求的 url3
  • 我们需要通过一个 url3 向服务器请求一个数据 data3, data3 中又包含了下一个请求的 url4
  • 发送网络请求 url4, 获取最终的数据 data4

上面的代码有什么问题?

  • 正常情况下, 不会有什么问题, 可以正常运行并且获取我们想要的数据
  • 但是, 这样的代码阅读性非常差, 而且非常不利于维护
  • 如果有多个异步同时执行, 无法确认他们的执行顺序, 所以通过嵌套的方式能保证代码的执行顺序问题
  • 我们更加期望的是一种更加优雅的方式来进行这种异步操作

Promise

什么是 Promise ?

ES6 中有一个非常重要和好用的特性就是 Promise

Promise 到底是做什么的?

  • Promise 是异步编程的一种解决方案, 比传统的解决方案回调函数和事件更合理和更强大

所谓 Promise, 简单说就是一个容器, 里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果

为了解决回调地狱所带来的问题, ES6 里引进了 Promise, 有了 Promise 对象, 就可以将异步操作以同步操作的流程表达出来, 避免了层层嵌套的回调函数
Promise 对象提供统一的接口, 使得控制异步操作更加容易

Promise 的特点

Promise 对象有以下两个特点

  1. 对象的状态不受外界影响, Promise 对象代表一个异步操作, 有三种状态: pending(进行中)、fulfill(已成功) 和 rejected(已失败), 只有异步操作的结果, 可以决定当前是哪一种状态, 任何其他操作都无法改变这个状态, 这也是 Promise 这个名字的由来, 它的英语意思就是 “承诺”, 表示其他手段无法改变
  2. 一旦状态改变, 就不会再变, 任何时候都可以得到这个结果, Promise 对象的状态改变, 只有两种可能: 从 pending 变为 fulfill从 pending 变为 rejected, 只要这两种情况发生, 状态就凝固了, 不会再发生改变, 会一直保持这个结果, 这时就称为 resolved(已定型), 如果改变已经发生了, 你再对 Promise 对象添加回调函数, 也会立即得到这个结果, 这与事件(Event)完全不同, 事件的特点是, 如果你错过了它, 再去监听, 是得不到结果的

Promise 的缺点

  • 首先, 无法取消 Promise, 一旦新建它就会立即执行, 无法中途取消
  • 其次, 如果不设置回调函数, Promise 内部抛出的错误, 不会反应到外部
  • 第三, 当处于 pending 状态时, 无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

Promise 的三种状态

  • pending : 等待(wait)状态, 比如正在进行网络请求, 或者定时器没有到时间
  • fulfilled : 满足状态, 当我们主动调用 resolve 时, 就处于该状态, 并且回调 .then()
  • rejected : 拒绝状态, 当我们主动调用 reject 时, 就处于该状态, 并且回调 .catch()

Promise 基本用法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Promise((resolve, reject) => {
// ... 某些异步代码

if (/* 异步操作成功 */){
resolve(data); // data 里是异步执行后的返回值
} else {
reject(error); // error 里是异步执行错误后的错误信息
}
}).then(data => {
// 这里对 data 就可以进行数据拿取操作了
console.log('success')
}).catch(error => {
console.log('failure')
})

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

resolve

  • resolve 函数的作用是将 Promise 对象的状态从 “未完成”变为“成功”(即从 pending 变为 fulfilled), 在异步操作成功时调用, 并将异步操作的结果, 作为参数传递出去

reject

  • reject 函数的作用是将 Promise 对象的状态从 “未完成”变为“失败”(即从 pending 变为 rejected), 在异步操作失败时调用, 并将异步操作报出的错误, 作为参数传递出去

then 方法还可以接受两个回调函数作为参数, 合并 .catch()

1
2
3
4
5
6
promise.then(data => { 
// 这里对 data 就可以进行数据拿取操作了
console.log('success')
}, error => {
console.log('failure')
})
  • 第一个回调函数是 Promise 对象的状态变为 fulfilled 时调用
  • 第二个回调函数是 Promise 对象的状态变为 rejected 时调用
  • 其中, 第二个回调函数是可选的, 不一定要提供, 这两个函数都接受Promise 对象传出的值作为参数

一般来说, 调用 resolve 或 reject 以后, Promise 的使命就完成了, 后继操作应该放到 then 方法里面, 而不应该直接写在 resolve 或 reject 的后面
所以, 最好在将它们加上 return 语句, 这样就不会有意外

1
2
3
4
5
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})

Promise 链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success2')
}, 1000)
})
}).then(res => {
console.log(res) // success2
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success3')
}, 1000)
})
}).then(res => {
console.log(res) // success3
})

Promise 链式调用简写

如果我们希望数据直接包装成 Promise.resolve, 那么在 then 中可以直接返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return 'success2'
}).then(res => {
console.log(res) // success2
return 'success3'
}).then(res => {
console.log(res) // success3
})

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何, 都会执行的操作

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中, 不管promise最后的状态, 在执行完thencatch指定的回调函数以后, 都会执行finally方法指定的回调函数

finally方法的回调函数不接受任何参数, 这意味着没有办法知道前面的 Promise 状态到底是fulfilled还是rejected, 这表明, finally方法里面的操作, 应该是与状态无关的, 不依赖于 Promise 的执行结果

Promise.all()

Promise.all()方法用于将多个 Promise 实例, 包装成一个新的 Promise 实例

1
const p = Promise.all([p1, p2])

上面代码中, Promise.all()方法接受一个数组作为参数, p1p2都是 Promise 实例, Promise.all()方法的参数可以不是数组, 但必须具有 Iterator 接口, 且返回的每个成员都是 Promise 实例

p的状态由p1p2决定, 分成两种情况

  1. 只有p1p2的状态都变成fulfilled, p的状态才会变成fulfilled, 此时p1p2的返回值组成一个数组, 传递给p的回调函数
  2. 只要p1p2之中有一个被rejected, p的状态就变成rejected, 此时第一个被reject的实例的返回值, 会传递给p的回调函数
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
/* 两个异步操作状态都为 fulfilled */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})

var p2 = new Promise((resolve, reject) => {
resolve('request2')
})

Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', 'request2']
.catch(e => console.log(e))

/* 其中有一个异步操作状态为 rejected */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})

var p2 = new Promise((resolve, reject) => {
reject('request2 error')
})

Promise.all([p1, p2])
.then(res => console.log(res))
.catch(e => console.log(e)) // 'request2 error'

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

1
2
3
4
5
6
7
8
9
10
11
const p1 = new Promise((resolve, reject) => {
resolve('request1')
})

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

Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', Error: 报错了]
.catch(e => console.log(e))

上面代码中, p1 会 resolved, p2 首先会 rejected, 但是 p2 有自己的catch方法, 该方法返回的是一个新的 Promise 实例, p2 指向的实际上是这个实例

该实例执行完catch方法后, 也会变成 resolved, 导致Promise.all()方法参数里面的两个实例都会resolved, 因此会调用then方法指定的回调函数, 而不会调用catch方法指定的回调函数

如果 p2 没有自己的catch方法, 就会调用Promise.all()catch方法

Promise.race()

Promise.race()方法同样是将多个 Promise 实例, 包装成一个新的 Promise 实例

1
const p = Promise.race([p1, p2])
  • 只要p1p2之中有一个实例率先改变状态, p的状态就跟着改变

  • 那个率先改变的 Promise 实例的返回值, 就传递给p的回调函数

  • Promise.race()方法的参数与Promise.all()方法一样

下面是一个例子

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
/* 第一个异步操作率先完成, 并且状态为 fulfilled */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 2000)
})
])
.then(res => console.log(res)) // request success
.catch(e => console.log(e))

/* 第二个异步操作先完成, 并且状态为 rejected */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 500)
})
])
.then(res => console.log(res))
.catch(e => console.log(e)) // request timeout
-------------本文结束感谢阅读-------------