你真的了解异步编程了吗?

  JavaScript发展到现在,异步编程已经得到了极大的改进。异步编程的进化史:callback -> promise -> generator -> async + await。现在我们来根据其发展历史一一进行分析。

callback

  在早期的JavaScript中,只支持定义回调函数来表明异步操作完成,它也是异步操作中最基本的一种方法。在这里我们需要知道,回调函数就是将函数作为变量看待,即在一个函数里将变量设置为函数,它不一定是异步代码,异步代码却一定是回调函数 。以下就是一个回调函数的例子:

function test(value, callback) {
    setTimeout(() => callback(value),1000)
}

test(3, (x) => console.log(`测试数据为: ${x}`))
// 测试数据为: 3  (大约在1000ms后)

  上述方式可以完成了一个回调操作,但是如果异步返回值依赖于另一个异步返回值,结果又会怎样呢?让我们看看接下来的一个例子:

function test(value, success, failure) {
    setTimeout(() => {
        try {
            if (typeof value !== 'number') {
                throw 'please provide number as first argument'
            } 
            success(3*value)   // 成功回调
        } catch(e) {
            failure(e)   // 失败回调
        }
    },1000)
}

const successCallback = (x) => {
    test(x, (y) => console.log(`suceess: ${y}`))
}

const failureCallback = (e) => {
    console.log(`failure:${e}`)
}

test(3, successCallback, failureCallback)    // suceess: 27(大约100ms之后)

  很显然,随着代码越复杂,回调策略就难以扩展,嵌套回调的代码也就难以维护,这就是著名的“回调地狱”。

Promise

  什么是Promise?
  Promise 是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在Promise返回给调用者时,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。
  正如前面所述,Promises是一个有状态的对象,它可能处于以下三种状态:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch()返回Promise的状态,此时请求还在进行中。
  • 兑现(fullfilled):操作成功完成,然后调用then()处理函数。
  • 拒绝(rejected):操作失败,然后调用catch()处理函数。

Promise状态图

  依图所示,我们知道无论落定为哪种状态都是不可逆的,只要从pending转换为fullfilled/rejected,期约的状态就不再改变。同时,我们应该了解到期约的状态是私有的,为了避免根据读取到的期约状态以同步方式处理Promise,它不能直接通过JavaScript检测到

  因为期约状态私有,所以只能在期约的执行器函数中进行内部操作。执行器函数主要有两个职责:初始化期约的异步行为和控制状态的最终转换。而一般来说切换期约状态是通过调用它的两个函数参数实现,该两个函数参数通常命名为resolve()和reject()。

let p1 = new Promise((resolve,reject) => resolve())
setTimeout(console.log,0,p1)  // Promise <resolved>

let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log,0,p2)  // Promise <rejected>

  好了,说了这么多,让我们通过一个例子来详细探讨下。在以下例子中,我们通过发送一个请求,获得一个JSON文件:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

console.log(fetchPromise);  // Promise { <state>: "pending" }

fetchPromise
  .then( response => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    } else {
      console.log(`已收到响应:${response.status}`);
    }
    return response.json();  // 获取JSON格式的数据
  })
  .then( json => {
    console.log(json[0].name);
  });

console.log("已发送请求……");

  在上述代码中,通过调用fetch()API将返回值赋予变量fetchPromise,然后将json()这个处理函数传递给Promise的then()方法进行调用。因为json()方法也是异步,我们必须连续调用两个异步函数。我们将一个新的then()处理程序传递response.json()返回的Promise。此处采用链式调用,从而能够避免了回调嵌套而导致的回调地域问题。
  以下为完整的输出结果:

Promise { <state>: "pending" } 
已发送请求…… 
已收到响应:200
baked beans

  可以看出,已发送请求…… 的消息在我们收到响应前就已被输出,即fetch()在请求进行时返回,这使得程序保持响应性。
  在Promise中,有着几个常用的API,分别是Promise.all()Promise.race(),大家可以去详细了解下,这里就不阐述了。

async/await

  在学习异步函数async/await前,让我们先来看一个例子:
    let p = new Promise((resolve, reject) => setTimeout(resolve,1000,2));
  这个期约在1000ms后解决为数值3,如果我们想要访问它,则必须写一个解决处理程序:
    p.then((x) => console.log(x);
  这样极其不方便,任何需要访问该期约产生值的代码都需要以处理程序的形式来接收这个值。为了解决这样的问题,ECMAScript提供了async/await关键字。

1.async

  async关键字用于声明异步函数,它可以用在函数声明、函数表达式、箭头函数和方法上。当异步函数中使用return关键字返回值(没有return则返回undefind)时,这个值会被Promise.resolve()包装成一个Promise对象,然后交由then()的处理程序进行‘解包’。同时,当在异步函数中抛出错误时,会返回一个拒绝期约,但拒绝期约的错误无法被函数捕获。

async function foo() {
  console.log(1);
  return 5;
}

async function qux() {
  console.log(2)
  throw 6;
}

async function baz() {
  console.log(3);
  Promise.reject(7);
}

foo().then(console.log);
qux().catch(console.log);
baz().catch(console.log);
console.log(4);

// 输出结果顺序为:1  2  3  4  5  6  Uncaught (in promise):7

  相信大家看了上面的例子都了解,async关键字就是一个标识符,其执行基本上与普通函数没什么区别。所以这个时候就需要await的暂停和恢复执行的能力了。

2.await

  await关键字能暂停异步函数后面的代码,记录在哪里暂停,让出JavaScript运行时的执行线程;然后尝试‘解包’对象,等到await右边的值可用,JavaScript运行时就会向消息队列中推送一个任务,这个任务就会恢复异步函数的执行。
  下面来看个例子:

async function foo() {
  console.log(1);
  console.log(await Promise.resolve(5));  
  console.log(6);
}

async function qux() {
  console.log(2)
  await (() => {throw 7})();
}

async function baz() {
  console.log(3);
  await Promise.reject(8);
  console.log(9);   // 该行代码不会执行
}

foo().then(console.log);
qux().catch(console.log);
baz().catch(console.log);
console.log(4);

// 输出结果顺序为:1  2  3  4  5  6  7  8

  值得注意一点的是,单独的Promise.reject()不会被异步函数捕捉,而在上述的baz()函数中,对拒绝期约使用await会释放错误值。同时,我们必须明确的一点是,await关键字必须在异步函数中使用,不能在顶级上下文(如<script> 标签)或模块中使用,它的特质也不会扩展到嵌套函数。


  本文就到这里啦!谢谢观看!
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qfbt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值