详谈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 npm package.json中文文档
Sep 04 NodeJs
Nodejs实现的一个简单udp广播服务器、客户端
Sep 25 NodeJs
基于NodeJS的前后端分离的思考与实践(二)模版探索
Sep 26 NodeJs
轻松创建nodejs服务器(6):作出响应
Dec 18 NodeJs
Nodejs中session的简单使用及通过session实现身份验证的方法
Feb 04 NodeJs
详解nodeJS中读写文件方法的区别
Mar 06 NodeJs
nodejs个人博客开发第一步 准备工作
Apr 12 NodeJs
nodejs实现的连接MySQL数据库功能示例
Jan 25 NodeJs
nodejs连接mysql数据库及基本知识点详解
Mar 20 NodeJs
Nodejs Express 通过log4js写日志到Logstash(ELK)
Aug 30 NodeJs
nodejs异步编程基础之回调函数用法分析
Dec 26 NodeJs
NodeJS配置CORS实现过程详解
Dec 02 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冒泡排序算法代码详细解读
2011/07/17 PHP
php cc攻击代码与防范方法
2012/10/18 PHP
php中删除、清空session的方式总结
2015/10/09 PHP
自制PHP框架之设计模式
2017/05/07 PHP
javascript的字符串按引用复制和传递,按值来比较介绍与应用
2012/12/28 Javascript
javascript parseInt() 函数的进制转换注意细节
2013/01/08 Javascript
上传的js验证(图片/文件的扩展名)
2013/04/25 Javascript
使用jquery hover事件实现表格的隔行换色功能示例
2013/09/03 Javascript
Jquery遍历checkbox获取选中项value值的方法
2014/02/13 Javascript
javascript浏览器窗口之间传递数据的方法
2015/01/20 Javascript
javascript+HTML5的Canvas实现Lab单车动画效果
2015/08/07 Javascript
js实现动态创建的元素绑定事件
2016/07/19 Javascript
JS键盘版计算器的制作方法
2016/12/03 Javascript
Bootstrap table两种分页示例
2016/12/23 Javascript
详解微信小程序——自定义圆形进度条
2016/12/29 Javascript
bootstrap jquery dataTable 异步ajax刷新表格数据的实现方法
2017/02/10 Javascript
vue.js的安装方法
2017/05/12 Javascript
express框架实现基于Websocket建立的简易聊天室
2017/08/10 Javascript
JavaScript数据结构与算法之队列原理与用法实例详解
2017/11/22 Javascript
原生JS实现列表子元素顺序反转的方法分析
2018/07/02 Javascript
javascript和php使用ajax通信传递JSON的实例
2018/08/21 Javascript
Vue仿Bibibili首页的问题
2021/01/21 Vue.js
python 寻找优化使成本函数最小的最优解的方法
2017/12/28 Python
对numpy中轴与维度的理解
2018/04/18 Python
用TensorFlow实现lasso回归和岭回归算法的示例
2018/05/02 Python
Python实现性能自动化测试竟然如此简单
2019/07/30 Python
python 使用pdfminer3k 读取PDF文档的例子
2019/08/27 Python
详解Django将秒转换为xx天xx时xx分
2019/09/27 Python
Python中six模块基础用法
2019/12/08 Python
css3中仿放大镜效果的几种方式原理解析
2020/12/03 HTML / CSS
迪斯尼商品官方网站:ShopDisney
2016/08/01 全球购物
美国购车网站:TrueCar
2016/10/19 全球购物
澳大利亚宠物食品和用品商店:PETstock
2020/01/02 全球购物
艺术教育实施方案
2014/05/03 职场文书
统计员岗位职责
2015/02/11 职场文书
Linux中如何安装并部署Redis
2022/04/18 Servers