• 【js】callback时代的变更


      最近团队开始越来越多的使用es7标准的async/await,从最开始的promise到后面的generator,再到现在async,对于异步,每个时期都有着其特有的解决方案,今天笔者就以自己的接触为线索,简单的回顾一下其发展。

      众所周知,js的事件处理模型决定了它内部很多行为都是异步的,最常见的如setTimeout、setInterval、我们通常的ajax,当然还有我们的事件,代码如:

    dom.addEventListener('keydown', function(e){
       console.log(e); 
    })

      这就是一段普通的键盘捕获程序,这本身当然是没什么问题的。有问题的是随着业务越来越复杂,我们需要不断的借助异步的方式处理各种各样的逻辑,然后代码就变成了这样:

    ajax('requestA', function(resA){
        //do sth
        ajax('requestB', function(resB){
            //do sth
            ajax('requestC', function(resC){
                //do sth
                ajax('requestD', function(resD){
                    //do sth
                    ajax('requestE', function(resE){
                        //do sth
                        ajax('requestF', function(resF){
                            //do sth
                            ajax('requestG', function(resG){
                                //do sth
                                ajax('requestH', function(resH){
                                    //do sth
                                })
                            })
                        })
                    })            
                })        
            })    
        })    
    })

      当然,这也就是我们常说的回调地狱(callback hell)。正因为出现了这样一种可读性很差的代码结果,在ES6初期便退出了promise来解决这一“怪异”的问题,先来看看promise的基本语法,形如:

    new Promise((resolve, reject) => {
        if(/*处理结果*/){
            reslove()
        }else{
            reject();
        }
    }).then(()=>{
        successCallback()
    }).catch(()=>{
        failCallback()
    })

      常见的promise的用法就是这样,当然还有诸如Promise.all等方法就不在这里展开了,接着我们看看用promise重构一下上面的回调地狱会变成什么样子:

    let resA = new Promise((resolve, reject) => {
        ajax('requestA', function(res){
            reslove(res)
        })
    });
    let resB = new Promise((resolve, reject) => {
        ajax('requestB', function(res){
            reslove(res)
        })
    });
    let resC = new Promise((resolve, reject) => {
        ajax('requestC', function(res){
            reslove(res)
        })
    });
    let resD = new Promise((resolve, reject) => {
        ajax('requestD', function(res){
            reslove(res)
        })
    });
    let resE = new Promise((resolve, reject) => {
        ajax('requestE', function(res){
            reslove(res)
        })
    });
    let resF = new Promise((resolve, reject) => {
        ajax('requestF', function(res){
            reslove(res)
        })
    });
    let resG = new Promise((resolve, reject) => {
        ajax('requestG', function(res){
            reslove(res)
        })
    });
    let resH = new Promise((resolve, reject) => {
        ajax('requestH', function(res){
            reslove(res)
        })
    });
    
    
    resA.then((resA)=>{
        //do sth
        resB.then((resB)=>{
            //do sth 
            resC.then((resC)=>{
                //do sth
                resD.then((resD)=>{
                    //do sth
                    resE.then((resE)=>{
                        //do sth
                        resF.then((resF)=>{
                            //do sth
                            resG.then((resG)=>{
                                //do sth
                                resH.then((resH)=>{
                                    //do sth
                                })
                            })
                        })
                    })
                })
            })
        })
    })

      理想很美好,但是现实似乎并不尽如人意,不过因为promise的产生主要针对的是回调函数剥夺了我们使用return和throw关键字的能力(比如try-catch不能对异步操作这种机制,不过上面这个例子由于太简略,连一个catch都没有。。),所以要完全取代回调我们还要往前走一步,使用generator,照例我们先看看generator的语法: 

    function* gen(){
      let res = 0;
      yield res++;
      yield res++;
      yield res++;
    }
    let myGen = gen();
    console.log(myGen.next().value);  //0
    console.log(myGen.next().value);  //1
    console.log(myGen.next().value);  //2

      其实语法也很简单,主要就是用“*”修饰了function,然后在内部使用yield关键字,构造了一种惰性调用的语境,然后我们可以将之前的callback hell代码改造为:

    function* Ajax(){
      let resA = yield new Promise((resolve, reject) => {
          ajax('requestA', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resB = yield new Promise((resolve, reject) => {
          ajax('requestB', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resC = yield new Promise((resolve, reject) => {
          ajax('requestC', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resD = yield new Promise((resolve, reject) => {
          ajax('requestD', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resE = yield new Promise((resolve, reject) => {
          ajax('requestE', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resF = yield new Promise((resolve, reject) => {
          ajax('requestF', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resG = yield new Promise((resolve, reject) => {
          ajax('requestG', (res) =>{
              resolve(res);
          })    
      });
      //dosth
      let resH = yield new Promise((resolve, reject) => {
          ajax('requestH', (res) =>{
              resolve(res);
          })    
      });
    }
    
    co(Ajax)

      这么看起来,似乎确实整个代码变得“同步”化了,虽然还要借助下co,不过这种写法因为要在外面包裹generator,通常结合koa在node端使用得比较多。但是这似乎仍然不能完全满足我们的需求,毕竟generator其实作为生成器,虽然能够满足我们同步请求的功能,但是它被创造的初衷似乎并不是单纯只干这事儿的,(它的产生原本是为了js的惰性求值功能)于是,到了ES7我们迎来了新的关键字async/await:

    async function Ajax(){
      async function _ajax(url){
          return new Promise((resolve, reject) => {
              ajax(url, (res)=>{
                  resolve(res)
              })
          });
      } 
      
      let resA = await _ajax('requestA');
      //do sth
      let resB = await _ajax('requestB');
      //do sth
      let resC = await _ajax('requestC');
      //do sth
      let resD = await _ajax('requestD');
      //do sth
      let resE = await _ajax('requestE');
      //do sth
      let resF = await _ajax('requestF');
      //do sth
      let resG = await _ajax('requestH');
      //do sth
      let resH = await _ajax('requestG');
    }
    Ajax();

      它与generator的写法类似,需要在function前面加上关键字async,然后在里面通过await的方式显示调用,于是,再最小程度的修改我们代码的基础上,我们完成了将异步调用变为同步调用的转换,一切变得那么的和谐~

      但是,毕竟浏览器厂商还有个更新同步,替换的过程,所以我们正常工作中会碰到很多情况需要使用polyfill的情况,笔者也颇有点好奇的async/await的polyfill的内部实现,我们都知道,babel的polyfill中对promise实现是基于while循环实现的,而且还需要自己手动引用,而generator也采用了相似的实现:

    //源码
    function* fn(){
      setTimeout(()=>console.log('hello generator'), 1000);
    }
    //babel transform后
    'use strict';
    
    var _marked = [fn].map(regeneratorRuntime.mark);
    
    function fn() {
      return regeneratorRuntime.wrap(function fn$(_context) {
        while (1) {
          switch (_context.prev = _context.next) {
            case 0:
              setTimeout(function () {
                return console.log('hello generator');
              }, 1000);
    
            case 1:
            case 'end':
              return _context.stop();
          }
        }
      }, _marked[0], this);
    }

      可以看出,其实主要依然是使用while。。而且还是while(1),而async/await也是惊人的相似:

    //源码
    async function fn(){
      setTimeout(()=>console.log('hello async'), 1000)
    }
    //bebal transform 后
    'use strict';
    
    var fn = function () {
      var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
        return regeneratorRuntime.wrap(function _callee$(_context) {
          while (1) {
            switch (_context.prev = _context.next) {
              case 0:
                setTimeout(function () {
                  return console.log('hello async');
                }, 1000);
    
              case 1:
              case 'end':
                return _context.stop();
            }
          }
        }, _callee, this);
      }));
    
      return function fn() {
        return _ref.apply(this, arguments);
      };
    }();
    
    function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

      虽然与generator不同,在最外层还用asyncToGenerator包装了一下,不过。。核心的while循环依然存在。。

      想来也是蛮有些讽刺的,为了解决一个问题,业界想出的三套方案,到最终,居然是依靠一个在我们写代码之初便不推荐使用的一种“死循环”的方式来达成的,虽然浏览器底层不会真这么实现,但是每每想到自己的代码经过babel编译后,会是这么一个样子,心里还是隐隐有些担忧的。

      想来再结合笔者最近看到的一些历史中的轶事,也颇是觉得其中微妙之处,当有亲身经历者,方可体会的感触。时代的浪潮都在滚滚向前,但愿迎接我们的是新升的朝阳,而非一个漫长的黑夜。

  • 相关阅读:
    虚拟机VMware配置centos7集群(亲测有效)
    linux虚拟机克隆后,虚拟机ping不通的解决方法
    VC++使用 GDI+等比例缩放图片,并且居中显示
    VS2015 编译OSG Plugins Giflib静态库
    Qt 读写文件操作
    OSG 常用快捷键(全屏、查看帧数、截屏)
    Navicat Premium v15 中文最新破解版(附:激活工具)
    redis 持久化机制及配置
    Redis 五种数据类型
    linux 安装redis
  • 原文地址:https://www.cnblogs.com/mfoonirlee/p/7192439.html
Copyright © 2020-2023  润新知