We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
现在有点 javascript 基础的人都在听说过 nodejs ,而只要与 javascript 打交到人都会用或者是将要使用 nodejs 。毕竟 nodejs 的生态很强大,与 javascript 相关的工具也做的很方便,很好用。
javascript 语言很小巧,但是一旦与 nodejs 中的运行环境放在一起,有些概念就很难理解,特别是异步的概念。有人会说不会啊,很好理解啊?不就是一个ajax请求加上一个回调函数,这个ajax函数就是能异步执行的函数,他在执行完了就会调用回调函数,我承认这个样做是很容易,早些时候我甚至认为在 javascript 中加了回调函数的函数都可以异步的,异步和回调函数成对出现。多么荒谬的理解啊!
异步
ajax
回调函数
直到有一天,我在写程序时想到一个问题:在 nodejs 中在不调用系统相关 I/O ,不调用 c++ 写的 plugin 的情况下,写一个异步函数?我查了资料,有人给我的答案是调用 setTimeout(fn,delay) 就变成了异步了。但是我还是不明白为什么要调用这样一个函数,这个函数的语义跟async完全不一样,为什么这样就行?
async
带着这个疑问,我查了很多资料,包括官方文档,代码,别人的blog。慢慢的理解,最后好像是知道了为什么会是这样,整篇文章就是对所了解东西的理解。恳请大家批评指正。
说明:nodejs 的文档是用的 v5.10.1 API,而代码方面:nodejs 和 libuv 是用的 master 分支。
在探索 nodejs 的异步时,首先需要对 nodejs 架构达成统一认识:
如果以上 5 点你不认同的话,那下面就不需要看了,看了会觉得漏洞百出。
上面的 5 点主要说明另一层意思了:
libevent2
新建调用系统线程
那nodejs中常谈的异步和回调是怎么回事?
回调
在 javascript 中使用回调函数可所谓登峰造极,基本上所有的异步函数都会要求有一个回调函数,以至于写 javascript 写多了,看到回调函数的接口,都以为是异步的调用。
但是真相是回调函数,只是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。下面简单了解一下这四个函数:
执行
setTimeout,setInterval,process.nextTick,setImmediate
setImeout 主要是延迟执行函数,其中有一个比较特别的调用:setTimeout(function(){/* code */},0),经常见使用,为什么这样使用看看后面。还有 setInterval 周期性调用一个函数。
setTimeout(function(){/* code */},0)
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 之前。
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的用处代码中讲到。
uv__update_time
定时器
uv__run_timers
uv__io_poll
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
run_timer
run_pool
loop
可以从代码中看出这四个时间函数执行时机的区别,而setTimeout(fn,0)是在 _callbackHandlers的队列中,而setImmediate,还有 nextTick 都在不同的队列中执行。
setTimeout(fn,0)
_callbackHandlers
总体来说,nextTick执行最快,而setTmmediate能保证每次tick都执行,而setTimeout是 libuv 的 Timber 保证,可能会有所延迟。
setTmmediate
setTimeout
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 实例处理。这样就能减少每个 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 吧,因为你不用再去学一门后端的语言了。
The text was updated successfully, but these errors were encountered:
这么优秀的文章居然没有人点赞,看不下去了!
Sorry, something went wrong.
No branches or pull requests
1 nodejs 中的异步存在吗?
现在有点 javascript 基础的人都在听说过 nodejs ,而只要与 javascript 打交到人都会用或者是将要使用 nodejs 。毕竟 nodejs 的生态很强大,与 javascript 相关的工具也做的很方便,很好用。
javascript 语言很小巧,但是一旦与 nodejs 中的运行环境放在一起,有些概念就很难理解,特别是
异步
的概念。有人会说不会啊,很好理解啊?不就是一个ajax
请求加上一个回调函数
,这个ajax
函数就是能异步执行的函数,他在执行完了就会调用回调函数
,我承认这个样做是很容易,早些时候我甚至认为在 javascript 中加了回调函数的函数都可以异步的,异步和回调函数成对出现。多么荒谬的理解啊!直到有一天,我在写程序时想到一个问题:在 nodejs 中在不调用系统相关 I/O ,不调用 c++ 写的 plugin 的情况下,写一个异步函数?我查了资料,有人给我的答案是调用 setTimeout(fn,delay) 就变成了异步了。但是我还是不明白为什么要调用这样一个函数,这个函数的语义跟
async
完全不一样,为什么这样就行?带着这个疑问,我查了很多资料,包括官方文档,代码,别人的blog。慢慢的理解,最后好像是知道了为什么会是这样,整篇文章就是对所了解东西的理解。恳请大家批评指正。
说明:nodejs 的文档是用的 v5.10.1 API,而代码方面:nodejs 和 libuv 是用的 master 分支。
2 nodejs 的架构基础
在探索 nodejs 的异步时,首先需要对 nodejs 架构达成统一认识:
如果以上 5 点你不认同的话,那下面就不需要看了,看了会觉得漏洞百出。
上面的 5 点主要说明另一层意思了:
中 PR,可以看这个,微软想把 javascript 运行环境换成自己家的。
libevent2
,证据在这里:链接。新建调用系统线程
的任何方法,所以在nodejs中执行 javascript,是没有办法新开线程的。结论
那nodejs中常谈的
异步
和回调
是怎么回事?3 nodejs 中的回调和异步的关系是什么?
在 javascript 中使用
回调函数
可所谓登峰造极,基本上所有的异步函数都会要求有一个回调函数,以至于写 javascript 写多了,看到回调函数的接口,都以为是异步的调用。但是真相是
回调函数
,只是javascript 用来解决异步函数调用如何处理返回值这个问题的方法,或这样来说:异步函数调用如何处理返回值这个问题上,在系统的设计方面而言,有很多办法,而 nodejs 选择了 javascript 的传统方案,用回调函数来解决这个问题
。这个选择好不好,我认为在当时来说,很合适。但随着 javascript 被用来写越来越大的程序,这个选择不是一个好的选择,因为回调函数嵌套多了真的很难受,我觉得主要是很难看,(就跟 lisp 的
))))))))))))
),让一般人不好接受,现在情况改善多了,因为有了Promise。结论
4 nodejs 中怎样解决异步的问题?
前面也说了,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
。下面简单了解一下这四个函数:setTimeout 和 setInterval
setImeout 主要是延迟执行函数,其中有一个比较特别的调用:
setTimeout(function(){/* code */},0)
,经常见使用,为什么这样使用看看后面。还有 setInterval 周期性调用一个函数。setImmediate 和 process.nextTick
setImmediate 的意思翻译过来是立刻调用的意思,但是官方文档的解释是:
翻译过来大意就是:被 setImmediate 的设置过的函数,他的执行是在 I/O 事件的回调执行之后,在 计时器触发的回调执行之前,也就是说在 setTimeout 和 setInterval 之前,好吧这里还有一个顺序之分。
process.nextTick 可就更怪了。官方的意思是:
翻译过来大意就是:他运行在任何的 I/O 和定时器的
subsequent ticks
之前。又多了很多的概念,不过别慌,在下面会讲 nodejs 的EventLoop,这里讲的很多的不理解地方就会在 EventLoop 中讲明白。
5 nodejs 中神秘的 EventLoop
EvevtLoop大体上来说就是一个循环,它不停的检查注册到他的事件有没有发生,如果发生了,就执行某些功能,一次循环通常叫tick。这里有讲EventLoop,还有这里。
在 nodejs 中也存在这样一个 EventLoop,不过它是在 libuv 中。它每一次循环叫 tick。而在每一次 tick 中会有不同的阶段,每一个阶段可以叫 subTick,也就说是这个tick的子tick,libuv就有很多的子 tick,如I/O 和定时器等。下面我用一张图来表示一下,注意该循环一直在 nodejs 的主线程中运行:
以上的流程图已经进行了裁减,只保留重要的内容,如果你想详细了解,可在 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。另外两个我再补吧。
以上代码是 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
,没有通过,理由是太多的代码依赖这个函数了,没有办法改名字,这里。6 nodejs 回调和大数据与大计算量的解决方案
回调解决方案- promise
我相信你一但用了promise,你就回不去以往的回调时代,promise 非常好使用,强列推荐使用。如果你还想了解promise怎么实现的,我给你透个底,必不可少
setTimeout
这个函数,可以参考 Q promise的设计文档,还有一步步来手写一个Promise也不错。大数据与大计算量的解决方案 - 分片数据或者分片计算
如果要写一个处理数据量很大的任务,我想这个函数可以给你思路:
yielding processes
函数节流
如果要写一个计算量很大的任务,这个函数也可以给你思路:
这两个函数是从JavaScript高级程序设计第612-615页摘出来的,本质是不要阻塞了Javascript的事件循环,把任务分片了。
做服务器请求多了,使用 cluster 模块
cluster 的方案就是
多进程方案
。cluster 能包证每个请求被一个 nodejs 实例处理。这样就能减少每个 nodejs 的处理的数据量。7 总结
从现在来看 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 吧,因为你不用再去学一门后端的语言了。
The text was updated successfully, but these errors were encountered: