当前位置:网站首页 > R语言数据分析 > 正文

从promise到await

在之前的这篇文章中,已经讲完了从异步操作到promise的诞生,但是promise也仅仅是做到了异步操作和结果的分离,当我们有多个异步操作,且结果前后有依赖的时候,不可避免的,就会出现一系列的.then方法。还是不大优雅。

最理想的情况是,我们能把异步操作写成同步的样子。当执行异步操作时,让代码卡在那里,等异步操作完成后再执行后续的代码。【注一】

这里,需要先了解迭代器和生成器。

一,可迭代对象和迭代器

Iterator是ES6提出来的迭代器。因为ES6开始数据结构新增了Set和Map,再加上已有的Array和Object,此外用户还可以自行组合它们来形成组合数据结构,复杂的数据结构导致循环遍历难度加大,为简化和统一循环方式,ES6就给出了迭代器(Iterator)这个接口来提供统一访问机制for..of或者…展开符。

迭代意味着遍历的同时能记录结果

它为各种不同的数据结构提供统一的访问机制。提供了一种按序访问集合内各个元素的方法,让我们可以方便地遍历集合内所有的元素。

1.1,可迭代对象和迭代器的区别

其实就是es6新增了迭代器的概念,所有满足迭代器协议的对象,都是可迭代对象。这里需要区分可迭代对象和迭代器的概念。

可迭代对象 迭代器
可使用方法 for...of...展开符 next()方法
来源 array,str,set,map等具备[Symbol.iterator]方法,且该方法返回一个迭代器的对象 具备next方法,能够指针移动实现遍历可迭代对象的对象
举例 array,str,set,map等 array,str,set,map等的[Symbol.iterator]方法的返回

迭代器协议:

具备[Symbol.iterator]方法,且该方法会返回一个迭代器对象,该对象的特征是具备next方法,能够进行迭代。 

1.2,迭代器的工作原理

创建一个指针对象,指向当前数据结构的起始位置 第一次调用next方法时,指针指向数据结构的第一个成员 接下来调用next方法,指针后移,直到指向最后一个成员 

不断地调用next重复获取过程,然后每次都返回一个结果。等到没有东西可返回了,就终止。因此next的返回对象有两个属性donevaluedone表示是否结束了,value表示当前迭代的结果。当donetrue的时候,表示迭代已结束,这时候是没有返回结果的也就是没有value这个属性。

//迭代器生成函数 function myIterator(arr) { 
    let index = 0 return { 
    next: function() { 
    let done = ( index >= arr.length ); let value = ! done ? arr[index++] : undefined; return { 
    value, done }; } } } let myIter = myIterator([1,2,3]); console.log(myIter.next());//{ value: 1, done: false } console.log(myIter.next());//{ value: 2, done: false }  console.log(myIter.next());//{ value: 3, done: false }  console.log(myIter.next());//{ value: undefined, done: true } 

1.3,Array等数据结构的默认迭代器

ES6中原生的可迭代对象迭有Array、Set、Map和String,for..of能够遍历它们是因为它们原型对象上具有Symbol.iterator属性,该属性指向该数据结构的默认迭代器方法,当使用for...of..迭代可迭代对象时,js引擎就会调用其Symbol.iterator方法,从而返回相应的默认迭代器。然后执行完其中的next方法。举例:

var arr = [1, 2, 3, 4, 5]; //数组是一个迭代器 // 使用for..of时,,js引擎就会调用其`Symbol.iterator`方法,从而返回相应数据的默认迭代器 for(var v of arr){ 
    console.log(v); // 12345 } 

那么既然它的原型对象上有Symbol.iterator方法,且返回的是对应的默认迭代器,我们就可以利用它生成对应的迭代器,然后使用next()方法访问值:

var arr = [1, 2, 3, 4, 5]; //数组是一个迭代器 //arr[Symbol.iterator]()会返回arr的默认迭代器,于是就可以使用next var it = arr[Symbol.iterator](); console.log(it.next())//{ value: 1, done: false } console.log(it.next())//{ value: 2, done: false } console.log(it.next())//{ value: 3, done: false } console.log(...it)//4 5,只打印出剩余的没有被迭代的,所以...应该也是利用的迭代器的next console.log(it.next())//{ value: undefined, done: true }//剩余的两个值被...迭代过了,于是这里就结束了 

1.2,手写一个可迭代对象

也就是说,一个数据结构只要有Symbol.iterator方法且Symbol.iterator方法返回具备next方法,就可以认为它是是可迭代的(iterable)对象

也就是说,需要满足两个条件:

1,该对象具备Symbol.iterator方法 2,Symbol.iterator方法返回一个对象,该对象具备next方法(迭代器)。 
// 实现一个可迭代对象 const myIterable = { 
    data: [1, 2, 3], [Symbol.iterator]() { 
    let index = 0; const data = this.data; return { 
    next() { 
    if (index < data.length) { 
    return { 
    value: data[index++], done: false }; } else { 
    return { 
    value: undefined, done: true }; } } } } }; // 遍历可迭代对象 for (const item of myIterable) { 
    console.log(item); // 输出 1 2 3 } // 通过迭代器对象遍历可迭代对象 const iterator = myIterable[Symbol.iterator](); console.log(iterator.next()); // {done: false, value: 1} console.log(iterator.next()); // {done: false, value: 2} console.log(iterator.next()); // {done: false, value: 3} console.log(iterator.next()); // {done: true, value: undefined} 

能够使用for...of遍历和...扩展符展开。当使用这两种方法时,js引擎其实是调用这个可迭代对象的[Symbol.iterator]方法,从而得到一个迭代器,然后执行这个迭代器的next方法,从而取到其中的值。

但是如果想要使用next方法手动遍历,就需要const myIterable2=createIterator(obj)[Symbol.iterator](),手动执行这个可迭代对象的[Symbol.iterator]方法,从而得到迭代器(具备next方法的对象)。

1.3,将不可迭代的object处理成可迭代

object类型之所以不能迭代,就是因为它的原型对象上没有[Symbol.iterator]属性,想想看,一个对象的属性间并没有严格的顺序要求,自然不要求能迭代。

// 自定义一个可迭代对象 function createIterator(obj){ 
    return { 
    // 返回一个迭代器对象 //模仿原生迭代器添加Symbol.iterator方法 [Symbol.iterator]: function () { 
    const keys=Object.keys(obj) let i=0 return { 
    next:function () { 
    //迭代器对象一定有next()方法 const done = ( i >= keys.length ); const key=keys[i] const value = !done ? obj[key] : undefined; i++ return { 
    //next()方法返回结果对象 value: value, done: done } } } } } } const obj={ 
    name:'名字', age:18 } const myIterable=createIterator(obj) // 使用 for-of 循环遍历可迭代对象 for(var v of myIterable){ 
    console.log(v) //名字 //18 } console.log([...myIterable])//['名字',18] const myIterable2=createIterator(obj)[Symbol.iterator]() console.log(myIterable2.next())//{ value: '名字', done: false } console.log(myIterable2.next())//{ value: 18, done: false }  console.log(myIterable2.next())//{ value: undefined, done: true } 

二,生成器Generator

它存在的最大作用就是帮助我们快速生成可迭代对象/生成器

JavaScript 中的生成器(Generator)是一种特殊的函数,可以用来定义在运行时生成一系列值的迭代器。Generator 函数以 function* 开头,内部使用 yield 关键字来指定生成器迭代过程中产生的每个值,每次遇到 yield 关键字时暂停函数的执行,可以通过调用迭代器上的 next 方法来恢复函数的执行。

2.1,创建一个生成器

function* myGenerator() { 
    yield 1; yield 2; yield 3; } const iter = myGenerator(); // 创建迭代器 for(var v of iter){ 
    console.log(v)//1,2,3 } const iter2 = myGenerator(); // 创建迭代器 console.log(iter2.next()); // { value: 1, done: false } console.log(iter2.next()); // { value: 2, done: false } console.log(iter2.next()); // { value: 3, done: false } console.log(iter2.next()); // { value: undefined, done: true } console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象 

生成器函数返回的,既是一个可迭代对象(可以使用for...of),又是一个迭代器(可以直接使用next())

每次yield就会生成一个断点。每次next就执行到这个断点过。 

2.2,既是可迭代对象,又是迭代器

类似于这样,Symbol.iterator是返回自身。自身上既有Symbol.iterator又有next,从而可以返回既是可迭代对象,又是迭代器。

function myGenerator(arr) { 
    let i=0 return { 
    next(){ 
    const done = i >= arr.length; const value = !done ? arr[i++] : undefined; return { 
    //next()方法返回结果对象 value: value, done: done } }, [Symbol.iterator]() { 
    return this } } } const iter = myGenerator([1,2,3,4,5,6]); // 创建迭代器 for(var v of iter){ 
    console.log(v)//1,2,3,4,5,6 } console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象 const iter2 = myGenerator([1,2,3,4,5,6,7]); // 创建迭代器 console.log(iter2.next()); // { value: 1, done: false } console.log(iter2.next()); // { value: 2, done: false } console.log(iter2.next()); // { value: 3, done: false } console.log(iter2.next()); // { value: 4, done: false } 

生成器生成的对象就有这样的特征。

2.3,next传递参数

yield 表达式本身没有返回值,或者说总是返回 undefined 。 next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。【注意这里是上一个】,也就是说,next在第一个yield处停住,第二次的next入参才是这第一个yield的返回值。

也就是说第一个next带入参没意义,因为它没有上一个断点yield。

function* foo(x) { 
    let y = x * (yield) return y console.log("111")//后续的代码不会被执行 yield 2 } const it = foo(6) it.next()//执行到yield过,x*这段语句还未执行 let res = it.next(7)//next传入7作为上一个yield的返回,代码接着执行,计算后赋值给y,然后return结束 console.log(res) // { value: 42, done: true } it.next(8) 

return 会强制生成器进入关闭状态,提供给 return 方法的值,就是终止迭代器对象的值,也就是说此时返回的对象状态为true,值为传入的值。

2.4,return方法提前终止生成器

和上文中一样,return方法也会强制生成器进入关闭状态,提供给 return 方法的值,就是终止迭代器对象的值,也就是说此时返回的对象状态为true,值为传入的值。

function* foo() { 
    console.log("11") yield 1 console.log("22") yield 2 console.log("33") yield 3 console.log("44") yield 4 } const it = foo() console.log("next",it.next())//next { value: 1, done: false } console.log("return",it.return("test"))//return { value: 'test', done: true } 

2.5,throw抛出错误终止生成器

throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。

function* foo() { 
    console.log("11") yield 1 console.log("22") yield 2 console.log("33") yield 3 console.log("44") yield 4 } const it = foo() console.log("next",it.next())//next { value: 1, done: false } console.log("return",it.throw(new Error('出错了!')))//Error: 出错了! 

简单的理解就是,next()、throw()、return()方法,都是把yield以及后面换成传入的参数。

2.6,yield* 表达式委托迭代

yield* 允许我们在 Generator 函数中调用另一个 Generator 函数或可迭代对象。

Generator 函数执行到一个 yield* 表达式时,它会暂停执行,并且将执行权转移到另一个 Generator 函数或可迭代对象中。直到这个函数或对象迭代结束后,执行权才会返回到原 Generator 函数中。

这也叫委托迭代。通过这样的方式,能将多个生成器连接在一起。

function * anotherGenerator(i) { 
    yield i + 1; yield i + 2; yield i + 3; } function * generator(i) { 
    yield* anotherGenerator(i); yield "最后一个" } var gen = generator(1); console.log(gen.next().value)//2 console.log(gen.next().value)//3 console.log(gen.next().value)//4 console.log(gen.next().value)//最后一个 for (let value of generator(2)) { 
    console.log(value); // 输出 3,4,5,'最后一个' } 

三,Generator在异步编程的应用到await的诞生

3.1,作为异步编程的解决方案

当我们知道Generator和yield配合使用,能够暂停代码的执行,就应该能敏锐地意识到它可以用来解决异步编程,尽管Promise已经极大的提升了异步编程的可读性和可维护性,避免了回调地狱的产生。但在某些情况下,代码还是会变得非常复杂,特别是在需要同时处理多个异步操作,或者需要控制这些异步操作的顺序时。使用 Promise 还是需要写大量的 then 语句来处理异步操作的结果,这可能在一定程度上增加了代码的复杂性。

而Generator函数凭借其阻塞式调用机制使得异步代码可以写得像同步代码一样,举之前读取文件的例子:

const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } let res1,res2,res3 readFile('test.txt') .then((res)=>{ 
    res1=res return readFile('test1.txt') }) .then((res)=>{ 
    res2=res return readFile('test2.txt') }) .then((res)=>{ 
    res3=res console.log("结果",res1+res2+res3) }) 

使用generator函数改写:

const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } function* myGenerator() { 
    //先按照顺序读取三个文件 const txt1= yield readFile('./text1.txt'); const txt2=yield readFile('./text2.txt'); const txt3=yield readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } const generator = myGenerator(); generator.next().value//取得第一个promise对象 .then((result1)=>{ 
    return generator.next(result1).value })//取得第二个promise对象 .then((result2)=>{ 
    return generator.next(result2).value })//取得第三个promise对象 .then((result3)=>{ 
    generator.next(result3)//取得最后一个文件的值,继续执行代码 }) 

如果只看:

function* myGenerator() { 
    //先按照顺序读取三个文件 const txt1= yield readFile('./text1.txt'); const txt2=yield readFile('./text2.txt'); const txt3=yield readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } 

是不是就和同步差不多了?

但是看起来底下的generator.next()还是有一堆的then,和之前使用promise差不多呀。

为了方便理解,我们把上面的代码改写成回调地狱的形式。

 const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } function* myGenerator() { 
    //先按照顺序读取三个文件 const txt1= yield readFile('./text1.txt'); const txt2=yield readFile('./text2.txt'); const txt3=yield readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } const generator = myGenerator(); generator.next().value.then((result1)=>{ 
    generator.next(result1).value.then((result2)=>{ 
    generator.next(result2).value.then((result3)=>{ 
    generator.next(result3) }) }) }) 

3.2 ,封装生成器的递归传值

注意到这一坨玩意儿其实是递归传值:

generator.next().value.then((result1)=>{ 
    generator.next(result1).value.then((result2)=>{ 
    generator.next(result2).value.then((result3)=>{ 
    generator.next(result3) }) }) }) 

那么,我们其实可以想办法把他封装一下,于是代码就变成了这个样子:

const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } function* myGenerator() { 
    //先按照顺序读取三个文件 const txt1= yield readFile('./text1.txt'); const txt2=yield readFile('./text2.txt'); const txt3=yield readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } //封装函数 function co(generator){ 
    const gen=generator(); function _next(val){ 
    var p=gen.next(val); if(p.done) return; //这里用Promise.resolve是为了避免p.value不是promise,将之转化一下 Promise.resolve(p.value).then(res=>{ 
    _next(res); }) } _next() } co(myGenerator) 

到这一步其实已经差不多了,网上有复杂一丢丢的实现,但是目前这样的封装,已经足够我们理解它是如何将异步转化成看起来同步的写法。

四,async和await的诞生

假如说js引擎帮我们在代码执行时引入co函数,并且执行co(myGenerator)的话,那么对于我们写代码的人而言,我们是不是只需要写:

const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } function* myGenerator() { 
    //先按照顺序读取三个文件 const txt1= yield readFile('./text1.txt'); const txt2=yield readFile('./text2.txt'); const txt3=yield readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } 

这不和async/await差不多了吗。

也就是说,可以这样子理解:

当我们写下asycn的时候,js引擎就把它当做一个生成器函数处理,并且帮助我们引入了co函数,并且做好递归传值的工作。 而await就等同于yield。 

请添加图片描述

于是就产生了async和await,它才让我们写异步代码像同步一样丝滑。

const fs = require('fs') function readFile(fileName){ 
    return new Promise((resolve,reject)=>{ 
    fs.readFile(fileName, (err,data) => { 
    resolve(data.toString()) }) }) } async function test() { 
    //先按照顺序读取三个文件 const txt1= await readFile('./text1.txt'); const txt2= await readFile('./text2.txt'); const txt3= await readFile('./text3.txt'); //这里再用结果处理其他代码 console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件 } test() 
到此这篇从promise到await的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • charles抓包手机的http2024-12-02 12:09:09
  • Tomcat和Servlet了解2024-12-02 12:09:09
  • pr中镜头防抖动2024-12-02 12:09:09
  • rollup学习笔记2024-12-02 12:09:09
  • 清除ElementUI的el-input标签的校验——使用validate方法-区别之resetFields()-移除校验结果并重置字段值 & clearValidate()-移除校验结果2024-12-02 12:09:09
  • 从异步到promise2024-12-02 12:09:09
  • 从window.history理解浏览器返回不触发页面刷新问题2024-12-02 12:09:09
  • element Transfer 穿梭框 内容太长显示不全,鼠标移动上去显示全部2024-12-02 12:09:09
  • postcss-pxtorem 插件自动转换 rem 单位的配置2024-12-02 12:09:09
  • 使用fabric画一个图形-练习2024-12-02 12:09:09
  • 全屏图片