内容
Generator函数
Generator
函数是es6提供的一种异步编程解决方案
Generator
函数是一个状态机
,封装了多个内部状态
- 执行
Generator
函数会返回一个遍历器对象
,返回的遍历器对象
可以依次遍历Generator
函数内部的每一个状态
Generator
函数与普通函数
的区别:
function
和函数名之间有一个*
号- 函数体内部使用
yield
表达式
function* hwGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
const hw = hwGenerator();
console.log(hw);
该函数有三个状态:hello
、world
、return
语句(结束执行)
调用Generator
函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态
的指针对象
,也就是遍历器Iterator
第一
次调用遍历器对象
的next()
,Generator
函数才开始执行,使得指针指向下一个
状态,遇到第一个yield
表达式停止
第二
次调用next()
,Generator
函数从上次yield
表达式停下的地方,执行到下一个yield
表达式,也就是指针指向下一个状态
直到调用next()
返回对象的value
属性为undefined
,以后再调用next()
方法,返回的都是undefined
hw.next();
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Generator
函数是分段执行的,yield
表达式是暂停执行
的标记,next()
方法可以恢复执行
yield表达式
yield
表达式是暂停
标志
⚠️:
yield
后面的表达式,只有当调用next()
方法、内部指针指向该语句的时候才会执行
function* sum() {
yield 123 + 456;
}
上面代码yield
后面的表达式123 + 456
不会立即求值,只有当指针指向这一句时才会求值
Generator
函数可以不用yield
表达式,这时就变成了一个单纯的暂缓执行
函数
function* func() {
console.log("执行了!");
}
函数func()
只有在调用next()
方法时才会执行,因为它是一个Generator
函数
yield
表达式只能用在Generator
函数里面,用在其他地方都会报错
next()的参数
yield
表达式本身没有返回值
,或者说总是返回undefined
next()
方法可以带一个参数
,该参数就会被当作上一个 yield
表达式的返回值
function* func() {
for (let i = 0; true; i++) {
let reset = yield i;
if (reset) {
i = -1;
}
}
}
const f = func();
f.next(); // { value: 0, done: false}
f.next(); // { value: 1, done: false}
f.next(true); // { value: 0, done: false}
上面代码定义了一个可以无限运行的Generator
函数func
当调用next()
但不传递参数,每次yield
表达式的返回值总是undefined
当调用next()
传递一个参数true
,变量reset
就会被赋值为true
,i
被赋值为-1
,下一轮循环就会从-1
开始递增
⚠️:由于next()
方法的参数表示上一个
yield表达式的返回值
,所以在第一次使用next()
方法时,传递参数是无效的
语义上讲,第一个next()
方法用来启动遍历器
对象,所以不用带参数
for…of 循环
for...of
循环可以自动遍历Generator
函数运行时生成的Iterator
对象,且此时不需要再调用next()
方法
function* func() {
yield 1;
yield 2;
yield 3;
return 4;
}
for (let item of func()) {
console.log(item);
}
// 1 2 3
// 一旦next方法的返回对象的done属性为true,for...of循环就会中止
Generator
函数为不具备Iterator
接口的对象提供了遍历操作,让它们可以使用for...of
循环
加上遍历器接口的一种写法是:将Generator
函数加到对象的Symbol.iterator
属性上
obj[Symbol.iterator] = generatorFuction;
Generator的应用
Generator
的应用给异步编程带来极大的便利,可以让我们的异步代码同步化
例1:
function* main() {
let res = yield request('https://api-hmugo-web.itheima.net/api/public/v1/goods/detail');
console.log(res);
// 执行后面的操作
console.log("数据请求完成,执行后面的操作")
}
const ite = main();
// 启动遍历器对象
ite.next();
function request() {
$.ajax({
url,
method: 'get',
success(res) {
ite.next(res);
}
})
}
第一次调用next()
,main()
恢复执行,进到request()
中,发送ajax请求,成功回调就会再次调用next()
,参数res
会被作为main()
中yield
表达式的返回值,所以在main()
中打印出来的res
就是成功回调中拿到的res
例2:
function* loading() {
loadUI();
yield showData();
hideUI();
}
const ite = loading();
// 启动
ite.next();
// 加载loading...页面
function loadUI() {
console.log("加载loading...页面");
}
// 数据加载完成...(异步操作)
function showData() {
setTimeout(() => {
console.log("数据加载完成");
// 数据加载完成之后,再次调用next(),让loading()函数继续往下执行
ite.next();
},1000)
}
// 关闭loading...页面
function hideUI() {
console.log("关闭loading...页面");
}
很明显如果我们按定义顺序依次调用loadingUI()
、showData()
、hideUI()
三个函数,执行顺序会是加载loading页面 -> 关闭loading页面 -> 数据加载完成,这不是我们想要的顺序
我们需要Generator
函数来帮助实现异步代码同步化
在loading()
函数中,第一次调用next()
,代码会在yield showData()
暂停,进入到showData()
函数中,里面的数据加载完成之后才会第二次调用next()
,让代码继续往下,调用hiedUI()
async 函数
async
函数是什么?
它是Generator
函数的语法糖
async
函数将 Generator
函数的星号(*)
替换成async
,将yield
替换成await
async
函数对Generator
函数的改进:
async
函数执行。只需要直接用函数名调用,Generator
函数需要调用next()
方法async
和await
比起*
和yield
,更加语义化。async
表示函数里有异步操作,await
表示紧跟在其后的表达式需要等待结果async
函数的返回值是Promise
对象,这比Generator
函数的返回值是Iterator
对象方便多了
返回Promise对象
async函数
的返回值为promise
对象,该promise
对象的结果是由async
函数的返回值
决定的
async function f() {
// 1.如果返回值是非promise值,结果就是成功的promise对象
// return 1;
// 2.如果返回值是promise对象,结果和这个promise对象一致
// return new Promise((resolve,reject) => {
// reject("err");
// })
// 3.抛出错误,结果就是失败的promise对象
// throw "出问题了";
}
使用then()
async
函数可以使用then()
方法添加回调函数
async
函数的return
语句返回的值,会成为then()
方法回调函数
的参数
async function f() {
return 'hello world';
}
f().then(value => console.log(value));
// 'hello world'
async
函数如果内部抛出错误,抛出的错误可以被catch()
方法回调函数接收到,或者被then()
方法失败的回调接收
async function f() {
throw "出问题了";
}
f().then(value => {
console.log(value);
}, reason => {
console.log(reason);
})
// 出问题了
返回的Promise对象的状态变化
async
函数返回的 Promise
对象,必须等到内部所有await
命令后面的 Promise
对象执行完,才会发生状态改变
,除非遇到return
语句或者抛出错误
也就是说,只有async
函数内部的异步操作
执行完,才会执行then()
方法指定的回调函数
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
上面代码中,函数getTitle()
内部有三个操作:抓取网页、取出文本、匹配页面标题
只有这三个操作全部完成,才会执行then()
方法里面的console.log
await表达式
正常情况下:
await
后是一个Promise
对象,await
返回的是promise
的成功值await
后不是Promise
对象,是其他值,直接将此值作为await
的返回值
另一种情况下:
await
后是一个定义了then()
方法的对象,此时await
会将其等同于Promise
对象
async function f() {
// await后为promise对象,它返回的就是promise对象的成功值
let res1 = await new Promise((resolve,reject) => {
resolve('ok');
})
console.log(res1); // ok
// await后是其他值,它返回的就是那个值
let res2 = await 20;
console.log(res2); // 20
}
⚠️:await
后的Promise
对象若变为reject
状态,则reject
的参数会被catch()
的回调函数接收到,且async
函数会中断执行
async function f() {
await new Promise((resolve, reject) => {
reject('err');
})
await new Promise((resolve, reject) => { // 不会执行
resolve('ok');
})
}
f().then(value => console.log(value))
.catch(err => console.log(err)) // err
第二个await
是不会执行的,因为第一个await
中断了async
函数
如果我们有时候希望:即使前一个异步操作失败,也不要中断后面的异步操作:
第一种方法:
- 将第一个
await
放在try...catch
里,这样不管第一个异步操作是否成功,第二个await
都会执行
async function f() {
try {
await new Promise((resolve, reject) => {
reject('err');
})
} catch (e) {}
return await new Promise((resolve, reject) => {
resolve('ok');
})
}
f().then(value => console.log(value)) // ok
第二种方法:
- 在
await
后的Promise
对象再接一个catch()
,处理可能会出现的错误
async function f() {
await new Promise((resolve, reject) => {
reject('err');
}).catch(err => console.log(err)); // err
return await new Promise((resolve, reject) => {
resolve('ok');
})
}
f().then(value => console.log(value)) // ok
错误处理
如果await
后面的异步操作出错,那么等同于async
函数返回的Promise
对象状态变为reject
防止出错的办法,将其放在try...catch
代码块中
async function f() {
try {
await new Promise((resolve,reject) => {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
使用await注意点
- 最好把
await
命令放在try...catch
代码块中
因为await
命令后面的Promise
对象运行结果可能是rejected
- 多个
await
后面的异步操作,如果不存在继发关系,最好让他们同时触发
// getFoo()和getBar()是两个独立的异步操作,被写成继发关系会比较耗时
// 因为只有getFoo()完成后才会执行getBar(),完全可以让它们同时触发
// let foo = await getFoo();
// let bar = await getBar();
// 像下面这样写, getFoo()和getBar()就会同时触发
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
await
命令只能用在async
函数中,用在普通函数中会报错
async函数的应用
需要实现的是发送一个ajax请求
要用到之前用promise
封装好的sendAJAX
函数
sendAJAX()
会返回一个promise
对象,且成功值就是响应体
我们直接将这个函数的调用放在await
后边,await
返回的就是成功值,也就是响应体
部分参考:
https://es6.ruanyifeng.com/?search=filter&x=0&y=0#docs/generator
https://es6.ruanyifeng.com/?search=filter&x=0&y=0#docs/async