详谈nodejs异步编程


Posted in NodeJs onDecember 04, 2014

目前需求中涉及到大量的异步操作,实际的页面越来越倾向于单页面应用。以后可以会使用backbone、angular、knockout等框架,但是关于异步编程的问题是首先需要面对的问题。随着node的兴起,异步编程成为一个非常热的话题。经过一段时间的学习和实践,对异步编程的一些细节进行总结。

1.异步编程的分类

     解决异步问题方法大致包括:直接回调、pub/sub模式(事件模式)、异步库控制库(例如async、when)、promise、Generator等。
1.1 回调函数

      回调函数是常用的解决异步的方法,经常接触和使用到,易于理解,并且在库或函数中非常容易实现。这种也是大家接使用异步编程经常使用到的方法。

      但是回调函数的方式存在如下的问题:

     1. 可能形成万恶的嵌套金字塔,代码不易阅读;

     2. 只能对应一个回调函数,在很多场景中成为一个限制。

1.2 pub/sub模式(事件)

     该模式也称为事件模式,是回调函数的事件化,在jQuery等类库中非常常见。

     事件发布订阅者模式本身并无同步与异步调用的问题,但是在node中,emit调用多半是伴随事件循环而异步触发的。该模式常用来解耦业务逻辑,事件发布者无须关注注册的回调函数,也不用关注回调函数的个数,数据通过消息的方式可以很灵活的传递。

     该模式的好处是:1. 便于理解;2. 不再局限于一个回调函数。

     不好的地方时:1. 需要借助类库; 2.事件与回调函数的顺序很重要

var img = document.querySelect(#id);

img.addEventListener('load', function() {
// 图片加载完成

    ......

});

img.addEventListener('error', function() {

// 出问题了

......

});

上述代码存在两个问题:

      a. img实际已经加载完成,此时才绑定load回调函数,结果回调不会执行,但依然希望执行该对应回调函数。

var img = document.querySelect(#id);

function load() {
...

}

if(img.complete) {

load();

} else {

img.addEventListener('load', load);

}

img.addEventListener('error', function() {

// 出问题了

......

});

b. 无法很好处理存在异常

      结论:事件机制最适合处理同一个对象上反复发生的事情,不需要考虑当绑定回调函数之前事件发生的情况。

1.3 异步控制库

      目前的异步库主要有Q、when.js、win.js、RSVP.js等。

      这些库的特点是代码是线性的,可以从上到下完成书写,符合自然习惯。

      不好的地方也是风格各异,不便于阅读,增加学习成本。

1.4 Promise

     Promise翻译成中文为承诺,个人理解是异步完成之后,就会给外部一个结果(成功或失败),并承诺结果不再发生改变。换句话就是Promise反应了一个操作的最终返回结果值(A promise represents the eventual value returned from the single completion of an operation)。目前Promise已经引入到ES6规范里面,Chrome、firefox等高级浏览器已经在内部实现了该原生方法,使用起来相当方便。

     下面从如下几个方面来解析Promise的特点:

    1.4.1 状态

     包含三种状态:pending、fulfilled、rejected,三种状态只能发生两种转换(从pending--->fulfilled、pending—>rejected),并且状态的转换仅能发生一次。

详谈nodejs异步编程

    1.4.2 then方法

    then方法用于指定异步事件完成之后的回调函数。

   这个方法可以说是Promise的灵魂方法,该方法让Promise充满了魔力。有如下几个具体表现:

    a) then方法返回Promise。这样就实现了多个异步操作的串行操作。

详谈nodejs异步编程

     关于上图中黄圈1的对value的处理是Promise里面较为复杂的一个地方,value的处理分为两种情况:Promise对象、非Promise对象。

     当value 不是Promise类型时,直接将value作为第二个Promise的resolve的参数值即可;当为Promise类型时,promise2的状态、参数完全由value决定,可以认为promsie2完全是value的傀儡,promise2仅仅是连接不同异步的桥梁。

详谈nodejs异步编程

Promise.prototype.then = function(onFulfilled, onRejected) {

    return new Promise(function(resolve, reject) {           //此处的Promise标注为promise2

        handle({

            onFulfilled: onFulfilled,

            onRejected: onRejected,

            resolve: resolve,

            reject: reject

        })

    });

}

function handle(deferred) {

    var handleFn;

    if(state === 'fulfilled') {

        handleFn = deferred.onFulfilled;

    } else if(state === 'rejected') {

        handleFn = deferred.onRejected;

    }

    var ret = handleFn(value);

    deferred.resolve(ret);                           //注意,此时的resolve是promise2的resolve

}

function  resolve(val) {

    if(val && typeof val.then === 'function') {

        val.then(resolve);                           // if val为promise对象或类promise对象时,promise2的状态完全由val决定

        return;

    }

    if(callback) {                                    // callback为指定的回调函数

        callback(val);

    }

}

b)实现了多个不同异步库之间的转换。

    在异步中存在一个叫thenable的对象,就是指具有then方法的对象,只要一个对象对象具有then方法,就可以对其进行转换,例如:

var deferred = $('aa.ajax');      // !!deferred.then  === true

var P = Promise.resolve(deferred);

p.then(......)

1.4.3 commonJS Promise/A规范

      目前关于Promise的规范存在Promise/A和Promise/A+规范,这说明关于Promise的实现是挺复杂的。

then(fulfilledHandler, rejectedHandler, progressHandler)

1.4.4 注意事项

     一个Promise里面的回调函数是共享value的,在结果处理中value作为参数传递给相应的回调函数,如果value是对象,那就要小心不要轻易修改value的值。

var p = Promise.resolve({x: 1});

p.then(function(val) {

    console.log('first callback: ' + val.x++);

});

p.then(function(val) {

    console.log('second callback: ' + val.x)

})

// first callback: 1

// second callback: 2

1.5 Generator

      上面所有的方法均是基于回调函数来完成异步操作的,无非是对回调函数进行封装而已。ES6里面提出了Generator,增加了解决异步操作的途径,不再依据回调函数来完成。

      Generator最大的特点就是可以实现函数的暂停、重启,这个特性非常有利于解决异步操作。将Generator的暂停与promise的异常处理结合起来,可以比较优雅地解决异步编程问题。具体实现参考:Kyle Simpson

2. 异步编程存在的问题

      2.1 异常处理

        a) 异步事件包括两个环节:发出异步请求、结果处理,这两个环节通过event loop来连接起来。那么try catch来进行异常捕获的时候就需要分来捕获。

try {

    asyncEvent(callback); 

} catch(err) {

    ......

}

     上述代码是无法捕获callback里面的异常,只能获取发出请求环节的异常。这样就存在问题:假如请求的发出和请求的处理是两个人完成的,那么在异常处理的时候就存在问题?

        b)promise实现异常的传递,这带来一些好处,在实际项目中保证代码不被阻塞。但是如果异步事件比较多的时候,不容易找出到底是那个异步事件产生了异常。

// 场景描述: 在CRM里面展示价格的报警信息,其中包含竞对的信息。但是获取竞对的信息时间比较长,后端为了避免慢查询,就把一条记录拆成两块分别获取。

// 第一步:获取价格报警信息,除了竞对信息

function getPriceAlarmData() {

    return new Promise(function(resolve) {

        Y.io(url, {

            method: 'get',

            data: params,

            on: function() {

                success: function(id, data) {

                    resolve(alarmData);

                }

            }

        });

    });

}

// 得到报警信息后,在去获取竞对信息

getPriceAlarmData().then(function(data) {

    // 数据渲染,除了竞对信息

    render(data);

    return new Promise(function(resolve) {

        Y.io(url, {

            method: 'get',

            data: {alarmList: data},

            on: function() {

                success: function(id, compData) {

                    resolve(compData);

                }

            }

        });

    });

})      //  获取完所有数据后进行竞对信息的渲染

.then(function(data) {

    // 渲染竞对信息

    render(data)

}, function(err) {

    // 异常处理

    console.log(err);

});

      可以把上述代码转换成如下:

try{

    // 获取除竞对以外的报警信息

    var alarmData = alarmDataExceptCompare();

    render(alarmData);

    // 根据报警信息查询竞对信息

    var compareData = getCompareInfo(alarmData);

    render(compareData);

} catche(err) {

    console.log(err.message);

}

在上述例子中把异常处理放到最后进行处理,这样当其中存在某个环节出现异常,我们无法准确知道到底是哪个事件产生的。     

2.2 jQuery.Deferred 的问题

     jQuery中也实现了异步操作,但是在实现上不符合promise/A+规范,主要表现在以下几个方面:

    a. 参数的个数:标准的Promise只能接受一个参数,而jQuery中则可以传递多个参数

function asyncInJQuery() {

    var d = new $.Deferred();

    setTimeout(function() {

        d.resolve(1, 2);

    }, 100);

    return d.promise()

}

asyncInJQuery().then(function(val1, val2) {

    console.log('output: ', val1, val2);

});

// output: 1 2

 b. 结果处理中异常的处理  

function asyncInPromise() {

      return new Promise(function(resolve) {

        setTimeout(function() {

          var jsonStr = '{"name": "mt}';

          resolve(jsonStr);

        }, 100);

      });

  }

  asyncInPromise().then(function(val) {

      var d = JSON.parse(val);

      console.log(d.name);

  }).then(null, function(err) {

    console.log('show error: ' + err.message);

  });

// show error: Unexpected end of input

function asyncInJQuery() {

    var d = new $.Deferred();

    setTimeout(function() {

        var jsonStr = '{"name": "mt}';

        d.resolve(jsonStr);

    }, 100);

    return d.promise()

}

asyncInJQuery().then(function(val) {

    var d = JSON.parse(val);

    console.log(d.name);

}).then(function(v) {

    console.log('success: ', v.name);

}, function(err){

    console.log('show error: ' + err.message);

});

//Uncaught SyntaxError: Unexpected end of input

     从中可以看出,Promise对回调函数进行了结果处理,可以捕获回调函数执行过程中的异常,而jQuery.Deferred却不可以。

NodeJs 相关文章推荐
nodejs中使用monk访问mongodb
Jul 06 NodeJs
Google官方支持的NodeJS访问API,提供后台登录授权
Jul 29 NodeJs
NodeJS学习笔记之(Url,QueryString,Path)模块
Jan 13 NodeJs
Windows 系统下设置Nodejs NPM全局路径
Apr 26 NodeJs
初探nodeJS
Jan 24 NodeJs
nodejs使用http模块发送get与post请求的方法示例
Jan 08 NodeJs
nodeJs实现基于连接池连接mysql的方法示例
Feb 10 NodeJs
nodejs基于express实现文件上传的方法
Mar 19 NodeJs
详解nodejs 配置文件处理方案
Jan 02 NodeJs
nodejs log4js 使用详解
May 31 NodeJs
Nodejs 微信小程序消息推送的实现
Jan 20 NodeJs
一文秒懂nodejs中的异步编程
Jan 28 NodeJs
nodejs下打包模块archiver详解
Dec 03 #NodeJs
nodejs中转换URL字符串与查询字符串详解
Nov 26 #NodeJs
nodejs教程之制作一个简单的文章发布系统
Nov 21 #NodeJs
nodejs教程之环境安装及运行
Nov 21 #NodeJs
nodejs教程之异步I/O
Nov 21 #NodeJs
nodejs教程之入门
Nov 21 #NodeJs
nodejs 提示‘xxx’ 不是内部或外部命令解决方法
Nov 20 #NodeJs
You might like
php $_SERVER windows系统与linux系统下的区别说明
2014/02/14 PHP
PHP.ini中配置屏蔽错误信息显示和保存错误日志的例子
2014/05/12 PHP
php面象对象数据库操作类实例
2014/12/02 PHP
php实现的微信红包算法分析(非官方)
2015/09/25 PHP
PHP面向对象之工作单元(实例讲解)
2017/06/26 PHP
Javascript打印网页部分内容的脚本
2008/11/17 Javascript
js onpropertychange输入框 事件获取属性
2009/03/26 Javascript
生成二维码方法汇总
2014/12/26 Javascript
jQuery Ajax调用WCF服务详细教程
2015/03/31 Javascript
JavaScript制作简易的微信打飞机
2015/03/31 Javascript
jQuery判断多个input file 都不能为空的例子
2015/06/23 Javascript
JavaScript中的this关键字使用详解
2015/08/14 Javascript
JavaScript小技巧整理篇(非常全)
2016/01/26 Javascript
jquery获取form表单input元素值的简单实例
2016/05/30 Javascript
详谈JS中实现种子随机数及作用
2016/07/19 Javascript
JS基于递归实现倒计时效果的方法
2016/11/26 Javascript
jQuery插件FusionWidgets实现的AngularGauge图效果示例【附demo源码】
2017/03/23 jQuery
JS+HTML5实现上传图片预览效果完整实例【测试可用】
2017/04/20 Javascript
JavaScript闭包_动力节点Java学院整理
2017/06/27 Javascript
jQuery选择器_动力节点Java学院整理
2017/07/05 jQuery
AngularJS+Bootstrap3多级导航菜单的实现代码
2017/08/16 Javascript
JavaScript之创意时钟项目(实例讲解)
2017/10/23 Javascript
[jQuery] 事件和动画详解
2019/03/05 jQuery
详解ES6中的Map与Set集合
2019/03/22 Javascript
小程序采集录音并上传到后台
2019/11/22 Javascript
微信小程序图片自适应实现解析
2020/01/21 Javascript
Python ORM框架SQLAlchemy学习笔记之映射类使用实例和Session会话介绍
2014/06/10 Python
Python内置模块hashlib、hmac与uuid用法分析
2018/02/12 Python
uwsgi+nginx部署Django项目操作示例
2018/12/04 Python
python如何将多个PDF进行合并
2019/08/13 Python
Python Tkinter Entry和Text的添加与使用详解
2020/03/04 Python
CSS3 清除浮动的方法示例
2018/06/01 HTML / CSS
资生堂美国官网:Shiseido美国
2016/09/02 全球购物
eBay瑞士购物网站:eBay.ch
2018/12/24 全球购物
临床护理求职信
2014/04/26 职场文书
辞职报告(范文三篇)
2019/08/27 职场文书