參考文獻
Javascript異步編程的4種方法
阮一峰ES6教程---Promise
相關(guān)技術(shù)
Promis, class, 面向?qū)ο?/p>
JS中的同步和異步
- 同步任務(wù)模式
在JS中,因為它的執(zhí)行時單線程的,也就是說一次只能執(zhí)行一個任務(wù),當(dāng)有多個任務(wù)需要執(zhí)行的時候,就需要排隊,一個一個的進行執(zhí)行,就像下面的數(shù)組一樣,從左到右一個一個的執(zhí)行:
[任務(wù)一,任務(wù)二,任務(wù)三]
這樣的任務(wù)執(zhí)行模式是同步執(zhí)行的,有可能會因為其中的某一個任務(wù)執(zhí)行時間非常長而阻塞后續(xù)任務(wù),導(dǎo)致頁面卡死; - 異步任務(wù)模式
而異步任務(wù)模式則解決了上述問題,例如數(shù)組中的任務(wù)一執(zhí)行完畢后,執(zhí)行的不是數(shù)組第二位的任務(wù)二,而是一個任務(wù)二回調(diào)函數(shù)(需要將任務(wù)二做成回調(diào)函數(shù)放入任務(wù)一中),而且任務(wù)二可以不等待任務(wù)一執(zhí)行完畢就進行執(zhí)行,這種模式的任務(wù)執(zhí)行是與任務(wù)隊列(上述數(shù)組)的排列順序是不一樣的,比如下面的代碼:
// 任務(wù)一
function fn1() {
console.log(3)
setTimeout(function() {
console.log(1)
}, 0)
}
// 任務(wù)二
function fn2() {
console.log(2)
}
fn1()
fn2()
在該例子中,setTimeout
函數(shù)是異步的,在任務(wù)一執(zhí)行過程中遇到了setTimeout
,于是將其內(nèi)的任務(wù)操作放到了事件隊列的隊尾,先去執(zhí)行任務(wù)二,執(zhí)行完畢后再回過頭來執(zhí)行隊尾的任務(wù)console.log(1)
,在此過程中,任務(wù)一內(nèi)的執(zhí)行沒有阻塞任務(wù)二的執(zhí)行
輸出結(jié)果
幾種異步任務(wù)模式
- 使用setTimeout
JS中的定時器setTimeout是實現(xiàn)異步任務(wù)執(zhí)行最簡單也最常見的方式,通過使用setTimeout(callback, 0)
可以讓執(zhí)行上一個任務(wù)執(zhí)行時間過長的操作放到下一個任務(wù)執(zhí)行完之后再執(zhí)行,比如上一小節(jié)舉的例子,而關(guān)于定時器setTimeout
和setInterval
相關(guān)可以參考下面鏈接:
setTimeout
setInterval
優(yōu)點:簡單明了;
缺點:①、耦合嚴重;②、容易陷入回調(diào)地獄
PS:回調(diào)地獄例子(該操作只有三步,如果是十步,嵌套將會非常嚴重)
function fn(callback1, callback2) {
// 耗時操作
let a = 0
for (let i = 0; i < 100; i++) {
a++
}
setTimeout(function() {
callback1(++a)
setTimeout(function() {
callback2(++a)
}, 0)
}, 0)
}
function fn1(a) {
console.log(a)
}
function fn2(a) {
console.log(a)
}
fn(fn1, fn2)
- 發(fā)布/訂閱
發(fā)布/訂閱模式是通過信號的發(fā)送時機來決定什么時候執(zhí)行其內(nèi)相應(yīng)的任務(wù),下面是一個發(fā)布訂閱模式的代碼實現(xiàn):
class EventCenter {
// 定義事件中心
constructor() {
this.events = {}
}
// 發(fā)布器
on(evt, handler) {
// 檢測事件信號是否存在,當(dāng)存在時不做操作,不存在時創(chuàng)建給予這個信號的方法存儲器(數(shù)組)
this.events[evt] = this.events[evt] || []
// 將傳入的方法放入數(shù)組中
this.events[evt].push({
handler: handler
})
}
// 訂閱器
fire(evt, params) {
// 檢測當(dāng)前被訂閱的信號是否存在,存在則執(zhí)行其內(nèi)的所有方法
if (!this.events[evt]) {
return
}
for (let i = 0; i < this.events[evt].length; i++) {
this.events[evt][i].handler(params)
}
}
}
let center = new EventCenter()
center.on('event', function(data) {
console.log('event執(zhí)行了第一個任務(wù)')
})
center.on('event', function(data) {
console.log('event執(zhí)行了第二個任務(wù)')
})
center.fire('event')
輸出結(jié)果:
- Promise
Promise是ES6中新增的內(nèi)置對象,專門用于解決異步相關(guān)的問題,其內(nèi)最重要的兩個方法是then
和catch
,then
方法第一個參數(shù)是resolve狀態(tài)時執(zhí)行的回調(diào),第二個參數(shù)則是reject狀態(tài)時執(zhí)行的回調(diào),而catch則是then
中有一環(huán)是reject
就執(zhí)行的回調(diào)函數(shù)
通常使用的姿勢是這樣的
function getData(){
let promise = new Promise((resolve, reject) => {
// AJAX獲取數(shù)據(jù)。。。。
if(success){
// 成功時執(zhí)行
resolve(fn1)
}else{
// 失敗時執(zhí)行
reject(fn2)
}
})
return promise
}
getData().then(fn1).catch(fn2)
可以從中發(fā)現(xiàn),除了then
和catch
兩個方法外,Promise還有鏈式調(diào)用的功能,那么下面就實現(xiàn)這樣的一個Promise。
Promise基本功能的實現(xiàn)
- 首先寫一段測試代碼,使用Promise的調(diào)用方式
let p = new Promise()
function f1() {
console.log('f1')
setTimeout(function() {
p.resolve('1')
}, 1000)
return p
}
function f2(result) {
console.log('f2', result)
setTimeout(function() {
p.resolve('2')
}, 1000)
}
function f3(result) {
console.log('f3', result)
setTimeout(function() {
p.resolve('3')
}, 1000)
}
function f4(result) {
console.log('f4', result)
}
f1().then(f2).then(f3).catch(f4)
// 或者 f1().then(f2, f4).then(f3, f4)
- 第一步、我們創(chuàng)建這么一個類,里面有
then
和catch
兩個方法,并且能夠鏈式調(diào)用,為了防止重復(fù),這里用小寫開頭的promise
class promise {
constructor(){
}
then(success, fail) {
// 鏈式調(diào)用
return this
}
catch (fail) {
// 鏈式調(diào)用
return this
}
}
- 第二步、因為有
resolve
和reject
兩種狀態(tài),那么另外再設(shè)立兩個函數(shù)resolve
和reject
分別進行不同的狀態(tài)管理
class promise {
constructor() {
}
then(success, fail) {
// 鏈式調(diào)用
return this
}
catch (fail) {
// 鏈式調(diào)用
return this
}
// 成功狀態(tài)的管理
resolve(result) {
}
// 失敗狀態(tài)的管理
reject(result) {
}
}
- 第三步、設(shè)定一個數(shù)組對需要執(zhí)行的方法進行暫存,以及一個方法的執(zhí)行器,該執(zhí)行器依賴于
resolve
和reject
傳入的狀態(tài)進行相應(yīng)的執(zhí)行
class promise {
constructor() {
this.callbacks = []
}
then(success, fail) {
// 鏈式調(diào)用
return this
}
catch (fail) {
// 鏈式調(diào)用
return this
}
// 成功狀態(tài)的管理
resolve(result) {
this.actuator('resolve', result)
}
// 失敗狀態(tài)的管理
reject(result) {
this.actuator('reject', result)
}
// 執(zhí)行器
actuator(status, result) {
}
}
- 第四步、編寫
then
函數(shù)與執(zhí)行器中的邏輯
// 為了方便查看放到了最上面
let p = new promise()
function f1() {
console.log('f1')
setTimeout(function() {
p.resolve('1')
}, 1000)
return p
}
// ①、在寫then函數(shù)之前,先看看最開始Promise的調(diào)用方式是怎么樣的:f1().then(f2).then(f3).catch(f4),
然后在f1中嵌套回調(diào)f2并且返回這個Promise對象。
此外,then函數(shù)可以接受兩個參數(shù),一個成功回調(diào)一個失敗回調(diào),
所以思路就是創(chuàng)建一個對象里面有'resolve'和'reject'對應(yīng)這兩個回調(diào)然后放入callbacks數(shù)組中進行管理;
then(success, fail) {
this.callbacks.push({
resolve: success,
reject: fail
})
// 鏈式調(diào)用
return this
}
// ②、這時候在調(diào)用f1時他會先返回Promise對象,然后再調(diào)用setTimeout里面的resolve回調(diào)并傳入?yún)?shù),而在resolve函數(shù)中調(diào)用了執(zhí)行器actuator,并且傳入了resolve這個狀態(tài)和在f1中傳入的參數(shù);
// 成功狀態(tài)的管理
resolve(result) {
this.actuator('resolve', result)
}
// ③、執(zhí)行actuator函數(shù),其實分析到了這一步就很簡單了,不過是將先前傳入callbaks中的函數(shù)取出來,然后執(zhí)行其中的成功回調(diào)就是了
actuator(status, result) {
// 取出之前傳入的回調(diào)函數(shù)對象(包含成功和失敗回調(diào)),然后執(zhí)行
let handlerObj = this.callbacks.shift()
handlerObj[type](result)
}
// ④、整體代碼
class promise {
constructor() {
this.callbacks = []
}
then(success, fail) {
this.callbacks.push({
resolve: success,
reject: fail
})
// 鏈式調(diào)用
return this
}
catch (fail) {
// 鏈式調(diào)用
return this
}
// 成功狀態(tài)的管理
resolve(result) {
this.actuator('resolve', result)
}
// 失敗狀態(tài)的管理
reject(result) {
this.actuator('reject', result)
}
// 執(zhí)行器
actuator(status, result) {
// 取出之前傳入的回調(diào)函數(shù)對象(包含成功和失敗回調(diào)),然后執(zhí)行
let handlerObj = this.callbacks.shift()
handlerObj[status](result)
}
}
其實到了這一步,Promise的基本功能(resolve和reject)已經(jīng)實現(xiàn)了,下面來看看f1().then(f2, f4).then(f3, f4).then(f4)
的執(zhí)行結(jié)果吧
①、全部resolve狀態(tài)執(zhí)行結(jié)果
②、f2為reject時候執(zhí)行結(jié)果
function f2(result) {
console.log('f2', result)
setTimeout(function() {
p.reject('2')
}, 1000)
}
- 最后、我們再添加
catch
方法進去
再上述代碼的基礎(chǔ)上,catch
方法的實現(xiàn)其實已經(jīng)變得很簡單了,只需要在constructor里設(shè)立一個oncatch用以保存?zhèn)魅隿atch的回調(diào),然后當(dāng)中間某個環(huán)節(jié)reject的時候調(diào)用這個oncatch方法就好了,全部實現(xiàn)代碼以及調(diào)用實例:
class promise {
constructor() {
// 定義回調(diào)函數(shù)管理器和catch
this.callbacks = []
this.oncatch
}
// 當(dāng)狀態(tài)為reject時候,傳入reject狀態(tài)給執(zhí)行器
reject(result) {
this.actuator('reject', result)
}
// 當(dāng)狀態(tài)為resolve時候,傳入resolve狀態(tài)給執(zhí)行器
resolve(result) {
this.actuator('resolve', result)
}
// 執(zhí)行器
actuator(status, result) {
// 檢測當(dāng)狀態(tài)為reject并且oncatch不為空時,執(zhí)行oncatch保存的失敗回調(diào), 適用于f1().then(f2).then(f3).catch(f4)
if (status === 'reject' && this.catch) {
this.callbacks = []
this.oncatch(result)
// 檢測當(dāng)callbacks第一位有方法時,執(zhí)行相應(yīng)狀態(tài)的方法,適用于f1().then(f2, f4).then(f3, f4)
} else if (this.callbacks[0]) {
let handlerObj = this.callbacks.shift()
if (handlerObj[status]) {
handlerObj[status](result)
}
}
}
then(success, fail) {
// 將傳入的成功和失敗回調(diào)組成對象,放入回調(diào)數(shù)組中進行管理
this.callbacks.push({
resolve: success,
reject: fail
})
// 用于鏈式調(diào)用
return this
}
catch (fail) {
// 保存?zhèn)魅氲氖』卣{(diào)
this.oncatch = fail
// 用于鏈式調(diào)用
return this
}
}
let p = new promise()
function f1() {
console.log('f1')
setTimeout(function() {
p.resolve('1')
}, 1000)
return p
}
function f2(result) {
console.log('f2', result)
setTimeout(function() {
p.resolve('2')
}, 1000)
}
function f3(result) {
console.log('f3', result)
setTimeout(function() {
p.resolve('3')
}, 1000)
}
function f4(result) {
console.log('f4', result)
}
// 第一種調(diào)用
f1().then(f2).then(f3).catch(f4)
// 第二種調(diào)用
f1().then(f2, f4).then(f3, f4)