精华 Node.js最新技术栈之Promise篇
发布于 21小时前 作者 i5ting 206 次浏览 最后一次编辑是 20小时前 来自 分享

技术分享:Node.js最新技术栈之Promise,9月24日(周四)21点分享的内容整理 时间原因,阉割了很多内容,其实内容少点应该更容易理解的,以下是直播的全部内容

前言

大家好,我是桑世龙,github和cnodejs上的i5ting,目前在天津创业,公司目前使用技术主要是nodejs,算所谓的MEAN(mongodb + express + angular + node),所有代码都部署在阿里云上

我的名字i5ting,原因是我媳妇叫张婷,哈哈

闲言少叙,下面开始正题 《Node.js最新技术栈之Promise篇》

js流程控制的演进过程,分以下5部分

    1. 回调函数Callbacks
    1. 异步JavaScript
    1. Promise/a+
    1. 生成器Generators/ yield
    1. Async/ await

1.png

首先回调和异步是大家常见的,这里跳过,本节课主要讲promise/a+原理、实现和实践,并展望下一代的Generators/yield和Async/await等。

0)痛点

我们知道nodejs的最大的优点是高并发,适合适合I/O密集型应用

arch.png

每一个函数都是异步,并发上去了,但痛苦也来了,比如

我现在要执行4个步骤,依次执行,于是就有下面的代码

step1(function (value1) {
    step2(value1, function(value2) {
        step3(value2, function(value3) {
            step4(value3, function(value4) {
                // Do something with value4
            });
        });
    });
});

这里只有4个,那如果更多呢?会不会崩溃,反正我会

有解决办法么?

答案是就是:promise/a+规范,下面我们看一下promise/a+规范

我们再来个伏笔,看一下链式写法

链式写法

先举个jQuery的链式写法例子

$('#tab').eq($(this).index()).show().siblings().hide();

链式写法的核心是:每个方法都返回this(自己看jquery源码)

下面举一个this的简单例子

var obj = {
  step1:function(){
    console.log('a');
    return this;
  },
  step2:function(){
    console.log('b');    
    return this;
  },
  step3:function(){
    console.log('c');
    return this;
  },
  step4:function(){
    console.log('d');
    return this;
  }
}

console.log('-----\n');
obj.step1().step2().step3();
console.log('-----\n');
obj.step4().step2().step1();

这只是一个简单返回this的链式写法而已

执行结果

$ node doc/this.js
-----

a
b
c
-----

c
b
a

为啥要讲这个呢?先回到异步调用的场景,我们能这样调用么?如果每一个操作都可以连起来,是不是很爽

比如

return step1().step2().step3().step4()

这样做的好处

  • 1)每一个操作都是独立的函数
  • 2)可组装,拼就好了

我们还可以再苛刻点

  • 1)如果要是上一步的结果作为下一步的输入就更好了(linux里的pipe:ps -ef|grep node|awk '{print $2}’|xargs kill -9)
  • 2)如果出错能捕获异常就更好了
  • 3)如果能在函数里面也能控制流程就更好了

会有人说:“你要求的是不是太多了?”

“but,很好,我们来制定一个叫promisesaplus的规范吧,涵盖上面的所有内容”

于是就有了promise/a+规范…

1)promise/a+规范

这里面讲很多链式相关的东西,是我们异步操作里常见的问题,我们希望有更好的解决方案,promise/a+规范便应运而生

什么是promise/a+规范?

Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。

想想我们刚才的那个例子,让我们来庖解一下

  • a) 异步操作的最终结果

简单点看,它也是一个链式操作,类似

return step1().step2().step3().step4()
  • b) 与Promise最主要的交互方法是通过将函数传入它的then方法

但它有点差别

return step1().then(step2).then(step3).then(step4)
  • c1) 从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因

比如step1执行成功了,那么就会执行step2,如果step2执行成功了就会执行下一个,以此类推

那么如果失败呢?于是应该改成这样:

return step1().then(step2).then(step3).then(step4).catch(function(err){
  // do something when err
});

加了一个catch error回调,当出现异常就会直接到这个流程,会不会很好呢?

  • c2) 从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因

这句话了有一个reject,其实还对应一个resolve方法

  • reject 是拒绝,跳转到catch error
  • resolve 是解决,下一步,即跳转到下一个promise操作

我们还以上面的例子来说,如果step1失败了,走step3,step3失败了step4,如果step4出错就结束,报错,看一下代码

function step1(){
  if(false){
    return step3().then(step4).catch(function(err){
      // do something when err
    });
  }
  
}
return step1().then(step2).then(step3).then(step4).catch(function(err){
  // do something when err
});

总结一下:以上就是promise/a+规范的定义,我们拆解了一下,涵盖了上面我提出的几个痛点

术语

promise

是一个包含了兼容promise规范then方法的对象或函数

我们可以这样理解,每一个promise只要返回的可以then的都可以。就像上面举例返回的this一样,只要每一个都返回this,她就可以无限的链式下去,

这里的this约定为每一个对象或函数返回的都是兼容promise规范then方法。

thenable

是一个包含了then方法的对象或函数。

这个可以这样理解,上面的例子,每个方法都返回this,比较麻烦,

那能不能只有then方法promise对象,每一个操作都返回一样的promise对象,是不是就可以无限链接下去了?

value

是任何Javascript值。 (包括 undefined, thenable, promise等).

promise/a+规范约定的几个概念而已

exception

是由throw表达式抛出来的值。

上面讲的,当流程出现异常的适合,把异常抛出来,由catch err处理。

reason

是一个用于描述Promise被拒绝原因的值。

就是给你一个犯了错误,自我交待,争取宽大处理的机会。

参考

还有一些promise里流程处理相关的东西,比较复杂,没啥大意义,有兴趣的可以看看官方网址

总结一下

    1. 每个操作都返回一样的promise对象,保证链式操作
    1. 每个链式都通过then方法
    1. 每个操作内部允许犯错,出了错误,统一由catch error处理
    1. 操作内部,也可以是一个操作链,通过reject或resolve再造流程

理解这些,实际上nodejs里就没有啥难点了。当然我还要告诉更多好处,让你受益一生(吹吹牛)

八卦一下

哪个语言不需要回调呢?callback并不是js所特有的,举个例子oc里的block(如果愿意扯,也可以扯扯函数指针),如果用asi或者afnetworking处理http请求,你都可以选择用block方式,那么你如果一个业务里有多个请求呢?

你也只能嵌套,因为嵌套没有太多层,因为可以有其他解耦方式,其实我最想表达的是,它也可以使用promise方式的。

这里展示一个例子

[self login].then(^{

    // our login method wrapped an async task in a promise
    return [API fetchKittens];

}).then(^(NSArray *fetchedKittens){

    // our API class wraps our API and returns promises
    // fetchKittens returned a promise that resolves with an array of kittens
    self.kittens = fetchedKittens;
    [self.tableView reloadData];

}).catch(^(NSError *error){

    // any errors in any of the above promises land here
    [[[UIAlertView alloc] init…] show];

}); 

详见http://promisekit.org/introduction/

是不是很奇怪,咋和上面的promise写法一样呢?

官方解释是

PromiseKit is not just a promises implementation

好吧,这只是冰山一角

如果各位熟悉前端js,相信你一定了解

  • jQuery(1.5+)的deferred
  • Angularjs的$q对象

nodejs里的实现

其他语言实现,详见 https://promisesaplus.com/implementations

其实,只要掌握了promise/a+规范,你就可以在n种语言里使用了

2)如何实现

promise/a+规范是一个通用解决方案,不只是对nodejs管用,只要你掌握了原理,就只是换个语言实现而已

下面我就带着大家看一下js里是如何实现的

var Promise = function () {
};

var isPromise = function (value) {
    return value instanceof Promise;
};

var defer = function () {
    var pending = [], value;
    var promise = new Promise();
    promise.then = function (callback) {
        if (pending) {
            pending.push(callback);
        } else {
            callback(value);
        }
    };
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            }
        },
        promise: promise
    };
};

首先

1) 声明promise对象

var promise = new Promise();

2)给promise对象增加then方法

promise.then = function (callback) {

3)给defer对象返回resolve和promise

4)value,在resolve事件里传参,是第几个就执行第几个

本来想讲的更多一点,但时间不允许啊(后面还有很多内容),此处暂时讲到这里

cnode里的William17写的挺好的,完整的实现可以参考

关于Q

q是一个不错的项目,也是比较早的promise实现,而且angularjs的$q就是它的精简版,如果掌握了q,学习angular和bluebird都会比较简单

当然它还有更棒的,它把q的7个版本是如何实现的都详细记录了,刚才我给出的v1实际上就q的早期版本

https://github.com/kriskowal/q/tree/v1/design

很多人都以为是跟着人学,但人有不确定性,而且互联网让世界是平的了,我们如果能够学会从开源项目里学习,这才是长久的学习力

曾经写过一篇《如何学习之善用github篇:向 @Pana 学习》,有兴趣的可以去翻翻

http://mp.weixin.qq.com/s?__biz=MzAxMTU0NTc4Nw==&mid=223428929&idx=1&sn=8296f5a96aa34f836ab83000f7ad995b#rd

3)真实项目里的实践

技术选项:先看基准测试

2015-01-05 当时最新的模块,比较结果如下

benchmark.png

对比一下结果

顺序执行
promises-bluebird-generator.js                235       38.04
promises-bluebird.js                          335       52.08

并发执行
promises-bluebird.js                     389       53.49
promises-bluebird-generator.js           491       55.52

不管是哪种,bluebird都是前三名。然后扒一扒第一名的callbacks-baseline.js,其实就我们最讨厌的最基本的回调写法,也就说,bb实际比原生js稍慢,但比其他promise库都快

综合一下bb的特性

  • 速度最快
  • api和文档完善,(对各个库支持都不错)
  • 支持generator等未来发展趋势
  • github活跃
  • 还有让人眼前一亮的功能点(保密,下面会讲)

我们的结论只有bb是目前最好的最好的选择

我个人的一个小喜好,对caolan的async挺喜欢的,我的选项

  • 公司项目是bb
  • 开源大点的还是bb
  • 小工具啥的我会选async

其实when,then,eventproxy等也不错,只是懒得折腾

大家在直播过程中有任何不明白或者想提问的可以随时私信给小助手,我会在答疑阶段统一回答

场景

下面举几个实际场景

神器,bluebird的promisify

promisify原理

就是你给他传一个对象或者prototype,它去遍历,给他们加上async方法,此方法返回promise对象,你就可以为所欲为了

  • 优点:使用简单
  • 缺点:谨防对象过大,内存问题

nodejs api支持

//Read more about promisification in the API Reference:
//API.md
var fs = Promise.promisifyAll(require("fs"));

fs.readFileAsync("myfile.json").then(JSON.parse).then(function (json) {
    console.log("Successful json");
}).catch(SyntaxError, function (e) {
    console.error("file contains invalid json");
}).catch(Promise.OperationalError, function (e) {
    console.error("unable to read file, because: ", e.message);
});

熟悉fs的API的都知道,fs有

  • fs.readFile
  • fs.readFileSync

但没有

  • fs.readFileAsync

实际上fs.readFileAsync是bluebird加上去的

var fs = Promise.promisifyAll(require("fs"));

其实也没什么神奇的,只是bb做的更多而已,让调用足够简便

下面我们来看一下mvc里的model层如何使用bb,我们先假定使用的是mongoose啊

先定义模型

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var Promise = require("bluebird");

UserSchema = new Schema({
  username: String,
  password: String,
  created_at: {
    type: Date,
    "default": Date.now
  }
});

var User = mongoose.model('User', UserSchema);

Promise.promisifyAll(User);
Promise.promisifyAll(User.prototype);

利用上面定义好的User模型,我们就可以写业务逻辑了

User.findAsync({username: username}).then(function(data) {
   ...
}).catch(function(err) {
   ...
});

再来个复杂的

TeamPartner.updateAsync({
    ...
}, {
    $set: {
        ...
    }
}).then(function() {
    return User.findByIdAsync(user_id);
}).then(function(team_owner) {
    if(typeof team_owner.partner_count != 'undefined') {
      team_new_count = team_owner.partner_count + 1;
  }else{
      team_new_count = 1;
  }

  // 创始人成员+1
  return User.findByIdAndUpdateAsync(user_id, {
    $set: {
       ...
    }
  }, {
      upsert: true
  });
}).then(function() {
    return Notify.findByIdAndUpdateAsync(notify_id, {
        $set: {
            read_status: true
        }
    });
}).then(function() {
  var notifySave = new Notify({
      ...
  });

  return notifySave.saveAsync();
}).then(function() {
    return User.findByIdAsync(user_join);
}).then(function(joiner) {
  var teamSave = new Team({
      ...
  });
  return teamSave.saveAsync();
}).then(function() {
    res.status(200).json({
        data: {},
        status: {
            code: 0,
            msg: 'success'
        }
    });
}).catch(function(err) {
    console.log(err);
    res.status(200).json({
        data: {},
        status: {
            code: err.code,
            msg: err.name
        }
    });
});

很抱歉,伤害大家了,但确切是有这样用,我想问,脑子抽么?

优化方案

  • mongoose上的static和method上扩展,别暴漏太多细节在控制层
  • 面向promise,保证每个操作都是一个函数,让流程可以组装,不要忘了最初then的初衷
function find_user() {
    return User.findByIdAsync(user_id);
}

function find_user2() {
    return User.findByIdAsync(user_id);
}

function error(ex){
  console.log(ex);
}

TeamPartner.updateByXXAsync(a,b,c).then(find_user).then(find_user2).catch(error)

开源项目里的promise

  • ioredis
  • mongoose && mongoskin

ioredis

https://github.com/luin/ioredis

ioredis是redis库里的首选,其实它是基于node_redis的,增加和优化了很多,其中关于promise的一点是

Delightful API. It works with Node callbacks and Bluebird promises.

用法

redis.get('foo').then(function (result) {
  console.log(result);
});

真心比node_redis好用的多。

luin是《Redis入门指南》一书作者,90后,全栈+设计,写过不少nodejs模块,目前就在群里

mongoose && mongoskin

mongoose是nodejs处理mongodb的不二选择,简单说一下mongoskin

官方说

The promise wrapper for node-mongodb-native.

做了很多api上的优化,还是不错的,可是mongoose也出了promise

mongoose仿佛在喊:“用我,我也可以then”。。。。

4)最后给出未来展望

未来主要是es6和es7上的实现

  • es6上的Generators/yield
  • es7上的Async/await

都是好东西,可以尝试,毕竟早晚js还是要升级的,先做技术储备吧

生成器Generators/yield

生成器是ES6(也被称为ES2015)的新特性。

想象下面这样的一个场景:

当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。

上面描述的场景正是JavaScript生成器函数所致力于解决的问题。

当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。

看例子

function* foo () {  
  var index = 0;
  while (index < 2) {
    yield index++; //暂停函数执行,并执行yield后的操作
  }
}

var bar =  foo(); // 返回的其实是一个迭代器

console.log(bar.next());    // { value: 0, done: false }  
console.log(bar.next());    // { value: 1, done: false }  
console.log(bar.next());    // { value: undefined, done: true }  

如果你想更轻松的使用生成器函数来编写异步JavaScript代码,我们可以使用 co 这个库,co是著名的tj大神写的,是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。

使用co,改写前面的示例代码:

co(function* (){  
  yield Something.save();
}).then(function() {
  // success
})
.catch(function(err) {
  //error handling
});

你可能会问:如何实现并行操作呢?答案可能比你想象的简单,如下

yield [a.save(), b.save()];  

其实它就是Promise.all,换种写法而已

Async/await

在ES7(还未正式标准化)中引入了Async函数的概念,目前如果你想要使用的话,只能借助于babel 这样的语法转换器将其转为ES5代码。

使用async关键字,可以轻松地达成之前使用生成器和co函数所做到的工作。当然,hack除外

可以这样理解,async函数实际使用的是Promise,然后使用await来执行异步操作,这里的await类似于上面讲的yield

因此,使用async重写上面的代码:

async function save(Something) {  
  try {
    await Something.save(); // 等待await后面的代码执行完,类似于yield
  } catch (err) {
    //error handling
  }
  console.log('success');
}

下一代web框架Koa也支持async函数,如果你也在使用koa,那么你现在就可以借助babel使用这一特性了

import koa from koa;  
let app = koa();

app.experimental = true;

app.use(async function (){  
    this.body = await Promise.resolve('Hello Reader!')
})

app.listen(3000);  

这里报个料,群里fundon 大神写的https://github.com/trekjs/trek 是基于koa的,使用babel编译,并有更多相当棒的特性。爱折腾的可以试试

最后还是要归纳总结一下,

先复习promise/a+的四个要点

  • a) 异步操作的最终结果,尽可能每一个异步操作都是独立操作单元
  • b) 与Promise最主要的交互方法是通过将函数传入它的then方法(thenable)
  • c) 捕获异常catch error
  • d) 根据reject和resolve重塑流程

已经第三遍,该记住了!

继续看第一个图

1.png

再来看这张图,从callback到promose是历史的必然产物

generator是一种新的定义方式,定义操作单元,尤其在迭代器的情况,搭配yield来执行,可读性上差了很多,好处是真的解耦了

co是一个中间产品,可以说是给generator增加了promise实现,可读性和易用性是愿意好于generator + yield的

最后我们看看async,它实际上是通过async这个关键词,定义的函数就可以返回promise对象,可以说async就是能返回promise对象的generator。yield关键词以及被generator绑架了,那它就换个名字,叫await

其实从这段历史来看,反复就是promise上的折腾,只是加了generator这个别名,只是async是能返回promise的generator

这样理解是不是更简单呢?

万变不离其宗,想精通js或者nodejs,promise是大家必须迈过去的坑。

谢谢大家,今天讲的内容就到这里,如果有什么讲的不对的、不合理的,请不吝赐教,共同学习

大家有任何不明白或者想提问的可以随时私信给小助手,我会在答疑阶段统一回答

空弦科技招聘

推荐资料

全文完

欢迎关注我的公众号【node全栈】

node全栈.png

5 回复

0)痛点下面那张图太老了,libuv早独立了。

不管是promise,还是generator/yield,还是async/await都不能代表异步的未来,异步的未来是没有显式的异步,是形式同步。

现在nodejs的异步模型是受限于javascript历史和现实的妥协产物。

这个世界是异步的吗?这个世界是异步的。我们可以跟其他人或物协作,形成异步,但就我们个体而言,我们是同步的,因为我们只有一个大脑。当我们只有一个人的时候,我们能做的顶多是将事情排个优先级,先做什么后做什么,但事情还是得由自己一个个去做。异步的本质是借助外部资源与自己协同做事情。

这个世界是并行的吗?这个世界是并行的。我们可以将任务分发给其他人或物,并行执行,但就我们个体而言,我们是无法并行的,因为我们只有一个大脑。当我们只有一个人的时候,我们就无法并行。并行的本质是借助外部资源一起做事情。

一句话概括起来就是,这个世界是异步,是并行的,但我们每个人却不是,在没有外部资源配合的时候,我们只能顺序同步的一件件完成任务。当然这个结论对某些牛人而言是不正确,他们或许可以自己跟自己异步,自我并行。。。

我们需要跟其他人合作,我们需要团队资源,我们需要异步,需要并行。但请注意,我们真正需要的不是异步和并行本身,而是由他们带来的好处,带来的效率上的提升。异步和并行是需要代价的,跟其他人合作,就要有沟通,如果团队沟通出现了问题,团队效率可能会不及个人。沟通方式是非常重要的,可以通过消息提醒,可以通过回调。消息提醒是人类常用的,我们要分一定的神去关注别人的消息,还要过滤垃圾消息,提取有用的,这个模式对小规模的业务是有效的,每个人专注一两件事情。回调则是按照我们规划好的,一层层响应关系事先注册好了,来了订单,形成了生产命令,经理注册的审批回调就触发了,经理审批完成,财务注册的财务审核的回调又触发了,财务审核通过,采购注册的采购回调,车间注册的生产回调触发了,开始备料,采购到货,品管注册的来料检验回调触发了,如此一层层的回调自动的触发下去,直至最终货到客户手中。回调由于是事先规划好了信息传递他规则,不需要一层层的检索信息,再进行转发,每个注册回调的人员都能及时的做自己该做的事情。从这个意义上来说,回调模式下的流程管理比消息转发模式高效得多。

这个例子其实不是要告诉大家回调好厉害,好伟大,这个大家都清楚。这个例子是要告诉大家,回调的本质,回调就是来保证执行顺序的,是保证流程的,这个过程中不管是异步,并行,都被层层注册的回调控制得死死的,先做什么后做什么,事先已经规划好,订单来了,经理不批,后面的流程就走不下去。讲到这里,聪明的人已经理解了我的用意,千万不要以为回调就是异步,回调跟异步半毛钱的关系都没有(同步代码,只要你愿意,同样可以用回调来表达),回调这个模式只是碰巧(相对效率高)能够准确的表达异步,能够有效的约束异步关系,形成准确的执行链条。回调保证的其实是同步,是顺序,是业务逻辑上的同步和顺序执行。

世界是异步的,世界是并行的,特么业务逻辑竟然是同步的,是顺序执行的。这就是nodejs异步痛苦的根源,这也是很多人不愿意承认的矛盾。这个矛盾也恰恰是promise,generator/yield,async/await出现的原因,但他们都不完美,他们或多或少的都要兼容过去的回调代码,这是语言的历史和现实造成的,估计短时间改变的可能性不大。

如何实现形式同步,如何隐藏回调,大家都在探索,fibjs,openresty,很多项目都在做,转换和隐藏回调的钥匙在协程。

总之,请记住,你所要写的业务代码逻辑上总是同步的顺序的,nodejs痛苦的根源就是用显式的异步代码去表达同步顺序的业务逻辑,你的内心,其实希望写同步代码。

@coordcn

那张图示意足够了,哈哈

干货,赞一个

@coordcn 确实如此,每个人的内心,其实希望写同步代码。

回到顶部