1. 程式人生 > >實操ES6之Promise

實操ES6之Promise

## 箭頭函式和this 寫Promise的時候,自然而然會使用`箭頭函式`的編寫方式。箭頭函式就是.Neter們熟知的lambda函式,已經被大部分主流語言支援,也受到了廣大碼農的交口稱讚,但是Jser們卻會遇到不大不小的一個坑。 眾所周知,js函式中的`this`由呼叫它的上下文決定,我們還可以通過`apply`、`call`、`bind`等方式繫結上下文,從而改變函式中this的執行時指向。而當this遇到lambda時,又有所不同。 ```js function Test() { this.name = "alice" } Test.prototype = { normal: function () { console.info(this.name) }, lambda: () => { console.info(this.name) } } let test = new Test() test.normal() //輸出:alice test.lambda() //輸出:undefined ``` 為什麼會這樣?網上很多分析,讓人云裡霧裡。其實只要瞭解了——lambda與普通方法一個主要區別就是它能保持外層上下文變數的引用——這個特性就明白了。用lambda在方法內寫過巢狀區域性方法的.Neter很容易理解這個說法。 ```c# private Action Test() { var name = "alice"; Action action = () => Console.WriteLine(name); //name將被捕獲,會一直生存到action被回收 return action; } ``` so,可以將js的箭頭函式理解為受`執行時外層`影響的內嵌函式,它是在執行時賦值的——以上述js程式碼為例,js直譯器解釋Test.prototype的定義,解釋到normal函式時,是不care其內部邏輯的,繼續往下解釋到lambda函式時,會過一遍其內部引用到的外部變數,若有則捕獲用於真正執行時(所謂`詞法作用域`)。此時這個this指的是執行環境的根物件(在瀏覽器中可能就是window物件),而不是test物件(此時還不存在噻)。*注:本段為個人理解。* 再看一個程式碼片段,請讀者自行嘗試分析下: ```js var alice = { age: 18, getAge: function () { var aliceAge = this.age;//this是alice var getAgeWithLambda = () => this.age;//this還是alice var getAgeWithFunction = function () { return this.age;// this是window } return[aliceAge,getAgeWithLambda(),getAgeWithFunction()] } } console.info(alice.getAge()) //輸出:[18, 18, undefined] ``` --- ## Promise `Promise`主要是將原本的`callback`變為`then`,寫出來的程式碼更便於閱讀。有多種方式得到一個Promise物件: 1. `Promise.resolve()`:`Promise.resolve('foo')` 等價於 `new Promise(resolve => resolve('foo'))` 2. 執行`async`修飾的函式: ```js async function newPromise(){} let p = newPromise() //p就是Promise物件 ``` 如果async函式中是return一個值,這個值就是Promise物件中resolve的值;如果async函式中是throw一個值,這個值就是Promise物件中reject的值。 3. 直接構造: ```js let p = new Promise((resolve, reject)=>{}) ``` 注意,構造Promise時內部程式碼已經開始執行,只是把`resolve`部分掛起放到後面執行。測試程式碼如下: ```js let p = new Promise((resolve, _) => { resolve(1); console.info(2); //率先執行 }); console.info(3); p.then(num => { console.info(num); //後置執行 }); console.info(4); //輸出:2 3 4 1 ``` 所以,這跟慣常認為的整個Promise程式碼塊都後置執行不一樣,需要注意。 我們可以如上述將回調邏輯寫在then裡,也可以將邏輯移到外層變為同步執行(而非後置執行),這就需要用到`await`關鍵字了,它將阻塞當前程式碼塊,等待resolve塊執行完再往後執行。程式碼如下: ```js async function test() { let p = new Promise((resolve, _) => { resolve(1); console.info(2); }); console.info(3); let num = await p; console.info(num); console.info(4); } test() //輸出:2 3 1 4 ``` *ES6引入的Generator函式,是async/await的基礎。* ~~await讓我們能用同步寫法寫出非同步方法,但事實真的如此嗎?在C#領域,這麼說尚且沒錯。後端語言大多支援多執行緒和執行緒池,await雖然阻塞了後續程式碼的執行,但只是上下文被掛起,執行緒本身是不會被阻塞的還可以幹其它事情,await返回後甚至還可以讓其它執行緒接手,可參看本人以前的博文[async、await在ASP.NET[ MVC]中之執行緒死鎖的故事](https://www.cnblogs.com/newton/archive/2013/05/13/3075039.html)。js的話,它是單執行緒,而且它也不像go一樣有完善的協程機制,無法手動(time.sleep()、select\{\}等)切換程式碼塊執行——除非等到await返回,否則執行緒是沒機會執行其它程式碼塊的。~~ 錯誤。 注意await掛起的不是執行緒,而是resolve上下文,推測本質上還是與js的執行佇列相關,只不過await後續邏輯都排在resolve之後罷了。 ```js async function test() { let p = new Promise((resolve, _) => { setTimeout(() => { resolve(1) }, 5000) }); setTimeout(() => { console.info(2) }, 3000) let num = await p console.info(num) } test() //輸出:2 1 ``` 但使用await時仍要注意避免不必要的等待,如果前後幾個Promise沒有依賴關係(更精確的說法是,任務的發起條件不依賴其它任務的結果),那麼最好同時發起它們,並在最後`await Promise.all(promises)`。 --- ## 異常捕獲 很多文章都說`try/catch`在非同步模式下無效,其實搭配await的話還是可以的(畢竟await可以使得回撥執行在try塊內),如下: ```js let testPromise = function () { // throw new Error("非同步異常測試") return Promise.reject(new Error("非同步異常測試")) } let testInvocation = async () => { try { await testPromise() } catch (err) { console.error(`catch: ${err}`) } } testInvocation() //輸出:catch: Error: 非同步異常測試 ``` 如果try的是整個testInvocation()那自然沒戲。 如果覺得在每個非同步方法內部try/catch太繁瑣,那麼可以抽離出一個模板方法,或者使用`process`物件註冊`uncaughtException`和`unhandledRejection`事件,注意這兩者的區別: ```js process.on('uncaughtException', e => { console.error(`uncaughtException: ${e.message}`) }); process.on('unhandledRejection', (reason, promise) => { console.error(`unhandledRejection: ${reason}`) }); let testPromise = function(){ throw new Error("非同步異常測試") } testPromise() //輸出:uncaughtException: 非同步異常測試 let testInvocation = async () => await testPromise() //.catch 因為testPromise()返回的不是Promise,所以catch無效 testInvocation() //輸出:unhandledRejection: Error: 非同步異常測試 //注意兩次異常型別不一樣 ``` *如果你使用electron開發桌面應用,可能無法[以`process.on('unhandledRejection', ...)`方式]捕獲`unhandledRejection`異常(本人使用v10.1.0版本測試發現)。遇到這種情況,只能老老實實在每個Promise後面寫catch()。* 使用process捕獲異常無法獲取異常的上下文,且丟失上下文堆疊使得node不能正常進行記憶體回收,從而導致記憶體洩露。 node中還有個東西`domain`用於彌補process的問題,但是個人認為domain使用不便,且織入業務程式碼程度過深,另外據說目前版本還不穩定(後續可能會更改),甚至有文章說已被node廢棄,具體什麼情況暫未深入瞭解。總之希望node或者js平臺能出一個關於異常捕獲的更好的解決方案。 --- ## 協程安全 *在js場景下,非同步機制更類似於Go的`協程`(畢竟js是單執行緒,多執行緒無從談起),所以此處取名為協程安全。* 直接看程式碼: ```js let policy = {} let testfun = async () => { let data = await policy //生成隨機數 data["key"] = utility.getRandomString(20) return data } //1 let testinfo = async () => { let data = await testfun() console.info(data.key) } for (let i = 0; i < 5; i++) { testinfo() } //輸出結果是5次相同的隨機數 //2 let testinfo2 = async () =>
{ for (let i = 0; i < 5; i++) { let data = await testfun() console.info(data.key) } } testinfo2() //如此則正常輸出5次不同的隨機數 ``` 由上可知:在使用await時,若多個await操作相同變數,並且它們的後續操作是在所有await都返回後執行,就容易出現與預期不符的情況,應儘量