Generator函数
引入资源
基本概念
- 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态
- 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
- 形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
- Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
- 必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行js
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true } - yield表达式只能用在 Generator 函数里面,用在其他地方都会报错
- yield表达式如果用在另一个表达式之中,必须放在圆括号里面
- yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
应用
- 异步操作同步化js
var clock = function* () { console.log(111); yield console.log(222); console.log(333); }; let c = clock() c.next() // 111 222 c.next() // 333
async和await
- async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await async会把后面的函数执行后return的结果包装成一个resolve的promisejs
const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; //等价于 const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; - async函数对 Generator 函数的改进,体现在以下四点
- 内置执行器:Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器
- async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
- async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作
- 语法
- 返回 Promise 对象
- async函数返回一个 Promise 对象。
- async函数内部return语句返回的值,会成为then方法回调函数的参数js
async function f() { return 'hello world'; } f().then(v => console.log(v)) // "hello world" - async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到js
async function f() { throw new Error('出错了'); } f().then( v => console.log(v), e => console.log(e) ) // Error: 出错了
- Promise 对象的状态变化
- async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数js
- await 命令
- 正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值js
async function f() { // 等同于 // return 123; return await 123; } f().then(v => console.log(v)) // 123 - await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中
- 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发js
let foo = await getFoo(); let bar = await getBar(); //上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。 // 写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; - await命令只能用在async函数之中,如果用在普通函数,就会报错。如果将forEach方法的参数改成async函数,也有问题
实例:按顺序完成异步操作
- Promise 的写法如下js
function logInOrder(urls) { // 远程读取所有URL const textPromises = urls.map(url => { return fetch(url).then(response => response.text()); }); // 按次序输出 textPromises.reduce((chain, textPromise) => { return chain.then(() => textPromise) .then(text => console.log(text)); }, Promise.resolve()); } //上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。这种写法不太直观,可读性比较差 - async 函数实现js
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } } //上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求 - 并发发出远程请求js
async function logInOrder(urls) { // 并发读取远程URL const textPromises = urls.map(async url => { const response = await fetch(url); return response.text(); }); // 按次序输出 for (const textPromise of textPromises) { console.log(await textPromise); } } //上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出