什么是同步和异步?
你可能知道, JavaScript 语言 的执行环境是“单线程”
所谓“单线程”, 就是指一次只能完成一件任务, 如果有多个任务, 就必须排队, 前面一个任务完成, 再执行后面一个任务, 以此类推
例如现实生活中的排队
这种模式的好处是实现起来比较简单, 执行环境相对单纯, 坏处是只要有一个任务耗时很长, 后面的任务都必须排队等着, 会拖延整个程序的执行
常见的浏览器无响应(假死), 往往就是因为某一段 JavaScript 代码长时间运行(比如死循环), 导致整个页面卡在这个地方, 其他任务无法执行
为了解决这个问题, JavaScript 语言将任务的执行模式分成两种
- 同步(Synchronous)
- 异步(Asynchronous)
这里的 “同步”和“异步” 与我们现实中的同步、异步恰恰相反
例如:
- 一边吃饭一边打电话, 我们认为这是同时进行(同步执行)的, 但在计算机中, 这种行为叫做异步执行
- 吃饭的同时, 必须吃完饭才能打电话, 我们认为这是不能同时进行(异步执行)的, 但在计算机中, 这种行为我们叫做同步执行
至于为什么, 那你要问英文单词了, 例如 异步(Asynchronous) 翻译成中文是异步的, 但在计算机中, 表示的是我们认知的同时执行的
什么时候我们需要异步处理事件?
- 一种很常见的场景自然就是网络请求了
- 我们封装一个网络请求的函数, 因为不能立即拿到结果, 所以不能像简单的 3 + 4 = 7 一样立刻获得结果
- 所以我们往往会传入另一个函数 (回调函数 callback), 在数据请求成功之后, 再将得到的数据以参数的形式传递给回调函数
JavaScript 和 Node.js 中的异步操作都会在最后执行, 例如 ajax、readFile、writeFile、setTimeout 等
获取异步操作的值只能使用回调函数的方式, 异步操作都是最后执行
回调函数
回调函数的方式获取异步操作内的数据
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, 我们用一个非常夸张的案例来说明
$.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 对象有以下两个特点
- 对象的状态不受外界影响, Promise 对象代表一个异步操作, 有三种状态: pending(进行中)、fulfill(已成功) 和 rejected(已失败), 只有异步操作的结果, 可以决定当前是哪一种状态, 任何其他操作都无法改变这个状态, 这也是 Promise 这个名字的由来, 它的英语意思就是 “承诺”, 表示其他手段无法改变
- 一旦状态改变, 就不会再变, 任何时候都可以得到这个结果, 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 实例
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()
promise.then(data => {
// 这里对 data 就可以进行数据拿取操作了
console.log('success')
}, error => {
console.log('failure')
})
- 第一个回调函数是 Promise 对象的状态变为 fulfilled 时调用
- 第二个回调函数是 Promise 对象的状态变为 rejected 时调用
- 其中, 第二个回调函数是可选的, 不一定要提供, 这两个函数都接受Promise 对象传出的值作为参数
一般来说, 调用 resolve 或 reject 以后, Promise 的使命就完成了, 后继操作应该放到 then 方法里面, 而不应该直接写在 resolve 或 reject 的后面
所以, 最好在将它们加上 return 语句, 这样就不会有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
Promise 链式调用
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 中可以直接返回数据
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 对象最后状态如何, 都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中, 不管promise
最后的状态, 在执行完then
或catch
指定的回调函数以后, 都会执行finally
方法指定的回调函数
finally
方法的回调函数不接受任何参数, 这意味着没有办法知道前面的 Promise 状态到底是fulfilled
还是rejected
, 这表明, finally
方法里面的操作, 应该是与状态无关的, 不依赖于 Promise 的执行结果
Promise.all()
Promise.all()
方法用于将多个 Promise 实例, 包装成一个新的 Promise 实例
const p = Promise.all([p1, p2])
上面代码中, Promise.all()
方法接受一个数组作为参数, p1
、p2
都是 Promise 实例, Promise.all()
方法的参数可以不是数组, 但必须具有 Iterator 接口, 且返回的每个成员都是 Promise 实例
p
的状态由p1
、p2
决定, 分成两种情况
- 只有
p1
、p2
的状态都变成fulfilled
,p
的状态才会变成fulfilled
, 此时p1
、p2
的返回值组成一个数组, 传递给p
的回调函数 - 只要
p1
、p2
之中有一个被rejected
,p
的状态就变成rejected
, 此时第一个被reject
的实例的返回值, 会传递给p
的回调函数
/* 两个异步操作状态都为 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
方法
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 实例
const p = Promise.race([p1, p2])
只要
p1
、p2
之中有一个实例率先改变状态,p
的状态就跟着改变那个率先改变的 Promise 实例的返回值, 就传递给
p
的回调函数Promise.race()
方法的参数与Promise.all()
方法一样
下面是一个例子
/* 第一个异步操作率先完成, 并且状态为 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