热搜:fiddler git ip 代理 ios
历史搜索

JavaScript异步编程

游客2025-01-09 12:30:01
目录文章目录
  1. 一、什么是异步
  2. 二、回调函数
  3. 三、Promise
  4. 4. Promise 的方法
  5. 5. Promise 的异常处理
  6. 6. Promise 的实现

浏览器中的 JavaScript 程序是典型的事件驱动型程序,即它们会等待用户触发后才真正的执行,而基于的 JavaScript 的服务器通常要等待客户端通过网络发送请求,然后才能执行。这种异步编程在 JavaScript 是很常见的,下面就来介绍几个异步编程的重要特性,它们可以使编写异步代码更容易。

本文将按照异步编程方式的出现时间来归纳整理:

JavaScript异步编程 1

2. await 到底在等啥?

那 await 到底在等待什么呢?

一般我们认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的结果是 Promise 对象或其它值。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数。但要清楚,它等的实际是一个返回值。注意,await 不仅用于等 Promise 对象,它可以等任意表达式的结果。所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test(); // something hello async

await 表达式的运算结果取决于它等的是什么:

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的内容;
  • 如果它等到的是一个 Promise 对象,await 就就会阻塞后面的代码,等着 Promise 对象 resolve,然后将得到的值作为 await 表达式的运算结果。

下面来看一个例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3 秒钟之后出现 hello world
  console.log('cuger')   // 3 秒钟之后出现 cug
}
testAwt();
console.log('cug')  //立即输出 cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以’cug’最先输出,hello world’和 cuger 是 3 秒钟后同时出现的。

3. async/await 的优势

单一的 Promise 链并不能凸显 async/await 的优势。但是,如果处理流程比较复杂,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程,这时async/await的优势就能体现出来了。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。首先用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:vartest>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来会清晰得多,几乎和同步代码一样。

async/await对比 Promise 的优势就显而易见了:

  • 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的理解负担;
  • Promise 传递中间值很麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅;
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise 的错误捕获比较冗余;
  • 调试友好,Promise 的调试很差,由于没有代码块,不能在⼀个返回表达式的箭头函数中设置断点,如果在⼀个.then 代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

4. async/await 的异常处理

利用 async/await 的语法糖,可以像处理同步代码的异常一样,来处理异步代码,这里还用上面的示例:

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
const run = async () => {
    try {
	await exe(false)();
	await exe(true)();
    } catch (e) {
	console.log(e);
    }
}
run();

这里定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象,因此通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉,即执行 exe(false)()exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。运行代码会得到这样的输出:

false
no

这个 false 就是 exe 方法对入参的输出,而这个 no 就是 setTimeout 方法 reject 的回调返回,它通过异常捕获并最终在 catch 块中输出。就像我们所认识的同步代码一样,第四行的 exe(true) 并未得到执行。

标签:async