如何讓 Promise 控制業務流程(Re 同步異步 開始一步一步深刻)

對寫文章這件事已經閣了3個月了,工做太忙很難抽出大塊時間來總結寫做。如今稍微閒賦些,準備好好對之前的技術作一下總結。爲何要寫關於 Promise 呢?有如下 3 點ios

  1. 以前面試時候,面試官問 Promise 問的比較多,實在要對它進行一個好好總結了,也是 JavaScript 比較難懂的一個技術點,平時我面試別人時候,也喜歡問 Promise 相關的,保證他在工做中可以熟練運用,本身能封裝axios,能控制比較複雜的同步異步流程等...es6

  2. 這三個月的工做中,本身也老是遇到關於 Promise 的一些運用場景,有時候又比較疑惑一些地方,也是爲本身總結一下 Promise,之後在工做中更加運用自如。面試

    接下來我從最基礎的同步異步談起,再經過圖示談他的語法,如何本身寫一個Promise,再談一下我在工做中遇到的 Promise,最後談一下 新的語法 asyncawaitPromise 的結合和比較axios

  3. 網上關於Promise的介紹大多數是一些語法的介紹,大多沒有結合場景和同步異步相關理解來談。對於理解Promise的前世此生仍是不夠深刻,不夠具體。數組

理解同步異步

理解JS執行原理

console.log(1)
setTimeout(() => {
	console.log(2)
}, 1000)
console.log(3)
複製代碼

若是延遲時間爲 0

console.log(1)
setTimeout(() => {
	console.log(2)
}, 0)
console.log(3)
複製代碼

console.log(1)
setTimeout(() => {
	console.log(2)
}, 2000)
console.log(3)
setTimeout(() => {
	console.log(4)
}, 1000)
console.log(5)
setTimeout(() => {
	console.log(6)
}, 0)
console.log(7)
setTimeout(() => {
	console.log(8)
}, 1000)
console.log(9)
複製代碼

這裏能夠發現幾點promise

  • 1,3,5,7,9 是同步代碼,6,4,8,2是異步代碼,同步先於異步執行
  • 單看同步代碼,按位置順序依次執行
  • 單看異步代碼,不一樣執行時間,越長越靠後執行。相同執行時間,按位置順序依次執行

這裏咱們總結下同步與異步的規律瀏覽器

  • 代碼中存在異步函數,無論須要時間多久,都要在同步完成後執行
  • 同步按位置順序執行;異步按時間長短執行,一樣時間長短時,按位置順序執行

這是一個怎樣的機制,JS引擎又是如何處理的呢?異步

我找到了一張這樣的圖async

首先來解釋一下這個圖:這是JS的事件循環,分爲3個步驟函數

  1. 引擎將同步、異步函數按次序載入到執行棧裏面
  2. 執行棧的將異步函數放入異步線程裏面
  3. 線程根據任務完成時間依次推入任務隊列中執行

如何處理異步呢, 最初的方法是使用回調

理解回調

引入一下知乎上的高贊回答

如今咱們把它用代碼寫出來看看

function fetchSomething() {
  console.log('去取貨!')
}

function buySomething(callback) {
  console.log('沒貨了!')
  setTimeout(() => {
    console.log('有貨了!')
    callback()
  }, 1000)
}

buySomething(fetchSomething)
複製代碼

用簡單的語言描述就是: 將一個函數做爲參數傳給另外一個函數調用

要搞清楚的一點是,回調和異步沒有直接的關係,也能夠同步回調,也能夠異步回調。咱們是經過回調這個機制來實現異步的操做,例如

// 這個一個請求的異步函數,來實現異步操做
function getSomePeopleName(params, callback) {
  setTimeout(() => {
    let data

    if (params === 'suo') {
      data = 'yue'
    }
    
    console.log(callback(data))
  }, 1000)
}
console.log(getSomePeopleName('suo', (data) => '我是回調函數:' + data))
複製代碼

回調函數的結果必定是在回調函數裏面,若是我是這樣一個流程呢?

A -> B -> C -> D

function A(callback) {
  console.log('開始執行A')
  setTimeout(() => {
      callback('A')
  }, 500)
}

function B(callback) {
  console.log('開始執行B')
  setTimeout(() => {
      callback('B')
  }, 400)
}

function C(callback) {
  console.log('開始執行C')
  callback('C')
}

function D(callback) {
  console.log('開始執行D')
  setTimeout(() => {
      callback('D')
  }, 200)
}

A((a) => {
  B((b) => {
    C((c) => {
      D((d) => {
        console.log(a, b, c, d)
      })
    })
  })
})
複製代碼

由上可知,回調函數有幾個特定

  • 使用回調函數嵌套,回調函數必定在上一個回調以後執行,用於能夠控制流程,不會出現異步函數在執行順序的混亂,即便同步異步函數混合
  • 回調函數解構嵌套,代碼不夠清晰,俗稱回調地獄

那麼有沒有更好的異步操做機制呢?這裏咱們就要談到 Promise

深刻 Promise

爲何要有Promise?

ES6 標準中 Promise 語法

  • Promise是一個構造函數

咱們寫一個簡單的 Promise 看看

new Promise(() => {})
複製代碼

// 有一個參數executor的構造函數
// executor 也是一個函數,具備兩個參數 resolve, reject 是兩個回調函數,當執行到回調函數時,會執行
const isResolve = true
new Promise((resolve, reject) => {
	if (isResolve) {
		resolve()
	} else {
		reject()
	}
})
複製代碼

能夠看到 Promise 的狀態從 pending變成 resolved

若是將 isResolve 置爲 false 呢?

Promise 狀態從 pending 變成 rejected 同時拋出一個異常,而且異常未被捕獲,因此咱們寫Promise時候必定要加上catch 來捕獲異常

const isResolve = false
new Promise((resolve, reject) => {
	if (isResolve) {
		resolve()
	} else {
		reject('我拒絕你')
	}
}).catch((err) => {
  console.log('捕獲異常', err)
})
複製代碼

如今咱們把異常捕獲到了,可是神奇的事情發生了,異常狀態應該是 rejected,怎麼變成 resolved了呢? 咱們或許很納悶,這個放在後面討論,咱們先看看 then 怎麼處理的

const isResolve = true
new Promise((resolve, reject) => {
	if (isResolve) {
		resolve('經過')
	} else {
		reject('我拒絕你')
	}
}).then((res) => {
  console.log('resolve', res)
}, (res) => {
  console.log('reject', res)
})
複製代碼

isResolve = false

咱們能夠看到 then 是怎麼處理的

  1. 若是 前面resolve()調用,Promise狀態爲 resolvedthen則執行第一個回調函數參數
  2. 若是 前面reject()調用,Promise狀態爲 rejectedthen則執行第二個回調函數參數
  3. then 執行回調函數以後Promise 的 狀態都爲 resolved

下面進一步驗證下

const isResolve = false
new Promise((resolve, reject) => {
	if (isResolve) {
		resolve('經過')
	} else {
		reject('我拒絕你')
	}
}).catch(err => {
  console.log('我捕獲到了', err)
}).then((res) => {
  console.log('resolve', res)
}, (res) => {
  console.log('reject', res)
})
複製代碼

果真呢, catch以後, then還會去執行,而且 resolve 的參數爲 undefined

問題是如今咱們如何控制一個流程呢?Promise並不能直接給咱們進行長流程的分支選擇

下面有一個簡單流程

嘗試使用 Promise去控制流程

const process = [102, 204]
new Promise((resolve, reject) => {
  if (process[0] === 101) {
     setTimeout(() => {
      console.log(101)
      resolve(101)
    }, 1000)
  } else {
    setTimeout(() => {
      console.log(102)
      reject(102)
    }, 1000)
  }
}).then(() => {
  if (process[1] === 201) {
    setTimeout(() => {
      console.log(201)
    }, 500)
  } else {
    setTimeout(() => {
      console.log(202)
    }, 500)
  }
}, () => {
    if (process[1] === 203) {
    setTimeout(() => {
      console.log(203)
    }, 500)
  } else {
    setTimeout(() => {
      console.log(204)
    }, 500)
  }
}).catch((err) => {
  console.log(err)
})
複製代碼

下面咱們試試更加複雜的流程

Promise 調用鏈:then 的做用

單個Promise沒辦法控制長流程,咱們怎麼將Promise造成一個控制鏈呢,須要理解then的返回在其中起到的做用

promise.then(onFulfilled, onRejected) 
複製代碼
  • 接收兩個參數,onFulfilledpromiseresolved 狀態被調用

  • 接收兩個參數,onRejectedpromiserejected 狀態被調用

  • 返回值比較複雜,下面用表格列出來

    序號 場景 返回的Promise狀態改變爲 回調函數參數值
    1 返回1 個值 resolved 返回值
    2 沒有返回 resolved undefined
    3 拋出錯誤 rejected 錯誤
    4 resolved的Promise resolved 返回的Promise回調的參數值
    5 rejected的Promise rejected 返回的Promise回調的參數值
    6 pending的Promise pending 返回的Promise回調的參數值

咱們能夠看出想要控制then 的後續流程,必須經過這 6 種狀況來控制 下面來測試一下 這 6 種狀況是否符合咱們的預期

  1. 返回一個值
new Promise((resolve, reject) => {
		resolve('經過')
}).then((res) => {
  console.log('1', res)
  return res // then的返回值
}).then((res) => {
  console.log('2', res)
})
複製代碼

2. 不返回

3. 拋出錯誤

4. resolved的Promise

5. rejected的Promise

6. pending的Promise

看圖寫代碼

分析特色:

這樣就比較複雜了,還要考慮暫停的問題,可是根據咱們上面測試到的,經過then的返回值控制流程也沒有想象那麼難

task([101, 201])
function task(testPath) {
  console.log('開始測試', testPath)
  new Promise((resolve, reject) => {
    console.log('000')
    if (101 === testPath[0]) {
      resolve('101')
    } else {
      reject('102')
    }
  }).then((res) => {
    console.log('201', '上一個返回:' + res)
    return new Promise(() => {})
  }, (res) => {
    if (202 === testPath[1]) {
      console.log('202',  '上一個返回:' + res)
      return '202'
    } else {
      console.log('203',  '上一個返回:' + res)
      throw '203'
    }
  }).then((res) => {
      console.log('301',  '上一個返回:' + res)
      return Promise.resolve('301')
  }, (res) => {
       console.log('302',  '上一個返回:' + res)
      return Promise.resolve('302')
  }).then((res) => {
    console.log('401',  '上一個返回:' + res)
    return Promise.reject('401')
  }).catch((err) => {
    console.log(err)
  })
}
複製代碼

  • Promise.resolve() 等同用 new Promise((resolve, reject) => resolve())
  • Promise.reject() 等同用 new Promise((resolve, reject) => reject())
  • Promise.all()
// 做爲參數promise 數組中,全部promise狀態都是resolved纔回調then第一個,只要有一個reject就reject
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]).then((res) => {
  console.log('經過', res)
}, (res) => {
  console.log('拒絕', res)
})
複製代碼

返回值爲所有值的一個數組

Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.reject(3)]).then((res) => {
  console.log('經過', res)
}, (res) => {
  console.log('拒絕', res)
})
複製代碼

返回值僅返回拒絕的那個

  • Promise.race()
Promise.race([new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(1)
  }, 1001)
}), new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1002)
})]).then((res) => {
  console.log('經過', res)
}).catch((res) => {
  console.log('拒絕', res)
})
複製代碼

race 至關於競賽,多個 Promise競賽,誰先狀態變成 resolvedrejected,誰就執行下面的回調(根據時間來抉擇)

下節預告

Promise 到 generator 再到 async & await

Promise 實際應用

對異步請求封裝

流程控制

手寫一個Promise

後記

總結一下,從同步異步到回調,在到Promise的語法和應用所有均可以在谷歌瀏覽器的控制檯中輸出測試。經過一點點代碼的編寫和輸出,纔會讓咱們思惟更清晰,對Promise的理解更深入。以後再總結工做中用到的Promise,之後也會慢慢將asyncawait結合Promise來談關於 es6 之後異步相關的新特性。

相關文章
相關標籤/搜索