IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> async和await随谈 -> 正文阅读

[JavaScript知识库]async和await随谈

以下只是个人观点,如果有出入或者错误之处,欢迎提出,你不需要纠结我的说法是否有一些小瑕疵,把其中你认为对的地方融入到自己的知识里去就好了,你学到的才是自己的。

在说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()

到这基本就结束了,虽然内容不是很深奥,都是些比较基础的知识,但也希望对你有所帮助,也算是我的一个记录,毕竟输出才是检验知识的标准,不喜勿喷,如果有错误,欢迎指正和交流。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-12-25 10:59:18  更:2022-12-25 11:03:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/27 1:28:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码