大前端進擊之路(二):JavaScript異步編程

2021-02-19 前端複習課

打工人!打工魂!前端才是人上人!此系列總結於大前端進擊之路過程中的學習,如果文章中有不對的地方,希望大家能進行批評改正,互相進步。

經典面試題

我們先來看一道經典的面試題,讓我們的小腦袋瓜子思考起來~如果你對這道題有清晰的思路並且了解背後的原因,那麼請直接點讚評論加關注!!!!!

//請寫出輸出內容
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
 console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

答案!你答對了嗎?沒對的不要跑,睜大你的小眼睛仔細看以下的內容

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

JS採用單線程模式工作的原因

為了回答這個問題我們首先需要知道JS的執行環境是單線程的,是因為JS語言最早是運行在瀏覽器端的語言,目的是為了實現頁面上的動態交互。實現動態交互的核心就是DOM操作,因此決定了JS必須是單線程模式工作。我們來假設一下如果JS是多線程一起工作的,其中一個線程修改了一個DOM元素,另外的一個線程同時又要刪除這個DOM元素,那麼此時瀏覽器就懵逼了,無法明確以哪個工作線程為準。所以為了避免線程同步的問題,JS就被設計成了單線程的工作模式。

注意,我們這裡說的單線程是JS的執行環境是單線程,瀏覽器中是多線程的。

單線程的優勢和弊端

採用單線程的工作模式可以節省內存,節約上下文切換時間,沒有鎖的問題。但弊端也很明顯,如果中間有一個任務需要花費大量的時間,那麼後面的任務就需要等待這個任務完成後才能執行,就會出現假死的情況,對用戶很不友好。為了解決這個問題JS給出了兩種執行模式:同步模式(Synchronous)和異步模式(Asynchronous)

同步模式和異步模式同步模式

同步模式其實很好理解,舉個慄子:

我們如果按照同步模式煮麵的話,首先先將鍋裡裝上水,打開火開始燒水,等待水燒開,再將面、雞蛋、火腿腸等材料拿出,材料準備好後放入鍋中進行煮,煮好後開始乾飯。

在這裡其實我們已經能夠看出來問題,我們必須等到水燒開後才去準備要煮的材料。回到概念裡就是在同步模式下我們的代碼是依次執行,後一個任務必須等待前一個任務結束才能開始執行。程序執行的順序和代碼編寫的順序是完全一致的。在單線程模式下,大多數任務都是以同步模式執行。

異步模式

上個例子中我們在等待水燒開的過程中什麼都沒幹,很浪費時間,我們可以在燒水的過程中將食材都準備好,等到水燒開後直接放入。

我們在燒水的過程中去幹了別的事情,就屬於異步模式,異步模式中不會等待異步任務的結束才開始執行下一個同步的任務,都是開啟過後就立即執行下一個任務。

異步模式對於JS很重要,沒有異步模式的話我們就無法同時處理大量的耗時任務,就會給用戶帶來卡頓和假死的體驗。對於我們開發者來說,會給我們打開代碼執行的順序混亂的問題。

EventLoop事件循環和消息隊列EventLoop是一種循環機制,主線程從消息隊列中讀取任務並按照順序執行,這個過程是循環不間斷的。消息隊列是存放異步任務的地方,當我們的同步任務都執行完畢後,EventLoop會從消息隊列中依次取出異步任務放到調用棧中進行執行。宏任務和微任務

宏任務可以理解為每次執行棧執行的代碼就是一個宏任務

瀏覽器為了讓JS內部宏任務與DOM操作能夠有序的執行,會在一個宏任務執行結束後,下一個宏任務執行開始前,對頁面進行重新渲染。

宏任務包括:script整體代碼、setTimeout、setInterval、I/O、UI交互事件、MessageChannel等。

微任務可以理解為每個宏任務執行結束後立即執行的任務,發生在宏任務後,渲染之前,執行微任務。

所以微任務的響應速度相比宏任務會更快,因為無需等待UI渲染

微任務包括:Promise.then、MutaionObserver、process.nextTick(Node.js環境下)等。

異步編程方案的本質—回調函數

回調函數:由調用者定製,交給執行者執行的函數。

我們通過 callback 回調函數、事件發布/訂閱、Promise 等來組織代碼,本質都是通過回調函數來實現異步代碼的存放與執行。

// callback就是回調函數
// 就是把函數作為參數傳遞,缺點是不利於閱讀,執行順序混亂。
function foo(callback) {
    setTimeout(function(){
        callback()
    }, 3000)
}

foo(function() {
    console.log('這是回調函數')
    console.log('調用者定義這個函數,執行者執行這個函數')
    console.log('其實就是調用者告訴執行者異步任務結束後應該做什麼')
})


更優異步編程統一方案——PromisePromise概述

Promise概念MDN傳送門

關於Promise概念性內容就不在贅述了,可直接點擊傳送門前往MDN查看。簡單來說如果我們是用傳統的回調函數方式來完成複雜的異步流程,就會無法避免大量的回調函數嵌套,產生回調地獄的問題。為了避免回調地獄讓我們開始愉快的Promise的學習時光吧!

// 我們想要執行完第一個再執行第二個再執行第三個
// 雖然我們使用同步的方式將異步的代碼學出來了,但是這樣的回調是不是讓我們的小腦袋瓜子嗡嗡的?
setTimeout(() => {
    console.log('執行第一個');
    setTimeout(() => {
        console.log('執行第二個');
        setTimeout(() => {
            console.log('執行第三個');
            setTimeout(() => {
                console.log('執行第四個');
                setTimeout(() => {
                    console.log('執行第五個');
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
}, 2000);


回調地獄圖示,取自網絡,侵即刪。

Promise基本用法
// Promise 基本示例
// promise的英文意思是承諾
// 在JS中Promise是一個對象,接收一個函數作為參數
const promise = new Promise(function (resolve, reject) {
  // 這裡用於「兌現」承諾
  // resolve(100) // 承諾達成
  reject(new Error('promise rejected')) // 承諾失敗
})

promise.then(function (value) {
  // 即便沒有異步操作,then 方法中傳入的回調仍然會被放入隊列,等待下一輪執行
  console.log('resolved', value)
}, function (error) {
  console.log('rejected', error)
})


Promise案例

我們用Promise來封裝一個AJax

function ajax (url) {
  return new Promise((resolve, rejects) => {
    // 創建一個XMLHttpRequest對象去發送一個請求
    const xhr = new XMLHttpRequest()
    // 先設置一下xhr對象的請求方式是GET,請求的地址就是參數傳遞的url
    xhr.open('GET', url)
    // 設置返回的類型是json,是HTML5的新特性
    // 我們在請求之後拿到的是json對象,而不是字符串
    xhr.responseType = 'json'
    // html5中提供的新事件,請求完成之後(readyState為4)才會執行
    xhr.onload = () => {
      if(this.status === 200) {
        // 請求成功將請求結果返回
        resolve(this.response)
      } else {
        // 請求失敗,創建一個錯誤對象,返回錯誤文本
        rejects(new Error(this.statusText))
      }
    }
    // 開始執行異步請求
    xhr.send()
  })
}

ajax('/api/user.json').then((res) => {
  console.log(res)
}, (error) => {
  console.log(error)
})


Promise的鏈式調用誤區嵌套使用的方式是使用Promise最常見的誤區。我們要使用promise的鏈式調用的方法儘可能保證異步任務的扁平化。
// 嵌套使用 Promise 是最常見的誤區
ajax('/api/urls.json').then(function (urls) {
  ajax(urls.users).then(function (users) {
    ajax(urls.users).then(function (users) {
      ajax(urls.users).then(function (users) {
        ajax(urls.users).then(function (users) {
        })
      })
    })
  })
})


鏈式調用的理解promise對象then方法,返回了全新的promise對象。可以再繼續調用then方法,如果return的不是promise對象,而是一個值,那麼這個值會作為resolve的值傳遞,如果沒有值,默認是undefined後面的then方法就是在為上一個then返回的Promise註冊回調前面then方法中回調函數的返回值會作為後面then方法回調的參數如果回調中返回的是Promise,那後面then方法的回調會等待它的結束Promise的異常處理
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .catch(function onRejected(error) {
    console.log('onRejected', error)
  })
  
// 相當於
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .then(undefined, function onRejected(error) {
    console.log('onRejected', error)
  })


簡單來說.catch是給整個promise鏈條註冊的一個失敗回調,推薦使用。

相關焦點

  • 大前端進擊之路|JavaScript異步編程
    異步模式上個例子中我們在等待水燒開的過程中什麼都沒幹,很浪費時間,我們可以在燒水的過程中將食材都準備好,等到水燒開後直接放入。我們在燒水的過程中去幹了別的事情,就屬於異步模式,異步模式中不會等待異步任務的結束才開始執行下一個同步的任務,都是開啟過後就立即執行下一個任務。
  • 重新認識javascript的settimeout和異步
    然後看了一下文章下面的評論,發現5樓和6樓的回答很有道理,主要意思就是說javascript引擎是單線程執行的,while循環那裡執行的時候,settimeout裡面的函數根本沒有執行的機會,這樣while那裡永遠為真,造成死循環。
  • 大前端進擊之路(一):函數式編程
    函數式編程概念一、什麼是函數式編程函數式編程(Functional Programming, FP),是一種編程風格,也可以認為是一種思維模式,和面向過程、面向對象是並列的關係。函數式編程是對運算過程的抽象,函數指的並不是程序中的函數或者方法,而是數學中的函數映射關係,例如:y=cos(x),是y和x的關係。
  • JavaScript 異步編程的終極演變
    以下是《JavaScript 異步編程的終極演變》內容。寫在前面有一個有趣的問題:為什麼Node.js約定回調函數的第一個參數必須是錯誤對象err(如果沒有錯誤,該參數就是null)?原因是執行回調函數對應的異步操作,它的執行分成兩段,這兩段之間拋出的錯誤程序無法捕獲,所以只能作為參數傳入第二段。
  • 從promise讀懂JavaScript異步編程
    從《setTimeout(fn,0)函數剖析JavaScript的執行機制》一文已經說明了同步和異步的區別,而這篇文章將更深入的去理解什麼是JS的異步編程,以及promise,async,await等的使用。從而更好的為前端編程打好堅實的基礎。
  • Javascript異步編程的4種方法
    為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。「異步模式」非常重要。在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,「異步模式」甚至是唯一的模式,因為執行環境是單線程的,如果允許同步執行所有http請求,伺服器性能會急劇下降,很快就會失去響應。
  • JavaScript異步編程之jsdeferred原理解析
    前言 最近在看司徒正美的《JavaScript框架設計》,看到異步編程的那一章介紹了jsdeferred這個庫,覺得很有意思,花了幾天的時間研究了一下代碼,在此做一下分享。 異步編程是編寫js的一個很重要的理念,特別是在處理複雜應用的時候,異步編程的技巧就至關重要。那麼下面就來看看這個被稱為裡程碑式的異步編程庫吧。 2.
  • JavaScript異步編程助手:Promise模式
    異步模式在Web編程中變得越來越重要,對於Web主流語言JavaScript來說,這種模式實現起來不是很利索,為此,許多JavaScript庫(比如 jQuery和Dojo、AngularJS)添加了一種稱為Promise的抽象(術語稱作Deferred模式)。
  • JavaScript 異步與 Promise 實現
    前言如果你已經對JavaScript異步有一定了解,或者已經閱讀過本系列的其他兩篇文章,那請繼續閱讀下一小節,若你還有疑惑或者想了解JavaScript異步機制與編程,可以閱讀一遍這兩篇文章:回調函數回調函數,作為JavaScript異步編程的基本單元,非常常見,你肯定對下面這類代碼一點都不陌生:component.do('purchase', funcA
  • .NET異步編程:使用CPS及yield實現異步
    【IT168 技術】在上一篇文章(.NET中的異步編程:傳統的異步編程)中我們圍觀了傳統的異步編程,感受到了異步編程不是簡單的事情。傳統的異步方式將本來緊湊的代碼都分成兩部分,不僅僅降低了代碼的可讀性,還讓一些基本的程序構造無法使用,所以大部分開發人員在遇到應該使用異步的地方都忍痛割愛。
  • 一行代碼證明編程能力,javascript程式語言中,經典語句精髓解析
    javascript程式語言中,經典語句精髓解析,一行代碼證明編程能力!程式設計師:十萬行代碼,證明編程基礎的掌握;之後,一行代碼證明編程的能力!1、if語句在javascript語言中,if條件語句是很常用到的。與其他程式語言相比,還是有差異的。
  • 大前端基本功系列 第二集
    (二): https://www.jianshu.com/p/652991a67186[25]最詳盡的 JS 原型與原型鏈終極詳解,沒有「可能是」。/blob/master/chapter6.markdown[32]JavaScript 中的繼承:ES3、ES5 和 ES6: http://tianfangye.com/2017/12/31/inheritance-in-javascript-es3-es5-and-es6/[33]你好,JavaScript異步編程---- 理解JavaScript異步的美妙: https
  • 【JavaScript】Promise函數的用法
    javascript
  • 編程老師告訴你,這5本JavaScript書籍你一定要讀!
    /5-javascript-books-you-should-read-a-coding-teachers-perspective-ecb15dfec832今天,我們將和大家推薦5本JavaScript書籍,這些書籍是由一個有著多年編程經驗的編程老師推薦的,以下是他的博客原文:作為一名老師,我在課程開發過程中需要閱讀很多東西,通常我會每周快速閱讀2-4本書,經常閱讀10-
  • JavaScript異步與Promise實現
    前言 如果你已經對JavaScript異步有一定了解,或者已經閱讀過本系列的其他兩篇文章,那請繼續閱讀下一小節,若你還有疑惑或者想了解JavaScript異步機制與編程,可以閱讀一遍這兩篇文章:JavaScript之異步編程簡述JavaScript之異步編程回調函數
  • 拆解 JavaScript 中的異步模式
    (給IT平頭哥聯盟加星標,提升前端技能)前言JavaScript 中有很多種異步編程的方式
  • JavaScript之Promise對象
    javascript是單線程語言,所以都是同步執行的,要實現異步就得通過回調函數的方式,但是過多的回調會導致回調地獄,代碼既不美觀,也不易維護,所以就有了promise;Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。
  • JS異步編程,回調函數與promise
    Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。
  • javascript優雅的異步處理爬蟲
    結果查看本文使用了promise,async,await等用來處理異步的過程,展現了其異步編程的魅力,希望能加深大家對於異步的了解,同時本人也是一個剛剛入坑前端的學生,希望大家可以和我交流。
  • javascript解決異步async、await和co庫的實現
    一、promise: 一個通過訂閱發布模式實現的可以將異步操作隊列化,按照期望的順序執行,返回符合預期的結果API,從最初一個庫到納入es規範(簡要講解)// Promise是一個構造函數,通過New來執行// Promise的參數是一個函數,函數裡面會有兩個參數resolve