異步編程

什么是同步和異步?

你可能知道, JavaScript 語言 的執行環境是“單線程”

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

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

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

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

這里的 “同步”和“異步” 與我們現實中的同步、異步恰恰相反

例如:

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

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

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

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

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

獲取異步操作的值只能使用回調函數的方式, 異步操作都是最后執行

回調函數

回調函數的方式獲取異步操作內的數據

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

這種方式雖然看似沒什么問題, 但是, 當網絡請求非常復雜時, 就會出現回調地獄

ok, 我們用一個非常夸張的案例來說明

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

上面的代碼有什么問題?

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

Promise

什么是 Promise ?

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

Promise 到底是做什么的?

  • Promise 是異步編程的一種解決方案, 比傳統的解決方案回調函數和事件更合理和更強大

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

為了解決回調地獄所帶來的問題, ES6 里引進了 Promise, 有了 Promise 對象, 就可以將異步操作以同步操作的流程表達出來, 避免了層層嵌套的回調函數
Promise 對象提供統一的接口, 使得控制異步操作更加容易

Promise 的特點

Promise 對象有以下兩個特點

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

Promise 的缺點

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

Promise 的三種狀態

  • pending : 等待(wait)狀態, 比如正在進行網絡請求, 或者定時器沒有到時間
  • fulfilled : 滿足狀態, 當我們主動調用 resolve 時, 就處于該狀態, 并且回調 .then()
  • rejected : 拒絕狀態, 當我們主動調用 reject 時, 就處于該狀態, 并且回調 .catch()

Promise 基本用法

ES6 規定, Promise 對象是一個構造函數, 用來生成 Promise 實例

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

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

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

resolve

  • resolve 函數的作用是將 Promise 對象的狀態從 “未完成”變為“成功”(即從 pending 變為 fulfilled), 在異步操作成功時調用, 并將異步操作的結果, 作為參數傳遞出去

reject

  • reject 函數的作用是將 Promise 對象的狀態從 “未完成”變為“失敗”(即從 pending 變為 rejected), 在異步操作失敗時調用, 并將異步操作報出的錯誤, 作為參數傳遞出去

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

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

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

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

Promise 鏈式調用

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 鏈式調用簡寫

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

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 對象最后狀態如何, 都會執行的操作

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

上面代碼中, 不管promise最后的狀態, 在執行完thencatch指定的回調函數以后, 都會執行finally方法指定的回調函數

finally方法的回調函數不接受任何參數, 這意味著沒有辦法知道前面的 Promise 狀態到底是fulfilled還是rejected, 這表明, finally方法里面的操作, 應該是與狀態無關的, 不依賴于 Promise 的執行結果

Promise.all()

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

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

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

p的狀態由p1p2決定, 分成兩種情況

  1. 只有p1p2的狀態都變成fulfilled, p的狀態才會變成fulfilled, 此時p1p2的返回值組成一個數組, 傳遞給p的回調函數
  2. 只要p1p2之中有一個被rejected, p的狀態就變成rejected, 此時第一個被reject的實例的返回值, 會傳遞給p的回調函數
/* 兩個異步操作狀態都為 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))

/* 其中有一個異步操作狀態為 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'

注意, 如果作為參數的 Promise 實例, 自己定義了catch方法, 那么它一旦被rejected, 并不會觸發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 實例, p2 指向的實際上是這個實例

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

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

Promise.race()

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

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

  • 那個率先改變的 Promise 實例的返回值, 就傳遞給p的回調函數

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

下面是一個例子

/* 第一個異步操作率先完成, 并且狀態為 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))

/* 第二個異步操作先完成, 并且狀態為 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
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容