異步編程

什么是同步和異步?

你可能知道, JavaScript 語言 的執(zhí)行環(huán)境是“單線程”

所謂“單線程”, 就是指一次只能完成一件任務(wù), 如果有多個任務(wù), 就必須排隊, 前面一個任務(wù)完成, 再執(zhí)行后面一個任務(wù), 以此類推
例如現(xiàn)實(shí)生活中的排隊

這種模式的好處是實(shí)現(xiàn)起來比較簡單, 執(zhí)行環(huán)境相對單純, 壞處是只要有一個任務(wù)耗時很長, 后面的任務(wù)都必須排隊等著, 會拖延整個程序的執(zhí)行
常見的瀏覽器無響應(yīng)(假死), 往往就是因為某一段 JavaScript 代碼長時間運(yùn)行(比如死循環(huán)), 導(dǎo)致整個頁面卡在這個地方, 其他任務(wù)無法執(zhí)行

為了解決這個問題, JavaScript 語言將任務(wù)的執(zhí)行模式分成兩種

  • 同步(Synchronous)
  • 異步(Asynchronous)

這里的 “同步”和“異步” 與我們現(xiàn)實(shí)中的同步、異步恰恰相反

例如:

  • 一邊吃飯一邊打電話, 我們認(rèn)為這是同時進(jìn)行(同步執(zhí)行)的, 但在計算機(jī)中, 這種行為叫做異步執(zhí)行
  • 吃飯的同時, 必須吃完飯才能打電話, 我們認(rèn)為這是不能同時進(jìn)行(異步執(zhí)行)的, 但在計算機(jī)中, 這種行為我們叫做同步執(zhí)行

至于為什么, 那你要問英文單詞了, 例如 異步(Asynchronous) 翻譯成中文是異步的, 但在計算機(jī)中, 表示的是我們認(rèn)知的同時執(zhí)行的

什么時候我們需要異步處理事件?

  • 一種很常見的場景自然就是網(wǎng)絡(luò)請求了
  • 我們封裝一個網(wǎng)絡(luò)請求的函數(shù), 因為不能立即拿到結(jié)果, 所以不能像簡單的 3 + 4 = 7 一樣立刻獲得結(jié)果
  • 所以我們往往會傳入另一個函數(shù) (回調(diào)函數(shù) callback), 在數(shù)據(jù)請求成功之后, 再將得到的數(shù)據(jù)以參數(shù)的形式傳遞給回調(diào)函數(shù)

JavaScript 和 Node.js 中的異步操作都會在最后執(zhí)行, 例如 ajax、readFile、writeFile、setTimeout 等

獲取異步操作的值只能使用回調(diào)函數(shù)的方式, 異步操作都是最后執(zhí)行

回調(diào)函數(shù)

回調(diào)函數(shù)的方式獲取異步操作內(nèi)的數(shù)據(jù)

function sum(a, b, callback) {
  console.log(1)
  setTimeout(function () {
    callback(a + b)
  }, 1000)
  console.log(2)
}

sum(10, 20, function (res) {
  console.log(res)
})

// log: 1 2 30

這種方式雖然看似沒什么問題, 但是, 當(dāng)網(wǎng)絡(luò)請求非常復(fù)雜時, 就會出現(xiàn)回調(diào)地獄

ok, 我們用一個非??鋸埖陌咐齺碚f明

$.ajax('url1', function (data1) {
  $.ajax(data1['url2'], function (data2) {
    $.ajax(data2['url3'], function (data3) {
      $.ajax(data3['url4'], function (data4) {
        console.log(data4)
      })
    })
  })
})
  • 我們需要通過一個 url1 向服務(wù)器請求一個數(shù)據(jù) data1, data1 中又包含了下一個請求的 url2
  • 我們需要通過一個 url2 向服務(wù)器請求一個數(shù)據(jù) data2, data2 中又包含了下一個請求的 url3
  • 我們需要通過一個 url3 向服務(wù)器請求一個數(shù)據(jù) data3, data3 中又包含了下一個請求的 url4
  • 發(fā)送網(wǎng)絡(luò)請求 url4, 獲取最終的數(shù)據(jù) data4

上面的代碼有什么問題?

  • 正常情況下, 不會有什么問題, 可以正常運(yùn)行并且獲取我們想要的數(shù)據(jù)
  • 但是, 這樣的代碼閱讀性非常差, 而且非常不利于維護(hù)
  • 如果有多個異步同時執(zhí)行, 無法確認(rèn)他們的執(zhí)行順序, 所以通過嵌套的方式能保證代碼的執(zhí)行順序問題
  • 我們更加期望的是一種更加優(yōu)雅的方式來進(jìn)行這種異步操作

Promise

什么是 Promise ?

ES6 中有一個非常重要和好用的特性就是 Promise

Promise 到底是做什么的?

  • Promise 是異步編程的一種解決方案, 比傳統(tǒng)的解決方案回調(diào)函數(shù)和事件更合理和更強(qiáng)大

所謂 Promise, 簡單說就是一個容器, 里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果

為了解決回調(diào)地獄所帶來的問題, ES6 里引進(jìn)了 Promise, 有了 Promise 對象, 就可以將異步操作以同步操作的流程表達(dá)出來, 避免了層層嵌套的回調(diào)函數(shù)
Promise 對象提供統(tǒng)一的接口, 使得控制異步操作更加容易

Promise 的特點(diǎn)

Promise 對象有以下兩個特點(diǎn)

  1. 對象的狀態(tài)不受外界影響, Promise 對象代表一個異步操作, 有三種狀態(tài): pending(進(jìn)行中)、fulfill(已成功) 和 rejected(已失敗), 只有異步操作的結(jié)果, 可以決定當(dāng)前是哪一種狀態(tài), 任何其他操作都無法改變這個狀態(tài), 這也是 Promise 這個名字的由來, 它的英語意思就是 “承諾”, 表示其他手段無法改變
  2. 一旦狀態(tài)改變, 就不會再變, 任何時候都可以得到這個結(jié)果, Promise 對象的狀態(tài)改變, 只有兩種可能: 從 pending 變?yōu)?fulfill從 pending 變?yōu)?rejected, 只要這兩種情況發(fā)生, 狀態(tài)就凝固了, 不會再發(fā)生改變, 會一直保持這個結(jié)果, 這時就稱為 resolved(已定型), 如果改變已經(jīng)發(fā)生了, 你再對 Promise 對象添加回調(diào)函數(shù), 也會立即得到這個結(jié)果, 這與事件(Event)完全不同, 事件的特點(diǎn)是, 如果你錯過了它, 再去監(jiān)聽, 是得不到結(jié)果的

Promise 的缺點(diǎn)

  • 首先, 無法取消 Promise, 一旦新建它就會立即執(zhí)行, 無法中途取消
  • 其次, 如果不設(shè)置回調(diào)函數(shù), Promise 內(nèi)部拋出的錯誤, 不會反應(yīng)到外部
  • 第三, 當(dāng)處于 pending 狀態(tài)時, 無法得知目前進(jìn)展到哪一個階段(剛剛開始還是即將完成)

Promise 的三種狀態(tài)

  • pending : 等待(wait)狀態(tài), 比如正在進(jìn)行網(wǎng)絡(luò)請求, 或者定時器沒有到時間
  • fulfilled : 滿足狀態(tài), 當(dāng)我們主動調(diào)用 resolve 時, 就處于該狀態(tài), 并且回調(diào) .then()
  • rejected : 拒絕狀態(tài), 當(dāng)我們主動調(diào)用 reject 時, 就處于該狀態(tài), 并且回調(diào) .catch()

Promise 基本用法

ES6 規(guī)定, Promise 對象是一個構(gòu)造函數(shù), 用來生成 Promise 實(shí)例

new Promise((resolve, reject) => {
  // ... 某些異步代碼

  if (/* 異步操作成功 */){
    resolve(data);  // data 里是異步執(zhí)行后的返回值
  } else {
    reject(error);  // error 里是異步執(zhí)行錯誤后的錯誤信息
  }
}).then(data => {
  // 這里對 data 就可以進(jìn)行數(shù)據(jù)拿取操作了
  console.log('success')
}).catch(error => {
  console.log('failure')
})

Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù), 該函數(shù)的兩個參數(shù)分別是 resolve 和 reject
它們是兩個函數(shù), 由 JavaScript 引擎提供, 不需要自己部署

resolve

  • resolve 函數(shù)的作用是將 Promise 對象的狀態(tài)從 “未完成”變?yōu)椤俺晒Α?即從 pending 變?yōu)?fulfilled), 在異步操作成功時調(diào)用, 并將異步操作的結(jié)果, 作為參數(shù)傳遞出去

reject

  • reject 函數(shù)的作用是將 Promise 對象的狀態(tài)從 “未完成”變?yōu)椤笆 ?即從 pending 變?yōu)?rejected), 在異步操作失敗時調(diào)用, 并將異步操作報出的錯誤, 作為參數(shù)傳遞出去

then 方法還可以接受兩個回調(diào)函數(shù)作為參數(shù), 合并 .catch()

promise.then(data => { 
  // 這里對 data 就可以進(jìn)行數(shù)據(jù)拿取操作了
  console.log('success')
}, error => {
  console.log('failure')
})
  • 第一個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?fulfilled 時調(diào)用
  • 第二個回調(diào)函數(shù)是 Promise 對象的狀態(tài)變?yōu)?rejected 時調(diào)用
  • 其中, 第二個回調(diào)函數(shù)是可選的, 不一定要提供, 這兩個函數(shù)都接受Promise 對象傳出的值作為參數(shù)

一般來說, 調(diào)用 resolve 或 reject 以后, Promise 的使命就完成了, 后繼操作應(yīng)該放到 then 方法里面, 而不應(yīng)該直接寫在 resolve 或 reject 的后面
所以, 最好在將它們加上 return 語句, 這樣就不會有意外

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的語句不會執(zhí)行
  console.log(2);
})

Promise 鏈?zhǔn)秸{(diào)用

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success1')
  }, 1000)
}).then(res => {
  console.log(res)  // success1
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success2')
    }, 1000)
  })
}).then(res => {
  console.log(res)  // success2
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success3')
    }, 1000)
  })
}).then(res => {
  console.log(res)  // success3
})

Promise 鏈?zhǔn)秸{(diào)用簡寫

如果我們希望數(shù)據(jù)直接包裝成 Promise.resolve, 那么在 then 中可以直接返回數(shù)據(jù)

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success1')
  }, 1000)
}).then(res => {
  console.log(res)  // success1
  return 'success2'
}).then(res => {
  console.log(res)  // success2
  return 'success3'
}).then(res => {
  console.log(res)  // success3
})

Promise.prototype.finally()

finally()方法用于指定不管 Promise 對象最后狀態(tài)如何, 都會執(zhí)行的操作

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中, 不管promise最后的狀態(tài), 在執(zhí)行完thencatch指定的回調(diào)函數(shù)以后, 都會執(zhí)行finally方法指定的回調(diào)函數(shù)

finally方法的回調(diào)函數(shù)不接受任何參數(shù), 這意味著沒有辦法知道前面的 Promise 狀態(tài)到底是fulfilled還是rejected, 這表明, finally方法里面的操作, 應(yīng)該是與狀態(tài)無關(guān)的, 不依賴于 Promise 的執(zhí)行結(jié)果

Promise.all()

Promise.all()方法用于將多個 Promise 實(shí)例, 包裝成一個新的 Promise 實(shí)例

const p = Promise.all([p1, p2])

上面代碼中, Promise.all()方法接受一個數(shù)組作為參數(shù), p1、p2都是 Promise 實(shí)例, Promise.all()方法的參數(shù)可以不是數(shù)組, 但必須具有 Iterator 接口, 且返回的每個成員都是 Promise 實(shí)例

p的狀態(tài)由p1、p2決定, 分成兩種情況

  1. 只有p1p2的狀態(tài)都變成fulfilled, p的狀態(tài)才會變成fulfilled, 此時p1、p2的返回值組成一個數(shù)組, 傳遞給p的回調(diào)函數(shù)
  2. 只要p1、p2之中有一個被rejected, p的狀態(tài)就變成rejected, 此時第一個被reject的實(shí)例的返回值, 會傳遞給p的回調(diào)函數(shù)
/* 兩個異步操作狀態(tài)都為 fulfilled */
var p1 = new Promise((resolve, reject) => {
  resolve('request1')
})

var p2 = new Promise((resolve, reject) => {
  resolve('request2')
})

Promise.all([p1, p2])
  .then(res => console.log(res)) // ['request1', 'request2']
  .catch(e => console.log(e))

/* 其中有一個異步操作狀態(tài)為 rejected */
var p1 = new Promise((resolve, reject) => {
  resolve('request1')
})

var p2 = new Promise((resolve, reject) => {
  reject('request2 error')
})

Promise.all([p1, p2])
  .then(res => console.log(res))
  .catch(e => console.log(e)) // 'request2 error'

注意, 如果作為參數(shù)的 Promise 實(shí)例, 自己定義了catch方法, 那么它一旦被rejected, 并不會觸發(fā)Promise.all()catch方法

const p1 = new Promise((resolve, reject) => {
  resolve('request1')
})

const p2 = new Promise((resolve, reject) => {
  throw new Error('報錯了')
}).catch(e => e)

Promise.all([p1, p2])
  .then(res => console.log(res)) // ['request1', Error: 報錯了]
  .catch(e => console.log(e))

上面代碼中, p1 會 resolved, p2 首先會 rejected, 但是 p2 有自己的catch方法, 該方法返回的是一個新的 Promise 實(shí)例, p2 指向的實(shí)際上是這個實(shí)例

該實(shí)例執(zhí)行完catch方法后, 也會變成 resolved, 導(dǎo)致Promise.all()方法參數(shù)里面的兩個實(shí)例都會resolved, 因此會調(diào)用then方法指定的回調(diào)函數(shù), 而不會調(diào)用catch方法指定的回調(diào)函數(shù)

如果 p2 沒有自己的catch方法, 就會調(diào)用Promise.all()catch方法

Promise.race()

Promise.race()方法同樣是將多個 Promise 實(shí)例, 包裝成一個新的 Promise 實(shí)例

const p = Promise.race([p1, p2])
  • 只要p1、p2之中有一個實(shí)例率先改變狀態(tài), p的狀態(tài)就跟著改變

  • 那個率先改變的 Promise 實(shí)例的返回值, 就傳遞給p的回調(diào)函數(shù)

  • Promise.race()方法的參數(shù)與Promise.all()方法一樣

下面是一個例子

/* 第一個異步操作率先完成, 并且狀態(tài)為 fulfilled */
Promise.race([
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('request success')
    }, 1000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('request timeout')
    }, 2000)
  })
])
  .then(res => console.log(res))  // request success
  .catch(e => console.log(e))

/* 第二個異步操作先完成, 并且狀態(tài)為 rejected */
Promise.race([
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('request success')
    }, 1000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('request timeout')
    }, 500)
  })
])
  .then(res => console.log(res))
  .catch(e => console.log(e)) // request timeout
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容