专栏名称: 奇舞周刊
《奇舞周刊》是由奇舞团维护的前端技术周刊。除周五外,每天向大家推荐一篇前端相关技术文章,每周五向大家推送周刊汇总内容。
目录
相关文章推荐
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  12 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  23 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  23 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  昨天  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
51好读  ›  专栏  ›  奇舞周刊

异步代码的进化之路:回调,Promise,async\/await

奇舞周刊  · 公众号  · 前端  · 2017-11-20 08:00

正文

编者按:本文由codercao在众成翻译平台上翻译。 原文名为《JavaScript是如何工作的:事件循环、异步编程的兴起、5个关于如何使用async/await编写更简洁代码的技巧》。

书接上回。昨天我们已经介绍了事件循环和Job Queue的机制,今天分享的是原文的后半部分——异步代码的发展历程,并比较三种异步模式的优缺点,希望大家看完能够收获新知识~

(图片来自网络)

回调

你已经知道,回调是目前为止在JavaScript程序中表达和管理异步的最常用的方式。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,都是基于回调编写的,除了回调并没有用到其他的异步基础。

不过回调并不完美。许多开发人员都在尝试寻找更好的异步模式。但是,如果你不理解底层的实际情况,就不可能有效地使用任何抽象。

在接下来的章节中,我们将深入探讨这些抽象概念,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是推荐的。

嵌套回调

看看下面这段代码

listen('click', function (e){

    setTimeout(function(){

        ajax('https://api.example.com/endpoint', function (text){

            if (text == "hello") {

            doSomething();

        }

        else if (text == "world") {

            doSomethingElse();

            }

        });

    }, 500);

});

我们已经将三个函数的嵌套成了一条链,每一环都表示异步序列中的一个步骤。

这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与缩进/缩进几乎没有任何关系。这是一个更深层次的问题。

首先,我们在等待“click”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次出现。

乍一看,这段代码似乎可以自然地将它的异步映射为连续的步骤:

listen('click', function (e) {

    // ..

});

然后我们有:

setTimeout(function(){

    // ..

}, 500);

然后我们有:

ajax('https://api.example.com/endpoint', function (text){

    // ..

});

最后:

if (text == "hello") {

    doSomething();

}

else if (text ==  "world") {

    doSomethingElse();

}

因此,这样一种顺序的方式来表达您的异步代码似乎更自然,不是吗?一定有这样的方法,对吧?

Promise

看看下面这段代码:

var x = 1;

var y = 2;

console.log(+ y);

这段代码非常简单:它对 x y 的值进行求和,并将其打印到控制台。但是,如果 x y 的值缺失了,还有待确定呢?比如,我们需要从服务器检索 x y 的值,然后才能在表达式中使用它们。假设我们有一个函数 loadX loadY ,分别从服务器加载 x y 的值。然后,想象一下,我们有一个函数 sum ,一旦它们都被加载了,它就将 x y 的值相加。 它可能是这样的(很难看,不是吗)

function sum(getX, getY, callback) {

    var x, y;

    getX(function(result) {

        x = result;

        if (!== undefined) {

            callback(+ y);

        }

    });

    getY(function(result) {

        y = result;

        if (!== undefined) {

            callback(+ y);

        }

    });

}

// A sync or async function that retrieves the value of `x`

function fetchX() {

    // ..

}


// A sync or async function that retrieves the value of `y`

function fetchY() {

    // ..

}

sum(fetchX, fetchY, function(result) {

    console.log(result);

});

这里有一些非常重要的东西——在这个片段中,我们将 x y 作为 未来 的值,并表示了一个操作 sum(…) (从外部看),它并不关心 x y 当前可不可用。

当然,这种粗糙的基于调用的方法还有很多值得期待的。这只是迈向理解 未来值 的好处的第一步,而不用担心时间的问题。

Promise 值

让我们简要地看看我们如何用Promise来表达 x+y 的例子:

function sum(xPromise, yPromise) {

    // `Promise.all([ .. ])` takes an array of promises,

    // and returns a new promise that waits on them

    // all to finish

    return Promise.all([xPromise, yPromise])


    // when that promise is resolved, let's take the

    // received `X` and `Y` values and add them together.

    .then(function(values){

        // `values` is an array of the messages from the

        // previously resolved promises

        return values[0] + values[1];

    } );

}


// `fetchX()` and `fetchY()` return promises for

// their respective values, which may be ready

// *now* or *later*.

sum(fetchX(), fetchY())


// we get a promise back for the sum of those

// two numbers.

// now we chain-call `then(...)` to wait for the

// resolution of that returned promise.

.then(function(sum){

    console.log(sum);

});

在这段代码中有两层Promise。

fetchX() fetchY() 被直接调用,它们返回的值(Promise!)被传递给 sum(...) 。这些Promise所代表的潜在值可能在 现在 或者 将来 准备好,但是无论如何,每个Promise都将其行为规范化为相同的。我们以一种独立于时间的方式来解释 x y 的值。它们在一段时间内,是 未来值

第二层是 sum(...) 创建(通过 Promise.all([ ... ]) )和返回的promise,我们通过调用 then(...) 来等待它完成。当 sum(...) 操作完成,我们的 未来值 ,即求和结果已经准备好了,我们可以打印出来。我们隐藏了在 sum(...) 中等待 x y 未来值 的逻辑。

注意 sum(…) 内部, Promise.all([ … ]) 调用创建了一个promise(它等待 promiseX promiseY 完成),链式调用 .then(...) 来创建另一个promise,返回的 values[0]+ values[1] 会立即决议(返回相加的结果)。因此,我们在 sum(...) 调用后加上的 then(...) ——在代码段的最后——实际上是在第二个promise的返回后执行,而不是第一个创建的 Promise.all([ ... ]) 。还有,虽然我们还没有在第二个 then(...) 后面继续添加 then ,它也创造了另一个promise,我们可以选择观察/使用它。本章后面的内容将在后面详细解释。 使用Promise, then(...) 的调用实际上可以有两个方法,第一个是完成(如上所示),第二个是拒绝 :

sum(fetchX(), fetchY())

.then(

    // fullfillment handler

    function(sum) {

        console.log( sum );

    },

    // rejection handler

    function(err) {

        console.error( err ); // bummer!

    }

);

如果在得到 x y 的时候出现了问题,或者在添加的过程中出现了一些失败,那么可以 sum(…) 返回的promise将被拒绝,传递给 then(...) 的第二个回调错误处理程序,它将收到来自promise拒绝的值。 因为Promises 封装了依赖于时间的状态——等待内在值的实现或拒绝——从外部来看,Promises 本身是时间独立的,因此可以以可预测的方式组合,而不考虑底层的时间和结果。

而且,一旦一个Promise得到决议,它就会永远保持这种状态——在那个时候它就变成了一个 不可改变的值 ——然后就可以在必要的时候多次被 观察

实际上你可以链式Promise非常有用:

function  delay(time) {

    return new Promise(function(resolve, reject){

        setTimeout(resolve, time);

    });

}


delay(1000)

.then(function(){

    console.log("after 1000ms");

    return delay(2000);

})

.then(function(){

    console.log("after another 2000ms");

})

.then(function(){

    console.log("step 4 (next Job)");

    return delay(5000);

})

// ...

调用 delay(2000) 创造一个在2000ms完成的Promise,然后我们从第一个 then(…) 完成回调中返回,导致第二个 then(...) 的promise再等待2000ms执行。

注意 : 因为Promise 一旦决议,从外部就不可改变了,所以现在可以安全地将这个值传递给任何一方,因为它知道它不能被意外或恶意地修改。对于观察该promise的多方来说,这一点尤其正确。任意一方不可能影响另一方观察到的决议结果。不变性可能听起来像是一个学术话题,但它实际上是Promise 设计最基本和最重要的方面之一,不应该被随意地忽略。

如何辨别 Promise

关于Promises的一个重要细节是确定是否某些值是真正的Promise。换句话说,它的值会像一个Promise吗?

我们知道Promises是由 new Promise(…) 语法构造的,你可能会认为 p instanceof Promise 是一个充分的检查。好吧,不完全是。

主要是因为你可以从另一个浏览器窗口(例如iframe)获得一个 Promise的值,它有自己独立的Promise类,不同于当前窗口或框架中的一个,因此该检查将无法识别Promise实例。

而且,一个库或框架可以选择发布它自己的Promise,而不是使用ES6原生的Promise实现。事实上,你很可能会在没有任何Promise的老式浏览器中使用第三方的 Promise。

吞掉异常

如果在创建Promise的任何时候,或者在对其决议的观察中,抛出了一个JavaScript异常错误,比如“TypeError”或“ReferenceError”,那么这个异常就会被捕获,它将迫使这个Promise被拒绝。

例如:

var p = new Promise(function(resolve, reject){

    foo.bar(); // `foo` is not defined, so error!

    resolve(374); // never gets here :(

});


p.then(

    function fulfilled(){

        // never gets here :(

    },

    function rejected(err){

        // `err` will be a `TypeError` exception object

    // from the `foo.bar()` line.

    }

);

但是如果一个Promise完成了,却在观察结果时(在 then(…) 注册回调)发生了JS异常会怎样呢?即使这个错误不会被丢失,你可能会对它们处理的方式感到惊讶。除非你进一步挖掘:

var p = new Promise( function(resolve,reject){

    resolve(374);

});


p.then(function fulfilled(message){

    foo.bar();

    console.log(message); // never reached

},

    function rejected(err){

        // never reached

    }

);

它看起来像“foo . bar()”真的是被吞没了。其实并不是。不过,一些更深层次的问题出现了,我们没能监听到。“p.then(…)调用本身会返回另一个promise,而这个promise将会被 TypeError 异常所拒绝。

处理未捕获异常

还有其他“更好”的方法。 常见的一个建议是应该给Promise增加一个 done(…) ,用于标志Promise链的结束。 done(…) 不会创建并返回一个Promise,所以传递到 done(…) 的回调显然不会把问题报告给一个不存在的链式Promise。 在未捕获错误的情况下,会按照你期望的方式处理:在 done(..) 中的拒绝处理函数中如果有任何异常,该异常将被抛出为一个全局未捕获的错误(通常在开发人员控制台能看到):

var p = Promise.resolve(374);


p.then(function fulfilled(msg){

    // numbers don't have string functions,

    // so will throw an error

    console.log(msg.toLowerCase());

})

.done(null, function() {

    // If an exception is caused here, it will be thrown globally

});

在ES8里 Async/await会发生什么

JavaScript ES8引入了 async/await ,使得Promise更容易使用。我们将简要介绍 async/await 提供的可能性,以及如何利用它们来编写异步代码。 因此,让我们看看async/await如何工作。

使用 async 函数声明来定义一个异步函数。这样的函数返回一个 AsyncFunction 对象, AsyncFunction 对象代表了执行代码的异步函数, AsyncFunction 包含在该函数中。 当调用async函数时,它返回一个 Promise 。当async函数返回一个值时,这不是一个 Promise 。而是会自动创建一个 Promise ,它将使用函数的返回值来决议。当 async 函数抛出异常时, Promise 将使用抛出的值来拒绝。 一个 async 函数可以包含一个 await 表达式,该表达式暂停执行该函数,并等待传递给它的Promise被决议,然后恢复async函数的执行并返回决议值。 你可以把JavaScript的 Promise 看作是Java的 Future c# 的任务。

async/await 的目的是简化使用Promise的行为。 让我们看一下下面的例子:

// Just a standard JavaScript function

function getNumber1() {

    return Promise.resolve('374');

}

// This function does the same as getNumber1

async function getNumber2() {

    return 374;

}

同样,抛出异常的函数等价于返回已被拒绝的promise的函数:

function f1() {

    return Promise.reject('Some error');

}

async function f2() {

    throw 'Some error';

}

await 关键字只能在 async 函数中使用,并允许您同步等待一个Promise。如果我们在 async 函数之外使用Promise,我们仍然需要使用 then 回调:

async function loadData() {

    // `rp` is a request-promise function.

    var promise1 = rp('https://api.example.com/endpoint1');

    var promise2 = rp('https://api.example.com/endpoint2');


    // Currently, both requests are fired, concurrently and

    // now we'll have to wait for them to finish

    var response1 = await promise1;

    var response2 = await promise2;

    return response1 + ' ' + response2;

}

// Since, we're not in an `async function` anymore

// we have to use `then`.

loadData().then(() => console.log('Done'));

你还可以使用“async函数表达式”来定义async函数。一个async函数表达式非常类似,它的语法和async函数声明差不多。async函数表达式和async函数声明之间的主要区别是函数名,它可以在async函数表达式中省略,以创建匿名函数。一个async函数表达式可以作为一个IIFE(立即执行函数表达式)来使用,当IIFE被定义完就会运行。 它看起来像这样:

var loadData = async function() {

    // `rp` is a request-promise function.

    var promise1 = rp('https://api.example.com/endpoint1');

    var promise2 = rp('https://api.example.com/endpoint2');


    // Currently, both requests are fired, concurrently and

    // now we'll have to wait for them to finish

    var response1 = await promise1;

    var response2 = await promise2;

    return response1 + ' ' + response2;

}

更重要的是,在所有主流浏览器中都支持async/await:

如果这个兼容性不是你想要的,那么也有几个JS的转换器,比如 Babel TypeScript

在一天结束的时候,重要的是不要盲目地选择“最新”的方法来编写异步代码。理解异步JavaScript的内部原理是很重要的,了解它为什么如此重要,并深入理解您选择的方法的内部原理。每种方法都有利有弊。

编写高度可维护的、健壮的异步代码的5个技巧

1. 干净的代码: 使用async/await允许您编写更少的代码。每次使用async/await你能跳过一些不必要的步骤:写 .then ,创建一个匿名函数来处理响应,在该回调中命名响应变量,比如:

// `rp` is a request-promise function.

rp(‘https://api.example.com/endpoint1').then(function(data) {

 // …

});

对比:

// `rp` is a request-promise function.

var response = await rp(‘https://api.example.com/endpoint1')







请到「今天看啥」查看全文