1. 程式人生 > 實用技巧 >【js】同步和非同步知識總結

【js】同步和非同步知識總結

  • 同步API
    • 連續的執行,就叫做同步
      • 連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。
    • 同步API:只有當前API執行完成後,才能繼續執行下一個API
    • 前一個任務結束後再執行後一個任務,程式的執行順序與任務的排列順序是一致的、同步的。
    • 同步任務都在主執行緒上執行,形成一個執行棧。
  • 非同步API
    • 所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
    • 比如,有一個任務是讀取檔案進行處理,非同步的執行過程就是下面這樣。
      • 任務的第一段是向作業系統發出請求,要求讀取檔案。
      • 然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)
    • 這種不連續的執行,就叫做非同步。
    • 非同步API:當前API的執行不會阻塞後續程式碼的執行
    • JS 的非同步是通過回撥函式實現的。
    • 非同步任務相關回調函式新增到任務佇列中(任務佇列也稱為訊息佇列)。
    • 一般而言,非同步任務有以下三種類型:
      • 1、普通事件,如click、resize等
      • 2、資源載入,如load、error等
      • 3、定時器,包括setinterval、setTimeout等
  • js執行機制
    • 1.先執行,執行棧中的同步任務
      • 當遇到非同步任務,將其放入回撥函式佇列中,繼續執行下面的同步任務
    • 2.非同步任務(回撥函式),非同步程序處理,將其放入回撥函式佇列中
    • 3.一旦執行棧中的所有同步任務執行完畢,系統就會按次序讀取回調函式佇列中的非同步任務,於是被讀取的非同步任務結束等待狀態,進入執行棧,開始執行。
      • 執行棧中同步任務執行完後,將回調函式佇列中的非同步任務放入,執行棧最下方開始執行
    • 非同步程序處理
      • 在主執行緒任務棧,遇到非同步任務,會將其提交給非同步程式碼執行區,當非同步任務被觸發後,非同步程序處理會將其提交個回撥函式佇列
      • 當主執行緒任務棧中同步任務執行完後,將回調函式佇列中的非同步任務放入,執行棧最下方開始執行
      • 當將放入執行棧的非同步任務也執行完後,主執行緒還會去任務佇列檢視是否還有任務可執行
    • 事件迴圈
      • 由於主執行緒不斷的重複獲得任務、執行任務、再獲取任務、再執行,所以這種機制被稱為事件迴圈(eventloop)。
  • 回撥函式
    • JavaScript 語言對非同步程式設計的實現,就是回撥函式。
    • 所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。
    • 比如:讀取檔案進行處理,是這樣寫的
      • readFile 函式的第二個引數,就是回撥函式,也就是任務的第二段。
      • 等到作業系統返回了 /etc/passwd 這個檔案以後,回撥函式才會執行。
    • 回撥函式的作用就是,將非同步程式碼寫在一個塊級作用域中,當其中的非同步函式執行後,才會呼叫回撥函式,這時非同步函式已經執行完成,所以呼叫的回撥函式中程式碼可以依次執行
      • 通過在函式形參中拿到的回撥函式引用地址,可以呼叫回撥函式並傳遞引數給回撥函式的形參,這樣回撥函式就可以獲得被呼叫函式中的資訊
  • Promise
    • 回撥地獄
      • 回撥函式本身並沒有問題,它的問題出現在多個回撥函式巢狀
      • 比如:假定讀取A檔案之後,再讀取B檔案,程式碼如下。
        • 不難想象,如果依次讀取多個檔案,就會出現多重巢狀。程式碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。這種情況就稱為“回撥地獄”(callback hell)。
    • Promise就是為了解決這個問題而提出的。
      • 在ES6中提供了Promise,它不是新的語法功能,而是一種新的寫法,允許將回調函式的橫向載入,改成縱向載入。
    • 語法
      • Promise實際上是一個建構函式,所以需要先使用new關鍵字建立例項
    • 引數
      • Promise的建構函式接收一個函式的引數,此函式有兩個引數:resolve , reject,分別表示非同步操作執行成功和非同步執行失敗的後的回撥函式。
        • resolve
          • resolve是將Promise的狀態置為fullfiled,fullfiled狀態將觸發then方法
          • 非同步操作執行成功可以使用resolve帶回一個實參
          • resolve實際上是一個回撥函式,可以將資料作為實參傳遞出非同步API
          • Promise物件呼叫then方法,then接收一個函式引數,並且會拿到Promise執行成功回撥resolve函式的引數。
            • 就相當於回撥函式帶回非同步API中資料
        • reject
          • reject是將Promise的狀態置為rejected,rejected狀態將觸發catch方法
          • 非同步操作執行失敗可以使用reject帶回一個實參
          • reject實際上是一個回撥函式,可以將資料作為實參傳遞出非同步API
          • Promise物件呼叫catch方法,catch接收一個函式引數,並且會拿到Promise執行成功回撥 reject函式的引數。
            • 就相當於回撥函式帶回非同步API中資料
    • Promise方法
      • Promise 提供了 .then(onFulfilled, onRejected) 和 .catch(onRejected) 方法用於註冊監聽 Promise 物件的狀態變更。
      • Promise物件呼叫then方法,then接收一個函式引數,並且會拿到Promise執行成功回撥resolve函式的引數。
        • then()方法是非同步執行。
        • 意思是:就是當.then()前的方法執行完後再執行then()內部的程式,這樣就避免了,資料沒獲取到等的問題。
        • 語法
          • promise.then([onFulfilled][, onRejected])
          • 其中onFulfilled,就是resolve狀態的回撥函式,用於指定發生錯誤時的回撥函式
          • onRejected就是reject
        • 就相當於回撥函式帶回非同步API中資料
        • 引數
          • 1.如果引數是 Promise 例項,那麼 promise.resolve 將不做任何修改、原封不動地返回這個例項。
          • 2.如果引數是一個 thenable 物件。
            • thenable 物件指的是具有 then 方法的物件
            • 比如:
            • Promise.resolve 方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then方法。
          • 3.如果引數是一個原始值,或者是一個不具有then方法的物件,則 Promise.resolve方法返回一個新的 Promise 物件,狀態為resolved。
            • 比如:下面程式碼生成一個新的 Promise 物件的例項p。
              • 由於字串Hello不屬於非同步操作(判斷方法是字串物件不具有 then 方法),返回 Promise 例項的狀態從一生成就是resolved,所以回撥函式會立即執行。
              • Promise.resolve方法的引數,會同時傳給回撥函式。
          • 4.Promise.resolve 方法允許呼叫時不帶引數,直接返回一個 resolved 狀態的 Promise 物件。
            • 需要注意的是,立即 resolve() 的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時執行,而不是在下一輪“事件迴圈”的開始時。
        • 返回值
          • 返回了一個值,那麼then返回的 Promise 將會成為接受狀態,並且將返回的值作為接受狀態的回撥函式的引數值。
          • 沒有返回任何值,那麼then返回的 Promise 將會成為接受狀態,並且該接受狀態的回撥函式的引數值為undefined。
          • 返回一個已經是接受狀態的 Promise,那麼then返回的 Promise 也會成為接受狀態,並且將那個 Promise 的接受狀態的回撥函式的引數值作為該被返回的Promise的接受狀態回撥函式的引數值。
          • 返回一個已經是拒絕狀態的 Promise,那麼then返回的 Promise 也會成為拒絕狀態,並且將那個 Promise 的拒絕狀態的回撥函式的引數值作為該被返回的Promise的拒絕狀態回撥函式的引數值。
          • 返回一個未定狀態(pending)的 Promise,那麼then返回 Promise 的狀態也是未定的,並且它的終態與那個 Promise 的終態相同;同時,它變為終態時呼叫的回撥函式引數與那個 Promise 變為終態時的回撥函式的引數是相同的。
      • Promise物件呼叫catch方法,catch接收一個函式引數,並且會拿到Promise執行成功回撥 reject函式的引數。
        • .catch() 等價於 .then(null, onRejected),用於指定發生錯誤時的回撥函式
        • 語法
          • promise.catch(error => console.log(error); // 失敗了
        • 就相當於回撥函式帶回非同步API中資料
    • Promise實際上就是在原本的非同步API中包裹一層函式,其中Promise引數函式的resolve , reject兩個引數,實際上和普通的回撥函式一樣,都接受一個回撥函式作為實參,而在執行時返回一個實參給呼叫他的then或catch方法,這樣就會獲得非同步API中的執行結果
    • 我們可以把任意一個非同步操作過程包裹進 Promise 的回撥函式體裡面,當非同步操作成功或失敗時分別呼叫 resolve(value) 或 reject(error) 變更 Promise 物件的狀態,這樣就成功地把任意形式的非同步操作狀態轉換為格式統一的 Promise 狀態。
      • 例項化的 promise 物件維護著一個狀態,resolve() 和 reject() 則是用於觸發 promise 變更的開關。
    • Promise能把原來的回撥寫法分離出來,在非同步操作執行完後,用鏈式呼叫的方式執行回撥函式。
      • Promise相對於普通的回撥函式(callback)來說從從表面上來說可以簡化層層回撥的寫法,Promise的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞callback函式要簡單、靈活的多。
    • 控制Promise的執行順序
      • 使用Promise雖然可以將非同步API的執行結果傳遞出來,但是無法控制非同步API的執行順序
      • 只是new了一個物件,並沒有呼叫它,我們傳進去的函式就已經執行了
      • 所以我們用Promise的時候一般是包在一個函式中,在需要的時候去執行這個函式
        • 在Promise的外面再巢狀一層函式,就可以實現控制Promise的執行
    • Promise的鏈式程式設計
      • Promise只是能夠簡化層層回撥的寫法,而實質上,Promise的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞callback函式要簡單、靈活的多。
      • Promise 的鏈式呼叫是通過 .then() 和 .catch() 方法實現的,其中 .catch() 等價於 .then(null, onRejected),因此我們在接下來的內容當中只需研究 .then() 方法的性質即可。
      • .then() 方法除了用於註冊監聽函式之外,本身也會建立並返回一個 Promise 物件,這個 Promise 物件用於表徵回撥函式的執行情況。
        • 當回撥函式執行成功時(內部的resolve函式呼叫) Promise 狀態將變更為 'fulfilled',執行過程丟擲異常 (內部的rejected函式呼叫)Promise 狀態則變成 'rejected'
      • 透傳
        • 在鏈式呼叫過程當中,假如某個環節的 Promise 不存在相應狀態的監聽回撥函式,那麼這個 Promise 的狀態將會往下透傳
          • .then 或者 .catch 的引數期望是函式,傳入非函式則會發生值穿透
        • 即,Promise中回撥函式執行,卻沒有相應的then()或catch()處理,所以會將上一個 Promise 的狀態持續透傳了下去。
      • Promise 值傳遞
        • .then() 方法會將回調函式的執行結果(then中回撥函式的return)記錄下來,並作為下一個 onFulfilled (resolve成功狀態)回撥的引數將其傳遞下去
          • 由於回撥函式可以返回任何結果,甚至返回一個 Promise 物件也是可行的。
        • 回撥函式所丟擲的錯誤將作為下一個 onRejected 的引數傳遞下去
  • Generator函式
    • 協程思想
      • 傳統的程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案),其中有一種叫做“攜程”(coroutine),意思是多個執行緒互相協作,完成非同步任務。
      • 協程有點像函式,又有點像執行緒。它的執行流程大致如下:
        • 第一步,協程A開始執行。
        • 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
        • 第三步,(一段時間後)協程B交還執行權。
        • 第四步,協程A恢復執行。
      • 上面流程的協程A,就是非同步任務,因為它分成兩段(或多段)執行。
      • 舉例來說,讀取檔案的協程寫法如下:
        • 上面程式碼的函式 asyncJob 是一個協程,它的奧妙就在其中的 yield 命令。
        • 它表示執行到此處,執行權將交給其他協程,也就是說,yield命令是非同步兩個階段的分界線。
        • 協程遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行,它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
    • 概念
      • Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)
      • 下面程式碼就是一個 Generator 函式。它不同於普通函式,是可以暫停執行的,所以函式名之前要加星號,以示區別。
    • 語法
      • Generator(生成器)是一類特殊的函式,跟普通函式宣告時的區別是function後加了一個*號
      • 整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器,非同步操作需要暫停的地方,都用yield 語句註明
    • 返回值
      • 呼叫 Generator 函式,會返回一個內部指標(即遍歷器),即執行它不會返回結果,返回的是指標物件
        • 上面程式碼中,呼叫 Generator 函式,會返回一個內部指標(遍歷器)g
      • 呼叫指標 的next 方法,會移動內部指標(即執行非同步任務的下一個yield 語句),指向下一個遇到的 yield 語句
      • next 方法的作用是分階段執行 Generator 函式
        • 每次呼叫 next 方法,會返回一個物件,表示當前階段的資訊( value 屬性和 done 屬性)。
        • value 屬性是 yield 語句後面表示式的值,表示當前階段的值;
        • done 屬性是一個布林值,表示 Generator 函式是否執行完畢,即是否還有下一個階段。
      • next()會比yield多了一個
        • 進行例項化之後,Generator函式裡的程式碼不會主動執行。
        • 第一個next()永遠是用於啟動生成器,生成器啟動後要想執行到最後,其內部的每個yield都會對應一個next(),所以說next()永遠都會比yield多一個了
    • 訊息傳遞
      • 通過yield ...和next(...)組合使用,可以在生成器的執行過程中構成一個雙向訊息傳遞系統。
      • next 方法可以接受引數,這是向 Generator 函式體內輸入資料
        • 第一個 next 方法的 value 屬性,返回表示式 x + 2 的值(3)。
        • 第二個 next 方法帶有引數2,這個引數可以傳入 Generator 函式,作為上個階段非同步任務的返回結果,被函式體內的變數 y 接收。
      • 當next(..)執行到yield語句處時會暫停生成器的執行,同時next(...)會得到一個帶有value屬性的物件,yield語句後面帶的值會賦給value(如果yield後面沒有值,value就為undefined)。可以將yield ...效果看成跟return ...類似。
      • 當生成器處於暫停狀態時,暫停的yield表示式處可以接收下一個啟動它的next(...)傳進來的值。當next(...)使生成器繼續往下執行時,其傳入的值會將原來的yield語句替換掉。
    • 異常處理
      • Generator 函式內部還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤
      • 使用指標物件的 throw 方法丟擲的錯誤,可以被函式體內的 try ... catch 程式碼塊捕獲
        • 上面程式碼的最後一行,Generator 函式體外,使用指標物件的 throw 方法丟擲的錯誤,可以被函式體內的 try ... catch 程式碼塊捕獲
  • Generator+Promise實現完美非同步
    • 例子:
    • 當多個Promise併發請求時,正確的寫法可以更好地提高效能
      • 請求1和2之間不存在依賴關係,是可以同時進行的
  • 非同步函式(async和await)
    • ES7中新增加了非同步函式的部分
    • 優化了promise中臃腫冗餘的部分封裝成API
    • 非同步函式是非同步程式設計語法的終極解決方案,它可以讓我們將非同步程式碼寫成同步的形式,讓程式碼不再有回撥函式巢狀,使程式碼變得清晰明瞭
    • 所謂非同步函式,實際上就是在普通函式定義的前面新增async關鍵字,這樣普通函式就會變成非同步函式
    • 語法
      • 在普通函式定義的前面新增async關鍵字,將其變成非同步函式
    • 返回值
      • 普通函式預設的返回值是undefined,而非同步函式的預設返回值是在原有返回值的基礎上,包裹了promise物件
      • 在非同步函式內部使用return關鍵字進行結果返回 結果會被包裹的promise物件中return關鍵字代替了resolve方法
    • 非同步函式返回promise物件可以使用then方法
      • 呼叫非同步函式再鏈式呼叫then方法獲取非同步函式執行結果
      • 非同步函式呼叫then方法,then中的回撥函式引數就是非同步函式的返回值
    • 丟擲異常 throw
      • 在非同步函式內部使用throw關鍵字丟擲程式異常
      • 在非同步函式中使用throw關鍵字,可以丟擲異常,將錯誤資訊丟擲到非同步函式外
      • 非同步函式返回promise物件可以使用catch方法
        • 呼叫非同步函式再鏈式呼叫catch方法獲取非同步函式執行的錯誤資訊
        • 非同步函式呼叫catch方法,catch中的回撥函式引數就是非同步函式中丟擲異常的資訊
      • throw關鍵字會導致非同步函式暫停,不在執行後續程式碼
    • await關鍵字
      • await關鍵字將會拿到promise物件中傳遞出來的回撥引數
      • await關鍵字只能出現在非同步函式中,也就是說,當使用await獲取結果的時候可能會出現被迫多次寫async的情況
      • await promise
        • await後面只能寫promise物件寫其他型別的API是不可以的
      • 使用await關鍵字可以讓promise物件脫離(起到呼叫promise物件then方法作用)
      • await 可以暫停非同步函式的執行,等待promise物件返回結果後再向下執行後續結果
        • 也就是說,使用await關鍵字可以有序的讓非同步函式執行
    • promisify方法
      • 只有非同步函式才能使用await,且await必須跟在promise物件前,而原有API並沒有返回promise物件
      • promisify方法可以包裝現有的非同步API函式,將其變為非同步函式
      • promisify方法存放在系統模組,util中
      • 語法
        • promisify(要改造的API函式)
        • 注意這裡是改造,不是呼叫,所以引數應該為引用
      • 返回值
        • promisify返回處理好的API函式,即一個返回promise物件的非同步函式