JavaScript的特色就是單線程,也就是說同一時間只能作一件事情,前面的任務沒作完,後面的任務只能處於等待狀態,(這就跟生活中的例子:排隊買票同樣,一個一個排隊按順序來)。這就產生了一個問題:爲何JavaScript不能是多線程的呢?多線程能夠提升多核CPU的利用率,從而提升計算能力啊,這與瀏覽器的用途是息息相關的,也能夠說是瀏覽器的用途直接決定了JavaScript只能是單線程。前端
假如說JavaScript是多線程,咱們能夠試想一下,若是某一時刻一個線程給某個DOM節點添加內容,另外一個線程在刪除這個DOM節點,這個時候瀏覽器聽那一個線程的?這樣會讓程序變得很是複雜,並且徹底沒有必要。同時這也說明JavaScript的單線程與它的用途有關,瀏覽器的主要用途是操做DOM、與用戶完成互動、添加一些交互行爲,若是是多線程將會產生很是複雜的問題。因此JS從一開始起爲了不復雜的問題產生,JavaScript就是單線程的,單線程已經成爲JavaScript的核心,並且從此也不會改變。node
在多核CPU出現以來,這給單線程帶來了很是的不便,不可以充分發揮CPU的做用。爲了提升現代多核CPU的計算能力,HTML5提出了Web Worker標準,容許JavaScript腳本建立多個線程,可是,這個本質並無改變JavaScript單線程的本質。ios
什麼是單線程與多線程,這個問題值得咱們思考?ajax
單線程:一個進程中只有一個線程在執行,若是把進程比做一間工廠,線程比做一個工人,所謂單線程就是這個工廠中只有一個工人在工做數據庫
多線程:一個進程中同時有多個線程在執行,比如這個工廠中有多個工人在一塊兒協同工做npm
進程:CPU資源分配的最小單位,一個程序要想執行就須要CPU給這個程序分配相應的資源出來,用完以後再收回去。例如:內存的佔用,CPU給這個程序提供相應的計算能力。axios
線程:CPU調度的最小單位,一個程序能夠理解爲有N個任務組成的集合,某一時刻執行那個任務就須要線程的調度promise
圖解:瀏覽器
注意點:有圖可知:工廠空間是共享的,說明一個進程中能夠存在一個或多個線程,工廠資源是共享的,說明一個進程的內存空間能夠被該進程中全部線程共享,多個進程之間相互獨立,例如:聽音樂的時候,不會影響到敲代碼,歌詞是不會出如今代碼編輯器中的。服務器
多進程:在同一時間裏,同一臺計算機系統中容許兩個或兩個以上的進程處於運行狀態,其實如今的計算機開機狀態下就是多個進程在運行,打開任務管理器就能夠看到那些進程在運行。多進程帶來的好處是很是明顯的,能夠充分利用CPU的資源,並且電腦同時能夠幹不少件事還互相不影響。例如:在使用編輯器寫代碼的時候,還可使用QQ音樂聽歌。編輯器和QQ音樂之間徹底不會影響。以Chrome爲例:每打開一個tab就就至關於開啓了一個進程,每一個tab之間是徹底不會影響。
上面提到HTML5的Web Worker能夠改善單線程的不便,瞭解Web Worker須要注意如下幾點:
同源限制
分配給Worker子線程運行的腳本文件,必須與主線程的腳本文件同源。
DOM限制
Worker線程與主線程不同,沒法讀取主線程所在網頁的DOM對象,沒法使用 document 、 window 、 parent 等對象,可是可使用 Navigator 對象、 location 對象
全局對象限制
Worker 的全局對象 WorkerGlobalScope ,不一樣於網頁的全局對象 Window ,不少接口拿不到,理論上Worker不能使用 console.log
通訊聯繫
Worker 線程和主線程不在同一個上下文環境,它們不能直接通訊,必須經過消息完成。
腳本限制
Worker 線程不能執行alert()
方法和confirm()
方法,但可使用 XMLHttpRequest 對象發出 AJAX 請求。
文件限制
Worker 線程沒法讀取本地文件,即不能打開本機的文件系統(file://
),它所加載的腳本,必須來自網絡。
2、瀏覽器的渲染流程
在理解渲染原理以前,先了解瀏覽器內核構成是很是有必要的,具體內容看下面:
瀏覽器工做方式:瀏覽器內核是經過取得頁面內容、整理信息(應用CSS)、計算和組合最終輸出可視化的圖像結果。
瀏覽器內核是多線程的,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:
GUI渲染線程:
JS引擎線程
定時觸發器線程
事件觸發線程
異步http請求線程
3、任務隊列
以前初步瞭解了瀏覽器的內核構成,提到了不少異步事件,那異步事件如何執行呢?這就跟任務隊列有關了。如今來談談什麼是任務隊列,爲何須要任務隊列?
單線程也就意味着全部任務都須要排隊,只有在前一個任務結束以後,纔會執行後一個任務,不然後面的任務只能處於等待狀態。若是某個任務執行須要很長的時間,後面的任務就都須要等待着,這會形成很是大的交互影響,給用戶有一種頁面加載卡頓的現象,影響用戶體驗!
若是等待是由於CPU忙不過來也能夠理解,大多數狀況並非CPU忙不過來的緣由,而是文件I/O的讀取,網絡請求、鼠標點擊事件等這些操做須要花費很是長的時間,只有等這些操做返回結果以後才能往下執行。
爲了解決這個問題,JavaScript的開發者也很快的意識到,腳本文件的執行,徹底能夠不用管那些很是耗時的I/O設備,異步請求,徹底能夠掛起等待中的任務,執行排在後面的位置,等I/O操做返回結果以後,再回過頭執行掛起的任務。
根據上面的理解能夠將任務分爲兩種:同步任務(synchronize task)、異步任務(asynchronize task)。
瀏覽器的運行機制
4、事件和回調函數
任務隊列是一個事件隊列(也能夠理解爲:消息隊列),異步操做如:I/O設備完成一項任務,就在任務隊列中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面的事件。
任務隊列中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列,等待主線程讀取。
所謂"回調函數"(callback),就是那些會被主線程掛起來的實現該任務的代碼,主線程幹開始運行的時候是不會執行的。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數代碼。
任務隊列是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器功能「」,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
5、瀏覽器中的事件循環(Event Loop)
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)
爲了更好的理解事件循環,請看下面的圖:
執行棧中的代碼(同步任務),老是在讀取"任務隊列"(異步任務)以前執行。
Micro Task 和 Macro Task:
瀏覽器端事件循環的異步隊列有兩種:macro(宏任務)隊列、micro(微任務)隊列,macro隊列能夠有多個,micro隊列只能有一個。
常見的Macro-Task: setTimeout、setInterval、script、I/O操做、UI渲染
常見的Micro-Task: new Promise.then() MutationObserve
如今再拿以前的圖來解釋任務執行的流程,進一步加深理解:
圖解:
執行流程:
總結一下:當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。
代碼演示:
1 <script> 2 Promise.resolve().then(() => { // Promise.then()是屬於micro-task 3 console.log('micro-task1'); 4 setTimeout(() => { // setTimeout是屬於macro-task 5 console.log('macro-task1'); 6 }, 0) 7 }) 8 setTimeout(() => { 9 console.log('macro-task2'); 10 Promise.resolve().then(() => { 11 console.log('micro-task2'); 12 }) 13 console.log('macro-task3'); 14 }) 15 console.log('同步任務'); 16 </script>
運行結果爲:同步任務--->micro-task1 --->macro-task2--->macro-task3--->micro-task2--->macro-task1
1.代碼開始執行,判斷是同步任務仍是異步任務,檢測到有同步任務(屬於宏任務)存在,先輸出「同步任務」
2. 同步任務執行完去查看是否有微任務隊列存在,上面代碼的微任務隊列爲:promise.resolve().then(),開始執行微任務,輸出micro-task1
3.執行微任務的過程當中發現有宏任務setTimeout()存在,將其添加到宏任務隊列,微任務執行完畢開始執行宏任務,因爲macro-task2所在的宏任務早於macro-task1,所以先執行macro-task2所在的宏任務,輸出macro-task2
4.輸出macro-task2以後發現存在微任務micro-task2,將其添加到微任務隊列,接着輸出macro-task3
5.宏任務執行完畢,接着開始執行微任務輸出:micro-task2
6.微任務執行完畢,接着執行macro-task1所在的宏任務,輸出:macro-task1
7.執行完畢宏任務,此時的macro-task隊列和micro-task隊列已空,程序中止。
Node.js 不是一門語言也不是框架,它只是基於 Google V8 引擎的 JavaScript 運行時環境,同時結合 Libuv 擴展了 JavaScript 功能,使之支持 io、fs 等只有語言纔有的特性,使得 JavaScript 可以同時具備 DOM 操做(瀏覽器)和 I/O、文件讀寫、操做數據庫(服務器端)等能力,是目前最簡單的全棧式語言。
目前Node.js在大部分領域都佔有一席之地,尤爲是 I/O 密集型的,好比 Web 開發,微服務,前端構建等。很多大型網站都是使用 Node.js 做爲後臺開發語言的,用的最多的就是使用Node.js作前端渲染和架構優化,好比 淘寶 雙11、去哪兒網的 PC 端核心業務等。另外,有很多知名的前端庫也是使用 Node.js 開發的,好比,Webpack 是一個強大的打包器,React/Vue 是成熟的前端組件化框架。
Node.js一般被用來開發低延遲的網絡應用,也就是那些須要在服務器端環境和前端實時收集和交換數據的應用(API、即時聊天、微服務)。阿里巴巴、騰訊、Qunar、百度、PayPal、道瓊斯、沃爾瑪和 LinkedIn 都採用了 Node.js 框架搭建應用。
Node.js 編寫的包管理器 npm 已成爲開源包管理了領域最好的生態,直接到2017年10月份,有模塊超過47萬,每週下載量超過32億次,每月有超過700萬開發者使用npm。
固然了,Node.js 也有一些缺點。Node.js 常常被人們吐槽的一點就是:回調太多難於控制(俗稱回調地獄)。可是,目前異步流程技術已經取得了很是不錯的進步,從Callback、Promise 到 Async函數,能夠輕鬆的知足全部開發需求。
至於其餘的特性這裏附一篇很值得一看的文檔:https://cnodejs.org/topic/5ab3166be7b166bb7b9eccf7
Node中的事件循環機制徹底和瀏覽器的是不一樣的,Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現。
Node.js的運行機制以下:
1.V8引擎解析JavaScript腳本
2.解析後的代碼,調用Node API
3.libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎
4.V8引擎再將結果返回給用戶
EventLoop的六個階段
libuv引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。
從上面的圖片能夠大體看出node的事件循環的順序爲:外部輸入階段--->輪詢階段(poll)--->檢查階段--->關閉事件回調階段(close callback)--->定時器檢查階段(timer)--->I/O回調階段(I/O callback)--->閒置階段(idle,prepare)--->輪詢階段 ,按照上面的順序循環反覆執行。
timer階段:這個階段執行setTimeout或setInterval回調,而且是有poll階段控制的。一樣在Node.js中定時器指定的時間也不是很是準確,只能是儘快執行。
I/O callbacks階段:處理一些上一輪循環中少數未執行的I/O回調
idle,prepare階段:進node內部使用
poll階段:獲取新的I/O事件,適當的條件下node將阻塞在這裏
check階段:setImmediate()回調函數的執行
close callbacks階段:執行socket的close事件回調。
注意點:上面的6個階段都是不包括process.nextTick(),在平常的開發中咱們使用最多的就是:timer poll check這三個階段,絕大多數的異步操做都是在這三個階段 完成的。
timer階段:這個階段執行setTimeout或setInterval回調,而且是有poll階段控制的。一樣在Node.js中定時器指定的時間也不是很是準確,只能是儘快執行。
poll階段:
1.這個階段是相當重要的階段,系統會作兩件事情。一是回到timer階段執行回調,二是執行I/O回調
2.若是在進入該階段的時候沒有設置timer,會發生兩件事情:
2.1.若是poll隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或達到系統限制
2.2.若是poll階段爲空,會發生下面兩件事:
2.2.1:若是有setImmediate()回調須要執行,poll階段會中止,進入到check階段執行回調
2.2.2:若是沒有setImmediate()回調須要執行,會等到回調被加入隊列中並當即執行回調,這裏一樣有個超時限制防止一致等待下去
3.若是設置了timer且poll隊列爲空,則會判斷是否有timer超時,若是有的話回到timer階段執行回調。
check階段:setImmediate()的回調會被加入到check隊列中,從event loop的階段圖能夠看出,check階段的執行是在poll階段以後的。
microTask和macroTask:
1.常見的micro-task: process.nextTick() Promise.then()
2.常見的macro-task: setTimeout、setIntevaral、setImmeidate、script、I/O操做
3.先分析一段代碼示例:
console.log('start'); setTimeout(() => { console.log('time1'); Promise.resolve().then(() => { console.log('promise1'); }) }, 0) setTimeout(() => { console.log('time2'); Promise.resolve().then(() => { console.log('promsie2'); }) }, 0) Promise.resolve().then(() => { console.log('promise3'); }) console.log('end'); // Node中的打印結果:start--->end--->promise3--->time1--->timer2--->promise1--->promise2 // 瀏覽器中的打印結果:start--->end--->promise3--->time1--->promise1--->time2--->promise2
4.node打印結果分析:
1.先執行同步任務(執行macro-task),打印輸出:start end
2.執行micro-task任務:輸出promise3,這一點跟瀏覽器的機制差很少
3.進入timer階段執行setTimeout(),打印輸入time1,並將promise1放入micro-task隊列中,執行timer2打印time2,而且將promise2放入到micro隊列中,這一點跟瀏覽器的差異比較大,timer階段有幾個setTimeout/setIntever就執行幾個,而不像瀏覽器同樣執行完一個macro-task以後當即執行一個micro-task
5.setTimeout()和setImmediate()很是類似,區別主要在調用的實際不一樣
5.1:setTimeout()設置在poll階段爲空閒時且定時時間到後執行,但它們在timer階段執行
5.2:setImmediate()設置在poll階段完成時執行,即check階段執行
5.3:實例分析:
1 setImmediate(() => { 2 console.log('setImmediate'); 3 }) 4 setTimeout(() => { 5 console.log('setTimeout'); 6 }, 0)
5.4:上面的代碼執行,返回結果是不肯定的,有可能先執行setTimeout() ,有可能先執行setImmediate(),首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的 進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調,若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了,能夠把setTimeout的第二個參數設置爲:1 ,2,3,4....看看運行結果
5.5:當兩者寫在I/O讀取操做的回調中時,老是先執行setImmediate(),由於I/O回調是寫在poll階段,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了
1 const fs = require('fs') 2 fs.readFile(__filename, (err, data) => { 3 setTimeout(() => { 4 console.log('setTimeout'); 5 }, 0) 6 setImmediate(() => { 7 console.log('setImmediate'); 8 }) 9 }) 10 console.log(__filename); // 打印當前文件所在的路徑
6.process.nextTick():這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。也就是說它指定的任務隊列,都是在全部異步任務以前發生
1 console.log('start') 2 setTimeout(() => { 3 console.log('time1'); 4 Promise.resolve().then(() => { 5 console.log('promise1'); 6 }) 7 }) 8 Promise.resolve().then(() => { 9 console.log('promise2'); 10 }) 11 process.nextTick(() => { 12 console.log('nextTick1'); 13 process.nextTick(() => { 14 console.log('nextTick2'); 15 process.nextTick(() => { 16 console.log('nextTick3'); 17 }) 18 }) 19 }) 20 console.log('end'); 21 // 運行結果:start --->end --->nextTick1 --->nextTick2 --->nextTick3 --->promise2 --->time1 --->promise1
重點:瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
圖解:
代碼演示:
console.log('start'); setTimeout(() => { console.log('timer1'); Promise.resolve().then(() => { console.log('promise1'); }); }) setTimeout(() => { console.log('timer2'); Promise.resolve().then(() => { console.log('promise2'); }) }, 0) console.log('end'); // 瀏覽器模式下輸出:start ---> end ---> timer1 ---> promise1 ---> timer2 ---> promise2 // node環境下面:start ---> end ---> timer1 ---> timer2 ---> promsie1 ---> promise2
瀏覽器和node環境下、micro-task隊列的執行時機不一樣:
瀏覽器端,微任務在事件循環的各個階段執行。
Node端,微任務是在macro-task執行完畢執行。