资讯专栏INFORMATION COLUMN

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

0xE7A38A / 1908人阅读

摘要:异步流程管理说白了就是为了解决回调地狱的问题。对象代表一个异步操作,有三种状态进行中已成功和已失败。如果改变已经发生了,你再对对象添加回调函数,也会立即得到这个结果。执行函数后返回的是一个遍历器对象,可以依次遍历函数内部的每一个状态。

javascript -- 深度解析异步解决方案
高级语言层出不穷, 然而唯 js 鹤立鸡群, 这要说道js的设计理念, js天生为异步而生, 正如布道者朴灵在 node深入浅出--(有兴趣的可以读一下, 很有意思^_^) , 异步很早就存在于操作系统的底层, 意外的是,在绝大多数高级编程语言中,异步并不多见,疑似被屏蔽了一搬. 造成这个现象的原因或许令人惊讶, 程序员不太适合通过异步来实现进行程序设计 ^_^.

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

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

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

ES 6以前:

回调函数
回调函数是最原始的异步编程方案, 上篇文章已经讲述, 这里不再累赘, 这里给出传送门 回调函数之美 然而如果业务逻辑过多时, 回调函数会产生深层嵌套, 对程序员极不友好,
如下代码所示有一个业务逻辑, 需要对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进行异步流程管理。异步流程管理说白了就是为了解决回调地狱的问题。所以说任何事情都有两面性,异步编程有它独特的优势,却也同时遇到了同步编程根本不会有的代码组织难题。

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

    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对象有以下特点

对象的状态不受外界影响。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 搭建服务器时需要面向 切面编程 ,这就需要各种各样的中间件

    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也会支持它, 我们拭目以待

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107112.html

相关文章

  • Nodejs高性能原理(下) --- 事件循环详解

    摘要:如果一个即时定时器是被一个正在执行的回调排入队列的,则该定时器直到下一次事件循环迭代才会被触发。参数描述在事件循环的当前回合结束时要调用的函数。事件轮询随后的调用,会在任何事件包括定时器之前运行。 系列文章 Nodejs高性能原理(上) --- 异步非阻塞事件驱动模型Nodejs高性能原理(下) --- 事件循环详解 前言 终于开始我nodejs的博客生涯了,先从基本的原理讲起.以前写...

    newsning 评论0 收藏0
  • RxJS Observables vs Promise 之简单对比

    摘要:最近在学习,它是使用的响应式编程的库,它使编写异步或基于回调的代码更容易。返回的是一个对于指定的。发送请求使用了,虽然了多次,但是仅发送一次请求,了结果。中有一些操作符可以让监听强制为异步的方式,例如。最近在学习RxJS,它是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易。 下面主要介绍Observables 与 promise的不同点。 单值与多值 c...

    klivitamJ 评论0 收藏0
  • JavaScript 的 4 种数组遍历方法: for VS forEach() VS for/in

    摘要:对于,除非使用箭头函数,它的回调函数的将会变化。使用测试下面的代码,结果如下打印打印要点使用的规则要求所有回调函数必须使用箭头函数。 译者按: JS 骚操作。 原文:For vs forEach() vs for/in vs for/of in JavaScript 译者: Fundebug 本文采用意译,版权归原作者所有 我们有多种方法来遍历 JavaScript 的数组或者...

    joyqi 评论0 收藏0
  • $.ajax vs axios vs fetch

    摘要:使用它可以让页面请求少量的数据,而不用刷新整个页面。这是一个比较粗糙的,不符合关注分离的设计原则,配置和使用都不是那么友好。它的一个优势异步操作,但的异步操作是基于事件的异步模型,没有那么友好。 Ajax 是什么? 答:Ajax是一种可以在浏览器和服务器之间使用异步数据传输(HTTP请求)的技术。使用它可以让页面请求少量的数据,而不用刷新整个页面。而传统的页面(不使用Ajax)要刷新...

    bitkylin 评论0 收藏0
  • node异步非阻塞的杂谈

    摘要:引言作为服务器的优势就在于适合处理高并发的请求,对于网站后台这种密集型的后台尤其有优势,其核心就在于是一个异步非阻塞模型。关于异步,同步,阻塞,非阻塞这些概念,本文不做讨论。另外两个的调用时间需要判断是否都在主线程中被执行。 引言 node作为服务器的优势就在于适合处理高并发的请求,对于web网站后台这种I/O密集型的后台尤其有优势,其核心就在于node是一个异步非阻塞模型。关于异步,...

    izhuhaodev 评论0 收藏0

发表评论

0条评论

0xE7A38A

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<