Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async函数 #3

Open
chenxiaochun opened this issue Aug 8, 2017 · 0 comments
Open

Async函数 #3

chenxiaochun opened this issue Aug 8, 2017 · 0 comments

Comments

@chenxiaochun
Copy link
Owner

chenxiaochun commented Aug 8, 2017

综述

现在以下几种async函数的用法都是可行的。

Async函数声明:

async function foo(){}

Async函数表达式:

const foo = async function(){}

Async方法定义:

let obj = {async foo(){}}

Async箭头函数:

const foo = async () => {}

Async函数通常返回的是 Promise:

async function asyncFunc() {
    return 123;
}

asyncFunc()
.then(x => console.log(x));
123
async function asyncFunc() {
    throw new Error('Problem!');
}

asyncFunc()
.catch(err => console.log(err));
Error: Problem!

只有操作符await可以放在 Async 函数里用来处理返回的 Promise 对象结果。所以await的处理结果随着 Promise 的状态不同而不同。

简单处理一个异步结果:

async function asyncFunc() {
    const result = await otherAsyncFunc();
    console.log(result);
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc()
    .then(result => {
        console.log(result);
    });
}

顺序处理多个异步结果:

async function asyncFunc() {
    const result1 = await otherAsyncFunc1();
    console.log(result1);
    const result2 = await otherAsyncFunc2();
    console.log(result2);
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc1()
    .then(result1 => {
        console.log(result1);
        return otherAsyncFunc2();
    })
    .then(result2 => {
        console.log(result2);
    });
}

平行处理多个异步结果:

async function asyncFunc() {
    const [result1, result2] = await Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ]);
    console.log(result1, result2);
}

// 等价于:

function asyncFunc() {
    return Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ])
    .then([result1, result2] => {
        console.log(result1, result2);
    });
}

处理错误异常:

async function asyncFunc() {
    try {
        await otherAsyncFunc();
    } catch (err) {
        console.error(err);
    }
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc()
    .catch(err => {
        console.error(err);
    });
}

理解async函数

在我解释async函数之前,我想通过把Promisesgenerator结合起来,用看起来像同步代码的方式去模拟异步方式。

对于处理获取一次性结果的异步函数,Promises是目前最流行的方式。下面是一个使用fetch方法获取文件的示例:

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}
fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

co是一个基于Promisesgenerator的实现,也能够让你以书写同步代码的方式来实现上面的示例。

const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});

cogenerator回调函数中每次检测到一个带有yield操作符的方法,就会产生一个Promise对象,co会先暂停回调代码的执行,直到Promise对象的状态发生变化再继续执行。无论Promise的状态为resolved或者rejectedyield都会将相应的结果值返回。

详细说明一下async函数的执行过程:

  1. async函数在开始执行的时候,通常都是返回一个Promise对象
  2. 函数体被执行之后,你可以使用return或者throw直接完成执行过程。也可以使用await暂时完成执行过程,然后根据情况再继续执行
  3. 最终返回一个Promise对象
  4. thencatch中的callback只有在当前所有代码执行完毕之后,才会被执行。从下面的输出结果可以看出函数asyncFunc的返回值等到所有代码包括循环逻辑都执行完毕之后,才最终得以被输出
async function asyncFunc() {
    console.log('asyncFunc()'); // (A)
    return 'abc';
}
asyncFunc().
then(x => console.log(`Resolved: ${x}`)); // (B)
console.log('main');

for(let i=0; i<5; i++){
    console.log(i)
}
asyncFunc()
main
0
1
2
3
4
Resolved: abc

使用returnResolve一个async函数状态,是一种很标准的操作方式。这意味着你可以:

  1. 直接返回一个非Promise对象类型的值,作为Resolve状态的参数值
  2. 返回的Promise对象代表了当前async的函数状态

使用await的若干贴示

使用async函数最常犯的一个错误就是忘记添加await关键字,在下面示例中value被指向了一个Promise对象,但它可能并不是你想要的结果:

async function asyncFunc() {
    const value = otherAsyncFunc(); // missing `await`!
    ···
}

await可以感知到后面跟的异步函数是否返回了结果值,然后它就可以告诉调用者当前的异步函数是否已经执行完毕了。例如在下面的示例中,await可以确保step1()执行完之前,不会招待foo()的剩余逻辑代码:

async function foo() {
    await step1(); // (A)
    ···
}

有时候你仅仅是想触发一个异步函数计算,并不想知道它会何时完成。在下面示例中,我们并不关心写文件的操作何时完成,只要它们是按正确的顺序执行就可以了。最后一行的await只是为了确保关闭写文件的操作能被成功执行即可。

async function asyncFunc() {
    const writer = openFile('someFile.txt');
    writer.write('hello'); // don’t wait
    writer.write('world'); // don’t wait
    await writer.close(); // wait for file to close
}

多个await异步函数是顺序执行的关系,想要它们同时执行就得使用Promise.all()了:

async function foo() {
    const result1 = await asyncFunc1();
    const result2 = await asyncFunc2();
}
async function foo() {
    const [result1, result2] = await Promise.all([
        asyncFunc1(),
        asyncFunc2(),
    ]);
}

async在回调函数中的应用

有一个需要知道的限制是,await操作符只会影响async函数的直接作用域环境。因此,你不能在async函数内的回调函数中使用await,这也会让那些基于callback的方法变得非常难于理解。

Array.prototype.map()

下面的示例是想下载若干url并返回其内容:

async function downloadContent(urls) {
    return urls.map(url => {
        // Wrong syntax!
        const content = await httpGet(url);
        return content;
    });
}

像上面这种在普通的箭头函数中使用await根本无法运行,会抛出语法错误。那我们应该怎么用,像下面这样吗:

async function downloadContent(urls) {
    return urls.map(async (url) => {
        const content = await httpGet(url);
        return content;
    });
}

function httpGet(url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if(url.indexOf('a') > -1){
                resolve('a.com');    
            }else{
                resolve('b.com');
            }
        }, 2000)  
    });
}

const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
    console.log(url);
});

输出结果:

2017-09-03 9 59 57

你会发现代码中有两个问题:

  1. 返回值是一个包含两个Promise对象的数组,并不是我们期望的包含resolve返回值的数组
  2. await只能暂停箭头回调函数里的httpGet()map本身的回调函数执行完成之后,并不能影响外层的downloadContent ()也执行完成

我们用Promise.all()来修复这两个问题,把返回的 包含两个Promise对象的数组 转换成 包含两个数组元素的Promise对象,看如下示例:

async function downloadContent(urls) {
    const promiseArray = urls.map(url => httpGet(url));
    return await Promise.all(promiseArray);
}

function httpGet(url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if(url.indexOf('a') > -1){
                resolve('a.com');    
            }else{
                resolve('b.com');
            }
        }, 2000)  
    });
}

const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
    console.log(url);
});

输出结果:

2017-09-03 10 14 42

OK,现在输出结果是正确的了。但是这段代码还是有一点点低效的地方需要改进,downloadContent ()函数里首先用await展开了Promise.all()的返回结果,后面又用return包装了一次,其实我们用return直接返回Promise.all()即可:

async function downloadContent(urls) {
    const promiseArray = urls.map(url => httpGet(url));
    return Promise.all(promiseArray);
}

Array.prototype.forEach()

我们这次换成forEach()方法来模拟输出若干文件内容。很显然,下面的示例会抛出语法错误,因为你不能在普通的箭头函数中直接使用await

async function logContent(urls) {
    urls.forEach(url => {
        // Wrong syntax
        const content = await httpGet(url);
        console.log(content);
    });
}

那我把代码修改成如下:

async function logContent(urls) {
    urls.forEach(async url => {
        const content = await httpGet(url);
        console.log(content);
    });
    // Not finished here
}

这次代码倒是运行了,但是httpGet()方法返回resolve状态的操作是异步的,也就是说当forEach()方法已经返回之后,它的callback还并没有执行完成。修复此问题,只需要将代码做一下更改:

async function logContent(urls) {
    for (const url of urls) {
        const content = await httpGet(url);
        console.log(content);
    }
}

这段示例中的httpGet()执行顺序是线性的,每一次的调用必须要等待上一次执行完毕。如果想要改成并行的执行顺序,就得用Promise.all()了:

async function logContent(urls) {
    await Promise.all(urls.map(
        async url => {
            const content = await httpGet(url);
            console.log(content);            
        }));
}

map()方法创建了一个Promise对象数组。我们并不关心这几个Promise对象的履行结果,只要它们履行了即可。也就是loginContent()方法执行完成就可以了。在这个示例中除非把Promise.all()直接返回,否则此函数的结果只会包含若干`undefined。

2017-09-03 4 32 51

使用async函数的若干贴示

async函数的基础就是Promise,所以充分理解下面的示例非常重要。尤其是在那些没有使用Promise机制的老代码中使用async函数时,你可能除了直接使用Promise之外没有别的选择。

下面是一个在XMLHttpRequest中使用Promise的示例:

function httpGet(url, responseType="") {
    return new Promise(
        function (resolve, reject) {
            const request = new XMLHttpRequest();
            request.onload = function () {
                if (this.status === 200) {
                    // Success
                    resolve(this.response);
                } else {
                    // Something went wrong (404 etc.)
                    reject(new Error(this.statusText));
                }
            };
            request.onerror = function () {
                reject(new Error(
                    'XMLHttpRequest Error: '+this.statusText));
            };
            request.open('GET', url);
            xhr.responseType = responseType;
            request.send();
        });
}

XMLHttpRequest的 API 设计都是基于 callback 的。使用async函数就意味着你要在内层回调函数里使用returnthrow返回Promise对象的状态,但这肯定是不可能的。因此,在这情况中使用async的风格就是:

  • 使用Promise直接构建一个异步的基元
  • 通过async函数来使用这些基元

在一个模块或者 script 的顶级作用域中使用await,可以像下面这样:

async function main() {
    console.log(await asyncFunction());
}
main();

或者

(async function () {
    console.log(await asyncFunction());
})();

或者

(async () => {
    console.log(await asyncFunction());
})();

不用太担心那些未处理rejections,以前这种情况可能都是静默失败,不过现在大多数的现代浏览器都会抛出一个未处理的异常:

async function foo() {
    throw new Error('Problem!');
}
foo();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant