回调地狱 promise async/await 三种理解与转换案例(附带获取微信运动数据方案)

166 阅读8分钟

回调函数:

mdn:回调函数 - MDN Web 文档术语表:Web 相关术语的定义 | MDN
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。

把一个函数当作参数传递,传递的是函数的定义并不会立即执行,而是在将来特定的时机再去调用,这个函数就叫做回调函数。在定时器setTimeout以及Ajax的请求时都会用到回调函数。

当存在异步任务的代码,不能保证能按照顺序执行,而我们又需要代码顺序执行就回产生回调地狱

setTimeout(function () {//第一层
    console.log(1);
    setTimeout(function () {//第二程
        console.log(2);
        setTimeout(function () {//第三层
            console.log(3);
        }, 1000);
    }, 1000);
}, 1000);

这种回调函数的层层嵌套,就叫做回调地狱。回调地狱会造成代码可复用性不强,可阅读性差,可维护性(迭代性差),扩展性差等等问题。


Promise对象

使用语法

Promise对象是一个原生的JavaScript对象,是一种异步编程的解决方案,可以替换掉传统的回调函数。

你可以把这个对象当一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。

从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
Promise有3个状态:pending(初始)、resloved(成功)、rejected(失败)

  • Promise 状态发生改变,就会触发.then()里的响应函数处理后续步骤。
  • Promise 状态一经改变,不会再变。
  • Promise 实例一经创建,执行器立即执行。

创建一个promise对象

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

  • resolve函数的作用:将Promise对象的状态从“Pending未完成”变成“Resolved成功
  • reject函数的作用在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
new Promise((resolve,reject)=>{
    resolve(); // 操作成功时调用,返回成功后的结果
    reject(); // 操作失败时调用,返回错误参数
})

实列方法:then**()、catch()**、finally()

then()

用来接收处理成功时响应的数据,因此与它对应数据是**resolve函数返回的数据。**每一个then方法中的返回值都是一个promise对象

then()方法有个小问题,每个 then 函数只能访问前一个 then 返回的结果。而解决方法也在下面的技巧中提到

使用demo:
我们来设置一个定时器异步函数

const delay = (ms) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve();
        }, ms);
    });
}

delay(1000)
    .then(() => {
        console.log(1);
        return delay(1000);
    })
    .then(() => {
        console.log(2);
        a = "我是变量a"
        return { a, c:() => { console.log("我是c变量执行的函数") } };//小技巧
    })
    .then((res) => {
        res.c()
        console.log("3", a );
    });

console:
// 1
// 2
// 我是c变量执行的函数
// 3 我是变量a
上述代码中有个小技巧,then中返回的对象里面可以存当前作用域的变量给下一个then中使用

catch()

用来接收处理失败时响应的数据,因此与它对应数据是reject函数返回的数据

finally()

无论成功与否,都会执行的操作( ES2018 引入)

Promise的链式编程可以保证代码的执行顺序,前提是每一次在then做完处理后,一定要return一个Promise对象,这样才能在下一次then时接收到数据。
let p = new Promise((resolve,reject)=>{
    resolve(); // 操作成功时调用,返回成功后的结果
    reject(); // 操作失败时调用,返回错误参数
})
p.then(data => {
    // 接收处理成功时响应的数据
}).catch(err => {
    // 接收上述所有then方法出错后处理失败时响应的数据
}).finally(()=>{
  //无论如何都会执行的方法
})

静态方法:all()、race()、any()、resolve()、reject()

all()

all方法可以完成并发任务, 它接收一个数组,数组的每一项都是一个promise对象,返回一个Promise实例。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。

race()

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。

any()

它接收一个数组,数组的每一项都是一个promise对象,该方法会返回一个新的 promise,数组内的任意一个 promise 变成了resolved状态,那么由该方法所返回的 promise 就会变成resolved状态。如果数组内的 promise 状态都是rejected,那么该方法所返回的 promise 就会变成rejected状态,

resolve()、reject()

巧妙创建Promise实例
一般情况下,都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});
  • Promise.reject

Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:

Promise.reject(new Error(“我错了!!”));
Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担,并且Promise传递中间值⾮常麻烦
并且Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
所以ES2017推出了新的语法async/await来更好的解决异步问题

async/await 语法糖

async函数是ES2017中引入的更为高级的异步处理机制,可以让异步的处理变的更加便捷,相当于是promise语法的 “高级写法”。

使用语法

async/await必须在函数中使用,因此我们将代码包进一个函数,并在函数前加上async,这样我们可以在函数中使用await关键字

async用法

const test = async () => {
    }
test()

await用法

const test = async () => {
  let a = 0
  const res = await new Promise((resolve, reject) => {
    setTimeout(() => {
      a = 1
      resolve(a)
    }, 1000)
  })
  console.log(res) //1
}
test()
await后面是必须是promise对象, 左侧的返回值就是这个promise对象的then方法中回调函数中的value值。换句话说就是,假如是个网络请求,返回的就是得到的数据

解决回调地狱案例

demo1:普通定时器回调函数嵌套

回调地狱

setTimeout(function () {//第一层
    console.log(1);
    setTimeout(function () {//第二程
        console.log(2);
        setTimeout(function () {//第三层
            console.log(3);
        }, 1000);
    }, 1000);
}, 1000);

promis

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

delay(1000)
  .then(() => {
    console.log(1);
    return delay(1000);
  })
  .then(() => {
    console.log(2);
    return delay(1000);
  })
  .then(() => {
    console.log(3);
  });

async/await

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

const run = async () => {
  await delay(1000);
  console.log(1);
  await delay(1000);
  console.log(2);
  await delay(1000);
  console.log(3);
};

run();

demo2:微信运动登录回调函数嵌套获取当前用户微信运动函数

回调地狱

wx.login({
  success (res) {
    if (res.code) {
      let code = res.code //根据官方文档来说需要用户code辅助解密(与本案例无关)
      //发起网络请求
      wx.getWeRunData({
        success: function(runRes) {
          // 获取微信运动加密信息和微信运动辅助解密向量
          let encryptedData = runRes.encryptedData;
          let iv = runRes.iv;
          this.getLoadWeChatStep({//解密api(随便写的来自后端的接口)
            code:code, //根据官方文档来说需要用户code辅助解密(与本案例无关)
            encryptedData:encryptedData,
            iv:iv,
            success: function(res) {
              //解密成功
            },
            fail: function() {
              //解密成功
            }});
        },
        fail: function() {
          // 未开通微信运动
        }
      });
    } else {
      console.log('登录失败!' + res.errMsg)
    }
  }
})

promis

// 首先调用 wx.login() 获取 code
wx.login()
  .then(code => {
    // 使用获取到的 code 继续请求微信运动数据
    return wx.getWeRunData().then(runRes => {
      // 解构赋值,获取加密数据和初始向量
      const { encryptedData, iv } = runRes;
      // 将 code, encryptedData, iv 传递给解密函数
      return { code, encryptedData, iv };
    });
  })
  .then(({ code, encryptedData, iv }) => {
    // 调用解密函数,这里假设 getLoadWeChatStep 已经定义在合适的上下文中
    return this.getLoadWeChatStep(code, encryptedData, iv);
  })
  .then(decryptedData => {
    // 解密成功后的处理
    console.log('解密成功', decryptedData);
  })
  .catch(error => {
    // 错误处理
    console.error(error);
  });

async/await

const processWeRunData = async () => {
  try {
    const code = await wx.login();
    const runRes = await wx.getWeRunData();
    const encryptedData = runRes.encryptedData;
    const iv = runRes.iv;
    const decryptedData = await this.getLoadWeChatStep(code, encryptedData, iv);
    // 解密成功
  } catch (error) {
    console.error(error);
  }
};

processWeRunData();

相关文章分享:
uniapp+vue2如何获取微信运动数据


参考:
async 函数 - JavaScript | MDN
await - JavaScript | MDN
回调函数 - MDN Web 文档术语表:Web 相关术语的定义 | MDN
回调地狱及Promise、async和await_await async 回调地狱-CSDN博客
什么是回调地狱,如何用Promise解决回调地狱,看完这篇你就明白了。 - 掘金 【JavaScript】【回调】回调函数 && 回调地狱 - 掘金
js Promise与async/await用法详解 - 掘金
优雅的处理async/await错误 - 掘金
如何解决回调地狱?教你简单粗暴解决问题 - 掘金

原文:
《回调地狱 promise async/await 三种理解与转换案例(附带获取微信运动数据方案)》