如今有点 javascript 基础的人都在据说过 nodejs ,而只要与 javascript 打交到人都会用或者是将要使用 nodejs 。毕竟 nodejs 的生态很强大,与 javascript 相关的工具也作的很方便,很好用。javascript
javascript 语言很小巧,可是一旦与 nodejs 中的运行环境放在一块儿,有些概念就很难理解,特别是异步
的概念。有人会说不会啊,很好理解啊?不就是一个ajax
请求加上一个回调函数
,这个ajax
函数就是能异步执行的函数,他在执行完了就会调用回调函数
,我认可这个样作是很容易,早些时候我甚至认为在 javascript 中加了回调函数的函数均可以异步的,异步和回调函数成对出现。多么荒谬的理解啊!php
直到有一天,我在写程序时想到一个问题:在 nodejs 中在不调用系统相关 I/O ,不调用 c++ 写的 plugin 的状况下,写一个异步函数?我查了资料,有人给个人答案是调用 setTimeout(fn,delay) 就变成了异步了。可是我仍是不明白为何要调用这样一个函数,这个函数的语义跟async
彻底不同,为何这样就行?html
带着这个疑问,我查了不少资料,包括官方文档,代码,别人的blog。慢慢的理解,最后好像是知道了为何会是这样,整篇文章就是对所了解东西的理解。恳请你们批评指正。java
说明:nodejs 的文档是用的 v5.10.1 API,而代码方面:nodejs 和 libuv 是用的 master 分支。node
在探索 nodejs 的异步时,首先须要对 nodejs 架构达成统一认识:python
若是以上 5 点你不认同的话,那下面就不须要看了,看了会以为漏洞百出。c++
上面的 5 点主要说明另外一层意思了:git
libevent2
,证据在这里:连接。新建调用系统线程
的任何方法,因此在nodejs中执行 javascript,是没有办法新开线程的。那nodejs中常谈的异步
和回调
是怎么回事?程序员
在 javascript 中使用回调函数
可所谓登峰造极,基本上全部的异步函数都会要求有一个回调函数,以致于写 javascript 写多了,看到回调函数的接口,都觉得是异步的调用。github
可是真相是回调函数
,只是javascript 用来解决异步函数调用如何处理返回值这个问题的方法,或这样来讲:异步函数调用如何处理返回值这个问题上,在系统的设计方面而言,有不少办法,而 nodejs 选择了 javascript 的传统方案,用回调函数来解决这个问题
。
这个选择好很差,我认为在当时来讲,很合适。但随着 javascript 被用来写愈来愈大的程序,这个选择不是一个好的选择,由于回调函数嵌套多了真的很难受,我以为主要是很难看,(就跟 lisp 的 ))))))))))))
),让通常人很差接受,如今状况改善多了,由于有了Promise。
前面也说了,nodejs 的 js 引擎不能异步执行 javascript 代码。那js中咱们常使用的异步是什么意思的?
答案分为两部分:
第一部分:与I/O和timer相关的任务,js引擎确实是异步,调用时委托 libuv 进行 I/O 和timer 的相关调用,好了以后就通知 nodejs,nodejs 而后调用 js 引擎执行 javascript 代码;
第二部分:其它部分的任务,js 引擎把异步
概念(该任务我委托别人执行,我接着执行下面的任务,别人执行完该任务后通知我)弱化成稍后执行
(该任务我委托本身执行但不是如今,我接着执行下面的任务,该任务我稍后
会本身执行,执行完成后通知我本身)的概念。
这就是 js 引擎中异步
的所有意思。基本上等同咱们常说的:我立刻作这件事
。不过仍是要近一步解释一下第二部分:
nodejs 中 js 引擎把异步
变成了稍后执行
,使写 javascript 程序看起来像异步执行,可是并无减小任务,所以在 javascript 中你不能写一个须要很长时间计算的函数(计算Pi值1000位,大型的矩阵计算),或者在一个tick(后面会说)中执行过多的任务,若是你这样写了,整个主线程就没有办法响应别的请求,反映出来的状况就是程序卡了
,固然若是非要写固然也有办法,须要一些技巧来实现。
而 js 引擎稍后执行
中稍后
究竟是多久,到底执行
哪些任务?这些问题就与 nodejs 中四个重要的与时间有关的函数有关了,他们分别是:setTimeout,setInterval,process.nextTick,setImmediate
。下面简单了解一下这四个函数:
setImeout 主要是延迟执行函数,其中有一个比较特别的调用:setTimeout(function(){/* code */},0)
,常常见使用,为何这样使用看看后面。还有 setInterval 周期性调用一个函数。
setImmediate 的意思翻译过来是马上调用的意思,可是官方文档的解释是:
Schedules "immediate" execution of callback after I/O events' callbacks and before timers set by setTimeout and setInterval are triggered.
翻译过来大意就是:被 setImmediate 的设置过的函数,他的执行是在 I/O 事件的回调执行以后,在 计时器触发的回调执行以前,也就是说在 setTimeout 和 setInterval 以前,好吧这里还有一个顺序之分。
process.nextTick 可就更怪了。官方的意思是:
It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.
翻译过来大意就是:他运行在任何的 I/O 和定时器的 subsequent ticks
以前。
又多了不少的概念,不过别慌,在下面会讲 nodejs 的EventLoop,这里讲的不少的不理解地方就会在 EventLoop 中讲明白。
EvevtLoop大致上来讲就是一个循环,它不停的检查注册到他的事件有没有发生,若是发生了,就执行某些功能,一次循环一般叫tick。这里有讲EventLoop,还有这里。
在 nodejs 中也存在这样一个 EventLoop,不过它是在 libuv 中。它每一次循环叫 tick。而在每一次 tick 中会有不一样的阶段,每个阶段能够叫 subTick,也就说是这个tick的子tick,libuv就有不少的子 tick,如I/O 和定时器等。下面我用一张图来表示一下,注意该循环一直在 nodejs 的主线程中运行:
+-------------+ | | | | | +-----v----------------------+ | | | | | uv__update_time(loop) | subTick | | | | +-----+----------------------+ | | | | | +-----v----------------------+ | | | | | uv__run_timers(loop) | subTick | | | tick| +-----+----------------------+ | | | | | +-----v----------------------+ | | | | | uv__io_poll(loop, timeout) | subTick | | | | +-----+----------------------+ | | | | | +-----v----------------------+ | | | | | uv__run_check(loop) | subTick | | | | +-----+----------------------+ | | | | | | +-------------+
以上的流程图已经进行了裁减,只保留重要的内容,若是你想详细了解,可在 libuv/src/unix/core.cc,第334行:uv_run函数进行详细了解。
下面来解释一下各个阶段的做用:
uv__update_time
是用来更新定时器
的时间。uv__run_timers
是用来触发定时器,并执行相关函数的地方。uv__io_poll
是用来 I/O触发后执行相关函数的地方。
uv__run_check
的用处代码中讲到。
了解到 nodejs 中 EventLoop 的执行阶段后,须要更深一步了解在 nodejs 中 js引擎和EvevtLoop是如何被整合在一块儿工做的。如下是一些伪代码,它用来讲明一些机制。
不过你须要知道在 nodejs 中 setTimeout、setInterval、setImmediate和process.nextTick都是系统级的调用,也就是他们都是c++ 来实现的。setTimeout和setInterval 可看看这个文件:timer_wrap.cc。另外两个我再补吧。
class V8Engine { let _jsVM; V8Engine(){ _jsVM = /*js 执行引擎 */; } void invoke(handlers){ // 依次执行,直到 handlers 为空 handlers.forEach(handler,fun => _jsVM.run(handler)); } } class EvenLoop { let _jsRuntime = null; let _callbackHandlers = []; 【1】 let _processTickHandlers = []; 【2】 let _immediateHandlers = []; 【3】 // 构造函数 EvenLoop(jsRuntime){ _jsRuntime = jsRuntime; } void start(){ where(true){ _jsRuntime.invoke(_processTickHandlers); 【4】 _processTickHandlers.clear(); update_time(); run_timer(); run_pool(); run_check(); if (process.exit){ _jsRuntime.invoke(_processTickHandlers); 【5】 _processTickHandlers.clear(); break; } } } void update_time(){ // 更新 timer 的时间 } void run_timer(){ 【6】 let handlers = getTimerHandler(); _callbackHandlers.push(handlers); _jsRuntime.invoke(_callbackHandlers); _jsRuntime.invoke(_processTickHandlers); _callbackHandlers.clear(); _processTickHandlers.clear(); } void run_pool(){ 【6】 let handlers = getIOHandler(); _callbackHandlers.push(handlers); _jsRuntime.invoke(_callbackHandlers); _jsRuntime.invoke(_processTickHandlers); _callbackHandlers.clear(); _processTickHandlers.clear(); } void run_check(){ 【7】 let handlers = getImmediateHandler(); _immediateHandlers.push(handlers); _jsRuntime.invoke(_immediateHandlers); _immediateHandlers.clear(); } } main(){ JsRuntime jsRuntime = new V8Engine(); EventLoop eventLoop = new EventLoop(jsRuntime); eventLoop.start(); } // 主线程中执行 main();
以上代码是 nodejs 的粗略的执行过程,还想进一步了解,能够看这从入口函数看起:node_main.cc
按标号进行说明:
nextTick
的回调对象先进先出队列。setImmediate
的回调对象先进先出队列。nextTick
的队列。nextTick
的队列。nextTick
队列会在run_timer
和 run_pool
以后执行。回到第三节说的nextTick
的执行时机,看出来该队列确实会在 I/O 和 Timer 以前运行。在文档中特别说明若是你递归调用 nextTick
会阻 I/O 事件的调用就像调用了 loop
。依照上面的伪代码,发现若是你递归调用nextTick
,那nextTick
回调对象先进先出队列就不会为空,js 引擎就一直在执行,影响以后的代码执行。setImmediate
回调对象先进先出队列,每一次 tick 就执行一次。能够从代码中看出这四个时间函数执行时机的区别,而setTimeout(fn,0)
是在 _callbackHandlers
的队列中,而setImmediate
,还有 nextTick
都在不一样的队列中执行。
整体来讲,nextTick
执行最快,而setTmmediate
能保证每次tick都执行,而setTimeout
是 libuv 的 Timber 保证,可能会有所延迟。
process.nextTick
名存实亡,得改个名字,变成 process.currentTick
,没有经过,理由是太多的代码依赖这个函数了,没有办法更名字,这里。我相信你一但用了promise,你就回不去以往的回调时代,promise 很是好使用,强列推荐使用。若是你还想了解promise怎么实现的,我给你透个底,必不可少setTimeout
这个函数,能够参考 Q promise的设计文档,还有一步步来手写一个Promise也不错。
若是要写一个处理数据量很大的任务,我想这个函数能够给你思路:
function chunk(array,process,context){ setTimeout(function(){ var item = array.shift(); process.call(context,item); if (array.length >0){ setTimeout(arguments.callee,100); } },100) }
若是要写一个计算量很大的任务,这个函数也能够给你思路:
var process = { timeout = null, // 实际进行处理的方法 performProcessing:function(){ // 实际执行的代码 }, // 初始处理调用的方法 process:function(){ clearTimeout(this.timeoutId); var that = this; this.timeoutId = setTimeout(function(){ that.performProcessing(); },100) } }
这两个函数是从JavaScript高级程序设计第612-615页摘出来的,本质是不要阻塞了Javascript的事件循环,把任务分片了。
作服务器请求多了,使用 cluster 模块。cluster 的方案就是nodejs的多进程方案
。cluster 能保证每一个请求被一个 nodejs 实例处理。这样就能减小每一个 nodejs 的处理的数据量。
从如今来看 nodejs 架构中对 js 引擎不支持线程调用是一个较大的遗憾,意味着在 nodejs 中你甚至不能作一个很大的计算量的事。不过又说回来,这也是一件好事。由于这样作的,使 javascript 变简单,写 js 不须要考虑锁的事情,想一想在 java 中集合类加锁,你还要考虑同步,你还要考虑死锁,我以为写 js 的人都很幸福。
一样的问题也出如今 python、ruby 和 php 上。这些语言在当前的主流版本(用c实现的版本)中都默认一把大锁 GIL,全部的代码都是主线程中运行,代码都是线程安全的,基本上第三方库也利用这个现实。致使的事实是它们都没有办法很好的利用如今的多核计算机,多么悲剧的事情啊!
不过好在,计算这事情,它们干不了,还有人来干,就是老大哥 c、c++还有 java 了。你没有看到分布式计算领域和大数据中核心计算被老大哥占领,其余是想占也占不了,不是不想占,是有心无力。
就目前的分析,我以为这篇文章说的很对。
当前 nodejs 的发展仍是在填别的语言中经历过的坑,由于 nodejs 发展毕竟才七年的时间(2009年创建),流行也才是近几年的事情。不过 nodejs 的进步很快(后发优点),作一个轻量级的网页应用已是继 python、ruby、php以后的另外一个选择了,可喜可贺。
可是若是还要更近一步发展,那就必须解决计算这个问题。当前 javascript 对于这个问题的解决基本仍是按着沿用 python、ruby 和 php 走过的路线走下去,采用单线程协程
的方案,也就是 yield、async/wait 方案。在这以后,也基本上会采用多线程方案 worker 。从这样的发展来看,将来的 nodejs 与 python、ruby、php 是并驾齐驱的解决方案,不见得比 python、ruby 和 php 更好,它们都差很少,惟一不一样的是咱们又多了一种选择而已。
想到程序员在论坛上问:新手学习网站开发,javacript、python、ruby和 php 哪一个好?我想说若是有师博他说什么好就学什么,若是没有师博那就学 javascript 吧,由于你不用再去学一门后端的语言了。