什么是同步与异步的定义, 在这里我就不做记录, 直接用代码来表示它们之间的区别.
首先使用 Promise 模拟一个发起请求的函数, 该函数执行后, 会在 1s 之后返回数值 30.
- function fn() { return new Promise(function(resolve, reject) {
- setTimeout(function() {
- resolve(30);
- }, 1000);
- })
- }
在该函数的基础上, 我们也可以使用 async/await 语法来模拟同步效果.
- var foo = async function() {
- var t = await fn();
- console.log(t);
- console.log('next');
- }
- foo();
输出结果为:
- Promise {<pending>} //1s 之后依次输出
- test:11 30
- test:12 next
而异步效果则会有不同的输出结果:
- var foo = function() {
- fn().then(function(res) {
- console.log(res);
- });
- console.log('next');
- }
输出结果:
next // 1s 后 30
好了, 接下来我们正式开始记录 Promise
Promise
1. Ajax
Ajax 是网页与服务端进行数据交互的一种技术. 我们可以通过服务端提供的接口, 用 Ajax 向服务端请求我们需要的数据. 过程如下:
- // 简单的 Ajax 原生实现
- // 服务端接口
- var url = 'api/xxxx';
- var result;
- var XHR = new XMLHttpRequest();
- XHR.open('GET', url, true);
- XHR.send();
- XHR.onreadystatechange = function() {
- if(XHR.readyState == 4 && XHR.status == 200) {
- result = XHR.response;
- }
- }
这样看上去并没有什么问题. 但是如果这个时候, 还需要做另一个 Ajax 请求, 那么这个新的 Ajax 请求中的一个参数, 则必须从上一个 Ajax 请求中获取, 这个时候我们就不得不就得在 result 得到后在进行一次请求.
当第三个 Ajax(甚至更多)仍然依赖上一个请求的时候, 此时的代码就变成了一场灾难. 我们需要不停地嵌套回调函数, 以确保下一个接口所需要的参数的正确性, 这样的灾难, 我们称为回调地狱.
所以随着发展, 就出现了 Promise, 他能解决这个问题.
我们想要确保某代码在某某之后执行时, 可以利用函数调用栈, 将想要执行的代码放入回调函数中(这是利用同步阻塞).
- function a(callback) {
- console.log('先结婚')
- callback();
- }
- function b() {
- console.log('再生孩子')
- }
- a(b);
插个题外话:"浏览器最早内置的 setTimeout 与 setInterval 就是基于回调的思想实现的".
但是这里也有一个问题, 我们想要在 a 中执行的代码必须现在 callback 之前才能输出我们想输出的. 那该怎么办?
其实问题很好解决, 除了利用函数调用栈的执行顺序外, 还可以利用队列机制来确保我们想要的代码压后执行.
- function a(callback) {
- // 将想要执行的代码放入队列中后, 根据事件循环机制,
- // 就不用把它放到最后面了.
- callback && setTimeout(callback, 0);
- console.log('先结婚')
- }
- function b() {
- console.log('再生孩子')
- }
- a(b);
与 setTimeout 类似, Promise 也可以认为是一种任务分发器, 它将任务分配到 Promise 队列中, 通常的流程是首先发起一个请求, 然后等待 (等待时间没法确定) 并处理请求结果.
- var tag = true;
- var p = new Promise(function(resolve, reject) {
- if(tag) {
- resolve('tag is true')
- } else {
- reject('tag is false')
- }
- })
- p.then(function(result) {
- console.log(result);
- })
- .catch(function(err) {
- console.log(err);
- })
下面简单介绍一下 Promise 的相关基础知识:
new Promise 表示创建一个 Promise 实例对象.
Promise 函数中的第一参数为一个回调函数, 也可以称之为 executor. 通常情况下, 在这个函数中, 会执行发起请求操作, 并修改结果的状态值.
请求结果有三种状态, 分别是 pending(等待中, 表示还没有得到结果),resolved(得到了我们想要的结果, 可以继续执行), 以及 rejected(得到了错误的, 或者不是我们期望的结果, 拒绝继续执行). 请求结果的默认状态为 pending. 在 executor 函数中, 可以分别使用 resolve 与 rejected 将状态修改为对应的 resolved 与 rejected.resolve,reject 是 executor 函数的两个参数, 它们能够将请求结果的具体数据传递出去.
Promise 实例拥有的 then 方法, 可以用来处理当请求结果的状态变成 resolved 时的逻辑. then 的第一个参数为一个回调函数, 该函数的参数是 resolve 传递出来的数据. 在上面的例子中, result = tag is true.
Promise 实例拥有的 catch 方法, 可用来处理当前请求结果的状态变成 rejectd 时的逻辑. catch 的第一个参数为一个回调函数, 该函数的参数是一个 reject 传递出来的数据. 在上面的例子中, err = tag is false.
下面通过例子来感受一下 Promise 的用法.
- //demo01.js
- function fn(num) {
- // 创建一个 Promise 实例
- return new Promise(function(resolve, reject) {
- if(typeof num == 'number') {
- // 修改结果状态值为 resolved
- resolve();
- } else {
- // 修改结果状态值为 rejected
- reject();
- }
- }).then(function() {
- console.log('参数是一个 number 值');
- }).catch(function() {
- console.log('参数不是一个 number 值');
- })
- }
- // 修改参数的类型, 观察输出的结果
- fn('12');
- // 注意观察该语句的执行顺序
- console.log('next code');
then 方法可以接收两个参数, 第一个参数用来处理 resolved 状态的逻辑, 第二个参数用来处理 rejected 状态的逻辑.
then 方法因为返回的仍是一个 Promise 实例对象, 因此 then 方法可以嵌套使用. 在这个过程中, 通过在内部函数末尾 return 的方式, 能够将数据持续往后传递.
下面我们来对 Ajax 进行一个简单的封装.
- var url = 'api/xxxx';
- // 封装一个 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 res = JSON.parse(XHR.responseText);
- // 得到正确的结果修改状态并将数据传递出去
- resolve(response);
- } catch(e) {
- reject(e)
- }
- } else {
- // 得到错误的结果并抛出异常
- reject(new Error(XHR.statusText));
- }
- }
- }
- })
- }
- // 封装好以后, 使用就很简单了
- getJSON(url).then(function(res){
- console.log(res)
- })
- 2. Promise.all
当有一个 Ajax 请求, 它的参数需要另外两个甚至更多个请求都有返回结果之后才能确定时, 就需要用到 Promise.all 来帮助我们应对这个场景.
Promise.all 接收一个 Promise 对象组成的数组作为参数, 当这个数组中所有的 Promise 对象状态都变成 resolved 或者 rejected 时, 它才会去调用 then 方法.
- var url1 = 'xxx';
- var url2 = 'xxxxx';
- function renderAll() {
- return Promise.all([getJSON(url1), getJSON(url2)]);
- }
- renderAll().then(function(value) {
- console.log(value);
- })
- 3. Promise.race
与 Promise.all 相似的是, Promise.race 也是一个 Promise 对象组成的数组作为参数, 不同的是, 只要当数组中的其中一个 Promise 状态变成了 resolved 或者 rejected 时, 就可以调用 then 方法.
async/await
异步问题不仅可以用 Promise, 还可以用 async/await, 都说这是终极解决方案.
async/await 是 ES7 中新增的语法, 虽然现在有些浏览器已经支持了该语法, 但在实际使用中, 仍然需要在构建工具中配置对该语法的支持才能放心使用.
在函数声明的前面, 加上关键字 async, 这就是 async 的具体使用.
- async function fn() {
- return 30;
- }
- // 或者
- const fn = async ()=> {
- return 30;
- }
- console.log(fn());
- // 打印结果
- Promise {<resolved>: 30}__proto__:Promise[[PromiseStatus]]:"resolved"[[PromiseValue]]:30
可以发现打印结果是一个 Promise 对象, 因此可以猜到 async 其实是 Promise 的一个语法糖, 目的是为了让写法更加简单, 因此也可以使用 Promise 的相关语法来处理后续的逻辑.
- fn().then(res=>{
- console.log(res);
- })
await 的含义是等待, 意思就代码需要等待 await 后面的函数运行完并且有了返回结果之后, 才继续执行下面的代码. 这正是同步的效果.
但是需要注意的是, await 关键字只能在 async 函数中使用, 并且 await 后面的函数运行后必须返回一个 Promise 对象才能实现同步的效果.
当使用一个变量去接收 await 的返回值时, 该返回值为 Promise 中 resolve 传递出来的值, 也就是 PromiseValue.
为了切实感受下 async/await 的用法. 我们结合实际开发中最常遇到的异步请求接口的场景.
- // 先定义接口请求的方法, 由于 jQuery 封装的几个请求方法都是返回 Promise 实例.
- // 因此可以直接使用 async/await 函数实现同步
- const getUserInfo = () => $.get('api/asdsd');
- const clickHandler = async ()=>{
- try{
- const res = await getUserInfo();
- console.log(res);
- // do something
- } catch(e){
- // 处理错误逻辑
- }
- }
为了保证逻辑的完整性, 在实践中 try/catch 必不可少.
来源: https://juejin.im/post/5b2b1f75f265da59b37e7577