圖解JavaScript——代碼實(shí)現(xiàn)【2】(重點(diǎn)是Promise、Async、發(fā)布/訂閱原理實(shí)現(xiàn))
本節(jié)主要闡述六種異步方案:回調(diào)函數(shù)、事件監(jiān)聽、發(fā)布/訂閱、Promise、Generator和Async。其中重點(diǎn)是發(fā)布/訂閱、Promise、Async的原理實(shí)現(xiàn),通過對這幾點(diǎn)的了解,希望我們前端切圖仔能夠在修煉內(nèi)功的路上更進(jìn)一步。
一、六種異步方案

1.1 回調(diào)函數(shù)
異步編程的最基本方法,把任務(wù)的第二段單獨(dú)寫在一個函數(shù)里面,等到重新執(zhí)行這個任務(wù)的時候,就直接調(diào)用這個函數(shù)。
優(yōu)點(diǎn):簡單、容易理解和實(shí)現(xiàn)。
缺點(diǎn):多次調(diào)用會使代碼結(jié)構(gòu)混亂,形成回調(diào)地獄。
function sleep(time, callback) {
setTimeout(() => {
// 一些邏輯代碼
callback();
}, time);
}
1.2 事件監(jiān)聽
異步任務(wù)的執(zhí)行不取決于代碼的執(zhí)行順序,而取決于某個事件是否發(fā)生。
優(yōu)點(diǎn):易于理解,此外對于每個事件可以指定多個回調(diào)函數(shù),而且可以“去耦合”,有利于實(shí)現(xiàn)模塊化。
缺點(diǎn):整個程序都要變成事件驅(qū)動型,運(yùn)行流程會變得很不清晰。
dom.addEventListener('click', () => {
console.log('dom被點(diǎn)擊后觸發(fā)!!!');
})
1.3 發(fā)布/訂閱
發(fā)布/訂閱模式在觀察者模式的基礎(chǔ)上,在目標(biāo)和觀察者之間增加一個調(diào)度中心。訂閱者(觀察者)把自己想要訂閱的事件注冊到調(diào)度中心,當(dāng)該事件觸發(fā)的時候,發(fā)布者(目標(biāo))發(fā)布該事件到調(diào)度中心,由調(diào)度中心統(tǒng)一調(diào)度訂閱者注冊到調(diào)度中心的處理代碼。
1.4 Promise
Promise 是異步編程的一種解決方案,是為解決回調(diào)函數(shù)地獄這個問題而提出的,它不是新的語法功能,而是一種新的寫法,允許將回調(diào)函數(shù)的嵌套改為鏈?zhǔn)秸{(diào)用。
優(yōu)點(diǎn):將回調(diào)函數(shù)的嵌套改為了鏈?zhǔn)秸{(diào)用;使用then方法以后,異步任務(wù)的兩端執(zhí)行看的更加清楚。
缺點(diǎn):Promise 的最大問題是代碼冗余,原來的任務(wù)被 Promise 包裝了一下,不管什么操作,一眼看去都是一堆then,原來的語義變得很不清楚。
const promise = new Promise((resolve, reject) => {
if (/*如果異步成功*/) {
resolve(value);
} else {
reject(error);
}
});
promise.then((value) => {
// ...success
}, (reason) => {
// ...failure
})
1.5 Generator
Generator 函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同。其最大特點(diǎn)是可以控制函數(shù)的執(zhí)行。
優(yōu)點(diǎn):異步操作表示的很簡潔,此外可以控制函數(shù)的執(zhí)行。
缺點(diǎn):流程管理不方便,不能實(shí)現(xiàn)自動化的流程管理。
function* genF() {
yield 'come on!';
yield 'Front End Engineer';
return 'goood';
}
const gF = genF();
gF.next();// {value: "come on!", done: false}
gF.next();// {value: "Front End Engineer", done: false}
gF.next();// {value: "goood", done: true}
gF.next();// {value: undefined, done: true}
1.6 Async
ES2017 標(biāo)準(zhǔn)引入了async函數(shù),使得異步操作變得更加方便。簡言之,該函數(shù)就是Generator函數(shù)的語法糖。
優(yōu)點(diǎn):內(nèi)置執(zhí)行器,可以自動執(zhí)行;語義相比Generator更加清晰;返回值是Promise,比Generator函數(shù)的返回值是Iterator對象操作更加方便。
增加學(xué)習(xí)成本。
async function asyncFun() {
await func1()
await func2();
return '666';
}
function func1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('888')
}, 100);
}).then((value) => {
console.log(value);
});
}
function func2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('777')
});
}).then((value) => {
console.log(value);
});
}
asyncFun().then((value) => {
console.log(value);
});
// 888
// 777
// 666
二、Promise原理實(shí)現(xiàn)
不管是實(shí)際開發(fā)中還是面試過程中,各位老鐵們對Promise肯定不會陌生,下面就讓我們一起來嘮一嘮Promsie的實(shí)現(xiàn)原理,根據(jù)PromiseA+規(guī)范來進(jìn)行實(shí)現(xiàn),然后對其相關(guān)的靜態(tài)方法(Promise.resolve()、Promise.reject()、Promise.all()、Promise.race())和實(shí)例方法(Promise.prototype.catch()、Promise.prototype.finally())進(jìn)行實(shí)現(xiàn)。

2.1 思考一下
首先用一幅圖來展示一下我考慮實(shí)現(xiàn)這個函數(shù)的思路吧。

2.2 根據(jù)Promise/A+規(guī)范實(shí)現(xiàn)Promise
人家有相關(guān)標(biāo)準(zhǔn),我們就要遵守,畢竟遵紀(jì)守法才是好公民,現(xiàn)在只能硬著頭皮把這個標(biāo)準(zhǔn)過一遍。

下面就是基于Promise/A+規(guī)范實(shí)現(xiàn)的代碼,已經(jīng)經(jīng)過promises-aplus-tests庫進(jìn)行了驗(yàn)證。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/**
* Promise構(gòu)造函數(shù)
* excutor: 內(nèi)部同步執(zhí)行的函數(shù)
*/
class Promise {
constructor(excutor) {
const self = this;
self.status = PENDING;
self.onFulfilled = [];// 成功的回調(diào)
self.onRejected = [];// 失敗的回調(diào)
// 異步處理成功調(diào)用的函數(shù)
// PromiseA+ 2.1 狀態(tài)只能由Pending轉(zhuǎn)為fulfilled或rejected;fulfilled狀態(tài)必須有一個value值;rejected狀態(tài)必須有一個reason值。
function resolve(value) {
if (self.status === PENDING) {
self.status = FULFILLED;
self.value = value;
// PromiseA+ 2.2.6.1 相同promise的then可以被調(diào)用多次,當(dāng)promise變?yōu)閒ulfilled狀態(tài),全部的onFulfilled回調(diào)按照原始調(diào)用then的順序執(zhí)行
self.onFulfilled.forEach(fn => fn());
}
}
function reject(reason) {
if (self.status === PENDING) {
self.status = REJECTED;
self.reason = reason;
// PromiseA+ 2.2.6.2 相同promise的then可以被調(diào)用多次,當(dāng)promise變?yōu)閞ejected狀態(tài),全部的onRejected回調(diào)按照原始調(diào)用then的順序執(zhí)行
self.onRejected.forEach(fn => fn());
}
}
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
// PromiseA+ 2.2.1 onFulfilled和onRejected是可選參數(shù)
// PromiseA+ 2.2.5 onFulfilled和onRejected必須被作為函數(shù)調(diào)用
// PromiseA+ 2.2.7.3 如果onFulfilled不是函數(shù)且promise1狀態(tài)是fulfilled,則promise2有相同的值且也是fulfilled狀態(tài)
// PromiseA+ 2.2.7.4 如果onRejected不是函數(shù)且promise1狀態(tài)是rejected,則promise2有相同的值且也是rejected狀態(tài)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
const self = this;
const promise = new Promise((resolve, reject) => {
const handle = (callback, data) => {
// PromiseA+ 2.2.4 onFulfilled或者onRejected需要在自己的執(zhí)行上下文棧里被調(diào)用,所以此處用setTimeout
setTimeout(() => {
try {
// PromiseA+ 2.2.2 如果onFulfilled是函數(shù),則在fulfilled狀態(tài)之后調(diào)用,第一個參數(shù)為value
// PromiseA+ 2.2.3 如果onRejected是函數(shù),則在rejected狀態(tài)之后調(diào)用,第一個參數(shù)為reason
const x = callback(data);
// PromiseA+ 2.2.7.1 如果onFulfilled或onRejected返回一個x值,運(yùn)行這[[Resolve]](promise2, x)
resolvePromise(promise, x, resolve, reject);
} catch (e) {
// PromiseA+ 2.2.7.2 onFulfilled或onRejected拋出一個異常e,promise2必須以e的理由失敗
reject(e);
}
})
}
if (self.status === PENDING) {
self.onFulfilled.push(() => {
handle(onFulfilled, self.value);
});
self.onRejected.push(() => {
handle(onRejected, self.reason);
})
} else if (self.status === FULFILLED) {
setTimeout(() => {
handle(onFulfilled, self.value);
})
} else if (self.status === REJECTED) {
setTimeout(() => {
handle(onRejected, self.reason);
})
}
})
return promise;
}
}
function resolvePromise(promise, x, resolve, reject) {
// PromiseA+ 2.3.1 如果promise和x引用同一對象,會以TypeError錯誤reject promise
if (promise === x) {
reject(new TypeError('Chaining Cycle'));
}
if (x && typeof x === 'object' || typeof x === 'function') {
// PromiseA+ 2.3.3.3.3 如果resolvePromise和rejectPromise都被調(diào)用,或者對同一個參數(shù)進(jìn)行多次調(diào)用,那么第一次調(diào)用優(yōu)先,以后的調(diào)用都會被忽略。
let used;
try {
// PromiseA+ 2.3.3.1 let then be x.then
// PromiseA+ 2.3.2 調(diào)用then方法已經(jīng)包含了該條(該條是x是promise的處理)。
let then = x.then;
if (typeof then === 'function') {
// PromiseA+ 2.3.3.3如果then是一個函數(shù),用x作為this調(diào)用它。第一個參數(shù)是resolvePromise,第二個參數(shù)是rejectPromise
// PromiseA+ 2.3.3.3.1 如果resolvePromise用一個值y調(diào)用,運(yùn)行[[Resolve]](promise, y)
// PromiseA+ 2.3.3.3.2 如果rejectPromise用一個原因r調(diào)用,用r拒絕promise。
then.call(x, (y) => {
if (used) return;
used = true;
resolvePromise(promise, y, resolve, reject)
}, (r) => {
if (used) return;
used = true;
reject(r);
})
} else {
// PromiseA+ 如果then不是一個函數(shù),變?yōu)閒ulfilled狀態(tài)并傳值為x
if (used) return;
used = true;
resolve(x);
}
} catch (e) {
// PromiseA+ 2.3.3.2 如果檢索屬性x.then拋出異常e,則以e為原因拒絕promise
// PromiseA+ 2.3.3.4 如果調(diào)用then拋出異常,但是resolvePromise或rejectPromise已經(jīng)執(zhí)行,則忽略它
if (used) return;
used = true;
reject(e);
}
} else {
// PromiseA+ 2.3.4 如果x不是一個對象或函數(shù),狀態(tài)變?yōu)閒ulfilled并傳值x
resolve(x);
}
}
2.2 其他方法
按照Promise/A+規(guī)范實(shí)現(xiàn)了Promise的核心內(nèi)容,但是其只實(shí)現(xiàn)了Promise.prototype.then()方法,那其它方法呢?下面我們就嘮一嘮其它方法,包括靜態(tài)方法(Promise.resolve()、Promise.reject()、Promise.all()、Promise.race())和實(shí)例方法(Promise.prototype.catch()、Promise.prototype.finally())。

2.2.1 Promise.resolve()

class Promise {
// ...
// 將現(xiàn)有對象轉(zhuǎn)為 Promise 對象
static resolve(value) {
// 如果參數(shù)是 Promise 實(shí)例,那么Promise.resolve將不做任何修改、原封不動地返回這個實(shí)例。
if (value instanceof Promise) return value;
// 參數(shù)是一個thenable對象(具有then方法的對象),Promise.resolve方法會將這個對象轉(zhuǎn)為 Promise 對象,然后就立即執(zhí)行thenable對象的then方法。
if (typeof value === 'object' || typeof value === 'function') {
try {
let then = value.then;
if (typeof then === 'function') {
return new Promise(then.bind(value));
}
} catch (e) {
return new Promise((resolve, reject) => {
reject(e);
})
}
}
// 參數(shù)不是具有then方法的對象,或根本就不是對象,Promise.resolve方法返回一個新的 Promise 對象,狀態(tài)為resolved。
return new Promise((resolve, reject) => {
resolve(value);
})
}
}
2.2.2 Promise.reject()

class Promise {
// ...
// 返回一個新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected。
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
}
2.2.3 Promise.all()

class Promise {
// ...
// 用于將多個 Promise 實(shí)例,包裝成一個新的 Promise 實(shí)例。只有所有狀態(tài)都變?yōu)閒ulfilled,p的狀態(tài)才會是fulfilled
static all(promises) {
const values = [];
let resolvedCount = 0;
return new Promise((resolve, reject) => {
promises.forEach((p, index) => {
Promise.resolve(p).then(value => {
resolvedCount++;
values[index] = value;
if (resolvedCount === promises.length) {
resolve(values);
}
}, reason => {
reject(reason);
})
})
})
}
}
2.2.4 Promise.race()

class Promise {
// ...
// 只要有一個實(shí)例率先改變狀態(tài),狀態(tài)就跟著改變。那個率先改變的 Promise 實(shí)例的返回值,就傳遞給回調(diào)函數(shù)。
static race(promises) {
return new Promise((resolve, reject) => {
promises.forEach((p, index) => {
Promise.resolve(p).then(value => {
resolve(value);
}, reason => {
reject(reason);
})
})
})
}
}
2.2.5 Promise.catch()

class Promise {
// ...
// 是.then(null, rejection)或.then(undefined, rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)。
catch(onRejected) {
return this.then(undefined, onRejected);
}
}
2.2.6 Promise.finally()

class Promise {
// ...
// 用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。
finally(callback) {
return this.then(
value => Promise.resolve(callback()).then(() => value),
reason => Promise.resolve(callback()).then(() => { throw reason })
)
}
}
三、Async原理實(shí)現(xiàn)
在開發(fā)過程中常用的另一種異步方案莫過于Async,通過async函數(shù)的引入使得異步操作變得更加方便。實(shí)質(zhì)上,async是Generator的語法糖,最大的亮點(diǎn)是async內(nèi)置執(zhí)行器,調(diào)用后即可自動執(zhí)行,不像Generator需要調(diào)用next()方法才能執(zhí)行。

這是Async的實(shí)現(xiàn)原理,即將Generator函數(shù)作為參數(shù)放入run函數(shù)中,最終實(shí)現(xiàn)自動執(zhí)行并返回Promise對象。
function run(genF) {
// 返回值是Promise
return new Promise((resolve, reject) => {
const gen = genF();
function step(nextF) {
let next;
try {
// 執(zhí)行該函數(shù),獲取一個有著value和done兩個屬性的對象
next = nextF();
} catch (e) {
// 出現(xiàn)異常則將該P(yáng)romise變?yōu)閞ejected狀態(tài)
reject(e);
}
// 判斷是否到達(dá)末尾,Generator函數(shù)到達(dá)末尾則將該P(yáng)romise變?yōu)閒ulfilled狀態(tài)
if (next.done) {
return resolve(next.value);
}
// 沒到達(dá)末尾,則利用Promise封裝該value,直到執(zhí)行完畢,反復(fù)調(diào)用step函數(shù),實(shí)現(xiàn)自動執(zhí)行
Promise.resolve(next.value).then((v) => {
step(() => gen.next(v))
}, (e) => {
step(() => gen.throw(e))
})
}
step(() => gen.next(undefined));
})
}
四、發(fā)布/訂閱實(shí)現(xiàn)
更加詳細(xì)內(nèi)容可以參考《圖解23種設(shè)計(jì)模式》
發(fā)布/訂閱模式在觀察者模式的基礎(chǔ)上,在目標(biāo)和觀察者之間增加一個調(diào)度中心。訂閱者(觀察者)把自己想要訂閱的事件注冊到調(diào)度中心,當(dāng)該事件觸發(fā)的時候,發(fā)布者(目標(biāo))發(fā)布該事件到調(diào)度中心,由調(diào)度中心統(tǒng)一調(diào)度訂閱者注冊到調(diào)度中心的處理代碼。

// 發(fā)布訂閱(TypeScript版)
interface Publish {
registerObserver(eventType : string, subscribe : Subscribe) : void;
remove(eventType : string, subscribe ?: Subscribe) : void;
notifyObservers(eventType : string) : void;
}
interface SubscribesObject{
[key : string] : Array
}
class ConcretePublish implements Publish {
private subscribes : SubscribesObject;
constructor() {
this.subscribes = {};
}
registerObserver(eventType : string, subscribe : Subscribe) : void {
if (!this.subscribes[eventType]) {
this.subscribes[eventType] = [];
}
this.subscribes[eventType].push(subscribe);
}
remove(eventType : string, subscribe ?: Subscribe) : void {
const subscribeArray = this.subscribes[eventType];
if (subscribeArray) {
if (!subscribe) {
delete this.subscribes[eventType];
} else {
for (let i = 0; i < subscribeArray.length; i++) {
if (subscribe === subscribeArray[i]) {
subscribeArray.splice(i, 1);
}
}
}
}
}
notifyObservers(eventType : string, ...args : any[]) : void {
const subscribes = this.subscribes[eventType];
if (subscribes) {
subscribes.forEach(subscribe => subscribe.update(...args))
}
}
}
interface Subscribe {
update(...value : any[]) : void;
}
class ConcreteSubscribe1 implements Subscribe {
public update(...value : any[]) : void {
console.log('已經(jīng)執(zhí)行更新操作1,值為', ...value);
}
}
class ConcreteSubscribe2 implements Subscribe {
public update(...value : any[]) : void {
console.log('已經(jīng)執(zhí)行更新操作2,值為', ...value);
}
}
function main() {
const publish = new ConcretePublish();
const subscribe1 = new ConcreteSubscribe1();
const subscribe2 = new ConcreteSubscribe2();
publish.registerObserver('1', subscribe1);
publish.registerObserver('2', subscribe2);
publish.notifyObservers('2', '22222');
}
main();
相關(guān)章節(jié)
圖解JavaScript——代碼實(shí)現(xiàn)【1】
圖解JavaScript————基礎(chǔ)篇
圖解JavaScript————進(jìn)階篇
圖解23種設(shè)計(jì)模式(TypeScript版)
參考鏈接
Prmose/A+
Promise源碼實(shí)現(xiàn)
ES6入門教程
