从零开始学_JavaScript_系列(59)——async函数

1、async函数简介

1.1、前注

在阅读本章节之前,建议先阅读 【Generator函数】章节和【Thunk函数】章节。

不然很有可能对本章节内容摸不着头脑,或者理解有所偏差。如果有所了解或者比较熟悉的话,那么对本章绝大部分内容上手是非常轻松的。

这里假定你已经对以上知识有所理解,不考虑完全不懂的人能否理解本博文。

1.2、兼容性

关于asnyc函数的兼容性:Can I use网站的说明

简单来说:支持async的有:IE11、Edge、58+的chrome、54+的火狐、10.1+的Safari。以下版本的基本都不支持。

1.3、是什么

他是Generator函数的语法糖,但相较来说比Generator函数更强大一些。

主要用于解决依次异步调用异步函数的问题。

即当第一个异步调用结束后,再调用第二个异步函数;等第二个调用完成后再调用第三个的这种情况。

在以往的写法里,需要进行回调函数多层嵌套才能实现,但这种写法会导致多层回调函数嵌套,形成可怕的回调地狱,既不方便维护,也不方便理解。

而新的写法可以解决以上这些问题,让原本的回调地狱,变成类似同步函数一样的写法。极大的减轻了代码的复杂度。

在async函数出来之前,解决这类问题如果不想用面对回调地狱,那么一般是借用Gererator函数,然后使用Thunk函数或者Co模块。

二者实质上是类似的。

1.4、async函数示例
function delay(dealCallback) {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            dealCallback()
            resolve(123)
        }, 1000)
    })
}

const byPromise = function (fn) {
    return function (...args) {
        return fn.call(this, ...args)
    }
}

async function G() {
    let r1 = await byPromise(delay)(function () {
        console.log('first')
    })
    console.log(r1)
    let r2 = await 'abc'
    return r2
}
let foo = G()
foo.then(data => console.log(data))
// first
// 123
// abc
1.5、解释和说明

1、首先,async函数相对于Generator函数的区别有几点:

①函数名没有了星号(*),取而代之是async关键字;
②函数内没有了yield表达式,取而代之的是await关键字;
③执行的时候不再返回遍历器,手动执行next方法来一步步执行;而是调用函数时,即时自动执行;
④await后面除了可以跟Promise对象后,还可以跟原始类型的值。但是不能跟Thunk函数,而Co模块在4.0之前是支持yield表达式后面跟Thunk函数的;
⑤返回值是Promise对象;

2、除此之外,其他方面跟Generator函数比较相近;

3、async关键字是标明这个函数不是一般的函数,而是Generator函数(并且是有自动流程管理的);

4、await类似yield表达式,要求后面跟的是一个Promise对象,或者基本类型(基本类型则视为同步);

5、async和await是成套出现的,单纯只有一个是没有用的;

6、await只能在async函数内,而不能在其他函数内(即使另外一个函数在async的函数内也不行);

7、await表达式的返回值,就是Promise对象传给resolve的值;

8、假如Promise对象执行了reject,那么会抛错一个错误来。

9、当第一个await表达式后面跟的Promise对象执行完毕之后(状态变成resolved)第二个才会继续执行。

10、async函数的返回的是一个Promise对象,他的值是async函数中,return返回的值。

1.6、async的几种写法

这里的写法不是函数内的写法,而是函数本身的写法:

①函数声明(声明式):

async在function前

async function foo() {
    // some code...
}

②函数表达式(赋值式):

async在function前

let foo = async function () {
    // some code...
}

③对象属性(属性式):

async在function前

let foo = {
    bar: async function () {
        // some code...
    }
}

④对象属性简写:

async在函数名前

let foo = {
    async bar () {
        // some code...
    }
}

⑤箭头函数:

let foo = async () => {
    // some code...
}

⑥Class写法:

记得先new一个实例才能用

class Foo {
    async bar() {
        // some code...
    }
}

2、错误捕获

2.1、async函数抛错

async函数内部的抛错,会使async函数返回的Promise对象的状态变为rejected,并使函数立即结束(不再执行抛错代码之后的代码)。

假如async函数没有错误捕获机制,或者返回的Promise对象没有例如catch之类的错误处理机制,就会导致报错冒泡到外面来。

虽然由于Promise对象本身是异步的原因,在全部代码执行完后错误才会被抛出来,不会影响其他代码的执行(一般不会)。但这显然是不好的。

如代码:

async function Foo() {
    throw new Error("error")
    console.log('123')
}
Foo()
console.log("456")
// 456
// Uncaught (in promise) Error: error
// 错误是红字

解决办法有两个,通过返回对象的catch来处理错误,或者是在async内通过try...catch来捕获错误。

方法一(ctach):

async function Foo() {
    throw new Error("error")
    console.log('123')
}
Foo().catch(error => {
    console.log("catch the error")
})
// catch the error

方法二(try…catch):

async function Foo() {
    try {
        throw new Error("error")
        console.log('123')
    } catch (err) {
        console.log("catch the error")
    }
}
Foo()
// catch the error
2.2、await表达式报错

await表达式我们可以通常认为是一个Promise对象(即使不是,也会被隐式转换为Promise对象)。

而Promise对象报错有两种情况:

①在Promise对象内部报错;

②在Promise对象内部的异步执行区域报错;

如示例代码:

new Promise((resolve, reject) => {
    throw new Error("out error")    // 情况一
    setTimeout(function () {
        throw new Error("in error") // 情况二
        resolve()
    }, 1000)
})

情况一的报错是可以被捕获到的,但是情况二的不行。

先来一个简单的示例:

function delay(isOut) {
    return new Promise((resolve, reject) => {
        if (isOut) {
            throw new Error("out error")    // 情况一
        }
        setTimeout(function () {
            if (!isOut) {
                throw new Error("in error") // 情况二
            }
            resolve()
        }, 1000)
    })
}

const byPromise = function (fn) {
    return function (...args) {
        return fn.call(this, ...args)
    }
}

async function Foo() {
    try {
        await byPromise(delay)(true)
    } catch (err) {
        console.log(err)
    }
    try {
        await byPromise(delay)(false).catch(err => {
            console.log(err)
        })
    } catch (err) {
        console.log(err)
    }

}
Foo().catch(err => {
    console.log(err)
})
// Error: out error  白字错误
// Uncaught Error: in error     红字错误

白字错误说明是被捕获到了,而红字的错误说明没有被捕获到。

由此可以证明:

  1. 无论是async函数内部的try...catch
  2. 错误所在的Promise对象的catch;
  3. async函数返回的Promise对象的catch;

都是无法捕获到红字错误的。

而上面三个方法,都可以捕获到白字错误(按照上面的2>1>3的顺序,择先捕获)。

因此需要注意对于异步返回结果的处理,如果报错,那么应让该Promise对象执行reject方法,避免抛出的错误无法被捕获。

当执行了reject后,就可以被catch或者try...catch或者async返回的Promise的对象的catch方法捕获了。

另外,除了setTimeout的回调函数里抛的错误无法捕获到,jQuery的ajax的回调函数里的错误也是无法捕获到的。如代码:

function delay() {
    return new Promise((resolve, reject) => {
        try {
            $.ajax({
                url: '/',
                type: "get"
            }).always(function () {
                throw new Error("test error")
            })
        } catch (err) {
            console.log(err)
        }
    })
}

const byPromise = function (fn) {
    return function (...args) {
        return fn.call(this, ...args)
    }
}

async function Foo() {
    await byPromise(delay)(true).catch(err => console.log(err))
}
Foo().catch(err => {
    console.log(err)
})
// 除了404之外,还有红字错误,通过三种方式都无法捕获到

不过axios由于没专门试过,也没琢磨过其源码,因此不太确定。

但axios是封装好的异步请求,我们写的代码一般是在then里面执行,而then里面抛的错误是会被catch捕获到的。所以倒不必担心。

3、一些细节

3.1、隐式转换

当await后面跟的不是Promise对象,而是基本类型时,该值会被await通过Promise.resolve隐式转换。

事实上,可以被转换的包括例如Boolean、String、Number、对象、数组等。甚至可以支持自定义类型,如代码:

async function Foo() {
    console.log(await 'a')
    console.log(await {b: 1})
    console.log(await [1, 2])
    console.log(await 3)
    console.log(await baz)
}
Foo()
// a
// {b:1}
// [1, 2]
// 3
// Bar {a:"a"}
3.2、await函数的限制

await所在的位置只能是async函数的直接所在位置,而不能是async函数外,或者是async函数内部的函数。

例如以下代码,虽然回调函数在async函数内,但也是会报错的

function bar(callback) {
    callback()
}
async function Foo() {
    bar(function () {
        await "a"
    })
}
Foo()
// Uncaught SyntaxError: Unexpected string
3.3、返回值的状态变化

async函数的返回值,是一个Promise对象,这些我们是知道的。

既然是Promise对象,那么他就有pending、resolved、rejected三种状态。

那么什么时候回从pending状态变化为另外两种呢?

想想Generator函数,可以类比Generator函数,猜测是当函数执行完毕(done变为true)的时候,pending状态变化。

证明代码如下:

async function Foo() {
    await 'a1'
    console.log(p)
}
let p = Foo()
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

另外注意,在p后面加console.log(p),其结果只可能是pending,原因在于Promise的状态变化不是实时的,而是添加到队列里,在下一轮代码执行的时候再变更状态。

注: Promise.resolve或类似的,会立即执行,而不等待下一轮。

3.4、async函数的实现原理

还记得不记得Thunk函数章节的co模块。co模块,Generator函数的自动执行器以及async函数,他们的实现思路是十分相似的。

具体思路在之前已经讲了,这里略略略。

给一个阮一峰写的吧,点击阅读

3.5、async函数的套用

之前学习Generator函数的时候,知道有yield*表达式,用于处理当一个Generator函数处在另一个Generator函数中的情况。

那么假如一个async处于另外一个async函数中会发生什么事情呢?

答案是相同的,会先执行完另外一个async函数中的内容,再继续执行本async函数的内容。看代码吧,理解起来很简单:

function delay(count) {
    return new Promise(resolve => {
        setTimeout(function () {
            resolve(count)
        }, 1000)
    })
}
async function Foo() {
    console.log(await delay('Foo1'))
    console.log(await Bar())
    console.log(await delay('Foo2'))
}
async function Bar() {
    console.log(await delay('Bar1'))
    console.log(await delay('Bar2'))
    return delay('Bar return')
}
Foo()
// Foo1
// Bar1
// Bar2
// Bar return
// Foo2
3.6、for await…of

因为是提案,所以兼容性是个问题。

阮一峰的博文里提到了这个东西,但我自己实测来说(chrome 60)外加打开浏览器的实验性特性,会提示报错:

Node8.4.0也不行

(async function () {
    for await (const x of ['a', 'b']) {
        console.log(x);
    }
})();
// Uncaught SyntaxError: Unexpected reserved word

所以略略略略。

4、异步Generator函数

兼容性存在问题,具体来说:

  1. chrome60外加打开实验性javascript功能;
  2. Node6.7左右版本和8.4.0版本;

以下代码依然不能执行:

async function* gen() {
  yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));

所以不太明白阮一峰这部分代码怎么写的,难道只是靠Babel转码后运行的?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值