如今有點 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 吧,由於你不用再去學一門後端的語言了。