以下只是个人观点,如果有出入或者错误之处,欢迎提出,你不需要纠结我的说法是否有一些小瑕疵,把其中你认为对的地方融入到自己的知识里去就好了,你学到的才是自己的。
在说async和await之前,先讲另一个东西迭代器。
迭代器是什么?
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。更具体地说,迭代器是通过使用
next()
方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象:value
,这是序列中的 next 值;和done
,如果已经迭代到序列中的最后一个值,则它为true
。如果value
和done
一起存在,则它是迭代器的返回值。一旦创建,迭代器对象可以通过重复调用 next()显式地迭代。迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。在产生终止值之后,对 next()的额外调用应该继续返回{done:true}。
以上是一个mdn官方的解释。
下面我用代码具体演示下什么叫迭代器:
// 创建一个迭代器函数,传入一个数组,自动创建一个迭代器
function createIterator(array) {
// 这里我是通过数组的下标获取数组的值,方式无所谓,你也可以换成其他实现方式
let index = 0
return {
next() {
if (index < array.length) {
return { done: false, value: array[index++] }
}
return { done: true }
}
}
}
const nums = [1, 3, 5, 6]
const numsIterator = createIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
// { done: false, value: 1 }
// { done: false, value: 3 }
// { done: false, value: 5 }
// { done: false, value: 6 }
// { done: true, value: undefined }
// { done: true, value: undefined }
概括下,什么叫迭代器?
1. 迭代器是一个对象
2. 该对象包含next函数,且next函数返回一个对象,该对象包含{ done: boolean, value: any }字段,且当值结束时,done为true
在这里提一下,es6的for of 方法
for of 可以遍历可迭代对象,非可迭代对象,for of 是不可以遍历的
for of 内部实现实际上就是调用可迭代对象的next方法,并返回其value值。
举个例子:for of 遍历 Array
const nums = [1, 2, 4, 7]
for(const num of nums) {
console.log(num)
}
console.log(nums[Symbol.iterator]); // [Function: values]
console.log(nums[Symbol.iterator]()); // Object [Array Iterator] {}
console.log(nums[Symbol.iterator]().next()); // { value: 1, done: false }
console.log(nums[Symbol.iterator]().next().value); // 1
可以从上面的代码上看出来,我们定义的数组里存在一个名字叫 Symbol.iterator 的函数,实际上这就是官方的规范,可迭代对象需要实现这个方法。
什么叫可迭代对象?
mdn的描述:
若一个对象拥有迭代行为,比如在 for...of 中会循环哪些值,那么那个对象便是一个可迭代对象。一些内置类型,如 Array 或 Map 拥有默认的迭代行为,而其他类型(比如Object)则没有。
为了实现可迭代,一个对象必须实现 @@iterator 方法,这意味着这个对象(或其原型链中的任意一个对象)必须具有一个带 Symbol.iterator 键(key)的属性。
简单的说,就是一个对象中必须包含Symbol.iterator方法,且该方法返回一个迭代器,它就是一个可迭代对象。
其实你了解过原型链的话,你就会知道,该方法是在对象本身,还是原型链的对象上,实际上都会被调用的。
常见的可迭代对象:Array、string、Map、Set、NodeList等
举个简单的例子,实现一个可迭代对象:
const iterableObj = {
// 这里我是叫nums,你也可以起一个其他名字,这个不是重要的,重要的是[Symbol.iterator]方法的实现
nums: [1, 2, 4, 7],
// 为什么使用[]把 Symbol.iterator 括起来,这个是对象中动态key的写法,大家可以去看下官方文档
[Symbol.iterator]: function() {
let index = 0
return {
// 这里为什么使用箭头函数呢
next: () => {
if (index < this.nums.length) {
return { done: false, value: this.nums[index++] }
}
return { done: true }
}
}
}
}
// for of 遍历,可以看到打印信息,是可以遍历的,且正确输出了值
for(const item of iterableObj) {
console.log(item);
}
// 1, 2, 4, 7
上面的代码里next方法为什么使用箭头函数呢?
大家可以看下,[Symbol.iterator]方法,返回的是一个对象,该对象中有一个next方法,如果我们不使用箭头函数,那它的this指向就不能保证一直是指向我们的当前可迭代对象。
至于为什么使用箭头函数就会始终指向我们的nums,这个就是箭头函数的特殊性了,箭头函数中是不会绑定this的,所以它就会沿着作用域向上查找,它的上层作用域,可以看出来就是[Symbol.iterator]函数,不知道为啥的,建议去看下this指向问题,这里不详细讲述了。
生成器是什么?
生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。生成器函数使用 function*语法编写。最初调用时,生成器函数不执行任何代码,而是返回一种称为 Generator 的迭代器。通过调用生成器的下一个方法消耗值时,Generator 函数将执行,直到遇到 yield 关键字。
可以根据需要多次调用该函数,并且每次都返回一个新的 Generator,但每个 Generator 只能迭代一次。
举个简单的例子:
// 可以通过打印信息查看生成器的执行流程
function* createIterator() {
console.log('----这是第一段代码----')
yield
console.log('----这是第二段代码----')
}
const foo = createIterator()
foo.next() // 执行第一段代码
foo.next() // 执行第二段代码
从上面的代码可以看出,生成器的标志是function后面加*,并且结合着yield使用,且是以yield为分割执行代码块。
function* createIterator() {
console.log('----开始执行了----')
yield
console.log('----执行结束了----')
return '1111'
}
const foo = createIterator()
console.log('第一段代码的值:', foo.next());
console.log('第二段代码的值:', foo.next());
// ----开始执行了----
// 第一段代码的值: { value: undefined, done: false }
// ----执行结束了----
// 第二段代码的值: { value: '1111', done: true }
从上面代码的打印信息来看,是不是有点熟悉,是不是和迭代器的返回值很像,本质上生成器就是一种特殊的迭代器。
function* createIterator() {
console.log('----第一段代码----')
const value1 = 100
console.log('第一段代码的值:', value1);
const n = yield value1
console.log('----第二段代码----')
const value2 = 200 * n
console.log('第二段代码的值:', value2);
yield value2
console.log('----第三段代码----')
}
const foo = createIterator()
console.log('第一段代码:', foo.next());
console.log('第二段代码:', foo.next(10));
console.log('第三段代码:', foo.next());
从上面代码可以看到生成器是可以传参的,我们通过next方法传入的10,在生成器里通过 yield 的返回值获取到了,看到这估计有人疑惑了,其实这就是生成器的规范,以方便做参数的传递,那估计又会有人有疑问了,那我第一个yield之前的代码如果想获取参数怎么办,其实这是陷入一个思维误区了,第一个yield之前的代码段传参,不需要通过next方法传参,直接在createIterator函数中传参即可。
终止执行生成器的方法:
function* createIterator() {
console.log('----第一段代码----')
const value1 = 100
console.log('第一段代码的值:', value1);
const n = yield value1
// 下面的foo.return相当于在此执行了 return n
console.log('----第二段代码----')
const value2 = 200 * n
console.log('第二段代码的值:', value2);
yield value2
console.log('----第三段代码----')
}
const foo = createIterator()
console.log('第一段代码:', foo.next());
console.log('第二段代码:', foo.return(10)); // return方法
console.log('第三段代码:', foo.next());
// ----第一段代码----
// 第一段代码的值: 100
// 第一段代码: { value: 100, done: false }
// 第二段代码: { value: 10, done: true }
// 第三段代码: { value: undefined, done: true }
可以通过生成器的内置方法return终止执行,它的原理很简单,就是在你执行代码块中插入一个return语句。
例如上面的代码我在第二个代码块调用了return,则相当于在第一个yield的后面直接return数据,通过打印的值也可以看出来。
还有一种就是抛出错误了,这个就不多说了,通过throw的方式抛出错误即可。
我们来看看async和await解决了一个什么问题?
正常情况下,我们实现一个请求的回调嵌套请求,在不使用async 的情况下怎么实现。
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url)
}, 2000)
})
}
requestData('hello').then(res => {
console.log('当前的返回值1:', res);
// 现在我们需要这个返回值作为参数,继续请求
requestData(res + ' world').then(res1 => {
console.log('当前的返回值2:', res1);
})
})
这种方式如果嵌套很多,可读性和维护性就很差,有可能到以后你再回来看你自己的代码,自己都看不懂。
当然在很早的时候也有人做一些非常简单的优化,例如:
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url)
}, 2000)
})
}
requestData('hello').then(res => {
console.log('当前的返回值1:', res);
// 现在我们需要这个返回值作为参数,继续请求
return requestData(res + ' world')
}).then(res1 => {
console.log('当前的返回值2:', res1);
})
虽然比之前的好一点,那本质问题还在,并没什么区别。
在之后在es6之前,也有社区有比较好的实现,使用promise+生成器实现,例如:
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url)
}, 2000)
})
}
function* getData() {
const res1 = yield requestData('hello')
const res2 = yield requestData(res1 + ' world')
console.log(res2);
}
// 这个函数,也是有一个第三方库的,叫co,可以搜一下
function excuGenerator(genFn) {
const generator = genFn()
function excu(res) {
const generatorValue = generator.next(res)
if (generatorValue.done) {
return generatorValue.value
}
generatorValue.value.then(res => {
excu(res)
})
}
excu()
}
excuGenerator(getData)
看到这里是不是感觉有点像,和async和await有点像,实际上async和await就是一个语法糖,它的实现逻辑和这个很像。
async 和 await
async你可以理解为异步,但不代表async函数就是一个异步函数,举例:
async function foo() {
console.log('我是在async中执行的');
}
foo()
console.log('我是在async后执行的');
// 我是在async中执行的
// 我是在async后执行的
看打印的数据顺序,能看出来什么,其实他还是按执行顺序打印的,这是因为虽然你在函数前面加了async 关键字,但是它执行顺序还是和普通函数一样,你就把它当成普通函数执行就好。
我们使用async和await重写之前的代码:
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url)
}, 2000)
})
}
async function getData() {
const res1 = await requestData('hello')
const res2 = await requestData(res1 + ' world')
console.log(res2);
}
getData()
异步函数和普通函数的区别:
1. 异步函数的返回值是一个promise,例如:
async function foo() {
}
const res = foo()
console.log(res);
// Promise { undefined }
2. 异步函数的异常会被catch捕获,如果捕获,不会影响后续的代码执行
async function foo() {
throw new Error('error message')
}
foo().catch((res) => {
console.log('error111: ', res);
})
console.log('后续代码');
3. 异步函数可以使用 await 关键字
function requestData(url) {
// 1. 第一种方式
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve(url)
// }, 2000)
// })
// 2. 第二种方式
// return {
// then(resolve, reject) {
// setTimeout(() => {
// resolve(url)
// }, 2000)
// }
// }
// 3. 第三种方式
return url
}
async function getData() {
const res1 = await requestData('hello')
const res2 = await requestData(res1 + ' world')
console.log(res2);
}
getData()
到这基本就结束了,虽然内容不是很深奥,都是些比较基础的知识,但也希望对你有所帮助,也算是我的一个记录,毕竟输出才是检验知识的标准,不喜勿喷,如果有错误,欢迎指正和交流。