從今天開始研究一下javascript的異步相關內容,感興趣的請關注
什麼是js異步?
我們知道JavaScript的單線程的,這與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?
所謂"單線程",就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),往往就是因爲某一段Javascript代碼長時間運行(比如死循環),導致整個頁面卡在這個地方,其他任務無法執行。
ajax的同步請求就會導致瀏覽器產生假死,因爲它會鎖定瀏覽器的UI(按鈕,菜單,滾動條等),並阻塞所有用戶的交互,jquery中的ajax有這樣一個同步請求的功能,一定要慎用,尤其是在請求的數據量很大的時候,要避免使用同步請求。
舉幾個栗子🌰感受一下異步
後臺接口使用easy-mock,官方地址:https://easy-mock.com/
ajax使用axios,基本代碼如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>javascript異步</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button>點擊</button>
<script>
{
let myData = null
//ajax請求
function ajax() {
axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
.then(data => {
console.log("ajax返回成功");// handle success
myData = data.data
console.log(myData);
})
.catch(error => {
// console.log(error); // handle error
console.log("ajax返回失敗");
})
}
}
</script>
</body>
</html>
我們通過添加一些js來看下效果,
異步-定時器
console.log(myData);
setTimeout(() => {
console.log('定時器');
}, 2000);
console.log(myData);
輸出,應該沒什麼懸念
//null
//null
//定時器
執行順序:
先執行第一個 console.log(myData);
然後遇到了定時器,將定時器掛起(就是暫停了這個定時器)
繼續執行第二個 console.log(myData);
沒有可以執行的js代碼,回頭把掛起的任務繼續執行下去
繼續看下一個栗子
異步-ajax
console.log(myData);
ajax()
console.log(myData);
看下輸出,依然沒有懸念
//null
//null
//ajax返回成功
//{success: true, data: {…}}(這是接口返回的數據,我們不必關心返回的具體內容,只要知道返回了就好,陌上寒注)
執行順序和上面的定時器基本類似,不在此贅述。
將兩個栗子合併,我們看下
console.log(myData);
ajax()
setTimeout(() => {
console.log('定時器');
}, 2000);
console.log(myData);
輸出,
//null
//null
//ajax返回成功
//{success: true, data: {…}}
//定時器
發現問題了嗎?兩個異步函數相遇了,先執行誰?誰跑的快就先執行誰?
也可以這麼說,其實這引發了另外一個知識點,
任務隊列和事件循環
兩個 console.log(myData);是同步執行的,他們都在js的主線程上執行,
在主線程之外還存在一個任務隊列,任務隊列中存放着需要異步執行的內容
當主線程運行完畢之後,就會去執行任務隊列中的任務(不斷的重複掃描)直到任務隊列清空
觀察這段代碼
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
console.log(3);
輸出:1,3,2,這沒什麼可解釋的
再看一段代碼
setTimeout(function(){console.log(1);}, 0);
console.log(2);
輸出:2,1,爲什麼會這樣?
console.log(2);在主線程中,先執行,
setTimeout(function(){console.log(1);}, 0);放在了任務隊列中,
只有在主線程執行完了纔會去執行任務列隊中的內容
爲什麼主線程的任務執行完了後需要不斷的掃描任務列隊中的內容呢?
看這段代碼,有助於你的理解
console.log(myData);
ajax()
setTimeout(() => {
console.log('定時器');
}, 2000);
console.log(myData);
const btn = document.querySelector('button')
btn.onclick = () => {
console.log("點擊了");
}
我們爲button按鈕添加了點擊事件,在瀏覽器刷新的同時不停地對按鈕進行點擊操作(當然是手動點擊)
看下輸出:
//null
//null
//(10次輸出)點擊了
//ajax返回成功
//{success: true, data: {…}}
//定時器
//點擊了
這樣是不是可以理解爲什麼主線程要去循環掃描任務列隊了?
事件循環的每一輪稱爲一個tick(有沒有聯想到vue中的nextTick?)
當產生用戶交互(鼠標點擊事件,頁面滾動事件,窗口大小變化事件等等),ajax,定時器,計時器等,會向事件循環中的任務隊列添加事件,然後等待執行,
前端異步有哪些場景?
- 定時任務:setTimeout,setInverval
- 網絡請求:ajax請求,img圖片的動態加載
- 事件綁定或者叫DOM事件,比如一個點擊事件,我不知道它什麼時候點,但是在它點擊之前,我該幹什麼還是幹什麼。用addEventListener註冊一個類型的事件的時候,瀏覽器會有一個單獨的模塊去接收這個東西,當事件被觸發的時候,瀏覽器的某個模塊,會把相應的函數扔到異步隊列中,如果現在執行棧中是空的,就會直接執行這個函數。
- ES6中的Promise
什麼時候需要異步:
- 在可能發生等待的情況
- 等待過程中不能像alert一樣阻塞程序的時候
- 因此,所有的“等待的情況”都需要異步
一句話總結就是需要等待但是又不能阻塞程序的時候需要使用異步
異步和並行
千萬不要把異步和並行搞混了,
異步是單線程的,並行是多線程的
異步:主線程的任務以同步的方式執行完畢,纔會去依次執行任務列隊中的異步任務
並行:兩個或多個事件鏈隨時間發展交替執行,以至於從更高的層次來看,就像是同時在運行(儘管在任意時刻只處理一個事件)
參考鏈接
關於js中的同步和異步
異步和單線程——什麼時候需要異步,前端使用異步的場景
Javascript異步編程的4種方法