最近碰到了非同步編程的問題,決定從原理開始重新擼一遍,徹底弄懂非同步編程。 1.非同步編程思想 非同步編程是為瞭解決同步模式的一些痛點,同步模式中任務是依次執行,後一個任務必須要等待前一個任務結束後才能開始執行,當某個函數耗時過長時就可能造成頁面的假死和卡頓,而非同步編程中,後一個任務不會去等待前一個任務結束 ...
最近碰到了非同步編程的問題,決定從原理開始重新擼一遍,徹底弄懂非同步編程。
1.非同步編程思想
非同步編程是為瞭解決同步模式的一些痛點,同步模式中任務是依次執行,後一個任務必須要等待前一個任務結束後才能開始執行,當某個函數耗時過長時就可能造成頁面的假死和卡頓,而非同步編程中,後一個任務不會去等待前一個任務結束後才開始,當前一個任務開啟過後就立即往後執行下一個任務。耗時函數的後續邏輯會通過回調函數的方式定義。在內部,耗時任務完成過後就會自動執行傳入的回調函數。
2.同步與非同步
同步行為對應記憶體中順序執行的處理器指令,每條指令都會嚴格按照出現的順序來執行,而每條指令執行後也能立即獲得儲存在系統本地的信息.這樣的執行流程容易分析程式在執行到代碼任意位置時的狀態.
如下例子:
///同步模式 console.log('global begin') function bar () { console.log('bar task') } function foo () { console.log('foo task') bar() } foo() console.log('global end') // 程式列印輸出: // global begin // foo task // bar task // global end
在程式執行的每一步都可以推斷程式的狀態,因為後面的指令需要前面的完成後才執行.等到最後一條指令執行完畢,存儲在X的值就可以立即使用.這兩行代碼首先操作系統會在棧記憶體上分配一個儲存浮點數值的空間,然後針對這個值做一次數學計算,再把計算結果寫回之前分配的記憶體中.這些指令都是單線程中按順序執行的.
非同步行為就是下達這個任務開啟的指令之後代碼就會繼續執行,代碼不會等待任務的結束,如下例子:
// 非同步模式 console.log('global begin') // 延時器 setTimeout(function timer1 () { console.log('timer1 invoke') }, 1800) // 延時器中又嵌套了一個延時器 setTimeout(function timer2 () { console.log('timer2 invoke') setTimeout(function inner () { console.log('inner invoke') }, 500) }, 1000) console.log('global end') // global begin // global end // timer2 invoke // inner invoke // timer1 invoke
3.以往的非同步編程模式
在早期的js中只支持定義回調函數來表明非同步操作的完成.串聯多個非同步操作是一個常見的問題,通常需要深度嵌套的回調函數來解決,這樣會造成回調地獄。
回調函數的理解:某個方法a自己沒調用,但是在另一個方法b被調用的時候,順便把方法a給調用了,那麼方法a就是一個回調函數
function double(value, callback) { setTimeout(() => { callback(value * 2) }, 2000) } double(3, (x) => { console.log(`回來的數值為:${x}`); }) //回來的數值為:6 //會在2000毫秒後列印
這個的setTimeout調用告訴js運行時在2000毫秒後把一個函數推到消息隊列.這個函數會由運行時負責非同步調用執行,而位於函數閉包中的回調及其參數在非同步執行時仍然時可用的.
4.嵌套非同步回調(回調地獄)
如果非同步返回值又依賴另一個非同步返回值,那麼回調的情況還會進一步變複雜,隨著代碼越來越複雜,回調策略是不具有擴展性,維護起來很麻煩.
// 第一參數為值 // 第二參數為正確的回調 // 第三參數為失敗的回調 function double(value, success, failure) { setTimeout(() => { try { if (typeof value !== 'number') { throw '必須提供數字作為第一個參數' } success(value * 2) } catch (e) { failure(e) } }, 2000) } const successCallback = (x) => double(x, (y) => { console.log(`success:${y}`); }) const failureCallback = (e) => console.log(`failure:${e}`); double(3, successCallback, failureCallback)//success:12
5.ES6的解決方案
ES6中推出了一個處理非同步的對象:Promise
Promise解決方式:
Promise 對象代表了未來將要發生的事件,用來傳遞非同步操作的消息。
Promise 對象代表一個非同步操作,有三種狀態:
- pending: 初始狀態,不是成功或失敗狀態。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
Promise是一個對象,它的參數是一個回調函數,這個回調函數裡面有兩個參數:resolve和reject,分別代表成功狀態fulfilled執行的代碼和失敗狀態rejected執行的代碼
需要註意的是,Promise裡面的代碼本來是當作同步代碼執行的,但是一旦遇到非同步代碼,就會掛起為pending,然後才有後面的非同步操作
Promise解決問題:
問題:從後臺獲取商品數量,並計算商品總價格。
// 這裡我用setTimeout模擬非同步請求,使用random函數隨機獲取商品的數量(假設每件商品的單價為50),最後需要輸出總價 new Promise((resolve,reject) => { console.log("開始請求數據。。。"); setTimeout(() => { // 這裡,我們考慮伺服器傳來錯誤的數據,比如負的數量,然後進入reject裡面 let count = parseInt(Math.random() * 10) - 5; // [-5,5) if (count >= 0){ resolve(count); }else{ reject(count); } }, 1000); }).then(data => { let price = 50; console.log(`商品的數量為:${data},總價為:${data * price}`) }).catch(err => { console.log(`錯誤的商品數量:${err}`) })
註意:需要註意的是,then只能寫在promise對象上,所以如果想寫多個then,則需要在每個then裡面重新編寫一個promise對象,然後return出去,就可以繼續往下寫then了
多個promise
// 多個promise的編寫 new Promise((reslove, reject) => { console.log(`開始向後臺獲取商品數量。。。`) setTimeout(() => { let count = parseInt(Math.random() * 10) - 5; if (count >= 0) { reslove(count); } else { reject(count); } }, 2000); }).then(res => { console.log(`商品數量正確,為:${res}`) // 開啟一個新的promise,用於計算商品的總價是否大於99(假設商品單價為50) let totalPrice = res * 50; let p = new Promise((reslove, reject) => { if (totalPrice > 99) { reslove(totalPrice); } else { reject(totalPrice); } }) // 如果想繼續往下編寫then的話,這裡必須要返回一個promise對象,因為then必須要學在promise對象後面 return p; }).then(res => { console.log(`商品的總價超過了99,打9折,打折後的價格為:${res * 0.9}`); }).catch(err => { if (err >= 0) { // 如果參數是大於等於0的,說明不是第一個promise的reject console.log(`商品的總價沒超過99,不打折,價格為:${err}`) } else { console.log(`商品的數量不能為負數,${err}`) } })
但是,如果then多了還是不夠優雅,看著不習慣,因為一般我們的閱讀代碼的習慣是一行一行的從嚮往下看,如果可以將非同步代碼寫成這種形式就好了,ES7就實現了這個功能,使用async/await
6.async/await解決方式
async其實可以理解為promise的語法糖,它將promise的編寫形式改寫為人們更容易理解的串列代碼的形式,使得非同步代碼像同步代碼
async 是一個修飾符,被它修飾的函數叫非同步函數,會預設的返回一個 Promise 的 resolve的值。
await也是一個修飾符,它後面修飾的函數必須返回一個promise對象,它將非同步代碼轉換為同步結果,會阻塞線程,執行完成後才能執行後面的代碼,但是必須用在被async修飾的函數裡面
// 使用async/await修改之前的promise // 獲取商品數量的方法 let getCount = () => { return new Promise((resolve, reject) => { console.log("開始請求數據。。。"); setTimeout(() => { // 這裡,我們考慮伺服器傳來錯誤的數據,比如負的數量,然後進入reject裡面 let count = parseInt(Math.random() * 10) - 5; // [-5,5) if (count >= 0) { resolve(count); } else { reject(count); } }, 1000); }) } // 計算出總價的方法 let totalPrice = (count) => { console.log("開始計算商品總價。。。"); let p = new Promise((resolve,reject) => { setTimeout(() => { let price = count * 50; if (price >= 99){ resolve(price); }else{ reject(price); } }, 1000); }) return p; } let getPrice = async () => { try{ // 使用await執行 let count = await getCount(); console.log(`商品的數量為:${count}`); let price = await totalPrice(count); console.log(`商品的總價是:${price}`); }catch(error){ console.log(`進入reject,error:${error}`) } } getPrice();
這裡除去上面的兩部非同步請求的方法,可以看到最後面那個async修飾的函數,裡面使用await,非常的易於理解,就像是同步代碼一樣,下一行代碼必須等上一次代碼執行完成才能繼續執行。說的底層一點就是await會阻塞線程,延遲執行await語句後面的語句。