Javascript語言的執行環境是“單線程”——一次只能完成一件任務,若有多個任務則必須排隊,前面的任務完成,再執行后面的一個任務。
一、同步和異步
這種模式實現簡單,執行環境也相對單純,但如果某個任務耗時很長,后面的任務必須排隊等候,會拖累整個程序運行。
為解決這個問題,javascript語言將任務的執行模式分為兩種:同步、異步。
1、同步
同步任務是指在主線程上排隊執行的任務,只有前一個任務執行完畢,才能繼續執行下一個任務,打開網站的渲染過程,其實就是一個同步任務。
讀取文件的同步任務執行如下:
2、異步
可以把異步理解為一個任務分成兩段。先執行第一段,再轉而執行其他任務,等准備好后,再回頭來執行第二段。
因此排在異步任務后面的代碼,不用等待異步任務結束會馬上運行,即:異步任務不具有“堵塞”效應。打開網站時,圖片加載、音樂加載都是一個異步任務。
讀取文件的異步任務執行如下:
3、異步模式意義
在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應(例如Ajax操作)。
在服務器端,“異步模式”幾乎是唯一的模式,如果執行環境是單線程的,若允許同步執行所有http請求,服務器性能將急劇下降,很快失去響應。
二、傳統異步方案
1、回調函數(Callback)
回調函數是異步的最基本實現方式。
思路:將回調函數作為參數傳入主函數,執行完主函數內容后,執行回調函數。
(1)回調示例
假定有兩個函數f1()和f2(),f2需要等待f1的執行結果。
如果f1是一個很耗時的任務,則可以改寫f1,將f2作為f1的回調函數。
function f1(callback){ setTimeout(function () { // f1的任務代碼 callback(); }, 1000); }
執行代碼就變為了f1(f2);
采用這種方法,就將同步操作變為了異步操作,f1不會堵塞程序運行,相當於先執行主要邏輯,將耗時的操作推遲執行。
(2)回調的優缺點
優點:簡單、容易理解和部署。
缺點:1)代碼耦合度太高,不利於代碼的閱讀和維護;
2)有多層回調的情況下,容易引起回調地獄;
3)每個任務只能指定一個回調函數,例如fs.readFile
等函數,只提供傳入一個回調函數,如果想觸發2個回調函數,就只能再用一個函數把這兩個函數包起來
// 例子1:回調地獄,依次執行f1,f2,f3... const f1 = (callback) => setTimeout(()=>{ console.log('f1') callback() },1000) const f2 = (callback) =>setTimeout(()=>{ console.log('f2') callback() },1000) ... // 假設還有f3,f4...fn都是類似的函數,那么就要不斷的把每個函數寫成類似的形式,然后使用下面的形式調用: f1(f2(f3(f4))) // 例子2:如果想給`fs.readFile`執行2個回調函數callback1,callback2 // 必須先包起來 const callback3 = ()=>{ callback1 callback2 } fs.readFile(filename,[encoding],callback3)
2、事件監聽(Listener)
事件監聽的含義:采用事件驅動模式,讓任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
(1)監聽示例
仍假定有兩個函數f1()和f2(),f2需要等待f1的執行結果。首先為f1綁定一個事件(這里為jQuery寫法):
f1.on('done', f2);
上面代碼含義:當f1發生done事件,就執行f2。對f1改寫如下:
const f1 = () => setTimeout(()=>{ console.log('f1') // 函數體 f1.trigger('done') // 執行完函數體部分 觸發done事件 },1000) f1.on('done',f2) // 綁定done事件回調函數 f1() // 一秒后輸出 f1,再過一秒后輸出f2
f1.trigger('done')表示:執行完成后,立即觸發done事件,從而開始執行f2。
(2)監聽原理
手動實現上面的例子,體會下面方案的原理:
const f1 = () => setTimeout(()=>{ console.log('f1') // 函數體 f1.trigger('done') // 執行完函數體部分 觸發done事件 },1000) /*----------------核心代碼start--------------------------------*/ // listeners 用於存儲f1函數各種各樣的事件類型和對應的處理函數 f1.listeners = {} // on方法用於綁定監聽函數,type表示監聽的事件類型,callback表示對應的處理函數 f1.on = function (type,callback){ if(!this.listeners[type]){ this.listeners[type] = [] } this.listeners[type].push(callback) //用數組存放 因為一個事件可能綁定多個監聽函數 } // trigger方法用於觸發監聽函數 type表示監聽的事件類型 f1.trigger = function (type){ if(this.listeners&&this.listeners[type]){ // 依次執行綁定的函數 for(let i = 0;i < this.listeners[type].length;i++){ const fn = this.listeners[type][i] fn() } } } /*----------------核心代碼end--------------------------------*/ const f2 = () =>setTimeout(()=>{ console.log('f2') },1000) const f3 = () =>{ console.log('f3') } f1.on('done',f2) // 綁定done事件回調函數 f1.on('done',f3) // 多個回調 f1() // 一秒后輸出 f1, f3,再一秒后輸出f2
核心原理:
- 用listeners對象儲存要監聽的事件類型和對應的函數;
- 調用on方法時,往listeners中對應的事件類型添加回調函數;
- 調用trigger方法時,檢查listeners中對應的事件,如果存在回調函數,則依次執行。
和回調相比,代碼的區別只是把原先執行callback的地方,換成執行對應監聽事件的回調函數,但從模式上看,轉換為事件驅動模型。
(3)監聽優缺點
- 優點:避免了直接使用回調的高耦合問題,可以綁定多個回調函數
- 缺點:整個程序變為事件驅動型,不容易看出執行的主流程
3、發布/訂閱模式(Publish/Subscribe)
上面的事件,完全可以理解為“信號”。
若假定一個“信號中心”,某個任務執行完,就向信號中心“發布”(publish)一個信號,其他任務則可以向信號中心“訂閱”(subscribe)這個信號,從而自己執行任務。
這就是“發布/訂閱模式”(publish-subscribe pattern),又稱為“觀察者模式”(observer pattern)。
(1)發布/訂閱示例
采用jQuery的插件——Ben Alman的Tiny Pub/Sub來實現這種模式。
首先,f2向“信號中心”jQuery訂閱“done”信號:
jQuery.subscribe("done", f2);
然后,改寫f1:
function f1(){ setTimeout(function () { // f1的任務代碼 jQuery.publish("done"); }, 1000); }
jQuery.publish("done")的意思是,f1執行完成后,向"信號中心"jQuery發布"done"信號,從而引發f2的執行。
f2完成執行后,也可以取消訂閱(unsubscribe):
jQuery.unsubscribe("done", f2);
(2)發布/訂閱原理
將事件監聽中f1的監聽函數和觸發事件功能,賦給一個新建的全局對象,就轉為了發布訂閱模式:
// 消息中心對象 const Message = { listeners:{} } // subscribe方法用於添加訂閱者 類似事件監聽中的on方法 里面的代碼完全一致 Message.subscribe = function (type,callback){ if(!this.listeners[type]){ this.listeners[type] = [] } this.listeners[type].push(callback) //用數組存放 因為一個事件可能綁定多個監聽函數 } // publish方法用於通知消息中心發布特定的消息 類似事件監聽中的trigger 里面的代碼完全一致 Message.publish = function (type){ if(this.listeners&&this.listeners[type]){ // 依次執行綁定的函數 for(let i = 0;i < this.listeners[type].length;i++){ const fn = this.listeners[type][i] fn() } } } const f2 = () =>setTimeout(()=>{ console.log('f2') },1000) const f3 = () => console.log('f3') Message.subscribe('done',f2) // f2函數 訂閱了done信號 Message.subscribe('done',f3) // f3函數 訂閱了done信號 const f1 = () => setTimeout(()=>{ console.log('f1') Message.publish('done') // 消息中心發出done信號 },1000) f1() // 執行結果和上面完全一樣
與監聽例子的區別:
- 創建了一個Message全局對象,並且listeners移到該對象
on
方法改名為subscribe
方法,並且移到Message對象上trigger
方法改名為publish
,並且移到Message對象上
- 在事件監聽模式中,消息傳遞路線:被監聽函數f1與監聽函數f2直接交流
- 在發布/訂閱模式中,是發布者f1和消息中心交流,訂閱者f2也和消息中心交流
(3)優缺點
這種方法的性質與"事件監聽"類似,但是明顯優於后者。
可以通過查看"消息中心",了解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行。
三、ES6異步方案——Promise
Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。ES6 將其寫進了語言標准,統一了用法,原生提供了Promise
對象。
Promise
對象是一個代理對象(代理一個值),被代理的值在 Promise 對象創建時可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的 promise 對象。
1、Promise的三種狀態
- pending:初始狀態,既不是成功,也不是失敗狀態;
- fulfilled:意味着操作成功完成;
- rejected:意味着操作失敗。
(1)狀態變化
Promise 對象只有兩種狀態變化可能:pending變為fulfilled狀態、pending變為rejected狀態。
當任一種情況出現時,狀態即凝固不再改變,而且Promise 對象的 then
方法綁定的處理方法(handlers )就會被調用(then方法包含兩個參數:onfulfilled 和 onrejected,它們都是 Function 類型。當Promise狀態為fulfilled時,調用 then 的 onfulfilled 方法,當Promise狀態為rejected時,調用 then 的 onrejected 方法, 所以在異步操作的完成和綁定處理方法之間不存在競爭)。
(2)Promise優缺點
優點:有了Promise
對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise
對象提供統一的接口,使得控制異步操作更加容易。
缺點:
1)無法取消Promise
,一旦新建它就會立即執行,無法中途取消。
2)如果不設置回調函數,Promise
內部拋出的錯誤,不會反應到外部。
3)當處於pending
狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
2、Promise實例
ES6規定,Promise對象是一個構造函數,用於生成 Promise 實例。基本語法如下:
new Promise( function(resolve, reject) {...} /* executor */ );
(1)參數
定義的Promise有兩個參數,resolve和reject。
- resolve:將異步的執行從 pending(請求)變成了resolve(成功返回),在異步操作成功時調用,並將異步操作的結果,作為參數傳遞出去。
- reject:將
Promise
對象的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在異步操作失敗時調用,並將異步操作報出的錯誤,作為參數傳遞出去。
(2)簡單示例
需要注意:Promise新建后就會立即執行。
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
Promise新建后立即執行,因此首先輸出“Promise”。由於執行then方法指定的回調函數,將在當前腳本所有同步任務執行完才執行,因此“resolved”最后輸出。
3、Promise的then和catch
(1)Promise.prototype.then(onFulfilled, onRejected)
then方法:定義在原型對象 Promise.prototype 上的。作用——為Promise實例添加狀態改變時的回調函數。then方法兩個參數:
- resolved狀態的回調函數
- rejected狀態的回調函數(可選)
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } setDelay(3000) .then((result)=>{ console.log(result) // 輸出“我延遲了2000毫秒后輸出的” })
添加解決(fulfillment)和拒絕(rejection)回調到當前 promise, 返回一個新的 promise, 將以回調的返回值來resolve.
(2)Promise.prototype.catch(onRejected)
catch方法:.then(null, rejection) 或 .then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } setDelay('我是字符串') .then((result)=>{ console.log(result) // 不進去了 }) .catch((err)=>{ console.log(err) // 輸出錯誤:“參數必須是number類型” })
添加一個拒絕(rejection) 回調到當前 promise, 返回一個新的promise。當這個回調函數被調用,新 promise 將以它的返回值來resolve,否則如果當前promise 進入fulfilled狀態,則以當前promise的完成結果作為新promise的完成結果.
promise拋出錯誤,由catch方法指定的回調函數獲取。三種寫法示例:
// 寫法一 const promise = new Promise(function(resolve, reject) { throw new Error('test'); }); promise.catch(function(error) { console.log(error); }); // 寫法二 const promise = new Promise(function(resolve, reject) { try { throw new Error('test'); } catch(e) { reject(e); } }); promise.catch(function(error) { console.log(error); }); // 寫法三 const promise = new Promise(function(resolve, reject) { reject(new Error('test')); }); promise.catch(function(error) { console.log(error); });
比較上面的寫法,可以發現reject()
方法的作用,等同於拋出錯誤。
4、Promise相互依賴
測試一個Promise里的resolve去返回另一個Promise:
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('參數必須是number類型,並且小於等於10')); setTimeout(()=> { console.log(`先是setDelaySeconds函數輸出,延遲了${seconds}秒,一共需要延遲${seconds+2}秒`) resolve(setDelay(2000)) // 這里依賴上一個Promise }, seconds * 1000) }) } setDelaySecond(3).then((result)=>{ console.log(result) }).catch((err)=>{ console.log(err); }) /* 依次輸出: * 先是setDelaySeconds函數輸出,延遲了3秒,一共需要延遲5秒 * 我延遲了2000毫秒后輸出的 * */
上面雖然做到了依次執行的目的,但耦合性太高,可以使用 Promise 的鏈式寫法改進。
5、Promise鏈式寫法
then
方法返回的是一個新的Promise
實例(注意,不是原來那個Promise
實例)。因此可以采用鏈式寫法,即then
方法后面再調用另一個then
方法。
采用鏈式的then
,可以指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的還是一個Promise
對象(即有異步操作),這時后一個回調函數,就會等待該Promise
對象的狀態發生變化,才會被調用。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('參數必須是number類型,並且小於等於10')); setTimeout(()=> { resolve(`我延遲了${seconds}秒后輸出的,是第二個函數`) }, seconds * 1000) }) } setDelay(2000) .then((result)=>{ console.log(result) console.log('我進行到第一步的'); return setDelaySecond(3) }) .then((result)=>{ console.log('我進行到第二步的'); console.log(result); }).catch((err)=>{ console.log(err); })
如上所示,第一個then
方法指定的回調函數,返回的是另一個Promise
對象。這時,第二個then
方法指定的回調函數,就會等待這個新的Promise
對象狀態發生變化。如果變為resolved
,就調用第一個回調函數,如果狀態變為rejected
,就調用第二個回調函數。
先執行setDelay再執行setDelaySecond,只需要在第一個then的結果中返回下一個Promise就可以一直鏈式寫下去,相當於依次執行。
可以看到then的鏈式寫法非常優美,這樣就可以脫離異步嵌套苦海。
6、鏈式寫法注意要點
then
鏈式寫法的本質其實是一直往下傳遞返回一個新的Promise,也就是說then在下一步接收的是上一步返回的Promise。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我進行到第一步的'); return setDelaySecond(20) }) .then((result)=>{ console.log('我進行到第二步的'); console.log(result); }, (_err)=> { console.log('我出錯啦,進到這里捕獲錯誤,但是不經過catch了'); }) .then((result)=>{ console.log('我還是繼續執行的!!!!') }) .catch((err)=>{ console.log(err); })
改寫代碼后,輸出結果進到了then的第二個參數(reject)中,不再經過catch了。
如果將catch移到then錯誤處理前:
setDelay(2000) .then((result)=>{ console.log(result) console.log('我進行到第一步的'); return setDelaySecond(20) }) .catch((err)=>{ // 挪上去了 console.log(err); // 這里catch到上一個返回Promise的錯誤 }) .then((result)=>{ console.log('我進行到第二步的'); console.log(result); }, (_err)=> { console.log('我出錯啦,但是由於catch在我前面,所以錯誤早就被捕獲了,我這沒有錯誤了'); }) .then((result)=>{ console.log('我還是繼續執行的!!!!') })
此時的情況是,先經過catch捕獲,后面不再出現錯誤。
注意要點:
catch
寫法是針對於整個鏈式寫法的錯誤捕獲的,而then
第二個參數是針對於上一個返回Promise
的。- 兩者的優先級:就是看誰在鏈式寫法的前面,在前面的先捕獲到錯誤,后面就沒有錯誤可以捕獲了,鏈式前面的優先級大,而且兩者都不是
break
, 可以繼續執行后續操作不受影響。
7、鏈式寫法的錯誤處理
Promise 對象的錯誤具有“冒泡”性質,會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch
語句捕獲。
因此,即使有很多Promise也只用寫一個catch。鏈式中任何一個環節出問題,都會被catch到,同時在某個環節后面的代碼就不再執行了。
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('參數必須是number類型,並且小於等於10')); setTimeout(()=> { console.log(`先是setDelaySeconds函數輸出,延遲了${seconds}秒,一共需要延遲${seconds+2}秒`) resolve(setDelay(2000)) // 這里依賴上一個Promise }, seconds * 1000) }) } setDelay('2000') .then((result)=>{ console.log('第一步完成了'); console.log(result) return setDelaySecond(3) }) .catch((err)=>{ // 這里移到第一個鏈式去,發現上面的不執行了,下面的繼續執行 console.log(err); }) .then((result)=>{ console.log('第二步完成了'); console.log(result); })
執行效果如下所示:
Error: 參數必須是number類型 at /Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:3:48 at new Promise (<anonymous>) at setDelay (/Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:2:10) at Object.<anonymous> (/Users/hqs/WebstormProjects/hsedu_mgr/hsedu_mgr/test.js:20:1) at Module._compile (internal/modules/cjs/loader.js:774:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10) at Module.load (internal/modules/cjs/loader.js:641:32) at Function.Module._load (internal/modules/cjs/loader.js:556:12) at Function.Module.runMain (internal/modules/cjs/loader.js:837:10) at internal/main/run_main_module.js:17:11 第二步完成了 undefined
可以看到雖然出現了錯誤,但是鏈式走完了。輸出undefined是由於上一個then沒有返回Promise。
由此可見:鏈式中的catch
並不是終點,catch完如果還有then還會繼續往下走。
catch
只是捕獲錯誤的一個鏈式表達,並不是break!
因此catch放的位置也很有講究,一般放在一些重要的、必須catch的程序的最后。這些重要的程序中間一旦出現錯誤,會馬上跳過其他后續程序的操作直接執行到最近的catch代碼塊,但不影響catch后續的操作!
到這就不得不體一個ES2018標准新引入的Promise的finally
,表示在catch后必須肯定會默認執行的的操作。細節可以參考:Promise的finally
8、Promise鏈式中間返回自定義值
Promise.resolve():返回一個狀態由給定value決定的Promise對象。如果該值是thenable(即,帶有then方法的對象),返回的Promise對象的最終狀態由then方法執行決定;否則的話(該value為空,基本類型或者不帶then方法的對象),返回的Promise對象狀態為fulfilled,並且將該value傳遞給對應的then方法。
通常而言,如果不知道一個值是否是Promise對象,使用Promise.resolve(value) 來返回一個Promise對象,這樣就能將該value以Promise對象形式使用。
(1)示例
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != 'number' || seconds > 10) reject(new Error('參數必須是number類型,並且小於等於10')); setTimeout(()=> { console.log(`先是setDelaySeconds函數輸出,延遲了${seconds}秒,一共需要延遲${seconds+2}秒`) resolve(setDelay(2000)) // 這里依賴上一個Promise }, seconds * 1000) }) } setDelay(2000).then((result)=>{ console.log('第一步完成了'); console.log(result); let message = '這是我自己想處理的值'; return Promise.resolve(message) // 這里返回我想在下一階段處理的值 }) .then((result)=>{ console.log('第二步完成了'); console.log(result); // 這里拿到上一階段的返回值 //return Promise.resolve('這里可以繼續返回') }) .catch((err)=>{ console.log(err); }) /* 輸出結果: * 第一步完成了 * 我延遲了2000毫秒后輸出的 * 第二步完成了 * 這是我自己想處理的值 * */
(2)Promise.resolve方法參數
- 參數是一個Promise實例
- 參數是一個thenable對象
- 參數不是具有then方法的對象,或根本不是對象
- 不帶有任何參數
9、跳出或停止Promise鏈式
不同於通過break跳出或停止循環和switch。
在使用Promise鏈式時,如果使用這樣的操作:func().then().then().then().catch()
的方式,想在第一個then
就跳出鏈式,后面的不執行,就需要去中斷后繼調用鏈。
(1)方法一:通過拋出一個異常終止
如果catch在中間(不再末尾),同時也不想執行catch后面的代碼,即實現鏈式的絕對中止。
要實現絕對終止,需要理解Promise的三種狀態:pending,resolve,rejected。pending狀態就是請求中的狀態,成功請求就是resolve,失敗就是reject,因此pending就是個中間過渡狀態。
then
的下一層級其實得到的是上一層級返回的Promise對象,也就是說原Promise對象與新對象狀態保持一致。
因此如果想讓鏈式在某一層終止,可以讓它永遠都pending下去,后續的操作就不存在了。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我進行到第一步的'); return setDelaySecond(1) }) .then((result)=>{ console.log(result); console.log('我主動跳出循環了'); // return Promise.reject('跳出循環的信息') // 重點在這 return new Promise(()=>{console.log('后續的不會執行')}) // 這里返回的一個新的Promise,沒有resolve和reject,那么會一直處於pending狀態,因為沒返回啊,那么這種狀態就一直保持着,中斷了這個Promise }) .then((result)=>{ console.log('我不執行'); }) .catch((mes)=>{ console.dir(mes) console.log('我跳出了'); }) .then((res)=>{ console.log('我也不會執行') })
執行結果:
我延遲了2000毫秒后輸出的
我進行到第一步的
先是setDelaySeconds函數輸出,延遲了1秒,一共需要延遲3秒
我延遲了2000毫秒后輸出的
我主動跳出循環了
后續的不會執行
這樣就實現了錯誤跳出且完全終止Promise鏈。但是這種方法會導致潛在的內存泄露問題。
因為這個一直處於pending狀態下的Promise會一直處於被掛起的狀態,而且瀏覽器的機制細節也不清楚,一般的網頁沒有關系,但大量的復雜的這種pending狀態勢必會導致內存泄漏,具體的沒有測試,這篇文章可以參考查閱:從如何停掉 Promise 鏈說起。
但是一般情況下是不會存在泄漏,只是有這種風險。取消問題一直是Promise的痛點。
(2)方法二:通過reject來中斷
依據鏈式的思想,拒絕掉某一鏈,相當於直接跳到了catch模塊。
setDelay(2000) .then((result)=>{ console.log(result) console.log('我進行到第一步的'); return setDelaySecond(1) }) .then((result)=>{ console.log('我進行到第二步的'); console.log(result); console.log('我主動跳出循環了'); return Promise.reject('跳出循環的信息') // 這里返回一個reject,主動跳出循環了 }) .then((result)=>{ console.log('我不執行'); }) .catch((mes)=>{ console.dir(mes) console.log('我跳出了'); })
執行輸出:
我延遲了2000毫秒后輸出的 我進行到第一步的 先是setDelaySeconds函數輸出,延遲了1秒,一共需要延遲3秒 我進行到第二步的 我延遲了2000毫秒后輸出的 我主動跳出循環了 '跳出循環的信息' 我跳出了
查看執行輸出,可以發現’我不執行‘這一條沒有輸出,跳過了這一鏈,直接跳到了catch模塊。
很容易看到缺點:有時不確定是因為錯誤跳出還是主動跳出。加入標識位優化:
return Promise.reject({ isNotErrorExpection: true // 返回的地方加一個標志位,判斷是否是錯誤類型,如果不是,那么說明可以是主動跳出循環的 })
或者根據上述的代碼判斷catch的地方輸出的類型是不是屬於錯誤對象的,是的話說明是錯誤,不是的話說明是主動跳出的,可以自己選擇(這就是為什么要統一錯誤reject的時候輸出new Error('錯誤信息')的原因,規范!)
當然也可以直接拋出一個錯誤跳出:
throw new Error('錯誤信息') // 直接跳出,那就不能用判斷是否為錯誤對象的方法進行判斷了
10、將多個Promise打包成一個
Promise.all()方法用於將多個Promise實例,包裝成一個新的Promise實例。
Promise.all(iterable); /* 參數 * iterable:一個可迭代對象,Arry或String等 */
(1)返回值
成功和失敗的返回值是不同的,成功的時候返回的是一個結果數組。
let p1 = new Promise((resolve, reject) => { resolve('成功了') }) let p2 = new Promise((resolve, reject) => { resolve('success') }) let p3 = Promse.reject('失敗') Promise.all([p1, p2]).then((result) => { console.log(result) //['成功了', 'success'] }).catch((error) => { console.log(error) })
失敗的時候則返回最先被reject失敗狀態的值。
let p1 = new Promise((resolve, reject) => { resolve('成功了') }) let p2 = new Promise((resolve, reject) => { resolve('success') }) let p3 = Promse.reject('失敗') Promise.all([p1,p3,p2]).then((result) => { console.log(result) }).catch((error) => { console.log(error) // 失敗了,打出 '失敗' })
(2)應用場景
Promse.all在處理多個異步處理時非常有用,比如說一個頁面上需要等兩個或多個ajax的數據回來以后才正常顯示,在此之前只顯示loading圖標。
let wake = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`${time / 1000}秒后醒來`) }, time) }) } let p1 = wake(3000) let p2 = wake(2000) Promise.all([p1, p2]).then((result) => { console.log(result) // [ '3秒后醒來', '2秒后醒來' ] }).catch((error) => { console.log(error) })
需要特別注意的是:Promise.all獲得的成功結果的數組里面的數據順序和Promise.all接收到的數組順序是一致的,即p1的結果在前,即便p1的結果獲取的比p2要晚。這帶來了一個絕大的好處:在前端開發請求數據的過程中,偶爾會遇到發送多個請求並根據請求順序獲取和使用數據的場景,使用Promise.all毫無疑問可以解決這個問題。
四、ES6異步方案——生成器Generators/yield
詳見:ES6異步方案——生成器Generators/yield
五、ES7異步方案——async/await
詳見: