前言
平日的編碼中,你能列出你常用的異步編碼?怎么理解同步與異步?
如果僅僅停留在文字上的理解,個人覺得有口無心,每當屢屢面試時,這都是一個常問的話題,牽扯到的是事件的執行順序,任務隊列,在js當中對於異步處理任務,是一個非常重要知識.
如何看待同步?
由於js是單線程的,換句話說,就是,在同一段時間內,只能處理一個任務,干一件事情,然后再去處理下一個任務,瀏覽器解析網頁中的js代碼,是逐行進行讀取,從上至下執行的 實例場景:打電話就是一個同步的例子,必須等待打完了一個,然后再接着打下一個的
在如何看待同步之前,有必要了解下計算機中兩個專業術語概念,就是進程和線程
進程: 它是系統進行資源分配和調度的一個獨立單位,具有一定獨立功能的程序關於某個數據集合上的一次運行活動,可以粗俗的理解為主(大)任務
**線程:**安排CPU執行的最小單位,可以理解為子任務
**關系:**線程可以視作為進程的子集,一個進程可以有多個線程並發執行
區別:進程和線程的主要差別在於,它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。
線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。
但對於一些要求同時進行並且又要共享某些變量的並發操作,只能用線程,不能用進程
在后文中會用具體的代碼,來認識同步的
為什么js是單線程?
JavaScript之所以設計為單線程,這與它的用途有關。它作為瀏覽器腳本語言,主要用途是負責與頁面的交互,以及操作DOM(添加,刪除等),它只能是單線程的,否則它就會帶來很復雜的同步問題。
比如,你在網頁上有若干個操作,也就是在主線程中有多個任務,一個線程任務是在某個DOM節點上添加內容,另一個線程任務是刪除這個節點,這時瀏覽器應該以哪個線程為准?
所以,為了避免復雜性,從一誕生,JavaScript就是單線程的,這已經是這門語言的核心特征,將來也不會改變
而單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個,但瀏覽器是多線程的,而js是單線程的,兩者並不矛盾,瀏覽器只是js宿主的運行環境
怎么理解異步?
瀏覽器是多線程的,但解析我們的js代碼,卻是單線程的,但有些任務是需要消耗時間的(比如:上傳,讀取文件,下載等),如果按照普通的同步方式,就會阻塞我們的代碼,主線程的任務沒有做完,那么下面的任務將不會執行
實例場景:給女票打電話,必須等待到對方接聽,有反應后,才能繼續后面的熱戀,你得一直等待,干不了別的事情,在那苦等的耗着
但發短信,微信就是一個異步的例子,也許對方正忙,沒有及時回復,你不必等待對方及時回應,你仍可以繼續干其他的事情。等到對方看見了,便會回應你.
單線程中有一些任務需要耗費一些時間,讓用戶去等待確認,把一些耗時的事情任務通過新開的線程方式來實現,瀏覽器會針對對於那些耗時間的任務,會開一些新的進程單獨去處理
主線程繼續往下走,那么這個時候,它既不影響后續代碼的執行,同時還能通過另外的線程去做事,然后等待另外的線程做完事之后
比如說:通過回調,事件的方式去通知我們的主線程,然后把Ajax等異步處理要做的事情,在推到主線程當中進行執行
那有哪些東西是需要重新開線程的?既然js是單線程的,那么他是如何是實現異步操作的?我們把這些任務稱為:異步任務 同一段時間內可以做多個任務,例如
setTimeout
setInterval
ajax
...
監聽DOM,修改頁面的操作,渲染我們的樣式,都是需要瀏覽器去處理的
這樣的話,所謂的異步請求就很好理解了
指web服務器對請求作出響應時不要求你等待,這說明,瀏覽器解析js代碼,當遇到異步任務時,不會僵持在那里不動,它會繼續做主線程的任務,並會在服務器處理完請求時通知你.
那么在具體的代碼中,是怎么體現的? 這里以Ajax為例: 我們先看寫一段簡單的后端代碼
/** * * @authors 川川 (itclancode@163.com) * @ID suibichuanji * @weChatNum 微信公眾號:itclancoder * @version $Id$ */ var http = require('http') // 使用http對象來引用http模塊 var url = require('url'); var jsonData = { "name": "川川", "age": 20, "job": "weber" }; var app = http.createServer(function(req, res) { // 使用http模塊的createServer方法來創建用於接收HTTP客戶端請求並返回的響應的HTTP服務器應用程序,在createServer方法中定義了當服務器接收到客戶端請求時所執行的回調函數,在該回調函數中指定當服務器接收到客戶端請求時所要執行的處理,第一個參數req代表的是客戶端請求對象,第二個參數代表服務器端所做出的響應對象 res.writeHead(200, { 'Content-Type': 'application/json;charset=utf-8', 'Access-Control-Allow-Credentials': true, 'Access-Control-Allow-Origin': '*' //可以是*,也可以是跨域的地址 }) // url.parse 方法來解析 URL 中的參數 var pathname = url.parse(req.url, true).pathname; if (pathname == '/index') { setTimeout(function() { res.end(JSON.stringify(jsonData)); // 通過響應對象res的end方法輸出一json對象,並結束響應流 }, 3000) } }) app.listen(8083, "127.0.0.1"); // createServer方法將返回被創建的HTTP服務器對象,我們使用該對象的listen方法指定服務器使用端口及服務器綁定的地止,並對該端口進行監聽 console.log('server running at http:127.0.0.1:8083/');
將這段代碼命名為server.js,然后在當前目錄下執行node server.js
,就會啟動后端的服務 在瀏覽器端地止欄:輸入http://127.0.0.1:8083/index
那么在瀏覽器前端: 如果想要把這個數據添加到瀏覽器前端頁面上,那該怎么操作? 如下代碼所示:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <title>01異步與同步</title> <meta name="description" content=""> <meta name="keywords" content=""> <link href="" rel="stylesheet"> <style> *{ padding: 0; margin: 0; } #box{ width: 100px; height: 100px; background: red; } </style> </head> <body> <button id="btn">按鈕</button> <div id="list-wrap"></div> <div id="box"></div> <script> var oBtn = document.querySelector('#btn'); var oBox = document.querySelector('#box'); var oListWrap = document.querySelector("#list-wrap"); var ul = document.createElement('ul'); oListWrap.appendChild(ul); var str = ""; oBtn.onclick = function(){ console.log("任務2"); var xhr = new XMLHttpRequest(); xhr.onload = function(){ console.log(xhr.readyState); if(this.readyState== 4 ){ if(this.status == 200){ var data = JSON.parse(this.responseText); } } console.log("任務3"); console.log(data); var listAttrs = Object.keys(data).map(function(item){ return data[item] }); var attrs = Object.keys(data).map(function(item){ return item; }) console.log(listAttrs); for(let i = 0;i<attrs.length;i++){ str += "<li>"+attrs[i]+":"+listAttrs[i]+"</li>"; ul.innerHTML = str; } } xhr.open('get', 'http:127.0.0.1:8083/index',true); console.log("任務4"); // true表示異步,ajax的事情還沒有處理完成的時候,我們點擊div,可以立馬變色,ajax的事情並不影響當前頁面中其他效果,開啟了一個新的線程去完成ajax的事情,並不影響主線程,其他頁面在主線程當中的其他任務的 // false同步,當前線程直接處理 xhr.send(); } // 點擊操作 oBox.onclick = function(){ this.style.background = "green"; } console.log("任務1"); </script> </body> </html>
上面代碼的主要功能是:點擊按鈕,加載后端數據,將數據添加到前端頁面中
如果把xhr.open()
的第三個參數設置為false
,則是同步的,當你點擊按鈕后,你點擊下面的方塊框,點擊事件它是不會執行的,必須得等到上面的事情(加載數據)做完了,在次點擊時,它才會生效
在使用Ajax的時候,應該推薦使用異步的方式,而不應該是同步的,不然的話,它就會阻塞我們后續的代碼執行
如果你把
xhr.open()
的第三個參數設置為false,那么當你點擊按鈕后,在點擊紅色的box,它是不會起作用的,只有等待響應的結果執行完后,點擊紅色的box,才會生效執行
JS為什么需要異步?
JS是單線程的,那肯定只能同步(排隊)順序執行代碼,是沒有疑問的,寫同步代碼的好處就是好理解,壞處就是容易阻塞,只能等待上一次任務做完了,在接着做下一個任務.
而寫異步代碼的好處,就是實現讓程序可控,想讓它按照我們的想要的結果進行輸出,壞處顯然就是不好理解,射出去的弓箭,又要繞回來. 如果JS中不存在異步,只能自上而下執行,萬一上一行解析代碼的時間很長,那么下面的代碼就會被阻塞。對於用戶而言,阻塞就意味着"卡死",這樣就導致了很差的用戶體驗
想想在一個聊天室里,你發一條信息,必須要等待對方回應后,才能在發一條信息,這顯然會令人奔潰的
那js單線程又是如何實現異步的呢
是通過事件循環(event loop)實現異步的,這個詞在很多前端技術書籍上都提到過,但是每次看完,總是不理解,知道有那么一回事,但就是解釋不清楚
下面這個經典的問題:猜猜它的輸出結果
console.log('1') setTimeout(function(){ console.log('2') },0) console.log('3')
想必大家閉着眼都能答上來,輸入的順序是1,3,2,但是解釋一下為什么,卻總是道不明白.
setTimeout
里的匿名函數並沒有立即執行,而是延遲了一段時間,等滿足一定條件后,才去執行的,匿名函數沒有立即被調用棧執行,而是添加一個隊列中,專業點稱為任務隊列,類似這樣的代碼,我們叫異步代碼。
首先我們知道了JS里的一種任務分類方式,就是將任務分為: 同步任務和異步任務
雖然JS是單線程的,但是瀏覽器的內核卻是多線程的,在瀏覽器的內核中不同的異步操作由不同的瀏覽器內核模塊調度執行,異步任務操作會將相關回調添加到任務隊列中。
而不同的異步操作添加到任務隊列的時機也不同,比如onclick, setTimeout, ajax 處理的方式都不同,這些異步操作是由瀏覽器內核來執行的,瀏覽器內核上包含3種 webAPI,分別是 DOM Binding(DOM綁定)、network(網絡請求)、timer(定時器)模塊。
按照這種分類方式:JS的執行機制是
- 首先判斷js代碼是同步還是異步,不停的檢查調用棧中是否有任務需要執行,如果沒有,就檢查任務隊列,從中彈出一個任務,放入棧中,如此往復循環,要是同步就進入主進程,異步就進入事件表
- 異步任務在事件表中注冊函數,當滿足觸發條件后,被推入事件隊列
- 同步任務進入主線程后一直執行,直到主線程空閑時,才會去事件隊列中查看是否有可執行的異步任務,如果有就推入主進程中
以上三步循環執行,這就是事件循環(event loop),它是連接任務隊列和控制調用棧的 小結:
同步任務可以保證順序一致,代碼可讀性好,相對容易理解,但是容易導致阻塞;異步任務可以解決阻塞問題,但是會改變任務的順序性,根據不同的需要去寫你的代碼
顯然異步代碼是我們常用的一種方式,也是比較復雜的,而在js中處理異步,也就誕生出了很多的工具處理異步問題
例如:回調函數(異步執行或稍后執行的函數,也可以理解為將一個函數的參數作為另一個函數的名字,那么這個參數就叫做回調函數),使用Es6中的承諾(promise),Es7中的async await
為了更好的理解回調函數,下面寫了幾行代碼,命名為callback.js
,讀取number.txt文件,在number.txt中寫了1234,然后執行node callback.js
var fs = require('fs'); var myNumber = undefined; function addOne(callback){ fs.readFile('number.txt', function doneReading(err, fileContents){ myNumber = parseInt(fileContents); myNumber++; callback(); }) } function logMyNumber(){ console.log(myNumber); } addOne(logMyNumber)
logMyNumber
函數作為
addOne
函數的實參傳入進去,而在
addOne
函數聲明處,用
callback
參數變量進行接收,並在
addOne
函數內進行調用執行(
callback()
),類似這種將一個函數作為參數傳遞被另一個函數調用執行的,這樣的函數就稱為回調函數
結語
整篇文章主要了解js中的同步與異步問題,js是一門單線程的語言,瀏覽器解析js代碼是同步順序執行的,但是瀏覽器本身是多線程的,js實現異步是通過事件循環來實現的
定時器setTimeout,setInterval本質上是瀏覽器提供API,它是異步執行的.也就是說,異步函數代碼它不會立即執行調用
一旦遇到異步的任務,會將它安排到一個任務隊列中掛起狀態,瀏覽器重新開一個新的線程單獨處理它,它並不會阻塞主線程的代碼,當主線程任務處理完了,有空閑時,此時,等待執行異步任務隊列中的事情
異步處理在js中是一個非常重要的問題,往往牽扯到什么宏任務,微任務,很多時候,這些抽象的概念,面試的時候,是虐人的
實際開發中,很多時候,更多是停留在,知道就是這么用的,但是卻道不清楚背后的原理,或者這就是與大神的差距吧...
在遇到復雜的業務邏輯時,處理異步任務肯定是繞不過的,所以還是有必要去了解瀏覽器解析代碼的流程,執行順序的。