js异步解决方案 --- 回调函数 vs promise vs generater/yield vs async/await

javascript -- 深度解析异步解决方案

高级语言层出不穷, 然而惟 js 鹤立鸡群, 这要说道js的设计理念, js天生为异步而生, 正如布道者朴灵在 node深刻浅出--(有兴趣的能够读一下, 颇有意思^_^) , 异步很早就存在于操做系统的底层, 意外的是,在绝大多数高级编程语言中,异步并很少见,疑似被屏蔽了一搬. 形成这个现象的缘由或许使人惊讶, 程序员不太适合经过异步来实现进行程序设计 ^_^.

异步的理念是很好的, 然而在程序员编程过程当中确实会出现一些问题, 并非这种理念不容以让人接受, 而是当有大量的异步操做时会让你的代码可读性下降, 其中回调函数异步编程容易产生毁掉陷阱, 即 callback hell--(不要急, 后面会详细讲解)javascript

然而 js 社区从为中止其脚步, 最新的 ES7 所推出的 async/await 终极异步解决方案, 说终很可能有所不严禁, 然而它确实已经彻底将原来经过模块侵入式的异步编程解脱出来, 可让程序员以接近传统意义上的函数调用实现异步编程, 这是 js 里程碑式变革中极其重要的一部分.java

Javascript异步编程解决方案历史与方法

ES 6之前:node

  • 回调函数
    回调函数是最原始的异步编程方案, 上篇文章已经讲述, 这里再也不累赘, 这里给出传送门 回调函数之美 然而若是业务逻辑过多时, 回调函数会产生深层嵌套, 对程序员极不友好,
    以下代码所示有一个业务逻辑, 须要对a, b, c三个文件一次读取程序员

    var fs = require('fs');
        
        fs.readFile('./a.txt', function(err1, data1) {
             fs.readFile('./b.txt', function(err2, data2) {
                  fs.writeFile('./ab.txt', data1 + data2, function(err) {
                       console.log('read and write done!');
                  });
             });
        });

    三个异步函数嵌套看起来挺简单的, 这里知识简单假设, 抛砖引玉, 若是有5个,10个甚至更多的异步函数要顺序执行,那要嵌套(你们都不喜欢身材横着长吧哈哈)说实话至关恐怖,代码会变得异常难读,难调试,难维护。这就是所谓的回调地狱或者callback hell。正是为了解决这个问题,才有了后面两节要讲的内容,用promise或generator进行异步流程管理。异步流程管理说白了就是为了解决回调地狱的问题。因此说任何事情都有两面性,异步编程有它独特的优点,却也同时遇到了同步编程根本不会有的代码组织难题。shell

  • 事件监听(事件发布/订阅)
    事件监听模式是一种普遍应用于异步编程的模式, 是回调函数的事件化,即发布/订阅模式,express

    var util = require('util');
        var events = require('events');
        
        function Stream() {
          events.EventEmitter.call(this);
        }
        util.inherits(Stream, events.EventEmitter)
        let got = new Stream();
        got.on("done", function (params) {
          console.log(params);
        });
        got.on("done", function (params) {
          console.log('QWER');
        });
        got.emit("done", 'diyige');
        console.log('-----------------');
        
        var emitter = new events.EventEmitter();
        
        emitter.on("done", function (params) {
          console.log(params);
        });
        emitter.on("done", function (params) {
          console.log('ZXCV');
        });
        emitter.emit("done", 'dierge');
        
        // diyige
        // QWER
        // dierge
        // ZXCV
  • Promise对象
    Promise 是异步编程的一种解决方案,它是比传统的解决方案——回调函数和事件——更合理和更强大, 它的目的是替换之前回调函数的比不编程方案, 也是后续介绍的异步解决方案的基础, 它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象, 如今的 js库几乎都支持这种异步方案编程

    promise对象有如下特色segmentfault

    • 对象的状态不受外界影响。Promise对象表明一个异步操做,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其余手段没法改变
    • 一旦状态改变,就不会再变,任什么时候候均可以获得这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种状况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。若是改变已经发生了,你再对Promise对象添加回调函数,也会当即获得这个结果。这与事件(Event)彻底不一样,事件的特色是,若是你错过了它,再去监听,是得不到结果的。

图片描述

下面为单个promise对象应用方法
var promise = new Promise(function(resolve,reject){
      // ... some code
      if(/* 异步操做成功 */){
        resolve(value);
      }else{
        reject(error);
      }
    });

一般用promise 的时候咱们通常把它相应的业务包装起来下图所示模拟了一个读取文件的异步
promise 函数,后端

var readFile =  function (params) {
      return new Promise(function(resolve, reject){
    
        setTimeout(function(){
            resolve(params);
        }, 2000);
      });
    }
    
    readFile('file1').then(function (data) {
      console.log(data);
      return readFile('file2')
    }).then(function (data) {
      console.log(data);
      return readFile('file3')
    }).then(function (data) {
      console.log(data);
      return readFile('file4')
    }).then(function (data) {
      console.log(data);
      return readFile('file5')
    }).then(function (data) {
      console.log(data);
    })
    //file1
    //file2
    //file3
    //file4
    //file5
  • 流程控制库
    还有一种须要手工调用采可以处理后续任务的, 在这里只简单介绍一种, 咱们称之为尾触发, 经常使用的关键字为 next , 为何要讲到它是由于它是 node 神级框架 express中采用的模式, 这里可能要涉及一些后端node的内容
    在 node 搭建服务器时须要面向 切面编程 ,这就须要各类各样的中间件promise

    var app = connect();
        // Middleware
        app.use(connect.staticCache());
        app.use(connect.static(__dirname + '/public'));
        app.use(connect.cookieParser());
        app.use(connect.session());
        app.use(connect.query());
        app.use(connect.bodyParser());
        app.use(connect.csrf());
        app.listen(3001);

    在经过 use() 方法监听好一系列中间件后, 监听端口上的请求, 中间件采用的是尾触发的机制, 下面是个一个简单的中间件

    function (req, res, next) {
        // express中间件
        }

    每一个中间件传递请求对象, 响应对象, 和尾触发函数, 经过队列造成一个处理流, 以下图
    图片描述
    中间件机制使得在处理网络请求时, 能够像面向切面编程同样进行过滤, 验证, 日志等功能.

ES 6:

  • Generator函数(协程coroutine)
    Generator 函数有多种理解角度。语法上,Generator 函数是一个状态机,封装了多个内部状态。
    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。执行函数后返回的是一个遍历器对象,能够依次遍历 Generator 函数内部的每个状态。

    function* helloWorldGenerator() {
              yield 'hello';
              yield 'world';
              return 'ending';
        }
        var hw = helloWorldGenerator();
        hw.next()
        // { value: 'hello', done: false }
        
        hw.next()
        // { value: 'world', done: false }
        
        hw.next()
        // { value: 'ending', done: true }
        
        hw.next()
        // { value: undefined, done: true }

    下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法能够恢复执行。

  • 基于 Promise 对象的自动执行
    generater/yield函数还没法真正解决异步方案的问题, 须要配合额外的执行模块 如 TJ Holowaychuk 的 co 模块, 在这里用promise模块进行generater函数的自动执行;

    var fs = require('fs');
        
        var readFile = function (fileName){
          return new Promise(function (resolve, reject){
            fs.readFile(fileName, function(error, data){
              if (error) return reject(error);
              resolve(data);
            });
          });
        };
        
        var gen = function* (){
          var f1 = yield readFile('/etc/fstab');
          var f2 = yield readFile('/etc/shells');
          console.log(f1.toString());
          console.log(f2.toString());
        };
    /*****************************************
           
            var g = gen();
            g.next().value.then(function(data){
              g.next(data).value.then(function(data){
                g.next(data);
              });
            });
    *****************************************/
        // 自动执行函数        
        function run(gen){
          var g = gen();
        
          function next(data){
            var result = g.next(data);
            if (result.done) return result.value;
            result.value.then(function(data){
              next(data);
            });
          }
        
          next();
        }
        run(gen);

ES 7:

  • async/await
    终于来到了咱们求之不得的的"终极"异步解决方案, 或许你有些失望, 固然这种失望是async/await 仅仅是语法糖, async/await 就是 generater/yield/promise + 自动执行模块的封装.相对于前辈 async 函数能够自动执行 而且 await 关键字后面则只能带promise队形--这里注意 await 后面支持其余数据类型, 可是底层也会将其转化为promise对象

    async函数对 Generator 函数的改进,体如今如下四点。

    • 内置执行器。
      Generator 函数的执行必须靠执行器,因此才有了co模块,而async函数自带执行器,这彻底不像 Generator 函数,须要调用next方法,或者用co模块,才能真正执行,获得最后结果。
    • 更好的语义。
      async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。
    • 更广的适用性。
      co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,能够是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)
    • 返回值是 Promise。
      async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用then方法指定下一步的操做。进一步说,async函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

      function name(params) {
            return new Promise(function (resolve, reject) {
              setTimeout(() => {
                resolve(params)
              }, 3000);
            });
          }
          async function myf () {
            let gf = await name('xiaohua');
            let gf2 = await name('xiaohong');
            return gf + gf2 
          }
          async function myf3 (params) {
            let aaa = await myf();
            return aaa;
          }
          myf3().then(function (params) {
            console.log(params);
          });
          
          // xiaohuaxiaohong
async/await 对前者的generater/yield 进行了高度的封装配合那些支持 promise 实现的库能够完美的像普通函数同样调用, 而且async函数与其余async函数也能够完美无缝链接, 堪称终极方案

koa2已经支持 async/await 可是最新的 express框架依然没有支持这种写法, async/await 是大势所趋, 或许不久的未来 express也会支持它, 咱们拭目以待

相关文章
相关标签/搜索