摘要:的輸入到瀏覽器解析的一系列事件很多大公司面試喜歡問這樣一道面試題,輸入到看見頁面發生了什么今天我們來總結一下。采用三次握手是為了防止失效的連接請求報文段突然又傳送到主機,因而產生錯誤。
URL的輸入到瀏覽器解析的一系列事件首先說明,本文很長,請泡一杯咖啡,抽出至少半個小時來慢慢回味。
很多大公司面試喜歡問這樣一道面試題,輸入URL到看見頁面發生了什么?,今天我們來總結一下。 簡單來說,共有以下幾個過程
DNS解析
發起TCP連接
發送HTTP請求
服務器處理請求并返回HTTP報文
瀏覽器解析渲染頁面
連接結束。
下面我們來看看具體的細節
DNS解析DNS解析實際上就是尋找你所需要的資源的過程。假設你輸入www.baidu.com,而這個網址并不是百度的真實地址,互聯網中每一臺機器都有唯一標識的IP地址,這個才是關鍵,但是它不好記,亂七八糟一串數字誰記得住啊,所以就需要一個網址和IP地址的轉換,也就是DNS解析。下面看看具體的解析過程
具體解析DNS解析其實是一個遞歸的過程
輸入www.google.com網址后,首先在本地的域名服務器中查找,沒找到去根域名服務器查找,沒有再去com頂級域名服務器查找,,如此的類推下去,直到找到IP地址,然后把它記錄在本地,供下次使用。大致過程就是.-> .com ->google.com. -> www.google.com.。 (你可能覺得我多寫 .,并木有,這個.對應的就是根域名服務器,默認情況下所有的網址的最后一位都是.,既然是默認情況下,為了方便用戶,通常都會省略,瀏覽器在請求DNS的時候會自動加上)DNS優化
既然已經懂得了解析的具體過程,我們可以看到上述一共經過了N個過程,每個過程有一定的消耗和時間的等待,因此我們得想辦法解決一下這個問題!
DNS存在著多級緩存,從離瀏覽器的距離排序的話,有以下幾種: 瀏覽器緩存,系統緩存,路由器緩存,IPS服務器緩存,根域名服務器緩存,頂級域名服務器緩存,主域名服務器緩存。
在你的chrome瀏覽器中輸入:chrome://dns/,你可以看到chrome瀏覽器的DNS緩存。
系統緩存主要存在/etc/hosts(Linux系統)中
不知道你們有沒有注意這樣一件事,你訪問baidu.com的時候,每次響應的并非是同一個服務器(IP地址不同),一般大公司都有成百上千臺服務器來支撐訪問,假設只有一個服務器,那它的性能和存儲量要多大才能支撐這樣大量的訪問呢?DNS可以返回一個合適的機器的IP給用戶,例如可以根據每臺機器的負載量,該機器離用戶地理位置的距離等等,這種過程就是DNS負載均衡
發起TCP連接TCP提供一種可靠的傳輸,這個過程涉及到三次握手,四次揮手,下面我們詳細看看 TCP提供一種面向連接的,可靠的字節流服務。 其首部的數據格式如下
字段分析
源端口:源端口和IP地址的作用是標識報文的返回地址。
目的端口:端口指明接收方計算機上的應用程序接口。
TCP報頭中的源端口號和目的端口號同IP數據報中的源IP與目的IP唯一確定一條TCP連接。
序號:是TCP可靠傳輸的關鍵部分。序號是該報文段發送的數據組的第一個字節的序號。在TCP傳送的流中,每一個字節都有一個序號。比如一個報文段的序號為300,報文段數據部分共有100字節,則下一個報文段的序號為400。所以序號確保了TCP傳輸的有序性。
確認號:即ACK,指明下一個期待收到的字節序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標志為1時才有效。比如建立連接時,SYN報文的ACK標志位為0。
首部長度/數據偏移:占4位,它指出TCP報文的數據距離TCP報文段的起始處有多遠。由于首部可能含有可選項內容,因此TCP報頭的長度是不確定的,報頭不包含任何任選字段則長度為20字節,4位首部長度字段所能表示的最大值為1111,轉化為10進制為15,15*32/8=60,故報頭最大長度為60字節。首部長度也叫數據偏移,是因為首部長度實際上指示了數據區在報文段中的起始偏移值。
保留:占6位,保留今后使用,但目前應都位0。
控制位:URG ACK PSH RST SYN FIN,共6個,每一個標志位表示一個控制功能。
緊急URG:當URG=1,表明緊急指針字段有效。告訴系統此報文段中有緊急數據
確認ACK:僅當ACK=1時,確認號字段才有效。TCP規定,在連接建立后所有報文的傳輸都必須把ACK置1。
推送PSH:當兩個應用進程進行交互式通信時,有時在一端的應用進程希望在鍵入一個命令后立即就能收到對方的響應,這時候就將PSH=1。
復位RST:當RST=1,表明TCP連接中出現嚴重差錯,必須釋放連接,然后再重新建立連接。
同步SYN:在連接建立時用來同步序號。當SYN=1,ACK=0,表明是連接請求報文,若同意連接,則響應報文中應該使SYN=1,ACK=1。
終止FIN:用來釋放連接。當FIN=1,表明此報文的發送方的數據已經發送完畢,并且要求釋放。
窗口:滑動窗口大小,用來告知發送端接受端的緩存大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小時一個16bit字段,因而窗口大小最大為65535。
校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,并由接收端進行驗證。
緊急指針:只有當 URG 標志置 1 時緊急指針才有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最后一個字節的序號。 TCP 的緊急方式是發送端向另一端發送緊急數據的一種方式。
選項和填充:最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。
數據部分: TCP 報文段中的數據部分是可選的。在一個連接建立和一個連接終止時,雙方交換的報文段僅有 TCP 首部。如果一方沒有數據要發送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多情況中,也會發送不帶任何數據的報文段。
需要注意的是: (A)不要將確認序號Ack與標志位中的ACK搞混了。 (B)確認方Ack=發起方Req+1,兩端配對。
客戶端發送syn包(Seq=x)到服務器,并進入SYN_SEND狀態,等待服務器確認;
服務器收到syn包,必須確認客戶的SYN(ack=x+1),同時自己也發送一個SYN包(Seq=y),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=y+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
握手過程中傳送的包里不包含數據,三次握手完畢后,客戶端與服務器才正式開始傳送數據。理想狀態下,TCP連接一旦建立,在通信雙方中的任何一方主動關閉連接之前,TCP 連接都將被一直保持下去。
建立連接的過程是利用客戶服務器模式,假設主機A為客戶端,主機B為服務器端。
采用三次握手是為了防止失效的連接請求報文段突然又傳送到主機B,因而產生錯誤。失效的連接請求報文段是指:主機A發出的連接請求沒有收到主機B的確認,于是經過一段時間后,主機A又重新向主機B發送連接請求,且建立成功,順序完成數據傳輸??紤]這樣一種特殊情況,主機A第一次發送的連接請求并沒有丟失,而是因為網絡節點導致延遲達到主機B,主機B以為是主機A又發起的新連接,于是主機B同意連接,并向主機A發回確認,但是此時主機A根本不會理會,主機B就一直在等待主機A發送數據,導致主機B的資源浪費。
采用兩次握手不行,原因就是上面說的失效的連接請求的特殊情況。而在三次握手中, client和server都有一個發syn和收ack的過程, 雙方都是發后能收, 表明通信則準備工作OK.
為什么不是四次握手呢? 大家應該知道通信中著名的藍軍紅軍約定, 這個例子說明, 通信不可能100%可靠, 而上面的三次握手已經做好了通信的準備工作, 再增加握手, 并不能顯著提高可靠性, 而且也沒有必要。
數據傳輸完畢后,雙方都可釋放連接。最開始的時候,客戶端和服務器都是處于ESTABLISHED狀態,假設客戶端主動關閉,服務器被動關閉。
客戶端發送一個FIN,用來關閉客戶端到服務器的數據傳送,也就是客戶端告訴服務器:我已經不 會再給你發數據了(當然,在fin包之前發送出去的數據,如果沒有收到對應的ack確認報文,客戶端依然會重發這些數據),但是,此時客戶端還可 以接受數據。 FIN=1,其序列號為seq=u(等于前面已經傳送過來的數據的最后一個字節的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。
服務器收到FIN包后,發送一個ACK給對方并且帶上自己的序列號seq,確認序號為收到序號+1(與SYN相同,一個FIN占用一個序號)。此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處于半關閉狀態,即客戶端已經沒有數據要發送了,但是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送連接釋放報文(在這之前還需要接受服務器發送的最后的數據)。
服務器發送一個FIN,用來關閉服務器到客戶端的數據傳送,也就是告訴客戶端,我的數據也發送完了,不會再給你發數據了。由于在半關閉狀態,服務器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,服務器就進入了LAST-ACK(最后確認)狀態,等待客戶端的確認。
主動關閉方收到FIN后,發送一個ACK給被動關閉方,確認序號為收到序號+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2?MSL(最長報文段壽命)的時間后,當客戶端撤銷相應的TCB后,才進入CLOSED狀態。
服務器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB后,就結束了這次的TCP連接??梢钥吹?,服務器結束TCP連接的時間要比客戶端早一些。
至此,完成四次揮手。
MSL(Maximum Segment Lifetime),TCP允許不同的實現可以設置不同的MSL值。
建立連接的時候, 服務器在LISTEN狀態下,收到建立連接請求的SYN報文后,把ACK和SYN放在一個報文里發送給客戶端。 而關閉連接時,服務器收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,而自己也未必全部數據都發送給對方了,所以己方可以立即關閉,也可以發送一些數據給對方后,再發送FIN報文給對方來表示同意現在關閉連接,因此,己方ACK和FIN一般都會分開發送,從而導致多了一次。
發送HTTP請求首先科補一個小知識,HTTP的端口為80/8080,而HTTPS的端口為443
發送HTTP請求的過程就是構建HTTP請求報文并通過TCP協議中發送到服務器指定端口 請求報文由請求行,請求抱頭,請求正文組成。
請求行請求行的格式為Method Request-URL HTTP-Version CRLF eg: GET index.html HTTP/1.1 常用的方法有: GET,POST, PUT, DELETE, OPTIONS, HEAD。
這里主要展示POST和GET的區別
常見的區別
GET在瀏覽器回退時是無害的,而POST會再次提交請求。
GET產生的URL地址可以被Bookmark,而POST不可以。
GET請求會被瀏覽器主動cache,而POST不會,除非手動設置。
GET請求只能進行url編碼,而POST支持多種編碼方式。
GET請求參數會被完整保留在瀏覽器歷史記錄里,而POST中的參數不會被保留。
GET請求在URL中傳送的參數是有長度限制的,而POST么有。
對參數的數據類型,GET只接受ASCII字符,而POST沒有限制。
GET比POST更不安全,因為參數直接暴露在URL上,所以不能用來傳遞敏感信息。
GET參數通過URL傳遞,POST放在Request body中。
注意一點你也可以在GET里面藏body,POST里面帶參數
重點區別
GET會產生一個TCP數據包,而POST會產生兩個TCP數據包。
詳細的說就是:
對于GET方式的請求,瀏覽器會把http header和data一并發送出去,服務器響應200(返回數據);
而對于POST,瀏覽器先發送header,服務器響應100 continue,瀏覽器再發送data,服務器響應200 ok(返回數據)。
請求報頭注意一點,并不是所有的瀏覽器都會發送兩次數據包,Firefox就發送一次
請求報頭允許客戶端向服務器傳遞請求的附加信息和客戶端自身的信息。
從圖中可以看出,請求報頭中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用于指定客戶端用于接受哪些類型的信息,Accept-Encoding與Accept類似,它用于指定接受的編碼方式。Connection設置為Keep-alive用于告訴客戶端本次HTTP請求結束之后并不需要關閉TCP連接,這樣可以使下次HTTP請求使用相同的TCP通道,節省TCP連接建立的時間。請求正文
當使用POST, PUT等方法時,通常需要客戶端向服務器傳遞數據。這些數據就儲存在請求正文中。在請求包頭中有一些與請求正文相關的信息,例如: 現在的Web應用通常采用Rest架構,請求的數據格式一般為json。這時就需要設置Content-Type: application/json。
更重要的事情-HTTP緩存HTTP屬于客戶端緩存,我們常認為瀏覽器有一個緩存數據庫,用來保存一些靜態文件,下面我們分為以下幾個方面來簡單介紹HTTP緩存
緩存的規則
緩存的方案
緩存的優點
不同刷新的請求執行過程
緩存規則分為強制緩存和協商緩存
當緩存數據庫中有客戶端需要的數據,客戶端直接將數據從其中拿出來使用(如果數據未失效),當緩存服務器沒有需要的數據時,客戶端才會向服務端請求。
又稱對比緩存。客戶端會先從緩存數據庫拿到一個緩存的標識,然后向服務端驗證標識是否失效,如果沒有失效服務端會返回304,這樣客戶端可以直接去緩存數據庫拿出數據,如果失效,服務端會返回新的數據
強制緩存的優先級高于協商緩存,若兩種緩存皆存在,且強制緩存命中目標,則協商緩存不再驗證標識。
上面的內容讓我們大概了解了緩存機制是怎樣運行的,但是,服務器是如何判斷緩存是否失效呢?我們知道瀏覽器和服務器進行交互的時候會發送一些請求數據和響應數據,我們稱之為HTTP報文。報文中包含首部header和主體部分body。與緩存相關的規則信息就包含在header中。boby中的內容是HTTP請求真正要傳輸的部分。舉個HTTP報文header部分的例子如下:
我們依舊分為強制緩存和協商緩存來分析。
對于強制緩存,服務器響應的header中會用兩個字段來表明——Expires和Cache-Control。
Exprires的值為服務端返回的數據到期時間。當再次請求時的請求時間小于返回的此時間,則直接使用緩存數據。但由于服務端時間和客戶端時間可能有誤差,這也將導致緩存命中的誤差,另一方面,Expires是HTTP1.0的產物,故現在大多數使用Cache-Control替代。
Cache-Control有很多屬性,不同的屬性代表的意義也不同。
private:客戶端可以緩存
public:客戶端和代理服務器都可以緩存
max-age=t:緩存內容將在t秒后失效
no-cache:需要使用協商緩存來驗證緩存數據
no-store:所有內容都不會緩存。
協商緩存需要進行對比判斷是否可以使用緩存。瀏覽器第一次請求數據時,服務器會將緩存標識與數據一起響應給客戶端,客戶端將它們備份至緩存中。再次請求時,客戶端會將緩存中的標識發送給服務器,服務器根據此標識判斷。若未失效,返回304狀態碼,瀏覽器拿到此狀態碼就可以直接使用緩存數據了。
對于協商緩存來說,緩存標識我們需要著重理解一下,下面我們將著重介紹它的兩種緩存方案。
Last-Modified:服務器在響應請求時,會告訴瀏覽器資源的最后修改時間。
if-Modified-Since:瀏覽器再次請求服務器的時候,請求頭會包含此字段,后面跟著在緩存中獲得的最后修改時間。服務端收到此請求頭發現有if-Modified-Since,則與被請求資源的最后修改時間進行對比,如果一致則返回304和響應報文頭,瀏覽器只需要從緩存中獲取信息即可。 從字面上看,就是說:從某個時間節點算起,是否文件被修改了
如果真的被修改:那么開始傳輸響應一個整體,服務器返回:200 OK
如果沒有被修改:那么只需傳輸響應header,服務器返回:304 Not Modified
if-Unmodified-Since:從字面上看, 就是說: 從某個時間點算起, 是否文件沒有被修改
如果沒有被修改:則開始`繼續"傳送文件: 服務器返回: 200 OK
如果文件被修改:則不傳輸,服務器返回: 412 Precondition failed (預處理錯誤)
這兩個的區別是一個是修改了才下載一個是沒修改才下載。
Last-Modified 說好卻也不是特別好,因為如果在服務器上,一個資源被修改了,但其實際內容根本沒發生改變,會因為Last-Modified時間匹配不上而返回了整個實體給客戶端(即使客戶端緩存里有個一模一樣的資源)。為了解決這個問題,HTTP1.1推出了Etag。
Etag:服務器響應請求時,通過此字段告訴瀏覽器當前資源在服務器生成的唯一標識(生成規則由服務器決定)
If-None-Match:再次請求服務器時,瀏覽器的請求報文頭部會包含此字段,后面的值為在緩存中獲取的標識。服務器接收到次報文后發現If-None-Match則與被請求資源的唯一標識進行對比。
不同,說明資源被改動過,則響應整個資源內容,返回狀態碼200。
相同,說明資源無心修改,則響應header,瀏覽器直接從緩存中獲取數據信息。返回狀態碼304.
但是實際應用中由于Etag的計算是使用算法來得出的,而算法會占用服務端計算的資源,所有服務端的資源都是寶貴的,所以就很少使用Etag了。
減少了冗余的數據傳遞,節省寬帶流量
減少了服務器的負擔,大大提高了網站性能
加快了客戶端加載網頁的速度 這也正是HTTP緩存屬于客戶端緩存的原因。
瀏覽器發現緩存中有這個文件了,不用繼續請求了,直接去緩存拿。(最快)
F5就是告訴瀏覽器,別偷懶,好歹去服務器看看這個文件是否有過期了。于是瀏覽器就戰戰兢兢的發送一個請求帶上If-Modify-since。
告訴瀏覽器,你先把你緩存中的這個文件給我刪了,然后再去服務器請求個完整的資源文件下來。于是客戶端就完成了強行更新的操作.
服務器處理請求并返回HTTP報文它會對TCP連接進行處理,對HTTP協議進行解析,并按照報文格式進一步封裝成HTTP Request對象,供上層使用。這一部分工作一般是由Web服務器去進行,我使用過的Web服務器有Tomcat, Nginx和Apache等等 HTTP報文也分成三份,狀態碼 ,響應報頭和響應報文
狀態碼狀態碼是由3位數組成,第一個數字定義了響應的類別,且有五種可能取值:
1xx:指示信息–表示請求已接收,繼續處理。
2xx:成功–表示請求已被成功接收、理解、接受。
3xx:重定向–要完成請求必須進行更進一步的操作。
4xx:客戶端錯誤–請求有語法錯誤或請求無法實現。
5xx:服務器端錯誤–服務器未能實現合法的請求。 平時遇到比較常見的狀態碼有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500
請求成功,通常服務器提供了需要的資源。
服務器成功處理了請求,但沒有返回任何內容。
請求的網頁已永久移動到新位置。 服務器返回此響應(對 GET 或 HEAD 請求的響應)時,會自動將請求者轉到新位置。
服務器目前從不同位置的網頁響應請求,但請求者應繼續使用原有位置來進行以后的請求。
自從上次請求后,請求的網頁未修改過。 服務器返回此響應時,不會返回網頁內容。
服務器不理解請求的語法。
請求要求身份驗證。 對于需要登錄的網頁,服務器可能返回此響應。
服務器拒絕請求。
服務器找不到請求的網頁。
請求格式正確,但是由于含有語義錯誤,無法響應
服務器遇到錯誤,無法完成請求。
響應報頭常見的響應報頭字段有: Server, Connection...。
響應報文你從服務器請求的HTML,CSS,JS文件就放在這里面
瀏覽器解析渲染頁面這個圖就是Webkit解析渲染頁面的過程。
解析HTML形成DOM樹
解析CSS形成CSSOM 樹
合并DOM樹和CSSOM樹形成渲染樹
瀏覽器開始渲染并繪制頁面 這個過程涉及兩個比較重要的概念回流和重繪,DOM結點都是以盒模型形式存在,需要瀏覽器去計算位置和寬度等,這個過程就是回流。等到頁面的寬高,大小,顏色等屬性確定下來后,瀏覽器開始繪制內容,這個過程叫做重繪。瀏覽器剛打開頁面一定要經過這兩個過程的,但是這個過程非常非常非常消耗性能,所以我們應該盡量減少頁面的回流和重繪
性能優化之回流重繪當Render Tree中部分或全部元素的尺寸、結構、或某些屬性發生改變時,瀏覽器重新渲染部分或全部文檔的過程稱為回流。
會導致回流的操作:
頁面首次渲染
瀏覽器窗口大小發生改變
元素尺寸或位置發生改變
元素內容變化(文字數量或圖片大小等等)
元素字體大小變化
添加或者刪除可見的DOM元素
激活CSS偽類(例如::hover)
查詢某些屬性或調用某些方法
一些常用且會導致回流的屬性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
當頁面中元素樣式的改變并不影響它在文檔流中的位置時(例如:color、background-color、visibility等),瀏覽器會將新樣式賦予給元素并重新繪制它,這個過程稱為重繪。
避免使用table布局。
盡可能在DOM樹的最末端改變class。
避免設置多層內聯樣式。
將動畫效果應用到position屬性為absolute或fixed的元素上。
避免使用CSS表達式(例如:calc())。
避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class并一次性更改class屬性。
避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最后再把它添加到文檔中。
也可以先為元素設置display: none,操作結束后再把它顯示出來。因為在display屬性為none的元素上進行的DOM操作不會引發回流和重繪。
避免頻繁讀取會引發回流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
對具有復雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及后續元素頻繁回流。
JS的解析JS的解析是由瀏覽器的JS引擎完成的。由于JavaScript是單進程運行,也就是說一個時間只能干一件事,干這件事情時其他事情都有排隊,但是有些人物比較耗時(例如IO操作),所以將任務分為同步任務和異步任務,所有的同步任務放在主線程上執行,形成執行棧,而異步任務等待,當執行棧被清空時才去看看異步任務有沒有東西要搞,有再提取到主線程執行,這樣往復循環(冤冤相報何時了,阿彌陀佛),就形成了Event Loop事件循環,下面來看看大人物
Event Loop先看一段代碼
setTimeout(function(){
console.log("定時器開始啦")
});
new Promise(function(resolve){
console.log("馬上執行for循環啦");
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log("執行then函數啦")
});
console.log("代碼執行結束");
結果我想大家都應該知道。主要來介紹JavaScript的解析,至于Promise等下一節再說
JavaScript是一門單線程語言,盡管H5中提出了Web-Worker,能夠模擬實現多線程,但本質上還是單線程,說它是多線程就是扯淡。
既然是單線程,每個事件的執行就要有順序,比如你去銀行取錢,前面的人在進行,后面的就得等待,要是前面的人弄個一兩個小時,估計后面的人都瘋了,因此,瀏覽器的JS引擎處理JavaScript時分為同步任務和異步任務
這張圖我們可以清楚看到
同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table并注冊函數。
當指定的事情完成時,Event Table會將這個函數移入Event Queue。
主線程內的任務執行完畢為空,會去Event Queue讀取對應的函數,進入主線程執行。
上述過程會不斷重復,也就是常說的Event Loop(事件循環)。
js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否為空,一旦為空,就會去Event Queue那里檢查是否有等待被調用的函數。 估計看完這些你對事件循環有一定的了解,但是事實上我們看對的沒這么簡單,通常我們會看到Promise,setTimeout,process.nextTick(),這個時候你和我就懵逼。
除了同步任務和異步任務,我們還分為宏任務和微任務,常見的有以下幾種
macro-task(宏任務):包括整體代碼script,setTimeout,setInterval
micro-task(微任務):Promise,process.nextTick 不同任務會進入不同的任務隊列來執行。 JS引擎開始工作后,先在宏任務中開始第一次循環(script里面先執行,不過我喜歡把它拎出來,直接稱其進入執行棧),當主線程執行棧全部任務被清空后去微任務看看,如果有等待執行的任務,執行全部的微任務(其實將其回調函數推入執行棧來執行),再去宏任務找最先進入隊列的任務執行,執行這個任務后再去主線程執行任務(例如執行```console.log("hello world")這種任務),執行棧被清空后再去微任務,這樣往復循環(冤冤相報何時了)
Tip:微任務會全部執行,而宏任務會一個一個來執行
下面來看一段代碼
setTimeout(function() {
console.log("setTimeout");
})
new Promise(function(resolve) {
console.log("promise");
}).then(function() {
console.log("then");
})
console.log("console");
我們看看它的執行情況
第一輪
這段代碼進入主線程
遇到setTimeout,將其回調函數注冊后分發到宏任務
第二輪
遇到Promise,new Promise立即執行(這個不解釋,想了解的我后續文章會介紹),輸出promise,遇到then,將其分發到微任務
第三輪
遇到console.log("console"),直接輸出console
第四輪
主線程執行棧已經清空,先去微任務看看,執行then函數,輸出then
第五輪
微任務執行完了,看看宏任務,有個setTimeout,輸出setTimeout,整體執行完畢。 具體的執行過程大致就是這樣,可能我有疏忽的地方,還望指正。 再來看看一段復雜的代碼
console.log("1");
setTimeout(function() {
console.log("2");
process.nextTick(function() {
console.log("3");
})
new Promise(function(resolve) {
console.log("4");
resolve();
}).then(function() {
console.log("5")
})
})
process.nextTick(function() {
console.log("6");
})
new Promise(function(resolve) {
console.log("7");
resolve();
}).then(function() {
console.log("8")
})
setTimeout(function() {
console.log("9");
process.nextTick(function() {
console.log("10");
})
new Promise(function(resolve) {
console.log("11");
resolve();
}).then(function() {
console.log("12")
})
})
我們來分析一下
整體script進入主線程,遇到console.log("1"),直接輸出
遇到setTimeout,將其回調函數分發到宏任務事件隊列,暫時標記為setTimeout1
遇到process.nextTick(),將其回調函數分發到微任務事件隊列,標記為process.nextTick1(這個地方有點出入,我一般認為```process.nextTick()推入主線程執行棧棧底,作為執行棧最后一個任務執行)
遇到Promise,立即執行,輸出7,then函數分發的微任務事件隊列,標記為Promise1。
遇到setTimeout,將其回調函數分發到微任務事件隊列,標記為setTimeout2。
現在已經輸出了1,7,宏任務和微任務的事件隊列 情況如下 我們接著來看
現在主線程執行棧被清空,去微任務看看,發現有兩個事件等待,由于隊列是先進先出,執行process.nextTick1,輸出6,接著執行Promise1,輸出8。
至此,第一輪循環已經結束,輸出了1,7,6,8,接下來執行第二輪循環 ,先從宏任務的setTimeout1開始
遇到console.log("2"),執行輸出。
遇到process.nextTick(),將其回調函數分發到微任務,標記為process.nextTick2,又遇到 Promise,立即執行,輸出4,將then函數推入微任務事件隊列,標記為Promise2
到此宏任務的一個任務執行完畢,輸出了2,4,來看看事件隊列
去微任務看看,我們先處理process.nextTick2,輸出3,接著再來執行Promise2,輸出5。 第二輪循環執行完畢?,F在一共輸出了1,7,6,8,2,4,3,5
從setTimeout2開始第三輪循環 ,先直接輸出9,遇到process.nextTick(),將其回調函數分發到微任務事件隊列,標記為process.nextTick3,又遇到惡心的Promise,立即執行輸出11,將then函數分發到微任務,標記為Promise3。
執行微任務,看看還有撒子事件 居然還有事件,能咋辦,接著執行唄,輸出10,12。 至此,全部任務執行完畢,輸出順序為1,7,6,8,2,4,3,5,9,11,10,12.
注意,這段代碼執行結果可能與node等環境不同而發生變化。
我想說的也說完了,不知道您懂了嘛
總結這篇文章由一個簡單的問題扯出了很多前端工程師必學也是很重要的東西,但是由于我本人水平較低,很多地方都是一筆帶過,甚至有些地方還有錯誤,望各位同仁指正批評。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7371.html
摘要:前言由于寫的文章已經是有點多了,為了自己和大家的檢索方便,于是我就做了這么一個博客導航。 前言 由于寫的文章已經是有點多了,為了自己和大家的檢索方便,于是我就做了這么一個博客導航。 由于更新比較頻繁,因此隔一段時間才會更新目錄導航哦~想要獲取最新原創的技術文章歡迎關注我的公眾號:Java3y Java3y文章目錄導航 Java基礎 泛型就這么簡單 注解就這么簡單 Druid數據庫連接池...
摘要:計算數組的極值微信面試題獲取元素的最終前端掘金一題目用代碼求出頁面上一個元素的最終的,不考慮瀏覽器,不考慮元素情況。 Excuse me?這個前端面試在搞事! - 前端 - 掘金金三銀四搞事季,前端這個近年的熱門領域,搞事氣氛特別強烈,我朋友小偉最近就在瘋狂面試,遇到了許多有趣的面試官,有趣的面試題,我來幫這個搞事 boy 轉述一下。 以下是我一個朋友的故事,真的不是我。 ... ja...
摘要:比如對于的,瀏覽器實際上不知道到底是什么東西,需要查找網站所在服務器的地址,才能找到目標,這就是下文要說的域名解析。二域名解析當用戶在瀏覽器中輸入后你使用的電腦會發出一個請求到本地服務器。 showImg(https://segmentfault.com/img/remote/1460000009317499?w=700&h=466); 這里markdown格式跟簡書不太一樣,排版可能...
摘要:春招前端實習面試記錄從就開始漸漸的進行復習,月末開始面試,到現在四月中旬基本宣告結束。上海愛樂奇一面盒模型除之外的面向對象語言繼承因為是視頻面試,只記得這么多,只感覺考察的面很廣,前端后端移動端都問了,某方面也有深度。 春招前端實習面試記錄(2019.3 ~ 2019.5) 從2019.1就開始漸漸的進行復習,2月末開始面試,到現在四月中旬基本宣告結束。在3月和4月經歷了無數次失敗,沮...
摘要:針對的初學者,從無到有的語言如何入門,主要包括了的簡介,如何下載,如何安裝,如何使用終端,等各種開發環境進行開發,中的語法和基本知識概念和邏輯,以及繼續深入學習的方法。 ...
閱讀 3677·2021-09-22 15:34
閱讀 1186·2019-08-29 17:25
閱讀 3399·2019-08-29 11:18
閱讀 1371·2019-08-26 17:15
閱讀 1740·2019-08-23 17:19
閱讀 1228·2019-08-23 16:15
閱讀 718·2019-08-23 16:02
閱讀 1335·2019-08-23 15:19