Node.js的事件机制

Node.js在其Github代码仓库(https://github.com/joyent/node)上有着一句短短的介绍:Evented I/O for V8 JavaScript。这句近似广告语的句子却道尽了Node.js自身的特色所在:基于V8引擎实现的事件驱动IO。在本文的这部分内容中,我来揭开这Evented这个关键词的一切奥秘吧。

Node.js能够在众多的后端JavaScript技术之中脱颖而出,正是因其基于事件的特点而受到欢迎。拿Rhino来做比较,可以看出Rhino引擎支持的后端JavaScript摆脱不掉其他语言同步执行的影响,导致JavaScript在后端编程与前端编程之间有着十分显著的差别,在编程模型上无法形成统一。在前端编程中,事件的应用十分广泛,DOM上的各种事件。在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的。在Rhino中,文件读取等操作,均是同步操作进行的。在这类单线程的编程模型下,如果采用同步机制,无法与PHP之类的服务端脚本语言的成熟度媲美,性能也没有值得可圈可点的部分。直到Ryan Dahl在2009年推出Node.js后,后端JavaScript才走出其迷局。Node.js的推出,我觉得该变了两个状况:

  1. 统一了前后端JavaScript的编程模型。
  2. 利用事件机制充分利用用异步IO突破单线程编程模型的性能瓶颈,使得JavaScript在后端达到实用价值。

有了第二次浏览器大战中的佼佼者V8的适时助力,使得Node.js在短短的两年内达到可观的运行效率,并迅速被大家接受。这一点从Node.js项目在Github上的流行度和NPM上的库的数量可见一斑。

至于Node.js为何会选择Evented I/O for V8 JavaScript的结构和形式来实现,可以参见一下2011年初对作者Ryan Dahl的一次采访:http://bostinno.com/2011/01/31/node-js-interview-4-questions-with-creator-ryan-dahl/ 。

事件机制的实现

Node.js中大部分的模块,都继承自Event模块(http://nodejs.org/docs/latest/api/events.html )。Event模块(events.EventEmitter)是一个简单的事件监听器模式的实现。具有addListener/on,once,removeListener,removeAllListeners,emit等基本的事件监听模式的方法实现。它与前端DOM树上的事件并不相同,因为它不存在冒泡,逐层捕获等属于DOM的事件行为,也没有preventDefault()、stopPropagation()、 stopImmediatePropagation() 等处理事件传递的方法。

从另一个角度来看,事件侦听器模式也是一种事件钩子(hook)的机制,利用事件钩子导出内部数据或状态给外部调用者。Node.js中的很多对象,大多具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,对象运行期间的中间值或内部状态,是我们无法获取到的。这种通过事件钩子的方式,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。

var options = {
    host: 'www.google.com',
    port: 80,
    path: '/upload',
    method: 'POST'
};
var req = http.request(options, function (res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log('BODY: ' + chunk);
    });
});
req.on('error', function (e) {
    console.log('problem with request: ' + e.message);
});
// write data to request body
req.write('data\n');
req.write('data\n');
req.end();

在这段HTTP request的代码中,程序员只需要将视线放在error,data这些业务事件点即可,至于内部的流程如何,无需过于关注。

值得一提的是如果对一个事件添加了超过10个侦听器,将会得到一条警告,这一处设计与Node.js自身单线程运行有关,设计者认为侦听器太多,可能导致内存泄漏,所以存在这样一个警告。调用:

emitter.setMaxListeners(0);

可以将这个限制去掉。

其次,为了提升Node.js的程序的健壮性,EventEmitter对象对error事件进行了特殊对待。如果运行期间的错误触发了error事件。EventEmitter会检查是否有对error事件添加过侦听器,如果添加了,这个错误将会交由该侦听器处理,否则,这个错误将会作为异常抛出。如果外部没有捕获这个异常,将会引起线程的退出。

事件机制的进阶应用

继承event.EventEmitter

实现一个继承了EventEmitter类是十分简单的,以下是Node.js中流对象继承EventEmitter的例子:

function Stream() {
    events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);

Node.js在工具模块中封装了继承的方法,所以此处可以很便利地调用。程序员可以通过这样的方式轻松继承EventEmitter对象,利用事件机制,可以帮助你解决一些问题。

多事件之间协作

在略微大一点的应用中,数据与Web服务器之间的分离是必然的,如新浪微博、Facebook、Twitter等。这样的优势在于数据源统一,并且可以为相同数据源制定各种丰富的客户端程序。以Web应用为例,在渲染一张页面的时候,通常需要从多个数据源拉取数据,并最终渲染至客户端。Node.js在这种场景中可以很自然很方便的同时并行发起对多个数据源的请求。

api.getUser("username", function (profile) {
    // Got the profile
});
api.getTimeline("username", function (timeline) {
    // Got the timeline
});
api.getSkin("username", function (skin) {
    // Got the skin
});

Node.js通过异步机制使请求之间无阻塞,达到并行请求的目的,有效的调用下层资源。但是,这个场景中的问题是对于多个事件响应结果的协调并非被Node.js原生优雅地支持。为了达到三个请求都得到结果后才进行下一个步骤,程序也许会被变成以下情况:

api.getUser("username", function (profile) {
    api.getTimeline("username", function (timeline) {
        api.getSkin("username", function (skin) {
            // TODO
        });
    });
});

这将导致请求变为串行进行,无法最大化利用底层的API服务器。

为解决这类问题,我曾写作一个模块(EventProxy,https://github.com/JacksonTian/eventproxy)来实现多事件协作,以下为上面代码的改进版:

var proxy = new EventProxy();
proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) {
    // TODO
});
api.getUser("username", function (profile) {
    proxy.emit("profile", profile);
});
api.getTimeline("username", function (timeline) {
    proxy.emit("timeline", timeline);
});
api.getSkin("username", function (skin) {
    proxy.emit("skin", skin);
});

EventProxy也是一个简单的事件侦听者模式的实现,由于底层实现跟Node.js的EventEmitter不同,无法合并进Node.js中。但是却提供了比EventEmitter更强大的功能,且API保持与EventEmitter一致,与Node.js的思路保持契合,并可以适用在前端中。

这里的all方法是指侦听完profile、timeline、skin三个方法后,执行回调函数,并将侦听接收到的数据传入。

最后还介绍一种解决多事件协作的方案:Jscex(https://github.com/JeffreyZhao/jscex )。Jscex通过运行时编译的思路(需要时也可在运行前编译),将同步思维的代码转换为最终异步的代码来执行,可以在编写代码的时候通过同步思维来写,可以享受到同步思维的便利写作,异步执行的高效性能。如果通过Jscex编写,将会是以下形式:

var data = $await(Task.whenAll({
    profile: api.getUser("username"),
    timeline: api.getTimeline("username"),
    skin: api.getSkin("username")
}));
// 使用data.profile, data.timeline, data.skin
// TODO

此节感谢Jscex作者@老赵(http://blog.zhaojie.me/)的指正和帮助。

利用事件队列解决雪崩问题

所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体响应缓慢。那么在Node.js中如何应付这种情景呢。

var select = function (callback) {
        db.select("SQL", function (results) {
            callback(results);
        });
    };

以上是一句数据库查询的调用,如果站点刚好启动,这时候缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,影响到服务的整体性能。一个改进是添加一个状态锁。

var status = "ready";
var select = function (callback) {
        if (status === "ready") {
            status = "pending";
            db.select("SQL", function (results) {
                callback(results);
                status = "ready";
            });
        }
    };

但是这种情景,连续的多次调用select发,只有第一次调用是生效的,后续的select是没有数据服务的。所以这个时候引入事件队列吧:

var proxy = new EventProxy();
var status = "ready";
var select = function (callback) {
        proxy.once("selected", callback);
        if (status === "ready") {
            status = "pending";
            db.select("SQL", function (results) {
                proxy.emit("selected", results);
                status = "ready";
            });
        }
    };

这里利用了EventProxy对象的once方法,将所有请求的回调都压入事件队列中,并利用其执行一次就会将监视器移除的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的调用,只需在队列中等待数据就绪即可,节省了重复的数据库调用开销。由于Node.js单线程执行的原因,此处无需担心状态问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。此处也可以用EventEmitter替代EventProxy,不过可能存在侦听器过多,引发警告,需要调用setMaxListeners(0)移除掉警告,或者设更大的警告阀值。

参考:

关于作者

田永强,新浪微博@朴灵,前端工程师,曾就职于SAP,现就职于淘宝,花名朴灵,致力于NodeJS和Mobile Web App方面的研发工作。双修前后端JavaScript,寄望将NodeJS引荐给更多的工程师。兴趣:读万卷书,行万里路。个人Github地址:http://github.com/JacksonTian

javascript中自定义事件和声明调用函数有什么区别?

Q:最近在琢磨javascript的自定义事件,我的理解是注册一个事件(addEvent),然后在需要的时候调用它(fireEvent),这感觉像声明好一个函数,然后在需要的时候调用,感觉不出它们之间的区别。
我觉得任何事物的存在都有它的道理,那这自定义事件到底用在哪些场景?

 

A:

你的感觉是对的,实际上事件机制就是从回调函数转化而来的。

实际上事件模式算是订阅/发布模式的一种,它的好处在于绑定事件和触发事件是互相隔离的,并且可以动态的添加和删除,下面我帮你一步一步的梳理出来。

现在有这么一个需求,首先,我有一个动作Action1,我想每次在Action1完成之后,都会触发一个服务Service1,那么代码我可以这么写。

// 服务1
function Service1(){}
// 动作1
function Action1(){
    //other things
    Service1();
}

这个就是楼主所想的,代码非常明了而且易懂,很不错。

然而现在需求来了,我的动作Action1,想在它完成以后,不仅能触发一个服务Service1,还能触发Service2,Service3……

你可能想到,应该声明另外一个叫ServiceAll的函数,然后在ServiceAll里依次调用Service1、Service2、Service3,这个主意是可行的。

function Service1(){}
function Service2(){}
function Service3(){}
function ServiceAll(){
    Service1();
    Service2();
    Service3();
}
// 动作1
function Action1(){
    //other things
    ServiceAll();
}

但问题随之而来,这里的ServiceAll事先定义好了,假如我要动态的添加一个Service4怎么办?假如我突然改变心意,动作Action1以后不再执行Service2,这个时候代码正在运行,必须重新定义ServiceAll函数,这无论开销还是实现都不方便。

我们再换个思路,我们弄一个叫ServiceArray的数组,然后把所有要执行的Service都放进去,动作Action1以后,直接循环调用ServiceArray岂不是就好了?

var ServiceArray=[];

// 动作1
function Action1(){
    //other things
    ServiceArray.each(function(Service){
        Service();
    })
}

//追加Service1,Service2,Service3
ServiceArray.push(Service1);
ServiceArray.push(Service2);
ServiceArray.push(Service3);

// 实现一个数组删除函数
function deleteVal(arr,val){
    var index = arr.indexOf(val);
    if (index !== -1) {
       arr.splice(index, 1);
    }
}

// 动态增加一个Service4,并且不要Service2了
ServiceArray.push(Service4);
deleteVal(ServiceArray,Service2);

好了,我们已经用数组队列来轻松实现动态删除和动态添加Service的功能了,接下来又有一个新的需求。

我们上面都是一个动作Action1,我现在有第二个动作Action2,它也要有一堆Service要执行,这个时候最简单的实现办法是和上面一样,但如果直接复制粘贴代码的话,动作一多,重复代码就太多了,所以为了代码复用,我们写个统一的处理函数

var actionObj={};
function hanldeAction(name,serviceArr){
    if(typeof actionObj[name] !=funtion){
        actionObj[name]=function(){
            serviceArr.forEach(function(Service){
                Service();
            })
        }
    }
    return actionObj[name];   
}

//调用
Action1=hanldeAction("action1",ServieArray1);
Action2=handleAction("action2",ServieArray2);

我们上面考虑的这些Action1,Action2都是相互独立的情况,下面再考虑一个问题,假设这些动作都是某个对象obj发出来的,我们必须要保证这个对象在Service1,Service2等等都能访问到,这个时候该怎么办?

你可能想到把obj带在handleAction的参数里,然后再其定义里由Servie(obj)带进去,这是可行的,不过我们换个面向对象的思路

function MyObj(){
    this.actions={};
}
// 动态给Action添加Service
MyObj.prototype.addService=function(actionName,Service){
    if(!this.actions[actionName])this.actions[actionName]=[];
    this.actions[actionName].push(Service);
}
// 动态删除Service
MyObj.prototype.removeService=function(actionName,Service){
    var ServiceArray=this.actions[actionName]?this.actions[actionName]:[];
    var index = ServiceArray.indexOf(Service);
    if (index !== -1) {
       ServiceArray.splice(index, 1);
    }
}
// Action调用Service
MyObj.prototype.emitAction=function(actionName){
    var ServiceArray=this.actions[actionName]?this.actions[actionName]:[];
    for(var i=0;i<ServiceArray.length;i++){
        ServiceArray[i].apply(this);
    }
}

看到这里,聪明的你应该就清楚了,我们是在实现一个简易的自定义事件机制,把其中的名字换一下就能更清晰的看出来了。

function EventEmitter(){
    this._events={};
}
EventEmitter.prototype.addListener=function(type, listener){
    if(!this._events[type])this._events[type]=[];
    this._events[type].push(listener);
}
EventEmitter.prototype.removeListener=function(type, listener){
    var listenerArray=this._events[type]?this._events[type]:[];
    var index = listenerArray.indexOf(listener);
    if (index !== -1) {
       listenerArray.splice(index, 1);
    }
}
EventEmitter.prototype.emit=function(type){
    var listenerArray=this._events[type]?this._events[type]:[];
    for(var i=0;i<listenerArray.length;i++){
        listenerArray[i].apply(this);
    }
}

这样我们就能通过new一个EventEmitter来使用它了。

//使用
var a = new EventEmitter();
a.addListener("type1",function(){
    console.log("service1");
})
a.addListener("type1",function(){
    console.log("service2");
})
a.emit("type1");
function otherService(){
    console.log("other service");
}
a.addListener("type2",otherService);
a.addListener("type2",function(){console.log("service 3")})
a.emit("type2");
a.removeListener("type2",otherService);
a.emit("type2");

这个简单的事件对象已经可以满足我上面提到的种种要求了,但还缺少附加参数、移除所有事件、限定域等等功能,把这些完善以后,再优化一下事件队列的存储方式,你就完成了一个真正的EventEmitter了。

所以题主你应该明白了事件模式的好处了吧?

一些前端开发的干货

标签(空格分隔): 资料

出处:https://coding.net/u/f2e/p/Books/git/tree/master/%E7%A7%BB%E5%8A%A8Web
包含pc端,移动端js库,框架,css框架,工具等等…在原文基础上添加修改

有些虽已经年代久远,但仍然可以学到很多有用的东西,可以整理资料的链接和其他链接资料或许有重复…… –>>另外一个链接:分享自己长期关注的前端开发相关的优秀网站、博客、以及活跃开发者 

常用库查询系统

移动端资料

学习资料/文章

系列文章

– 45个实用的JavaScript技巧、窍门和最佳实践

其他工具

前端组件库

搭建web app常用的样式/组件等收集列表(移动优先)

0. 前端自动化(Workflow)

1. 前端框架(Frameworks)

3. 前端游戏框架(动画引擎)

4. ui组件库

5. 基础模版

6. 排版

7. 网格系统

9. UA 识别

10. 表单处理

10.1 表单验证(Form Validator)/表单提示
10.2 < select > 相关
10.3 单选框/复选框相关
10.4 上传组件
10.7 标签插件(Tag)
10.8 自动完成插件
10.9 样式修正

11. 图表绘制/图形库(Graphics)

13.1 Slider
13.2 瀑布流
13.3 懒加载/加载监听/预加载
13.4 图片轮播(幻灯片)/图片展示
13.5 图片剪裁/图片处理
13.8 菜单(Menu)
13.9 滚动侦测(ScrollSpy)
13.10 滚动加载更多/下拉刷新(Pull to Refresh)
13.11 平滑滚动插件(Smooth Scroll)
13.12 全屏滚动/全屏切换
13.13 分屏滚动
13.14 转场效果
13.15 固定元素(Sticky)
13.16 触控事件
13.17 拖拽组件
13.18 隐藏或展示页面元素
13.19 滚动条(Scrollbar)
13.20 视差滚动(Parallax Scrolling)

14. 代码高亮插件/代码编辑器

15. UI Icon 组件

16. 动画(Animate)

17. 本地存储

18. 模板引擎

19. 通知组件/弹框组件/模态窗口

20. 提示控件(Tooltips)

21. 对话框/遮罩层/弹出层(lightbox)

22. 文档/表格/PDF

23. 目录树插件

24. Ajax模块

25. 音频/视频

26. 按钮

27. 富文本编辑器/Markdown编辑器/Markdown解析器

28. 内容提取(Readability)

29. 颜色(CSS Colors)/SVG

30. 选项卡(Tabs)

31. 文本处理

32. 布局(Layout)

33. 实用工具/其他插件

34. sass 库

34.1 分页

35. 未知分类

前端参考集

有哪些是你踏入社会才明白的道理?

作者:林书豪
链接:https://www.zhihu.com/question/51671791/answer/403256589
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1、成年人的世界里,没有容易二字。当你踏入社会,你会深刻的明白并且坚信,钱真的真的太重要了。鸡汤里的“钱不是万能的,有钱也买不到快乐”这些都是屁话。实际上,钱几乎就是万能的,虽然有钱不一定能买来快乐,但是钱能让你避免99.9%的烦恼。

2、执行力比智商更重要。做自由职业的的时候,我深刻感受到执行力的重要性。当你还在犹豫纠结的时候,别人已经开始开始了。当你一天只肯付出8个小时的时候,别人投入了12个小时。慢慢的,差距就会出来了。我深信一句话:你的努力程度还远远不到拼智商的时候。

3、校招是最容易进入大企业的机会,能有这个机会的请一定要珍惜。你的第一份工作决定了你以后的工作岗位,发展起点,空间。能把握这个机会进入大企业的就争取,能不去私企就不去私企。你在大公司做几年后出来,不管去到哪里都基本会有面试机会。小公司出来很有可能连面试机会都没有。别瞎听那些私企老板的胡说,小企业锻炼人,各个方面都接触。实际上你就是个打杂的,啥都不精通。

4、别让你大学的专业限制你的人生。见过身边很多人,不喜欢自己的专业,毕业后却依旧找回本专业相关的工作。原因是不想浪费了专业知识,转行也不知道做什么好。按我说,别用自己一辈子的时间去为当初高三暑假的时候选错的专业买单。现在网络发达的时代,想学习任何新的东西都不算晚,能找到的学习渠道太多了,缺的是你去学习的心。你的人生有太多可能性了,别把自己束缚在一个角落。不喜欢的工作,那就去想自己真正喜欢什么工作然后去学习,去跳槽。

5、应届生的话就不要去做那些没有技术含量的销售了。很多企业招销售都说能挑战自己,锻炼自己,累积人脉。这些都是屁话。我见过太多的销售是靠着底薪混口饭吃的。而更为沮丧的是,销售行业大部分人都是在30岁的时候开始出现了职业危机。到那个时候再去转行的话,会痛不欲生。

6、你所在的行业有前景不代表你公司有前景,你公司有前景不代表你的岗位有前景,你的岗位有前景不代表你个人有前景。一个人不管任何时候都要靠自己,努力提升自己的技能,掌握核心竞争力,积累自己的资源。别指望着公司,昔日的诺基亚就是一典型的例子,今日的华为裁34岁以上的员工也是典型的例子。

7、大部分时候,面子真的一文不值。尤其是你的。见过很多大学生因为面子问题而错过很多难得的机会。

8、粗心大意真不是小事,你做事粗心大意的话很容易给人留下一个不靠谱的形象。这样的印象让人很难将任务交给你。在社会,夸一个人做事靠谱是最好的赞美。

9、能不借钱就尽可能不借钱,除非是救命钱。否则你极有可能人财两失。

10、不要在人前抱怨,更不要在背后说人坏话。哪怕遇到非常糟心的事情,也不要在人前抱怨,抱怨不能解决任何问题,反而会给人带来负面影响。让你觉得你这个人很无能。不要在背后说人坏话这个就不解释了。

11、带来巨大差异的不仅是努力,更是一次次的选择。人的成功,既要考虑到自我奋斗的过程,也要考虑到历史进程,有时候成功很简单,只要你选对了,你就成功了90%,剩下的只要你努力跟着步骤走下去就好了。雷军说过:“不要用你战术上的勤奋去掩盖你战略上的懒惰”

12、拒绝二手信息,找到信息的源头。跟随和成为知识的源头,提升效率 。 一手信息:论文,行业大牛领军人物的最新沟通和思考,二手信息:书和演讲,名校教科书,网课推荐的一手材料,维基百科,行业调查报告,三手信息:畅销书, 四手信息:公众号。我们平时接受信息的来源多数来自于公众号,知乎,微博等,这些平台的信息往往是作者根据我们的喜好而加工的快消文化。阅读这些内容只会浪费自己的时间和精力,要懂得去找到信息的源头,而这些精华信息的摄取,才是真正有帮助的。

13、读书读书读好书。我很走运,在毕业的时候遇到了我第一个老板,他教我学会了很多技能,但最核心的是他培养了我读书的好习惯。同时也分享了我他个人的书单。通过阅读,这两年我进步飞快,真的为自己养成阅读的好习惯而感到幸运。

我害怕阅读的人。一跟他们谈话,我就像一个透明的人,苍白的脑袋无法隐藏。通过读书,我这两年判若两人。现在回想,真的真的很庆幸自己能养成读书的好习惯。(很多知友不知道要看什么书,私信我要书单的,就不逐个回复了,需要的知友可以到我的公众号:chiu2048 ,在后台回复:书单,即可获得我私人分类整理的书单和2000本电子书。)

14、最后,送一个关于恋爱的小小经验:不管两个人在一起多久,一定要经常称赞对方,互相称赞真的能让你们的爱情保鲜,就像你在知乎上点赞,答主也会很开心一样。

居然有这么多人点赞,我后继想到会持续更新还有关于销售是否真的不值得去做。在这里我的观点是,能不做就不做。有做过销售的前辈也可以在评论区谈一下。