摘要:如果過程中遇到引擎執(zhí)行會被掛起線程,更新保存在一個隊列中等待引擎空閑才執(zhí)行引擎線程負責解析運行執(zhí)行時間過程會導(dǎo)致頁面渲染加載阻塞事件觸發(fā)線程,瀏覽器用以控制事件循環(huán)。
序
總所周知,javascript是一門依賴宿主環(huán)境的單線程的弱腳本語言,這意味著什么?
javascript的運行環(huán)境一般都由宿主環(huán)境(如瀏覽器、Node、Ringo等)和執(zhí)行環(huán)境(Javascript引擎V8,JavaScript Core等)共同構(gòu)成;
弱類型定義語言:數(shù)據(jù)類型可以被忽略的語言。例如計算時會在不同類型之間進行隱式轉(zhuǎn)換;
在某一時刻內(nèi)只能執(zhí)行特定的一個任務(wù),并且會阻塞其它任務(wù)執(zhí)行;
本文主要講的就是第三點,從中引出下一個問題
單線程的設(shè)計原因?Javascript當初誕生的目的其實就是因為當年網(wǎng)絡(luò)技術(shù)十分低效,如表單驗證等個幾十秒才能得到反饋的用戶體驗十分糟糕,為了給瀏覽器做些簡單處理以前由服務(wù)器端負責的一些表單驗證。被Netscape公司指派花了十天就負責設(shè)計出一門新語言的Javascript之父就是Brendan Eich。盡管他并不喜歡自己設(shè)計的這作品,就有了大家都聽過的一句話:
"與其說我愛Javascript,不如說我恨它。它是C語言和Self語言一夜情的產(chǎn)物。十八世紀英國文學(xué)家約翰遜博士說得好:"它的優(yōu)秀之處并非原創(chuàng),它的原創(chuàng)之處并不優(yōu)秀。"(the part that is good is not original, and the part that is original is not good.)"
作為瀏覽器腳本語言而誕生的JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只需要是單線程就足以解決目的,否則會帶來很復(fù)雜的同步問題。但是沒想到的是之后的網(wǎng)絡(luò)越發(fā)的發(fā)達,這些年來的瀏覽器大戰(zhàn)為了爭奪地盤,反而讓Javascript被賦予了更多的職責跟可能性,今時今日的Javascript必須想方設(shè)法把自身的潛力激發(fā)出來,而單線程的弱點就被無限放大了,因為在阻塞任務(wù)的過程中不一定是因為CPU被占用了,而可能是因為I/O太慢(如AJAX請求,定時器任務(wù),Dom事件交互等并不消耗CPU的等待造成資源時間浪費)。
瀏覽器中Javascript執(zhí)行線程我們一直都在說Javascript是單線程,但瀏覽器是多線程的,在內(nèi)核控制下互相配合以保持同步,主要的常駐線程有:
GUI渲染線程:負責渲染界面,解析HTML,CSS,構(gòu)建DOM和Render樹布局繪制等。如果過程中遇到JS引擎執(zhí)行會被掛起線程,GUI更新保存在一個隊列中等待JS引擎空閑才執(zhí)行;
JS引擎線程:負責解析運行Javascript;執(zhí)行時間過程會導(dǎo)致頁面渲染加載阻塞;
事件觸發(fā)線程,瀏覽器用以控制事件循環(huán)。當JS引擎執(zhí)行過程中觸發(fā)的事件(如點擊,請求等)會將對應(yīng)任務(wù)添加到事件線程中,而當對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時會把對應(yīng)任務(wù)添加到處理隊列的尾部等到JS引擎空閑時處理;
定時器觸發(fā)線程:因為JS引擎是單線程容易阻塞,所以需要有多帶帶線程為setTimeout和setInterval計時并觸發(fā),同樣是符合觸發(fā)條件(記時完畢)被觸發(fā)時會把對應(yīng)任務(wù)添加到處理隊列的尾部等到JS引擎空閑時處理;W3C標準規(guī)定時間間隔低于4ms被算為4ms。
異步http請求線程:XMLHttpRequest在連接后瀏覽器新開線程去請求,檢測到狀態(tài)變化如果有設(shè)置回調(diào)函數(shù)會產(chǎn)生狀態(tài)變更事件,然后把對應(yīng)任務(wù)添加到處理隊列的尾部等到JS引擎空閑時處理;
好像鋪墊的有點多,往外偏了,接下來往回拉一點談?wù)勥@些怎么運行的。
什么是堆(heap)和棧(stack)?自己畫了一個丑丑的圖,大家將就看著吧。
function addOne(n) { var x = n + 1; return addTwo(x); } function addTwo(n) { return n + 2; } console.log(addOne(1)) //4;
以這個例子做說明。
當調(diào)用addOne時創(chuàng)建一個包含addOne入?yún)⒑途植孔兞康膸⑻砑舆M去stack,當調(diào)用到addTwo時也同樣創(chuàng)建一個包含addTwo入?yún)⒑途植孔兞康膸⑻砑舆M去在首部,執(zhí)行完addTwo函數(shù)并返回時addTwo幀被移出stack,addOne執(zhí)行完后addOne幀也被移除。
原理:當執(zhí)行方法時都會建立自己的內(nèi)存棧,在這個方法內(nèi)定義的入?yún)⒆兞慷紩4嬖跅?nèi)存里,執(zhí)行結(jié)束后該方法的內(nèi)存棧也將自然銷毀了。
一般來說,程序會劃分有兩種分配內(nèi)存的空間 -- 堆(heap)和棧(stack)。
內(nèi)存空間 | 分配方式 | 結(jié)構(gòu) | 大小 | 存取速度 | 釋放機制 |
---|---|---|---|---|---|
stack | 靜態(tài)分配 | 有 | 小 | 快 | 隨方法執(zhí)行結(jié)束而銷毀 |
heap | 動態(tài)分配 | 沒有 | 大 | 慢 | 系統(tǒng)的垃圾回收機制銷毀 |
因為棧只能存放下確定大小的簡單數(shù)據(jù),所以像變量(其實也就是一個記錄了指向復(fù)雜結(jié)構(gòu)數(shù)據(jù)的地址指向,所以變量也是保存在棧里的)和基本類型Undefined、Null、Boolean、Number 和 String等是按值傳遞的都會保存在棧里,隨著方法執(zhí)行完畢而被銷毀。
堆負責存放復(fù)雜結(jié)構(gòu)的對象,數(shù)組,函數(shù)等創(chuàng)建成本較高并且可重用數(shù)據(jù),即使方法執(zhí)行完也不會被銷毀,直到系統(tǒng)的垃圾回收機制核實了沒有任何引用才會回收。
其實這只是棧的含義之一,Stack的三種含義
有時候我們代碼有問題導(dǎo)致棧堆溢出原因大概是這種情況:
常見情況 | 可能情況 |
---|---|
棧溢出 | 無限遞歸死循環(huán),遞歸越深層分配內(nèi)存越多直至超過限制 |
堆溢出 | 循環(huán)生成復(fù)雜結(jié)構(gòu)數(shù)據(jù) |
好了,現(xiàn)在再看回上圖,除了heap和stack之外還有一個。。。
什么是Queue(任務(wù)隊列)?Javascript里分兩種隊列:
宏任務(wù)隊列(macro tasks):事件循環(huán)中可以有多個macro tasks,每次循環(huán)只會提取一個,包括script(全局任務(wù)), setTimeout, setInterval, setImmediate, I/O, UI rendering等.
微任務(wù)隊列(micro tasks):事件循環(huán)中只有一個并且有優(yōu)先級區(qū)別micro tasks,每次循環(huán)會提取多次直至隊列清空,包括process.nextTick, Promise, Object.observer, MutationObserver等.
console.log("log start!"); setTimeout(function () { console.log("setTimeout300"); }, 300) Promise.resolve().then(function () { console.log("promise resolve"); }).then(function () { console.log("promise resolve then"); }) new Promise(function (resolve, reject) { console.log("promise pending"); resolve(); }).then(function () { console.log("promise pending then"); }) setTimeout(function () { console.log("setTimeout0"); Promise.resolve().then(function () { console.log("promise3 in setTimeout"); }) }, 0) console.log("log end!"); // log start! // promise pending // log end! // promise resolve // promise pending then // promise resolve then // setTimeout0 // promise3 in setTimeout // setTimeout300
例子過程,具體分析下面再說。
第一次執(zhí)行事件打印:log start!, promise pending, log end!, promise resolve,promise pending then,promise resolve then;
第二次執(zhí)行事件打印:setTimeout0,promise3 in setTimeout;
第三次執(zhí)行事件打印:setTimeout300;
下面終于開始走到正題了
事件循環(huán)(Event Loop)!我在上面鋪墊了這么多東西,大家大概都能有個初步印象,然后所謂的Event Loop就是把這些東西串聯(lián)起來的一種機制吧,因為這東西各有理解,比如兩位前端大牛之間就有分歧。
阮一峰:JavaScript 運行機制詳解:再談Event Loop
樸靈:樸靈評注
我看過他們很多的博客和書籍,對我?guī)椭己艽螅揖陀米约旱目捶ㄖv講我眼中的Event Loop。
1,所有的任務(wù)都被放主線程上運行形成一個執(zhí)行棧(execution context stack),其中的方法入?yún)⒆兞勘4嬖跅?nèi)存中,復(fù)雜結(jié)構(gòu)對象被保存在堆內(nèi)存中;
2,同步任務(wù)直接執(zhí)行并阻塞后續(xù)任務(wù)等待結(jié)束,其中遇到一些異步任務(wù)會新開線程去執(zhí)行該任務(wù)(如上面提到的定時器觸發(fā)線程,異步http請求線程等)然后往下執(zhí)行,異步任務(wù)執(zhí)行完返回結(jié)果之后就把回調(diào)事件加入到任務(wù)隊列(Queue);
3,當執(zhí)行棧(execution context stack)所有任務(wù)執(zhí)行完之后,會到任務(wù)隊列(Queue)里提取所有的微任務(wù)隊列(micro tasks)事件執(zhí)行完;
4,一次循環(huán)結(jié)束,GUI渲染線程接管檢查,重新渲染界面;
5,執(zhí)行棧(execution context stack)到宏任務(wù)隊列(macro tasks)提取一個事件到執(zhí)行,接著主線程就一直重復(fù)第3步;
大概理解就這樣子,當然可能會有點偏差,歡迎指正!
我在上面線程說過
定時器觸發(fā)線程:因為JS引擎是單線程容易阻塞,所以需要有多帶帶線程為setTimeout和setInterval計時并觸發(fā),同樣是符合觸發(fā)條件(記時完畢)被觸發(fā)時會把對應(yīng)任務(wù)添加到處理隊列的尾部等到JS引擎空閑時處理;W3C標準規(guī)定時間間隔低于4ms被算為4ms。
里面有一些需要特別注意的地方:
1,計時完畢只是把對應(yīng)任務(wù)添加到處理隊列,依然要等執(zhí)行棧空閑才會去提取隊列執(zhí)行,這個概念很重要,切記!即使設(shè)置0秒也不會立馬執(zhí)行,因為W3C標準規(guī)定時間間隔低于4ms被算為4ms,具體看瀏覽器,我個人認為不管怎樣始終都會被放置到處理隊列等待處理;
2,setTimeout重復(fù)執(zhí)行過程中每次時間誤差會影響后續(xù)執(zhí)行時間,而setInterval是每次精確時間執(zhí)行,當然這是指他們把對應(yīng)任務(wù)添加到處理隊列的精確性;
但是setInterval也有一些問題:
累計效應(yīng),如果執(zhí)行棧阻塞時間足夠長以至于隊列中已經(jīng)存在多個setInterval的對應(yīng)任務(wù)的情況,執(zhí)行時間會遠低于開發(fā)者期望的結(jié)果;
部分瀏覽器(如Safari等)滾動過程不執(zhí)行JS,容易造成卡頓和未知錯誤;
瀏覽器最小化顯示時setInterval會繼續(xù)執(zhí)行,但是對應(yīng)任務(wù)會等到瀏覽器還原再一瞬間全部執(zhí)行;
結(jié)語坦白講,我原本時打算寫一篇關(guān)于異步編程的文章,然后在鋪墊前文的路上拉不回來了就變成了一篇梳理Javascript執(zhí)行機制了,不過沒關(guān)系,理解這些也是很重要的
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/106220.html
摘要:的單線程,與它的用途有關(guān)。只要指定過回調(diào)函數(shù),這些事件發(fā)生時就會進入任務(wù)隊列,等待主線程讀取。四主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為事件循環(huán)。令人困惑的是,文檔中稱,指定的回調(diào)函數(shù),總是排在前面。 原文:http://www.cnblogs.com/Master... 一、為什么JavaScript是單線程? JavaScript語言的一大特點...
摘要:機制詳解與中實踐應(yīng)用歸納于筆者的現(xiàn)代開發(fā)語法基礎(chǔ)與實踐技巧系列文章。事件循環(huán)機制詳解與實踐應(yīng)用是典型的單線程單并發(fā)語言,即表示在同一時間片內(nèi)其只能執(zhí)行單個任務(wù)或者部分代碼片。 JavaScript Event Loop 機制詳解與 Vue.js 中實踐應(yīng)用歸納于筆者的現(xiàn)代 JavaScript 開發(fā):語法基礎(chǔ)與實踐技巧系列文章。本文依次介紹了函數(shù)調(diào)用棧、MacroTask 與 Micr...
摘要:使用了一個事件驅(qū)動非阻塞式的模型,使其輕量又高效。的包管理器,是全球最大的開源庫生態(tài)系統(tǒng)。按照這個定義,之前所述的阻塞,非阻塞,多路復(fù)用信號驅(qū)動都屬于同步。 系列文章 Nodejs高性能原理(上) --- 異步非阻塞事件驅(qū)動模型Nodejs高性能原理(下) --- 事件循環(huán)詳解 前言 終于開始我nodejs的博客生涯了,先從基本的原理講起.以前寫過一篇瀏覽器執(zhí)行機制的文章,和nodej...
摘要:回調(diào)函數(shù),一般在同步情境下是最后執(zhí)行的,而在異步情境下有可能不執(zhí)行,因為事件沒有被觸發(fā)或者條件不滿足。同步方式請求異步同步請求當請求開始發(fā)送時,瀏覽器事件線程通知主線程,讓線程發(fā)送數(shù)據(jù)請求,主線程收到 一直以來都知道JavaScript是一門單線程語言,在筆試過程中不斷的遇到一些輸出結(jié)果的問題,考量的是對異步編程掌握情況。一般被問到異步的時候腦子里第一反應(yīng)就是Ajax,setTimse...
摘要:由于是單線程的,這些方法就會按順序被排列在一個單獨的地方,這個地方就是所謂執(zhí)行棧。事件隊列每次僅執(zhí)行一個任務(wù),在該任務(wù)執(zhí)行完畢之后,再執(zhí)行下一個任務(wù)。 Event Loop 是 JavaScript 異步編程的核心思想,也是前端進階必須跨越的一關(guān)。同時,它又是面試的必考點,特別是在 Promise 出現(xiàn)之后,各種各樣的面試題層出不窮,花樣百出。這篇文章從現(xiàn)實生活中的例子入手,讓你徹底理解 E...
閱讀 680·2021-09-30 09:47
閱讀 2873·2021-09-04 16:40
閱讀 857·2019-08-30 13:18
閱讀 3452·2019-08-29 16:22
閱讀 1555·2019-08-29 12:36
閱讀 586·2019-08-29 11:11
閱讀 1478·2019-08-26 13:47
閱讀 1132·2019-08-26 13:32