详谈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中的非阻塞方法介绍
Jun 05 NodeJs
轻松创建nodejs服务器(6):作出响应
Dec 18 NodeJs
轻松创建nodejs服务器(10):处理上传图片
Dec 18 NodeJs
Express与NodeJs创建服务器的两种方法
Feb 06 NodeJs
Nodejs多站点切换Htpps协议详解及简单实例
Feb 23 NodeJs
详解nodejs的express如何自动生成项目框架
Jul 12 NodeJs
在Debian(Raspberry Pi)树莓派上安装NodeJS的教程详解
Sep 19 NodeJs
详解Nodejs 通过 fs.createWriteStream 保存文件
Oct 10 NodeJs
详解IWinter 一个路由转控制器的 Nodejs 库
Nov 15 NodeJs
nodejs实现大文件(在线视频)的读取
Oct 16 NodeJs
nodeJS微信分享
Dec 20 NodeJs
NodeJS实现自定义流的方法
Aug 01 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分页集合包括使用方法
2013/10/21 PHP
php判断数组元素中是否存在某个字符串的方法
2014/06/14 PHP
Yii入门教程之目录结构、入口文件及路由设置
2014/11/25 PHP
php将日期格式转换成xx天前的格式
2015/04/16 PHP
php设计模式之职责链模式实例分析【星际争霸游戏案例】
2020/03/27 PHP
怎么让脚本或里面的函数在所有图片都载入完毕的时候执行
2006/10/17 Javascript
jquery中的$(document).ready()与window.onload的区别
2009/11/18 Javascript
IE6、IE7中获取Button元素的值的bug说明
2011/08/28 Javascript
jQuery实现点击标题输入详细信息
2013/04/16 Javascript
js获取通过ajax返回的map型的JSONArray的方法
2014/01/09 Javascript
jquery $("#variable") 循环改变variable的值示例
2014/02/23 Javascript
如何让你的Lightbox支持滚轮缩放及Base64图片
2014/12/04 Javascript
js进行表单验证实例分析
2015/02/10 Javascript
vue2 自定义动态组件所遇到的问题
2017/06/08 Javascript
浅谈Vue的加载顺序探讨
2017/10/25 Javascript
Vue中的Props(不可变状态)
2018/09/29 Javascript
JavaScript面试技巧之数组的一些不low操作
2019/03/22 Javascript
vue 动态表单开发方法案例详解
2019/12/02 Javascript
Node.js学习之内置模块fs用法示例
2020/01/22 Javascript
python3 中的字符串(单引号、双引号、三引号)以及字符串与数字的运算
2019/07/18 Python
PyQt5基本控件使用详解:单选按钮、复选框、下拉框
2019/08/05 Python
为什么称python为胶水语言
2020/06/16 Python
Python plt 利用subplot 实现在一张画布同时画多张图
2021/02/26 Python
css3动画过渡实现鼠标跟随导航效果
2018/02/08 HTML / CSS
一款超酷的js+css3实现的3D标签云特效兼容ie7/8/9
2013/11/18 HTML / CSS
AmazeUI 模态窗口的实现代码
2020/08/18 HTML / CSS
班组长安全职责
2014/01/05 职场文书
护士毕业自我鉴定
2014/02/07 职场文书
品牌宣传方案
2014/03/21 职场文书
请假条范文大全
2014/04/10 职场文书
先进员工获奖感言
2014/08/14 职场文书
个人遵守党的政治纪律情况对照检查材料
2014/09/26 职场文书
2014年党员教师自我剖析材料
2014/09/30 职场文书
党建工作汇报材料
2014/12/24 职场文书
捐助倡议书
2015/01/19 职场文书
任命书标准格式
2015/03/02 职场文书