《深入理解ES6》筆記—— Promise與非同步程式設計(11)

二月發表於2017-08-01

為什麼要非同步程式設計

我們在寫前端程式碼時,經常會對dom做事件處理操作,比如點選、啟用焦點、失去焦點等;再比如我們用ajax請求資料,使用回撥函式獲取返回值。這些都屬於非同步程式設計。

也許你已經大概知道JavaScript引擎單執行緒的概念,那麼這種單執行緒模式和非同步程式設計有什麼關係呢?

JavaScript引擎中,只有一個主執行緒,當執行JavaScript程式碼塊時,不允許其他程式碼塊執行,而事件機制和回撥機制的程式碼塊會被新增到任務佇列(或者叫做堆疊)中,當符合某個觸發回撥或者事件的時候,就會執行該事件或者回撥函式。

上面這段話的意思可以這樣理解,假設你是一個修仙者,你去闖一個祕境,這個祕境就是主執行緒,你只能一直深入下去,直到找到寶物和出口,而你還有一個自身的儲物空間,這個空間就類似堆疊,你在儲物空間放了很多你可能用到的法寶或者丹藥,這些東西就是回撥函式和事件函式,當你遇到危險或者滿足某個條件時,就可以從儲物空間拿出你當前需要的東西。

好吧,不扯這麼遠,下面看正題。

事件模型:
瀏覽器初次渲染DOM的時候,我們會給一些DOM繫結事件函式,只有當觸發了這些DOM事件函式,才會執行他們。

const btn = document.querySelector(`.button`)
btn.onclick = function(event) {
  console.log(event)
}

回撥模式:
nodejs中可能非常常見這種回撥模式,但是對於前端來說,ajax的回撥是最熟悉不過了。ajax回撥有多個狀態,當響應成功和失敗都有不同的回撥函式。

$.post(`/router`, function(data) {
  console.log(data)
})

回撥也可能帶來一個問題,那就是地獄回撥,不過幸運的是,我從進入前端界開始,就使用react,跳過了很多坑,特別是地獄回撥,一直沒有機會在工作中遇見到,真是遺憾。

Promise

事件函式沒有問題,我們用的很爽,問題出在回撥函式,尤其是指地獄回撥,Promise的出現正是為了避免地獄回撥帶來的困擾。

推薦你看JavaScript MDN Promise教程,然後再結合本文看,你就能學會使用Promise了。

Promise是什麼

Promise的中文意思是承諾,也就是說,JavaScript對你許下一個承諾,會在未來某個時刻兌現承諾。

Promise生命週期

react有生命週期,vue也有生命週期,就連Promise也有生命週期,現在生命週期咋這麼流行了。

Promise的生命週期:進行中(pending),已經完成(fulfilled),拒絕(rejected)

Promise被稱作非同步結果的佔位符,它不能直接返回非同步函式的執行結果,需要使用then(),當獲取異常回撥的時候,使用catch()。

這次我們使用axios外掛的程式碼做例子。axios是前端比較熱門的http請求外掛之一。

1、建立axios例項instance。

import axios from `axios`
export const instance = axios.create()

2、使用axios例項 + Promise獲取返回值。

const promise = instance.get(`url`)

promise.then(result => console.log(result)).catch(err => console.log(err))

使用Promise構建函式建立新的Promise

Promise建構函式只有一個引數,該引數是一個函式,被稱作執行器,執行器有2個引數,分別是resolve()和reject(),一個表示成功的回撥,一個表示失敗的回撥。

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(5), 0)
}).then(v => console.log(v)) // 5

記住,Promise例項只能通過resolve或者reject函式來返回,並且使用then()或者catch()獲取,不能在new Promise裡面直接return,這樣是獲取不到Promise返回值的。

1、我們也可以使用Promise直接resolve(value)。

Promise.resolve(5).then(v => console.log(v)) // 5

2、也可以使用reject(value)

Promise.reject(5).catch(v => console.log(v)) // 5

3、執行器錯誤通過catch捕捉。

new Promise(function(resolve, reject) {
  if(true) {
    throw new Error(`error!!`)
  }
}).catch(v => console.log(v.message)) // error!!

全域性的Promise拒絕處理

不重要的內容,不用細看。

這裡涉及到nodejs環境和瀏覽器環境的全域性,主要說的是如果執行了Promise.reject(),瀏覽器或者node環境並不會強制報錯,只有在你呼叫catch的時候,才能知道Promise被拒絕了。

這種行為就像是,你寫了一個函式,函式內部有true和false兩種狀態,而我們希望false的時候丟擲錯誤,但是在Promise中,並不能直接丟擲錯誤,無論Promise是成功還是拒絕狀態,你獲取Promise生命週期的方法只能通過then()和catch()。

nodejs環境:

node環境下有個物件叫做process,即使你沒寫過後端node,如果寫過前端node伺服器,也應該知道可以使用process.ENV_NODE獲取環境變數。為了監聽Promise的reject(拒絕)情況,NodeJS提供了一個process.on(),類似jQuery的on方法,事件繫結函式。

process.on()有2個事件

unhandledRjection:在一個事件迴圈中,當Promise執行reject(),並且沒有提供catch()時被呼叫。

正常情況下,你可以使用catch捕捉reject。

Promise.reject("It was my wrong!").catch(v => console.log(v))

但是,有時候你不總是記得使用catch。你就需要使用process.on()

let rejected
rejected = Promise.reject("It was my wrong!")

process.on("unhandledRjection", function(reason, promise) {
  console.log(reason.message) // It was my wrong!
  console.log(rejected === promise) // true
})

rejectionHandled:在一個事件迴圈後,當Promise執行reject,並且沒有提供catch()時被呼叫。

let rejected
rejected = Promise.reject(new Error("It was my wrong!"))

process.on("rejectionHandled", function(promise) {
  console.log(rejected === promise) // true
})

異同:

事件迴圈中、事件迴圈後,你可能很難理解這2個的區別,但是這不重要,重要的是,如果你通過了catch()方法來捕捉reject操作,那麼,這2個事件就不會生效。

瀏覽器環境:

和node環境一樣,都提供了unhandledRjection、rejectionHandled事件,不同的是瀏覽器環境是通過window物件來定義事件函式。

let rejected
rejected = Promise.reject(new Error("It was my wrong!"))

window.rejectionHandled = function(event) {
  console.log(event) // true
}
rejectionHandled()

將程式碼在瀏覽器控制檯執行一遍,你就會發現報錯了:Uncaught (in promise) Error: It was my wrong!

耶,你成功了!報錯內容正是你寫的reject()方法裡面的錯誤提示。

Promise鏈式呼叫

這個例子中,使用了3個then,第一個then返回 s * s,第二個then捕獲到上一個then的返回值,最後一個then直接輸出end。這就叫鏈式呼叫,很好理解的。我只使用了then(),實際開發中,你還應該加上catch()。

new Promise(function(resolve, reject) {
try {

resolve(5)

} catch (error) {

reject(`It was my wrong!!!`)

}
}).then(s => s * s).then(s2 => console.log(s2)).then(() => console.log(`end`))
// 25 “end”

Promise的其他方法

在Promise的建構函式中,除了reject()和resolve()之外,還有2個方法,Promise.all()、Promise.race()。

Promise.all():

前面我們的例子都是隻有一個Promise,現在我們使用all()方法包裝多個Promise例項。

語法很簡單:引數只有一個,可迭代物件,可以是陣列,或者Symbol型別等。

Promise.all(iterable).then().catch()

示例:傳入3個Promise例項

Promise.all([
  new Promise(function(resolve, reject) {
    resolve(1)
  }),
  new Promise(function(resolve, reject) {
    resolve(2)
  }),
  new Promise(function(resolve, reject) {
    resolve(3)
  })
]).then(arr => {
  console.log(arr) // [1, 2, 3]
})

Promise.race():語法和all()一樣,但是返回值有所不同,race根據傳入的多個Promise例項,只要有一個例項resolve或者reject,就只返回該結果,其他例項不再執行。

還是使用上面的例子,只是我給每個resolve加了一個定時器,最終結果返回的是3,因為第三個Promise最快執行。

Promise.race([
  new Promise(function(resolve, reject) {
    setTimeout(() => resolve(1), 1000)
  }),
  new Promise(function(resolve, reject) {
    setTimeout(() => resolve(2), 100)
  }),
  new Promise(function(resolve, reject) {
    setTimeout(() => resolve(3), 10)
  })
]).then(value => {
  console.log(value) // 3
})

Promise派生

派生的意思是定義一個新的Promise物件,繼承Promise方法和屬性。

class MyPromise extends Promise {

  //重新封裝then()
  success(resolve, reject) {
    return this.then(resolve, reject)
  }
  //重新封裝catch()
  failer(reject) {
    return this.catch(reject)
  }
}

接著我們來使用一下這個派生類。


new MyPromise(function(resolve, reject) {
  resolve(10)
}).success(v => console.log(v)) // 10

如果只是派生出來和then、catch一樣的方法,我想,你不會幹這麼無聊的事情。

Promise和非同步的聯絡

Promise本身不是非同步的,只有他的then()或者catch()方法才是非同步,也可以說Promise的返回值是非同步的。通常Promise被使用在node,或者是前端的ajax請求、前端DOM渲染順序等地方。

比Promise更牛逼的非同步方案

在本章你只需要瞭解有async這個未來的方案,推薦不會的趕緊去網上找資料學,反正我是已經在實際專案中全面開展async了。

async function a() {
    await function() {}}
}

總結

Promise是什麼、怎麼用、怎麼獲取返回值?是本章的中心內容,多看幾遍,你會發現使用Promise是非常簡單的事情。

=> 返回文章目錄

相關文章