前端的異步解決方案之Promise和Await/Async

異步編程模式在前端開發(fā)過程中柄粹,顯得越來越重要赊琳。從最開始的XHR到封裝后的Ajax都在試圖解決異步編程過程中的問題韩肝。隨著ES6新標準的出來诈悍,處理異步數(shù)據(jù)流的解決方案又有了新的變化祸轮。Promise就是這其中的一個。我們都知道侥钳,在傳統(tǒng)的ajax請求中适袜,當異步請求之間的數(shù)據(jù)存在依賴關系的時候,就可能產(chǎn)生很難看的多層回調(diào)舷夺,俗稱”回調(diào)地獄”(callback hell)苦酱。另一方面,往往錯誤處理的代碼和正常的業(yè)務代碼耦合在一起给猾,造成代碼會極其難看疫萤。為了讓編程更美好,我們就需要引入promise來降低異步編程的復雜性敢伸。

Promise

Promise 對象是一個返回值的代理扯饶,這個返回值在promise對象創(chuàng)建時未必已知。它允許你為異步操作的成功返回值或失敗信息指定處理方法池颈。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值尾序。 ——MDN

我們來看一下官方定義,Promise實際上就是一個特殊的Javascript對象躯砰,反映了"異步操作的最終值"每币。"Promise"直譯過來有預期的意思,因此琢歇,它也代表了某種承諾兰怠,即無論你異步操作成功與否,這個對象最終都會返回一個值給你李茫。
先寫一個簡單的demo來直觀感受一下:

const promise = new Promise((resolve, reject) => {
  $.ajax('https://github.com/users', (value) =>  {
    resolve(value);
  }).fail((err) => {
    reject(err);
  });
});

promise.then((value) => {
  console.log(value);
},(err) => {
  console.log(err);
});
//也可以采取下面這種寫法
promise.then(value => console.log(value)).catch(err => console.log(err));

上面的例子揭保,會在Ajax請求成功后調(diào)用resolve回調(diào)函數(shù)來處理結(jié)果,如果請求失敗則調(diào)用reject回調(diào)函數(shù)來處理錯誤涌矢。Promise對象內(nèi)部包含三種狀態(tài)掖举,分別為pending,fulfilled和rejected。這三種狀態(tài)可以類比于我們平常在ajax數(shù)據(jù)請求過程的pending,success,error娜庇。一開始請求發(fā)出后塔次,狀態(tài)是Pending,表示正在等待處理完畢,這個狀態(tài)是中間狀態(tài)而且是單向不可逆的名秀。成功獲得值后狀態(tài)就變?yōu)閒ulfilled励负,然后將成功獲取到的值存儲起來,后續(xù)可以通過調(diào)用then方法傳入的回調(diào)函數(shù)來進一步處理匕得。而如果失敗了的話继榆,狀態(tài)變?yōu)閞ejected,錯誤可以選擇拋出(throw)或者調(diào)用reject方法來處理。

請求的幾個狀態(tài):

  1. pending( 中間狀態(tài))—> fulfilled , rejected
  2. fulfilled(最終態(tài))—> 返回value 不可變
  3. rejected(最終態(tài)) —> 返回reason 不可變

來一個簡單的圖:

promises.png

一個promise內(nèi)部可以返回另一個promise汁掠,這樣就可以進行層級調(diào)用略吨。

const getAllUsers = new Promise((resolve, reject) => {
  $.ajax('https://github.com/users', (value) =>  {
    resolve(value);
  }).fail((err) => {
    reject(err);
  });
});

const getUserProfile = function(username) {
  return new Promise((resolve, reject) => {
  $.ajax('https://github.com/users' + username, (value) =>  {
    resolve(value);
  }).fail((err) => {
    reject(err);
  });
};

getAllUsers.then((users) => {
  //獲取第一個用戶的信息
  return getUserProfile(users[0]);
 }).then((profile) => {
    console.log(profile)
 }).catch(err => console.log(err));

Promise實現(xiàn)原理

目前,有多種Promise的實現(xiàn)方式考阱,我選擇了https://github.com/then/promise的源碼進行閱讀翠忠。

function Promise(fn) {
    var state = null; //用以保存處理狀態(tài),true為fulfilled狀態(tài)乞榨,false為rejected狀態(tài)
    var value = null; //用以保存處理結(jié)果值
    var deferreds = []; 
    var self = this;
    this.then = function(onFulfilled, onRejected) {
        return new self.constructor(
            function(resolve, reject) {...}
        );
    }; //返回一個延遲處理函數(shù)秽之,調(diào)用這個方法,就能觸發(fā)用戶傳入的處理函數(shù)吃既,分別對應處理promise的fulfilled狀態(tài)和rejected狀態(tài)

    function handle(deferred) {...} //延遲隊列處理

    function resolve(newValue) {...} //更新value值考榨,并把state更新為true,代表結(jié)果正常

    function reject(newValue) {...} //更新vlaue值,并把state更新為false,代表結(jié)果錯誤鹦倚,這個value值就是錯誤原因方便后面調(diào)用處理

    function finale() {...} //清空異步隊列
    
    doResolve(fn, resolve, reject); //調(diào)用resolve和reject兩個回調(diào)函數(shù)處理結(jié)果
}

通過閱讀promise的源碼河质,我們可以很清楚地看到,在構(gòu)建一個promise對象的時候震叙,是利用函數(shù)式編程的特性愤诱,如惰性求值和部分求值等來進行將異步處理的。而內(nèi)部的隊列是通過setTimeout的機制將一些作業(yè)加入到事件隊列中捐友,而不阻塞主線程的操作淫半。如果你感興趣的話,可以去看一下實現(xiàn)源碼以及事件隊列匣砖、事件循環(huán)的相關文章科吭。

構(gòu)造Promise

Promise構(gòu)造函數(shù)的初始函數(shù)需要有兩個參數(shù),resolve和reject猴鲫,分別對應fulfilled和rejected兩個狀態(tài)的處理对人。

var promise = new Promise((resolve, reject) => {
  try {
    var value = doSomething();
    resolve(value);
  } catch(err) {
    reject(err);
  }
});

Promise的常用方法

1.Promise.all(iterator):

? 返回一個新的promise對象,其中所有promise的對象成功觸發(fā)的時候拂共,該對象才會觸發(fā)成功牺弄,若有任何一個發(fā)成錯誤,就會觸發(fā)改對象的失敗方法宜狐。成功觸發(fā)的返回值是所有promise對象返回值組成的數(shù)組势告。直接看例子吧:

//設置三個任務
const tasks = {
  task1() {
    return new Promise(...); //return 1
  },
  
  task2() {
    return new Promise(...); // return 2
  },
  
  task3() {
    return new Promise(...); // return 3
  }
};

//列表中的所有任務會并發(fā)執(zhí)行蛇捌,當所有任務執(zhí)行狀態(tài)都為fulfilled后,執(zhí)行then方法
Promise.all([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//最終結(jié)果為:[1,2,3]

2.Promise.race(iterable): 返回一個新的promise對象咱台,其回調(diào)函數(shù)迭代遍歷每個值络拌,分別處理。同樣都是傳入一組promise對象進行處理回溺,同Promise.all不同的是春贸,只要其中有一個promise的狀態(tài)變?yōu)?code>fulfilled或rejected,就會調(diào)用后續(xù)的操作遗遵。

//設置三個任務
const tasks = {
  task1() {
    return new Promise(...); //return 1
  },
  
  task2() {
    return new Promise(...); // return 2
  },
  
  task3() {
    return new Promise(...); // return 3
  }
};

//列表中的所有任務會并發(fā)執(zhí)行萍恕,只要有一個promise對象出現(xiàn)結(jié)果,就會執(zhí)行then方法
Promise.race([tasks.task1(), tasks.task2(), tasks.task3()]).then(result => console.log(result));
//假設任務1最開始返回結(jié)果车要,則控制臺打印結(jié)果為`1` 

3.Promise.reject(reason): 返回一個新的promise對象允粤,用reason值直接將狀態(tài)變?yōu)?code>rejected。

const promise2 = new Promise((resolve, reject) => {
  reject('Failed');
});

const promise2 = Promise.reject('Failed');

上面兩種寫法是等價的屯蹦。

4.Promise.resolve(value): 返回一個新的promise對象维哈,這個promise對象是被resolved的。

與reject類似登澜,下面這兩種寫法也是等價的阔挠。

const promise2 = new Promise((resolve, reject) => {
  resolve('Success');
});

const promise2 = Promise.resolve('Success');

5.then 利用這個方法訪問值或者錯誤原因。其回調(diào)函數(shù)就是用來處理異步處理返回值的脑蠕。

6.catch 利用這個方法捕獲錯誤购撼,并處理。

Generator & Iterator 迭代器和生成器

雖然Promise解決了回調(diào)地獄(callback hell)的問題谴仙,但是仍然需要在使用的時候考慮到非同步的情況迂求,而有沒有什么辦法能讓異步處理的代碼寫起來更簡單呢?在介紹解決方案之前晃跺,我們先來介紹一下ES6中有的迭代器和生成器揩局。
迭代器(Iterator),顧名思義掀虎,它的作用就是用來迭代遍歷集合對象凌盯。
在ES6語法中迭代器是一個有next方法的對象,可以利用Symbol.iterator的標志返回一個迭代器烹玉。

const getNum = {
  [Symbol.iterator]() {
    let arr = [1,2,3];
    let i = 0;
    return {
      next() {
        return i < arr.length ? {value: arr[i++]} : {done: true};
      }
    }
  }
}

//利用for...of語法遍歷迭代器
for(const num of getNum) {
  console.log(num);
}

而生成器(Generator)可以看做一個特殊的迭代器驰怎,你可以不用糾結(jié)迭代器的定義形式,使用更加友好地方式實現(xiàn)代碼邏輯二打。
先來看一段簡單的代碼:

function* getNum() {
  yield 1;
  yield 2;
  yield 3;
}
//調(diào)用生成器县忌,生成一個可迭代的對象
const gen = getNum(); 

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: true}

生成器函數(shù)的定義需要使用function*的形式,這也是它和普通函數(shù)定義的區(qū)別。yield是一個類似return的關鍵字症杏,當代碼執(zhí)行到這里的時候装获,會暫停當前函數(shù)的執(zhí)行,并保存當前的堆棧信息鸳慈,返回yield后面跟著表達式的值饱溢,這個值就是上面代碼所看到的value所對應的值喧伞。而done這個屬性表示是否還有更多的元素走芋。當donetrue的時候,就表明這個迭代過程結(jié)束了潘鲫。需要注意的是這個next方法中所傳入?yún)?shù)翁逞,其實是上一個yield語句的返回值。如果你給next方法傳入了參數(shù)溉仑,就會將上一次yield語句的值設置為對應值挖函。

利用generator的異步處理

先來看一下下面這段代碼:

function getFirstName() {
  setTimeout(() => {
    gen.next('hello');
  },2000);
}

function getLastName() {
  setTimeout(() => {
    gen.next('world');
  },1000);
}

function* say() {
  let firstName = yield getFirstName();
  let lastName = yield getLastName();
  console.log(firstName + lastName);
}

var gen = say();

gen.next(); // {value: undefined, done: false}
//helloworld

我們可以發(fā)現(xiàn),當?shù)谝淮握{(diào)用gen.next()后浊竟,程序執(zhí)行到第一個yield語句就中斷了,而在getFirstName里顯式地將上一個yield語句的返回值改為hello,觸發(fā)了第二yield語句的執(zhí)行怨喘。以此類推,最終就打印出我們想要的結(jié)果了振定。

spawn函數(shù)

我們可以考慮把上面的代碼改寫一下必怜,在這里將Promise和Generator結(jié)合起來,將異步操作用Promise對象封裝好后频,然后梳庆,resolve出去,而創(chuàng)建一個spawn函數(shù)卑惜,這個函數(shù)的作用是自動觸發(fā)generatornext方法膏执。來看一下代碼:

function getFirstName() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('hello');
    }, 2000);
  });
}

function getLastName() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('world');
    }, 1000);
  });
}

function* say() {
  let firstName = yield getFirstName();
  let lastName = yield getLastName();
  console.log(firstName + lastName);
}

function spawn(generator) {
  return new Promise((resolve, reject) => {
    var onResult =  (lastPromiseResult) => {
      var {value, done} = generator.next(lastPromiseResult);
      if(!done) {
        value.then(onResult, reject);
      }else {
        resolve(value);
      }
    }
    onResult();
  });
}

spawn(say()).then((value) => {console.log(value)});

到這里,這個解決方案就很接近接下來要介紹的async/await的實現(xiàn)方式了露久。

Async/Await

這兩個關鍵字其實是一起使用的更米,async函數(shù)其實就相當于funciton *的作用,而await就相當與yield的作用毫痕。而在async/await機制中征峦,自動包含了我們上述封裝出來的spawn自動執(zhí)行函數(shù)。
利用這兩個新的關鍵字镇草,可以讓代碼更加簡潔和明了:

function getFirstName() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('hello');
      resolve('hello');
    }, 2000);
  });
}

function getLastName() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('world');
      resolve('world');
    }, 1000);
  });
}
 
async function say() {
  let firstName = await getFirstName();
  let secondName = await getLastName();
  return firstName + lastName;
}

console.log(say()); 

執(zhí)行結(jié)果為眶痰,先等待2秒打印hello,再等待1秒打印world,最后打印'helloworld'梯啤,與預期的執(zhí)行順序是一致的竖伯。

上面的代碼你需要注意的是,你必須顯式聲明await,否則你會得到一個promise對象而不是你想要獲得的值七婴。

比起Generator函數(shù),async/await的語義更好祟偷,代碼寫起來更加自然。將異步處理的邏輯放在語法層面去處理打厘,寫的代碼也更加符合人的自然思考方式修肠。

錯誤處理

對于async/await這種方法來說,錯誤處理也比較符合我們平常編寫同步代碼時候處理的邏輯户盯,直接使用try..catch就可以了嵌施。

function getUsers() {
    return $.ajax('https://github.com/users');  
}

async function getFirstUser() {
    try {
        let users = await getUsers();
        return users[0].name;
    } catch (err) {
        return {
          name: 'default user'
        }
    }
}

寫在最后

目前,Service Workers莽鸭、 Fetch吗伤、 StreamsLoader 等全部基于 Promise硫眨∽阆可以預見,在未來的Javascript異步編程中礁阁,Promise及其衍生出來的技術(shù)必將大放異彩巧号。那么,你準備好了嗎姥闭?

Read More

MDN Promise
https://www.promisejs.org/
https://promisesaplus.com/
es6 promises in depth
[https://ponyfoo.com/articles/understanding-javascript-async-await](understanding javascript async await)
ECMAScript 6入門
Javascript下的setTimeout(fn,0)意味著什么丹鸿?
https://www.youtube.com/watch?v=lil4YCCXRYc 視頻
https://channel9.msdn.com/Events/Build/2015/3-644
談談使用 promise 時候的一些反模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市泣栈,隨后出現(xiàn)的幾起案子卜高,更是在濱河造成了極大的恐慌,老刑警劉巖南片,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掺涛,死亡現(xiàn)場離奇詭異,居然都是意外死亡疼进,警方通過查閱死者的電腦和手機薪缆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伞广,“玉大人拣帽,你說我怎么就攤上這事〗莱” “怎么了减拭?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長区丑。 經(jīng)常有香客問我拧粪,道長修陡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任可霎,我火速辦了婚禮魄鸦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘癣朗。我一直安慰自己拾因,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布旷余。 她就那樣靜靜地躺著绢记,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荣暮。 梳的紋絲不亂的頭發(fā)上庭惜,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天罩驻,我揣著相機與錄音穗酥,去河邊找鬼。 笑死惠遏,一個胖子當著我的面吹牛砾跃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播节吮,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弊琴?” 一聲冷哼從身側(cè)響起棋弥,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帚豪,沒想到半個月后碳竟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡狸臣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年莹桅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烛亦。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡诈泼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出煤禽,到底是詐尸還是另有隱情铐达,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布檬果,位于F島的核電站瓮孙,受9級特大地震影響贾节,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衷畦,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一栗涂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧祈争,春花似錦斤程、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沮峡,卻和暖如春疚脐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邢疙。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工棍弄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疟游。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓呼畸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親颁虐。 傳聞我的和親對象是個殘疾皇子蛮原,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的另绩,如果沒有異步編程儒陨,根本...
    呼呼哥閱讀 7,313評論 5 22
  • 簡單介紹下這幾個的關系為方便起見 用以下代碼為例簡單介紹下這幾個東西的關系, async 在函數(shù)聲明前使用asyn...
    _我和你一樣閱讀 21,232評論 1 24
  • 弄懂js異步 講異步之前笋籽,我們必須掌握一個基礎知識-event-loop蹦漠。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,713評論 0 5
  • 異步 不連續(xù)的執(zhí)行,就叫做異步干签。相應地津辩,連續(xù)的執(zhí)行就叫做同步。 通常異步是處理一些耗時的操作容劳。 回想在ES6沒出現(xiàn)...
    哎嘿沁閱讀 830評論 0 0
  • 你不知道JS:異步 第三章:Promises 在第二章喘沿,我們指出了采用回調(diào)來表達異步和管理并發(fā)時的兩種主要不足:缺...
    purple_force閱讀 2,070評論 0 4