关于Js的promise、generator、async、await

关于Js的promise、generator、async、await

第一章 前言

​ 大家都知道Javascript是单线程的,而且他的耗时操作是异步的,比如网络请求以及IO操作。在一般来说,我们比较喜欢他的异步,因为异步效率比较高,资源得到了合理的利用,但是有的时候我们为了控制流程,而且流程里面存在一些耗时操作,如果还是使其异步的话就会使得我们的流程非常难控制,所以这个时候我们就要同步执行我们的耗时操作。

第二章 关于单线程

​ 有的时候我会想,javascript既然是单线程的,那为什么他又可以异步的呢?(因为作为小白的我的认知来说,异步就是开个新的线程去执行这个耗时任务 o(╥﹏╥)o)

​ 这里就有一个主线程的概念了,所谓单线程就是Javascript 引擎在解释、处理javascript代码的线程只有一个,这个线程就是主线程。实际上浏览器还存在其他线程,比如处理网络请求、处理DOM等的线程,这些线程称为工作线程,这里说的单线程的意思是javascript无论什么时候都只有一个线程在运行javascript程序

这样的好处就是javascript的单线程简化了处理事件的机制,不必理会资源竞争和线程同步这些复杂的问题。

第三章 关于同步、异步、阻塞、非阻塞

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 同步

    同步就是程序一行一行的执行代码,只有等上一行的代码执行完了,才会继续执行下一行代码

    function sync{
      console.log("1")
      console.log("2")
      console.log("3")
      console.log("4")
      console.log("5")
    }
    sync() // 输出 1,2,3,4,5
    
  • 阻塞

    阻塞调用是指在返回结果之前,程序会等待这个调用直到这个调用返回结果,才会继续往下执行

    function block(){
        console.log(1);
        console.log(2);
        // 这里假设这个文件比较大,需要花费1分钟才能打开它
        let res = fs.readFileSync("xxx.json")
        console.log(3);
        console.log(4);
    }
    block() // 输出 1,2,(这里过了1分钟之后) 继续输出 3,4    
    
  • 异步

    异步操作在js中的原理是当遇到异步操作时(比如网络请求、IO耗时操作等),这个异步任务会挂起,放到任务队列,任务队列的任务会等到任务队列之外的所有代码执行完毕之后在执行,因此程序的执行顺序可能和代码中的顺序不一致。

    function async() {
        console.log("开始准备请求数据");
        console.log("马上要开始请求了...");
        $.ajax('http://xxxx.com', function(resp) {
            console.log('请求完成~');
        });
        console.log("请求发出完成");
    }
    async()
    // 开始准备请求数据
    // 马上要开始请求了...
    // 请求发出完成
    // 请求完成~
    
  • 非阻塞

    非阻塞调用是指程序执行到非阻塞调用时,会将该任务放置任务队列,然后程序继续往下执行,等任务队列以外的代码都执行完成之后,才开始执行任务队列中的方法

    function nonBlocking(){
        console.log("开始读文件了");
        fs.readFile("./package.json",(err,data)=>{
            console.log("文件读取完成...");
        })
        console.log("发起文件读取完毕");
    }
    nonBlocking()
    // 开始读文件了
    // 发起文件读取完毕
    // 文件读取完成...
    

第四章 异步编程的四种方式

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。 ------摘自 阮一峰

  • 回调函数

    /**
     * 吃饭
     */
    function eat(){
        console.log("开始吃饭");
    }
    /**
     * 洗手
     * @param {function} afterTask 
     */
    function wishHands(afterTask) {
        /**
         * 洗手要洗一分钟
         */
        console.log("开始洗手...");
        setTimeout(_=>{
            console.log("手洗干净了...");
            afterTask()
        },1000*60)
    }
    
    /**
     * 吃饭前需要洗手
     */
    wishHands(eat)
    // 开始洗手...
    // 手洗干净了...
    // 开始吃饭
    
  • 事件监听

    function task(){
        console.log("task start");
        setTimeout(_=>{
            task.trigger('finish') // 触发finish事件
        },1000*3)
    }
    function afterTask(){
        console.log("task finish");
    }
    // 监听finish事件
    task.on('finish',afterTask)
    task()
    

    这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

  • 发布/订阅

    我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

    这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

    function task(){
        console.log("task start");
        setTimeout(_=>{
            jQuery.publish("finish")
        },1000*3)
    }
    function afterTask(){
        console.log("task finish");
    }
    jQuery.subscribe('finish',afterTask)
    task()
    
  • Promise 对象

    Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口

    它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。

    function task(){
        console.log("开始执行任务");
        return new Promise((resolve,reject)=>{
            setTimeout(_=>{
                resolve("我完成啦")
            })
        })
    }
    function afterTask(){
        console.log("afterTask 开始");
    }
    task().then(res=>{
        console.log(res);
        afterTask()
    })
    

第五章 关于Promise

​ Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

​ 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

  • 基本用法

    Promise对象是一个构造函数,接受一个包含resolve回调和reject回调参数的函数为参数,执行结果符合预期可以调用resolve,不符合预期可以执行reject抛出异常。

    let promise = new Promise((resolve,reject)=>{
        let res = task()
        if (res is expect) {
            resolve("good")
        }
        reject("res is not expect")
    })
    

    Promise有三种状态:执行中已成功已失败

    resolve调用之后,promise实例的状态就从执行中->已成功

    reject调用之后,promise实例的状态就从执行中->已失败

    要对Promise的执行结果做处理可以执行它的then方法,then方法包含两个参数,第一个是成功的回调,第二个是失败可选回调:

    promise.then(res=>{
        console.log("exec success:"+res); // 这里res就是resolve的参数
    },err=>{
        console.log("exec fail:"+err);  // 这里 err就是reject的参数
    })
    
  • Promise 实例的属性

    promise.then(res=>{})  // 可以理解成结果的回调
    promise.catch(reason=>{}) // 执行失败或发生异常的回调
    promise.finally(_=>{})  // 执行结束的回调,不管成功与否都会回调
    
    // then()方法和catch()返回的结果仍然是promise,所以可以使用链式写法
    
    //写法一
    promise.then(res=>{
    
    },err=>{
    
    }).finally(_=>{
        
    })
    // 写法二
    promise.then(res=>{
    
    }).catch(err=>{
        
    }).finally(_=>{
        
    })
    
    // 写法一和写法二效果是一样的
    
  • Promise静态方法

    • Promise.all(...promise[])将多个 Promise 实例,包装成一个新的 Promise 实例
    Promise.all(p1,p2,p3)
    

    上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

    p的状态由p1p2p3决定,分成两种情况。

    (1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    (2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

    • Promise.race(...promise[])方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
    Promise.race(p1,p2,p3)
    

    上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

    • Promise.resolve(task) 将一个方法包装成promise,比如他ajax请求
    • Promise.reject(task) 同上
  • 更多详情请参考阮一峰 promise

第六章 关于Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • 示例

    function* generatorTest(){
        console.log("generator test start ");
        yield console.log("step 1");
        yield console.log("step 2");
        yield console.log("step 3");
        console.log("generator test finish");
    }
    
    let test = generatorTest()
    // 什么都没有
    test.next()
    // generator test start 
    // step 1
    test.next()
    // step 2
    test.next()
    // step 3
    test.next()
    // generator test finish
    test.next()
    // 什么都没有
    

    ​ 从上面实例中我们可以看出来,generator方法的声明需要加上*号,里面还有关键字yield调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。然后需要执行next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

  • 更多详情请参考阮一峰 generator

第七章 async和await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

  • 使用async与generator的对比

    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);
        });
      });
    };
    // 使用generator
    const gen = function* () {
      const f1 = yield readFile('/etc/fstab');
      const f2 = yield readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    // 使用async
    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 函数的星号(*)替换成async,将yield替换成await,仅此而已。
    
  • 基本用法

    async声明的方法返回的是一个promise对象,可以使用then方法添加回调函数,当程序执行的过程中,一旦遇到await 声明的语句,程序将会等待这个语句返回结果之后才会执行后面的方法。

    • 实例一:
    // 这里假设getStudentIdByNumber和getStudentScoreById都是耗时的网络请求,所以是异步的
    async function getStudentScoreByStudentNumber(studentNumber)
    {
          // 根据学号拿到id
        let studentId = await getStudentIdByNumber(studentNumber)
        // 通过id获取分数
        let score = await getStudentScoreById(studentId)
        // 获取分数后返回
        return score
    }
    // 使用then去获取执行结果
    getStudentScoreByStudentNumber("662297").then(score=>{
        console.log(score);  
    })
    
    • 实例二:
    function request(){
        return new Promise((resolve,reject)=>{
            setTimeout(resolve,1000*5)
        })
    }
    async function networkTask(){
        console.log("request start");
        await request()
        console.log("request finish");
    }
    networkTask()
    // 输出 request start
    // 等待5秒
    // 输出 request finish
    

1、正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

2、根据语法规则,await命令只能出现在 async函数内部,否则都会报错。

3、await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

第八章 注意

  • 如果多个异步请求之间存在前后关系,可以像上一章一样使用await来改造成同步

  • 如果多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    let foo = await getFoo();
    let bar = await 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函数会有问题

    // 错误一
    async function dbFuc(db) {
      let docs = [{}, {}, {}]; 
      // 报错
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }
    // 错误二
    function dbFuc(db) { //这里不需要 async
      let docs = [{}, {}, {}];
      // 可能得到错误结果
      docs.forEach(async function (doc) {
        await db.post(doc);
      });
    }
    // 改成for of 准确
    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      for (let doc of docs) {
        await db.post(doc);
      }
    }
    // 改成reduce
    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      await docs.reduce(async (_, doc) => {
        await _;
        await db.post(doc);
      }, undefined);
    }
    

参考资料

阮一峰 异步编程的四种方法

阮一峰 promise

阮一峰 generator

阮一峰 async

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容