JavaScript 异步操作进阶第三步:async 函数

一、含义

在早期,ES6 最初的异步处理方案引入 Promise 对象,后经过升级成更好的异步编程解决方案:Generator 函数。
但是,Generator 函数是不能自动执行的,返回的是一个遍历器对象。需要借助 Thunk 函数或者 co 模块实现自动流程管理。
为了使得异步操作变得更加合理,ES2017标准引入了 async 函数。相较于 Generator 函数,async 函数有一下几点优势:
1. 内置执行器
async 函数自带执行器,不需要像 Generator 函数需要 co 模块实现自动流程管理。像调用普通函数一样:asyncReadFile() ,只要一行,Generator 函数需要调用 next 方法或者借助 co 模块才能得到最终结果。

2. 更好的语义性
async 和 await 比起星号和 yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

3. 更广的适用性
co 模块约定,yield 命令后面只能是 Thunk 函数 或 Promise对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串、布尔值,但这时等同于同步操作)。

4. 返回值是 Promise
async 函数的 返回值是 Promise对象,这比 Generator 函数的返回值是 Iterator 对象方便许多,可以用 then 方法指定下一步操作。

通俗讲,async 函数是 Generator 函数的语法糖,是由多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。

二、用法

1、多种使用形式
// 函数声明
async function foo() {};

// 函数表达式
const = foo = async function() {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...);

// calss 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('yuan');
  }

  async getYuan(name) {
    const cache = await this.cachePromise;
    return cache.match(`/yuan/${ name } .png`);
  }
}

const storage = new Storage();
storage.getYuan('monkey').then(...);

// 箭头函数
const foo = async () => {};

2、返回 Promise 对象

async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数

async function f() {
  return "hello world";
}
f().then(v => console.log(v)); // hello world

3、Promise 对象的状态变化

只有 async 函数内部的 await 命令执行完,才会执行 then 方法指定的回调函数:

async function getTitle(url) {
  let response = await fetch(url);
  let  html = response.text(0;
  return html..match(/<title>([\s\S]+<\title>/i)[1];
}
getTitle("http://www.jianshu.com/writer#/notebooks/15137721/notes/24616981/preview").then(...);

上述代码中 getTitle 函数内部的3个操作:抓取网页、取出文本。匹配页面标题。只有执行完这三个操作,才能执行 then 方法里的操作。

4、await 命令

await 命令后面是一个 Promise 对象,如果不是,会自动被转为一个 resolve 的 Promise 对象:

async function f() {
  return await 'yuan';
}
f().then(v => console.log(v)); // yuan

await 命令后没的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到:

async function foo() {
  await Promise.reject('报错了');
}
foo()
.then(v => console.log(v))
.catch( e => console.log(e)) 
// 浏览器抛出错误

只要有一个 await 命令后面的 Promise 对象变为 reject,那么整个 async 函数都会中断。

三、使用注意点

1、await 命令后面的 Promise 对象运行的结果可能是 reject 的状态,所以最好把 await 命令放在 try...catch 代码块里,或者在
await 命令后加一个 catch 方法:

// await 命令写在 try...catch 
async function f() {
  try {
    await somePromise();
  }
  catch(err) {
    console.log(err);
  }
}

// 在 await 后加 catch
async function f() {
  await somePromise()
  .catch (function (err) {
    console.log(err)
  });
}

2、多个 await 名利后面的异步操作如果不存在继发关系,最好让他们同时出发

let foo = await getFoo();
let bar = await getBar();

3、await 命令只能用在 async 函数之中,如果用在普通函数中会报错
4、多个请求并发执行,可以使用 Promise.all 方法:

async function dbFunc(db) {
  let docs = [{}, {}, {}];
  let promises = doc.map((doc) = > db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

四、异步应用对比:Promise、Generator、async

以 setTimeout 来模拟异步操作:

function foo (obj) {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      let data = {
        height: 180
      }
      data = Object.assign({}, obj, data)
      resolve(data)
    }, 1000)
  })
}
function bar (obj) {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      let data = {
        talk () {
          console.log(this.name, this.height);
        }
      }
      data = Object.assign({}, obj, data)
      resolve(data)
    }, 1500)
  })
}

两个函数都返回了 Promise 对象,使用 Object.assign() 方法合并传递过来的参数。
Promise 实现异步:

function main () {
  return new Promise((resolve, reject) => {
    const data = {
      name: 'keith'
    }
    resolve(data)
  })
}
main().then(data => {
  foo(data).then(res => {
    bar(res).then(data => {
      return data.talk()   // keith 180
    })
  })
})

此方法的缺点:语义不好,不够直观,使用多个 then 方法,有可能出现回调地狱。
Generator 函数实现异步:

function *gen () {
  const data = {
    name: 'keith'
  }
  const fooData = yield foo(data)
  const barData = yield bar(fooData)
  return barData.talk()
}
function run (gen) {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value.then(data => {
      next(data)
    })
  }
  next()
}
run(gen)

Generator 函数相较于 Promise 实现异步操作,优点在于:异步过程同步化,避免回调地狱的出现。缺点在于:需要借助run 函数或者 co 模块实现流程的自动管理。
async 函数实现异步:

async function main () {
  const data = {
    name: 'keith'
  }
  const fooData = await foo(data)
  const barData = await bar(fooData)
  return barData
}
main().then(data => {
  data.talk()
})

async 函数优点在于:实现了执行器的内置,代码量减少,且异步过程同步化,语义化更强。

五、应用实例

1、从豆瓣API上获取数据:

var fetchDoubanApi = function() {
    return new Promise((resolve, reject) = >{
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    var response;
                    try {
                        response = JSON.parse(xhr.responseText);
                    } catch(e) {
                        reject(e);
                    }
                    if (response) {
                        resolve(response, xhr.status, xhr);
                    }
                } else {
                    reject(xhr);
                }
            }
        };
        xhr.open('GET', 'https://api.douban.com/v2/user/aisk', true);
        xhr.setRequestHeader("Content-Type", "text/plain");
        xhr.send(data);
    });
};
(async function() {
    try {
        let result = await fetchDoubanApi();
        console.log(result);
    } catch(e) {
        console.log(e);
    }
})();

2、根据电影名称,下载对应的海报

import fs from 'fs';
import path from 'path';
import request from 'request';
var movieDir = __dirname + '/movies',
exts = ['.mkv', '.avi', '.mp4', '.rm', '.rmvb', '.wmv'];
// 读取文件列表
var readFiles = function() {
    return new Promise(function(resolve, reject) {
        fs.readdir(movieDir,
        function(err, files) {
            resolve(files.filter((v) = >exts.includes(path.parse(v).ext)));
        });
    });
};
// 获取海报
var getPoster = function(movieName) {
    let url = `https: //api.douban.com/v2/movie/search?q=${encodeURI(movieName)}`;
    return new Promise(function(resolve, reject) {
        request({
            url: url,
            json: true
        },
        function(error, response, body) {
            if (error) return reject(error);
            resolve(body.subjects[0].images.large);
        })
    });
};
// 保存海报
var savePoster = function(movieName, url) {
    request.get(url).pipe(fs.createWriteStream(path.join(movieDir, movieName + '.jpg')));
}; 
(async() = >{
    let files = await readFiles();
    // await只能使用在原生语法
    for (var file of files) {
        let name = path.parse(file).name;
        console.log(`正在获取【$ {
            name
        }】的海报`);
        savePoster(name, await getPoster(name));
    }
    console.log('=== 获取海报完成 ===');
})();

六、结语

本章,我们需要对比 Promise 和 Generator 异步操作的优势了解 async 函数的由来,掌握 async 函数的用法及其注意事项,再结合相应的实例深入了解 async 函数的实现原理。下一章,我们来了解 ES6 另一个语法糖: class,尽情期待吧!

戳我博客

章节目录

1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for...of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容