JS異步編程方案(promise)


  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() 
// 執行結果和上面完全一樣

  與監聽例子的區別:

  1. 創建了一個Message全局對象,並且listeners移到該對象
  2. on方法改名為subscribe方法,並且移到Message對象上
  3. 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方法包含兩個參數:onfulfilledonrejected,它們都是 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

  詳見:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM