JavaScript 解決非同步程式設計有兩種主要方式:事件模型和回撥函式。但是隨著業務越來越複雜,這兩種方式已經不能滿足開發者的需求了,Promise 可以解決這方面的問題。
為了更好的理解 Promise 是如何工作的,我們先來了解一下傳統的非同步程式設計的方式。
非同步程式設計的方式
1. 事件模型:
let button = document.getElementId("my-btn");
button.onclick = function() {
console.log("Hello");
}
任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。上面程式碼中,console.log(“Hello”) 直到 button 被點選後才會被執行。當 button 被點選,賦值給 onclick 的函式就被新增到作業佇列的尾部,並在佇列前部所有任務結束之後再執行。
事件模型適用於處理簡單的互動,若將多個獨立的非同步呼叫連線在一起,必須跟蹤每個事件的事件目標,會使程式更加複雜,執行流程會變得很不清晰。
2. 回撥函式
我們來看一下比較經典的使用 jsonp 解決跨域問題的示例:
function callback (res) {
document.getElementById(`d1`).innerHTML = res.result.address
console.log(`Your public IP address is: `, res)
}
function jsonp (lat, lng) {
let src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=yourKey&output=jsonp&callback=callback`
let script = document.createElement(`script`)
script.setAttribute("type","text/javascript")
script.src = src;
document.body.appendChild(script)
}
jsonp(39.92, 116.43)
初看這種模式運作得相當好,簡單、容易理解,但你可能會迅速的發現這樣的模式不利於程式碼的閱讀和維護,各個部分之間耦合度太高,容易陷入回撥地獄。就像這樣:
method1(function(err, result) {
if (err) {
throw err
}
method2(function(err, result) {
if (err) {
throw err
}
method3(function(err, result) {
if (err) {
throw err
}
method4(function(err, result) {
if (err) {
throw err
}
method5(result)
})
})
})
})
Promise 能大幅度改善這種情況。我們來看下Promise 是如何實現的:
let promise = new Promise((resolve, reject) => {
// ... method 1 some code
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
promise.then((value) => {
// method 2 some code
}).then((value) => {
// method 3 some code
}).then((value) => {
// method 4 some code
}).then((value) => {
// method 5 some code
}).catch((err) => {
// some err code
})
是不是清晰很多?是不是很神奇?接下來一起來學習一下 Promise 吧!
Promise 相關知識
1. Promise 的基礎知識
我們先來看下 Promise 的方法有哪些:
Promise.Prototype.then()
Promise.Prototype.catch()
Promise.Prototype.finally()
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()
Promise.try()
Promise 函式的執行,都是依賴於狀態的改變,這三種狀態要記牢哦:
Pending:進行中
Fulfilled:已成功
Rejected:已失敗
Promise 優點:
1)物件的狀態不受外界影響。
2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
3)將非同步操作以同步操作的流程表達出來,避免了層層巢狀回撥函式。
4)提供統一的介面,使得控制非同步操作更加容易。
Promise 缺點:
1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消。
2)如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反映到外部。
3)當處於 pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
瞭解了 Promise 的方法,3種狀態以及特點和優缺點之後,接下來我們來看一下 Promise 是怎麼使用的。
2. Promise 的基本用法
我們來創造一個讀取檔案的 Promise 例項:
const fs = require(`fs`)
const path = require(`path`)
function readFile (filename) {
return new Promise (function (resolve, reject) {
// reject(new Error(`err`))
//觸發非同步操作
fs.readFile(filename, {encoding: `utf8`}, function (err, contents) {
// 檢查錯誤
if (err) {
reject(err)
return
}
//讀取成功
resolve(contents)
})
})
}
let promise = readFile(path.resolve(__dirname, `../json/a.json`))
上述例項中 resolve 函式的作用是,將 Promise 物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;
reject 函式的作用是,將 Promise 物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
2.1 Promise.Prototype.then()
Promise 例項生成以後,就可以用 then 方法來分別指定 resolved 狀態和 rejected 狀態的回撥函式了。
promise.then(function (contents) {
// 完成
console.log(contents)
return(contents)
}, function (err) {
// 失敗
console.log(err.message)
})
我們可以看到,then 方法接受兩個回撥函式作為引數;
第一個回撥函式在 Promise 物件的狀態變為 resolved 時呼叫;
第二個回撥函式在 Promise 物件的狀態變為 rejected 時呼叫;
其中,第二個函式是可選的。這兩個函式都接受 Promise 物件傳出的值作為引數。
Promise.then 方法每次呼叫,都返回一個新的 Promise 物件,所以支援鏈式寫法。
let taskA = (value) => {
console.log("Task A")
console.log(value)
return value
}
let taskB = (value) => {
console.log("Task B")
console.log(value)
}
promise
.then(taskA)
.then(taskB)
.catch((err) => {
console.log(err.message)
})
2.2 Promise.Prototype.catch()
Promise.prototype.catch 方法是 .then(null, rejection) 的別名,相當於 then 函式的第一個引數傳入 null,第二個引數傳入發生錯誤時的回撥函式。
promise.then(function(value) {
// 成功
console.log(value)
}).catch(function (err) {
// 失敗
console.log(err.message)
})
2.3 Promise.Prototype.finally()
finally 方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的,目前大部分瀏覽器還不支援,不過可以自己實現。
finally 方法的實現:
Promise.prototype.finally = function (callback) {
let P = this.constructor
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
)
}
finally 方法的使用:
promise
.then((contents) => {
console.log(contents)
return contents
})
.catch((err) => {
console.log(err.message)
})
.finally(() => {
console.log(`finally`)
})
2.4 Promise.all()
Promise.all 方法可以將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.all([p1, p2, p3]);
新的 Promise p 的狀態由 p1、p2、p3 決定,只有當 p1、p2、p3 的狀態都變成了 fulfilled,p 的狀態才會變成 fulfilled;只要 p1、p2、p3 之中有一個被 rejected,p 的狀態就變成了 rejected。
注意,如果作為引數的 Promise 例項,自己定義了 catch 方法,那麼它一旦被 rejected,並不會觸發Promise.all()的 catch 方法的。
如果 p2 有自己的 catch 方法,就不會呼叫 Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => {
resolve(`hello`);
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error(`報錯了`);
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]
如果 p2 沒有自己的 catch 方法,就會呼叫 Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => {
resolve(`hello`);
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error(`報錯了`);
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
2.5 Promise.race()
Promise.race 方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.race([p1, p2, p3]);
新的Promise p,只要 p1、p2、p3 之中有一個例項率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給 p 的回撥函式。
function timerPromisefy(delay) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(delay)
}, delay)
})
}
Promise.race([
timerPromisefy(10),
timerPromisefy(20),
timerPromisefy(30)
]).then(function (values) {
console.log(values) // 10
})
2.6 Promise.resolve()
有時需要將現有物件轉為 Promise 物件,Promise.resolve 方法就起到這個作用,返回一個 fulfilled 狀態的 Promise 物件。
const promise = Promise.resolve(`hello`);
promise.then(function(value){
console.log(value);
});
// 相當於
const promise = new Promise(resolve => {
resolve(`hello`);
});
promise.then((value) => {
console.log(value)
})
2.7 Promise.reject()
Promise.reject(reason) 方法也會返回一個新的 Promise 例項,該例項的狀態為 rejected。
const p = Promise.reject(`出錯了`);
p.then(null, (value) => {
console.log(value)
})
// 等同於
const p = new Promise((resolve, reject) => reject(`出錯了`))
p.then(null, (value) => {
console.log(value)
})
2.8 Promise.try()
讓同步函式同步執行,非同步函式非同步執行。
const f = () => console.log(`now`);
Promise.try(f);
console.log(`next`);
// now
// next
應用
非同步載入圖片
實現方法:
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error(`Could not load image at ` + url))
}
image.src = url
})
}
呼叫:
loadImageAsync(`圖片路徑`).then((value) => {
document.getElementById(`d1`).appendChild(value)
}).catch((err) => {
console.log(err.message)
})
非同步載入 js
實現方法:
let loadScript = function () {
return function _loadScript(url, callBack) {
return new Promise(function (resolve) {
let script = document.createElement(`script`)
script.type = `text/javascript`
if (script.readyState) {
// 相容IE的script載入事件
script.onreadystatechange = function () {
// loaded : 下載完畢 complete: 資料準備完畢。這兩個狀態ie可能同時出現或者只出現一個
if (script.readyState === `loaded` || script.readyState === `complete`) {
// 防止載入兩次
script.onreadystatechange = null
callBack()
// 把函式傳遞下去,保證能順序載入js
resolve(_loadScript)
}
}
} else {
script.onload = function () {
callBack()
resolve(_loadScript)
}
}
script.src = url
document.head.appendChild(script)
})
}
}()
呼叫:
loadScript(`http://code.jquery.com/jquery-3.2.1.min.js `, () => {})
.then(() => {
$("#d1").on(`click`, () => {alert(1)})
}).catch((err) => {
console.log(err)
})
request 請求的封裝
import axios from `./axios`
import qs from `qs`
const config = {
time: +new Date() + ``,
timeout: 6000,
headers: {
`Content-Type`: `application/x-www-form-urlencoded`,
time: new Date().getTime()
}
}
function checkResponse (response, notice) {
return new Promise((resolve, reject) => {
let code = Number(response.code)
if (code === 0 || code === 200 || code === 2000 || code === 1 || code === 2 || code === `0` || code === 109) {
resolve(response)
} else {
if (notice) {
// 提示資訊
console.log(`response-notice`, notice)
}
reject(response)
}
})
}
function fixURL (url, type) {
let result = ``
switch (type) {
case `r`:
result += `/api/v2${url}`
break
}
return result
}
/**
* Requests a URL, returning a promise.
*
* @param {object} [options] The options we want to pass to axios
* @param {string} [options.url] 請求的url地址(必須)
* @param {string} [options.method] 請求方式, get or post,預設post
* @param {object} [options.data] 請求引數
* @param {number} [options.timeout] 請求超時時間
* @param {boolean} [options.notice] 請求失敗是否顯示提示,預設true
* @return {object} promise物件
*/
function request (options = {}) {
let {
url,
method,
data,
timeout,
headers,
type,
notice
} = options
method = method || `post`
data = data || {}
type = type || `t`
timeout = timeout || config.timeout
headers = Object.assign({}, config.headers, headers)
notice = notice === undefined ? true : notice
let result = {}
if (method === `get`) {
result = new Promise((resolve, reject) => {
axios({
method: `get`,
url: fixURL(url, type),
params: data,
timeout,
headers
})
.then((res) => {
checkResponse(res.data, notice).then((data) => {
resolve(data)
})
.catch((data) => {
reject(data)
})
})
.catch((data) => {
reject(data)
})
})
} else if (method === `post`) {
result = new Promise((resolve, reject) => {
axios({
method: `post`,
url: fixURL(url, type),
data: headers[`Content-Type`] === `application/x-www-form-urlencoded` ? qs.stringify(data) : data,
timeout,
headers
})
.then((res) => {
checkResponse(res.data, notice).then((data) => {
resolve(data)
})
.catch((data) => {
reject(data)
})
})
.catch((data) => {
reject(data)
})
})
}
return result
}
export default request
附 Promise 程式碼實現
// 判斷變數否為function
const isFunction = variable => typeof variable === `function`
// 定義Promise的三種狀態常量
const PENDING = `PENDING`
const FULFILLED = `FULFILLED`
const REJECTED = `REJECTED`
class MyPromise {
constructor (handle) {
if (!isFunction(handle)) {
throw new Error(`MyPromise must accept a function as a parameter`)
}
// 新增狀態
this._status = PENDING
// 新增狀態
this._value = undefined
// 新增成功回撥函式佇列
this._fulfilledQueues = []
// 新增失敗回撥函式佇列
this._rejectedQueues = []
// 執行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 新增resovle時執行的函式
_resolve (val) {
const run = () => {
if (this._status !== PENDING) return
// 依次執行成功佇列中的函式,並清空佇列
const runFulfilled = (value) => {
let cb;
while (cb = this._fulfilledQueues.shift()) {
cb(value)
}
}
// 依次執行失敗佇列中的函式,並清空佇列
const runRejected = (error) => {
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(error)
}
}
/* 如果resolve的引數為Promise物件,則必須等待該Promise物件狀態改變後,
當前Promsie的狀態才會改變,且狀態取決於引數Promsie物件的狀態
*/
if (val instanceof MyPromise) {
val.then(value => {
this._value = value
this._status = FULFILLED
runFulfilled(value)
}, err => {
this._value = err
this._status = REJECTED
runRejected(err)
})
} else {
this._value = val
this._status = FULFILLED
runFulfilled(val)
}
}
// 為了支援同步的Promise,這裡採用非同步呼叫
setTimeout(run, 0)
}
// 新增reject時執行的函式
_reject (err) {
if (this._status !== PENDING) return
// 依次執行失敗佇列中的函式,並清空佇列
const run = () => {
this._status = REJECTED
this._value = err
let cb;
while (cb = this._rejectedQueues.shift()) {
cb(err)
}
}
// 為了支援同步的Promise,這裡採用非同步呼叫
setTimeout(run, 0)
}
// 新增then方法
then (onFulfilled, onRejected) {
const { _value, _status } = this
// 返回一個新的Promise物件
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封裝一個成功時執行的函式
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果當前回撥函式返回MyPromise物件,必須等待其狀態改變後在執行下一個回撥
res.then(onFulfilledNext, onRejectedNext)
} else {
//否則會將返回結果直接作為引數,傳入下一個then的回撥函式,並立即執行下一個then的回撥函式
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函式執行出錯,新的Promise物件的狀態為失敗
onRejectedNext(err)
}
}
// 封裝一個失敗時執行的函式
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果當前回撥函式返回MyPromise物件,必須等待其狀態改變後在執行下一個回撥
res.then(onFulfilledNext, onRejectedNext)
} else {
//否則會將返回結果直接作為引數,傳入下一個then的回撥函式,並立即執行下一個then的回撥函式
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函式執行出錯,新的Promise物件的狀態為失敗
onRejectedNext(err)
}
}
switch (_status) {
// 當狀態為pending時,將then方法回撥函式加入執行佇列等待執行
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 當狀態已經改變時,立即執行對應的回撥函式
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
// 新增catch方法
catch (onRejected) {
return this.then(undefined, onRejected)
}
// 新增靜態resolve方法
static resolve (value) {
// 如果引數是MyPromise例項,直接返回這個例項
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
// 新增靜態reject方法
static reject (value) {
return new MyPromise((resolve ,reject) => reject(value))
}
// 新增靜態all方法
static all (list) {
return new MyPromise((resolve, reject) => {
/**
* 返回值的集合
*/
let values = []
let count = 0
for (let [i, p] of list.entries()) {
// 陣列引數如果不是MyPromise例項,先呼叫MyPromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有狀態都變成fulfilled時返回的MyPromise狀態就變成fulfilled
if (count === list.length) resolve(values)
}, err => {
// 有一個被rejected時返回的MyPromise狀態就變成rejected
reject(err)
})
}
})
}
// 新增靜態race方法
static race (list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一個例項率先改變狀態,新的MyPromise的狀態就跟著改變
this.resolve(p).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
finally (cb) {
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason => MyPromise.resolve(cb()).then(() => { throw reason })
);
}
}
參考文章
Promise 官網
ECMAScript 6 入門
Promise 原始碼詳解
Promise 實現原理(附原始碼)
es6-promise-try npm
JavaScript Promise:簡介