澳门新葡萄京官网首页 2

澳门新葡萄京官网首页Node.js开发者必须熟悉的四个JavaScript概念

前言

4月初在北京的时候,徐昊同学表示我们公司的同事们写的文章都太简单,太注重细节,然后捡起了芝麻丢了西瓜,于是我就不再更新博客(其实根本原因是项目太忙)。上周和其他几个同事一起参加“Martin
Fowler深圳行”的活动,我和同事扎西贡献了一个《FullStack Language
JavaScript》,一起的还有杨云(江湖人称大魔头)的话题是《掌握函数式编程,控制系统复杂度》,李新(江湖人称新爷)的话题是《并发:前生来世》。

和其他同事预演的时候,突然发现其实我们的主题或多或少都有些关联,我讲的部分也涉及到了基于事件的并发机制和函数式编程。仔细想想,应该与JavaScript本身的特性不无关系:

  1. 基于事件(Event-Based)的Node.js的正是并发中很典型的一个模型
  2. 函数式编程使其天然支持回调,从而非常适合异步/事件机制
  3. 函数式编程特性使其非常适合DSL的编写

会后的第二天,我在项目代码里忽然想要将一个聚合模型用函数式编程的方式重写一下,结果发现思路竟然与NoSQL依稀有些联系,进一步发现自己很多不足。

下面这个例子来自于实际项目中的场景,不过Domain做了切换,但是丝毫不影响阅读和理解背后的机制。

Node.js开发者必须熟悉的四个JavaScript概念

一个书签应用

设想有这样一个应用:用户可以看到一个订阅的RSS的列表。列表中的每一项(称为一个Feed),包含一个id,一个文章的标题title和一个文章的链接url

数据模型看起来是这样的:

var feeds = [
    {
        'id': 1,
        'url': 'http://abruzzi.github.com/2015/03/list-comprehension-in-python/',
        'title': 'Python中的 list comprehension 以及 generator'
    },
    {
        'id': 2,
        'url': 'http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/',
        'title': '使用inotify/fswatch构建自动监控脚本'
    },
    {
        'id': 3,
        'url': 'http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/',
        'title': '使用underscore.js构建前端应用'
    }
];

当这个简单应用没有任何用户相关的信息时,模型非常简单。但是很快,应用需要从单机版扩展到Web版,也就是说,我们引入了用户的概念。每个用户都能看到一个这样的列表。另外,用户还可以收藏Feed。当然,收藏之后,用户还可以查看收藏的Feed列表。

澳门新葡萄京官网首页 1

由于每个用户可以收藏多个Feed,而每个Feed也可以被多个用户收藏,因此它们之间的多对多关系如上图所示。可能你还会想到诸如:

$ curl http://localhost:9999/user/1/feeds

来获取用户1的所有feed等,但是这些都不重要,真正的问题是,当你拿到了所有Feed之后,在UI上,需要为每个Feed填加一个属性makred。这个属性用来标示该feed是否已经被收藏了。对应到界面上,可能是一枚黄色的星星,或者一个红色的心。

澳门新葡萄京官网首页 2

Node.js开发者必须熟悉的四个JavaScript概念

服务器端聚合

由于关系型数据库的限制,你需要在服务器端做一次聚合,比如将feed对象包装一下,生成一个FeedWrapper之类的对象:

public class FeedWrapper {
    private Feed feed;
    private boolean marked;

    public boolean isMarked() {
        return marked;
    }

    public void setMarked(boolean marked) {
        this.marked = marked;
    }

    public FeedWrapper(Feed feed, boolean marked) {
        this.feed = feed;
        this.marked = marked;
    }
}

然后定义一个FeedService之类的服务对象:

public ArrayList<FeedWrapper> wrapFeed(List<Feed> markedFeeds, List<Feed> feeds) {
    return newArrayList(transform(feeds, new Function<Feed, FeedWrapper>() {
        @Override
        public FeedWrapper apply(Feed feed) {
            if (markedFeeds.contains(feed)) {
                return new FeedWrapper(feed, true);
            } else {
                return new FeedWrapper(feed, false);
            }
        }
    }));
}

好吧,这也算是一个还凑合的实现,但是静态强类型的Java做这个事儿有点勉强,而且一旦发生新的变化(几乎肯定会发生),我们还是把这部分逻辑放在JavaScript中,来看看它是如何简化这一个过程的。

 

Node.js是一个服务器端的开发框架,它基于Google Chrome的V8
JavaScript引擎构建。尽管Node.js自身是使用C++开发的,但是它使用JavaScript作为其应用语言。
Node.js有四个概念对于初学者非常重要,应该理解并掌握它们。如下:

客户端聚合

快要说到主题了,这篇文章我们会使用lodash作为函数式编程的库来简化代码的编写。由于JavaScript是一个动态弱类型的语言,我们可以随时为一个对象添加属性,这样一个简单的map澳门新葡萄京官网首页,操作就可以完成上边的Java对应的代码了:

_.map(feeds, function(item) {
    return _.extend(item, {marked: isMarked(item.id)});
});

其中函数isMarked会做这样一件事儿:

var userMarkedIds = [1, 2];
function isMarked(id) {
    return _.includes(userMarkedIds, id);
}

即查看传入的参数是否在一个列表userMarkedIds,这个列表可能由下列的请求来获得:

$ curl http://localhost:9999/user/1/marked-feed-ids

之所有只获取id是为了减少网络传输的数据大小,当然你也可以将全部的/marked-feeds都请求到,然后在本地做_.pluck(feeds, 'id')来抽取所有的id属性。

嗯,代码是精简了许多。但是如果仅仅能做到这一步的话,也没有多大的好处嘛。现在需求又有了变化,我们需要在另一个页面上展示当前用户的收藏夹(用以展示用户所有收藏的feed)。作为程序员,我们可不愿意重新写一套界面,如果能复用同一套逻辑当然最好了。

比如对于上面这个列表,我们已经有了对应的模板:

{{#each feeds}}
<li class="list-item">
    <div class="section" data-feed-id="{{this.id}}">
        {{#if this.marked}}

        {{else}}

        {{/if}}
        <a href="/feeds/{{this.url}}">
            <div class="detail">
                <h3>{{this.title}}</h3>
            </div>
        </a>
    </div>
</li>
{{/each}}

事实上,这段代码在收藏夹页面上完全可以复用,我们只需要把所有的marked属性都设置为true就行了!简单,很快我们就可以写出对应的代码:

_.map(feeds, function(item) {
    return _.extend(item, {marked: true});
});

漂亮!而且重要的是,它还可以如正常工作!但是作为程序员,你很快就发现了两处代码的相似性:

_.map(feeds, function(item) {
    return _.extend(item, {marked: isMarked(item.id)});
});

_.map(feeds, function(item) {
    return _.extend(item, {marked: true});
});

消除重复是一个有追求的程序员的基本素养,不过要消除这两处貌似有点困难:位于marked:后边的,一个是函数调用,另一个是值!如果要简化,我们不得不做一个匿名函数,然后以回调的方式来简化:

function wrapFeeds(feeds, predicate) {
    return _.map(feeds, function(item) {
        return _.extend(item, {marked: predicate(item.id)});
    });
}

对于feed列表,我们要调用:

wrapFeeds(feeds, isMarked);

而对于收藏夹,则需要传入一个匿名函数:

wrapFeeds(feeds, function(item) {return true});

lodash中,这样的匿名函数可以用_.wrap来简化:

wrapFeeds(feeds, _.wrap(true));

好了,目前来看,简化的还不错,代码缩减了,而且也好读了一些(当然前提是你已经熟悉了函数式编程的读法)。

一、非阻塞或异步I/O

由于Node.js是一个服务器端框架,因此它的主要工作之一就是处理来自浏览器的请求。在传统的I/O系统中,只有先前请求的响应返回来后,新的请求才能发出。这也就是为什么称之为阻塞I/O的通信。服务器阻塞了下一个到来的请求,然后处理当前的请求,直到请求处理完成,发出响应,再解除下一个到来的请求的阻塞。

Node.js不遵循上面的阻塞I/O通信原则。如果一个请求需要的处理时间较长,Node.js会把请求发送到事件循环中,然后在调用栈上处理下一个请求。一旦事件循环中的请求完成了处理,它会通知Node.js,Node.js会返回响应给浏览器。下面可以看一个例子:

更进一步

如果仔细审视isMarked函数,会发现它对外部的依赖不是很漂亮(而且这个外部依赖是从网络异步请求来的),也就是说,我们需要在请求到markedIds的地方才能定义isMarked函数,这样就把函数定义绑定到了一个固定的地方,如果该函数的逻辑比较复杂,那么势必会影响代码的可维护性(或者更糟糕的是,多出维护)。

要将这部分代码隔离出去,我们需要将ids作为参数传递出去,并得到一个可以当做谓词(判断一个id是否在列表中的谓词)的函数。

简而言之,我们需要:

var predicate = createFunc(ids);
wrapFeeds(feeds, predicate);

这里的createFunc函数接受一个列表作为参数,并返回了一个谓词函数。而这个谓词函数就是上边说的isMarked。这个神奇的过程被称为柯里化currying,或者偏函数partial。在lodash中,这个很容易实现:

function isMarkedIn(ids) {
    return _.partial(_.includes, ids);
}

这个函数会将ids保存起来,当被调用时,它会被展开为:_.includes(ids, <id>)。只不过这个<id>会在实际迭代的时候才传入:

$('/marked-feed-ids').done(function(ids) {
    var wrappedFeeds = wrapFeeds(feeds, isMarkedIn(ids));
    console.log(wrappedFeeds);
});

这样我们的代码就被简化成了:

$('/marked-feed-ids').done(function(ids) {
    var wrappedFeeds = wrapFeeds(feeds, isMarkedIn(ids));
    var markedFeeds = wrapFeeds(feeds, _.wrap(true));

    allFeedList.html(template({feeds: wrappedFeeds}));
    markedFeedList.html(template({feeds: markedFeeds}));
});

1、阻塞式I/O

// 餐桌1,获取订单1
var order1 = orderBlocking(['Coke', 'Iced Tea']);
// 服务订单1
serveOrder(order1);
// 一旦订单服务完成,服务员去另一张餐桌
// 餐桌2,订单2
var order2 = orderBlocking(['Coke', 'Water']);
// 服务订单2
serveOrder(order2);
// 一旦订单服务完成,服务员去另一张餐桌
// 餐桌3,订单3
var order3 = orderBlocking(['Iced Tea', 'Water']);
// 服务订单3
serveOrder(order3);
// 一旦订单服务完成,服务员去另一张餐桌

上面的例子中,服务员在第一个餐桌获得订单,然后向订单提供服务,服务完成后,服务员立刻移动到下一张餐桌获得订单。订单是按时间顺序进行处理的,服务器仅仅是服务于订单和阻塞其它的订单。

2、非阻塞式I/O

// 在餐桌1取走交付的订单并移动到下一个餐桌
orderNonBlocking(['Coke', 'Iced Tea'], function(drinks){
  return serveOrder(drinks);
});
// 在餐桌2取走交付的订单并移动到下一个餐桌
orderNonBlocking(['Beer', 'Whiskey'], function(drinks){
  return serveOrder(drinks);
});
//在餐桌3取走交付的订单并移动到下一个餐桌
orderNonBlocking(['Hamburger', 'Pizza'], function(food){
  return serveOrder(food);
});

在上面的例子中,服务员去获取订单并通知厨师,再去下一个餐桌。在第一个订单被处理期间,服务员移动到下一个餐桌去获取订单,服务员不阻塞订单。

二、原型

在JavaScript中,原型即Prototype,是一个比较复杂的概念。Node.js使用了原型的地方很多,因此每一个JavaScript开发者都应该熟悉这个概念。

像Java、C++等编程语言都实现了典型的继承,这有助于代码的重用。首先构建一个基类(作为对象的蓝图),然后从这个类创建对象或扩展这个类。

但是JavaScript语言没有这样的概念。首先在JavaScript中创建一个对象,然后扩展这个对象或者从这个对象中创建新的对象。这就是所谓的原型继承,它通过原型来实现。

每一个JavaScript对象都链接到一个原型对象,并且可以从原型对象中继承其属性。原型有点类似于面向对象语言中的类,但实际上是不同的,它们自身都是对象。每一个对象都链接到Object.prototype,它是JavaScript预定义的对象。

如果你在通过obj.propName或obj[‘propName’]来查看属性时,这个对象有没有这样的属性,可以通过obj.hasOwnProperty(‘propName’)来检查,JavaScript的运行时会查看原型对象中是否有这个属性。如果原型对象没有这样的属性,然后再依次检查此对象本身有没有这样的属性(有可能对象继承了几级),直到匹配到此属性。如果整个属性链都没有这样的属性,那么就会返回未定义的值。

用例子来说明这一点:

if (typeof Object.create !== 'function') {
    Object.create = function (o) {
        var F = function () {};
        F.prototype = o;
        return new F();
    };
var otherPerson = Object.create(person);

当创建了一个新对象时,你可以选择这个对象的原型。在上面的代码中,我们增加了一个Object函数的create方法。create方法创建了一个新对象,并且使用了另一个对象作为它的原型,并作为参数传递到新对象。

当我们修改了新对象,它的原型还保留原样,不受影响。但是如果我们要修改原型对象,那么就会影响到所有基于此原型对象的对象。

原型是一个很复杂的概念,需要继续深入。

三、模块

如果你熟悉Java语言的包Package概念,那么你会理解Node.js的模块这个概念,两者没什么不同。模块是简单的JavaScript文件,它包含了特定目的的代码。模块模式(Module
Pattern)用于简化代码的组织和导航。要使用模块的属性,你必须在JavaScript中require导入它,与Java语言中的import导入相同。

Node.js中有两者类型的模块:

1、核心模块(Core Module)

核心模块包含了预编译到Node.js的库。核心模块的模板是向开发者提供经常出现和重复的代码片段,如果没有这些,那么开发者会陷入这些大量重复且冗长无趣的工作中。常见的核心模块包括:HTTP模块、URL模块、EVENTS模块、文件系统模块等。

2、用户定义的模块(User Defined Module)

用户定义的模块是开发者为了特定功能自己实现的模块。通常是核心模块不能满足所需的功能时开发的。用户定义的模块同样需要require导入。如果是核心模块,require导入只需传入模块名,而对于用户定义的模块,require导入还需要传入文件系统的路径。

比如:

// 导入核心模块
var http = require('http);
// 导入用户定义的模块
var something = require('./folder1/folder2/folder3/something.js');

四、回调

在JavaScript语言中,函数被认为是第一级的对象。这意味着开发者可以把函数当作是常规对象那样,做所有的操作。可以把函数赋值给一个变量,还可以把函数作为参数传递给方法,还可以把函数作为对象的属性,甚至可以从函数返回函数。

回调是JavaScript语言中的一种匿名函数,它可以作为参数传递给另一个函数,还可以在随后的函数执行中执行回调函数或返回回调函数。这是回调函数广泛使用的编程范式。

把回调函数作为参数传递给另一个函数时,我们只需传递函数定义,也即,我们无需知道回调函数什么时候得以执行。这完全取决于调用函数的机制,故以回调命名。回调函数是Node.js的非阻塞通信和异步处理的基础。

setTimeout(function() {
    console.log(world);
}, 2000)
console.log(hello);

这是最简单的回调函数的例子之一,我们把匿名函数当作参数传递给setTimeout函数,而匿名函数仅仅是在控制台输出日志“world”。由于这只是函数定义,我们并不知道它什么时候得以执行,执行取决于setTimeout函数的2000毫秒后输出。

故第二个日志语句先在控制台输出“hello”,然后等两秒后再输出回调函数定义的日志“world”。

// output
hello
world

理解以上四个概念有助于深入Node.js。

Node.js开发者必须熟悉的四个JavaScript概念
Node.js是一个服务器端的开发框架,它基于Google Chrome的V…

发表评论

电子邮件地址不会被公开。 必填项已用*标注