javascript
JS之Promise
開(kāi)胃菜,先做如下思考:
- Promise 有幾種狀態(tài)?
- Promise 狀態(tài)之間可以轉(zhuǎn)化嗎?
- Promise 中的異常可以被 try...catch 捕獲嗎?
Promise前世
callback hell
大家都知道JS是異步操作(執(zhí)行)的,在傳統(tǒng)的異步編程中最大的問(wèn)題就是回調(diào)函數(shù)的嵌套,一旦嵌套次數(shù)過(guò)多,我們的代碼就難以維護(hù)和理解,這就是所謂的 回調(diào)地獄callback hell 。以jQuery為例:
$.ajax({url: "/getA",success: function(a) {$.ajax({url: '/getB',data: a.data,success: function(b){$.ajax({url: '/getC',data: b.data,success: function(c){console.log('運(yùn)行到這真不容易')}})}});} }); 復(fù)制代碼特別是在ajax(請(qǐng)求參數(shù)依賴上一次請(qǐng)求的結(jié)果)中經(jīng)常可以出現(xiàn)這種現(xiàn)象。 當(dāng)然實(shí)際情況,我們不會(huì)那樣寫(什么?你就是這樣,趕緊改),而是會(huì)使用函數(shù)封裝一下再調(diào)用:
$.ajax({url: "/getA",success: function(a) {getB(a.data)} });getB(data){$.ajax({url: '/getB',data: data,success: function(b){getC(b.data)}}) }getC(data){$.ajax({url: '/getC',data: data,success: function(c){console.log('運(yùn)行到這真不容易')}}) } 復(fù)制代碼but,這還是回調(diào)函數(shù)調(diào)用,只不過(guò)換了種便于維護(hù)的另一種寫法。
除非是老項(xiàng)目不想進(jìn)行改動(dòng),新項(xiàng)目中最好不要這么干了
jQuery.defered
為了解決上述情況,jQuery在v1.5 版本中引入了 deferred 對(duì)象,初步形成 promise 概念。ajax 也成了一個(gè) deferred 對(duì)象。
$.ajax({url: "/getA", }).then(function(){console.log('a') }); 復(fù)制代碼我們也可以這樣來(lái)定義一個(gè) deferred 對(duì)象:
function loadImg(src){var dtd = $.Deferred(); // 定義延遲對(duì)象var img = new Image();img.onload = function(){dtd.resolve(img)}img.onerror = function(){dtd.reject()}img.src = src;return dtd; // 記得要返回哦 } var result = loadImg('img.png'); result.then(function(img){console.log(img.width) },function(){console.log('fail') }) 復(fù)制代碼完整例子:戳我
在 ES6 Promise 出現(xiàn)之前,有很多典型的Promise庫(kù),如:bluebird、Q 、when 等。
bluebird
bluebird是一個(gè)第三方Promise規(guī)范實(shí)現(xiàn)庫(kù),它不僅完全兼容原生Promise對(duì)象,且比原生對(duì)象功能更強(qiáng)大。
安裝
Node:
npm install bluebird 復(fù)制代碼Then:
const Promise = require("bluebird"); 復(fù)制代碼Browser: 直接引入js庫(kù)即可,就可以得到一個(gè)全局的 Promise 和 P(別名) 對(duì)象。
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script> 復(fù)制代碼使用
function loadImg(src) {return new Promise((resolve,reject) => {const img = new Image();img.onload = ()=>{resolve(img);}img.onerror = () => {reject()}img.src = src;}) } const result = loadImg('http://file.ituring.com.cn/SmallCover/17114893a523520c7382'); result.then(img => {console.log(img.width) },() => {console.log('fail') }); console.log(Promise === P) // true 復(fù)制代碼為了與原生的 Promise 進(jìn)行區(qū)別,我們?cè)诳刂婆_(tái)中打印了 Promise === P 的結(jié)果。結(jié)果和預(yù)期一樣:
完整demo:戳我
bluebird 相比原生規(guī)范實(shí)現(xiàn)來(lái)說(shuō),它的功能更強(qiáng)大,瀏覽器兼容性好(IE8沒(méi)問(wèn)題),提供了很多豐富的方法和屬性:
- Promise.props
- Promise.any
- Promise.some
- Promise.map
- Promise.reduce
- Promise.filter
- Promise.each
- Promise.mapSeries
- cancel
- ...more
更多詳細(xì)的功能查看 官網(wǎng)API 。
我們發(fā)現(xiàn),這里的 Promise 是可以取消的。
Promise今生
Promise最早是由社區(qū)提出和實(shí)現(xiàn)的,ES6寫成了語(yǔ)言標(biāo)準(zhǔn),它給我們提供一個(gè)原生的構(gòu)造函數(shù)Promise,無(wú)需使用第三方庫(kù)或者造輪子來(lái)實(shí)現(xiàn)。
Promise 語(yǔ)法
function loadImg(src) {return new Promise((resolve,reject) => {const img = new Image();img.onload = ()=>{resolve(img);}img.onerror = () => {reject()}img.src = src;}) } const result = loadImg('http://file.ituring.com.cn/SmallCover/17114893a523520c7382'); result.then(img => {console.log(img.width) },() => {console.log('fail') }) 復(fù)制代碼Promise 對(duì)象代表一個(gè)異步操作,有三種狀態(tài):pending (進(jìn)行中)、fulfilled(已成功)和 rejected (已失敗)。在代碼中,經(jīng)常使用 resolve 來(lái)表示 fulfilled 狀態(tài)。
Promise 特點(diǎn)
- 狀態(tài)的不可改變,狀態(tài)一旦由 pending 變?yōu)?fulfilled 或從 pending 變?yōu)?rejected ,就不能再被改變了。
- Promise無(wú)法取消,一旦新建它就會(huì)立即執(zhí)行,無(wú)法中途取消,對(duì)于 ajax 類的請(qǐng)求無(wú)法取消,可能存在資源浪費(fèi)情況。
- Promise內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部,無(wú)法被外部 try...catch 捕獲,只能設(shè)置 catch 回調(diào)函數(shù)或者 then(null, reject) 回調(diào)
方法
- Promise.prototype.then() then方法的第一個(gè)參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個(gè)參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)。 then 會(huì)創(chuàng)建并返回一個(gè)新的promise,可以用來(lái)實(shí)現(xiàn)Promise 鏈?zhǔn)讲僮鳌?/li>
思考:Promise.then 鏈?zhǔn)胶?jQuery的鏈?zhǔn)讲僮饔泻尾煌?#xff1f; jQuery的鏈?zhǔn)椒椒ǚ祷氐亩际莏Query當(dāng)前對(duì)象
-
Promise.prototype.catch() 是.then(null, rejection)的別名,用于指定發(fā)生錯(cuò)誤時(shí)或者狀態(tài)變成 rejected的回調(diào)函數(shù)。
-
Promise.prototype.finally() ES 2018引入的標(biāo)準(zhǔn),不管 promise 的狀態(tài)如何,只要完成,都會(huì)調(diào)用該函數(shù)。
-
Promise.all() 將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。
只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會(huì)變成fulfilled,否則p的狀態(tài)就變成rejected。 這種模式在傳統(tǒng)上稱為_(kāi)_門__:所有人到齊了才開(kāi)門。 適用場(chǎng)景:需要等待多個(gè)并行任務(wù)完成之后才能繼續(xù)下一個(gè)任務(wù)。 典型例子:一個(gè)頁(yè)面有多個(gè)請(qǐng)求,在請(qǐng)求完成或失敗前需要一直顯示loading效果。
-
Promise.race() 和 Promise.all 一樣,將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。不同的是,只要p1、p2、p3之中有一個(gè)實(shí)例率先改變狀態(tài)( fulfilled或者rejected),p的狀態(tài)就跟著改變。 這種模式傳統(tǒng)上稱為_(kāi)_門閂__:第一個(gè)到達(dá)的人就打開(kāi)門閂。 典型例子:超時(shí)檢測(cè)
-
Promise.resolve() 將現(xiàn)有對(duì)象轉(zhuǎn)為 Promise 對(duì)象,Promise.resolve等價(jià)于:
- Promise.reject() 將現(xiàn)有對(duì)象轉(zhuǎn)為 Promise 對(duì)象,Promise.reject等價(jià)于:
Generator
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,Generator 函數(shù)被調(diào)用后并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象(Iterator對(duì)象)。
與普通函數(shù)相比,它有兩個(gè)特征:
- function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào);
- 函數(shù)體內(nèi)部使用yield表達(dá)式
ES6 沒(méi)有規(guī)定 function 關(guān)鍵字與函數(shù)名之間星號(hào)的位置,下面寫法都能通過(guò):
function * foo() { ··· } function *foo() { ··· } function* foo() { ··· } function*foo() { ··· } 復(fù)制代碼function *helloWorldGen() {yield 'hello';yield 'world';return 'ending'; }const hw = helloWorldGen(); 復(fù)制代碼定義之后,需要調(diào)用遍歷器對(duì)象的next方法。每次調(diào)用next方法,從函數(shù)頭部或上一次停下來(lái)的地方開(kāi)始執(zhí)行,直到遇到下一個(gè)yield表達(dá)式(或return語(yǔ)句)為止。next方法返回一個(gè)對(duì)象,包含value和done屬性,value屬性是當(dāng)前yield表達(dá)式值或者return語(yǔ)句后面表達(dá)式的值,如果沒(méi)有,則是undefined。done屬性表示是否遍歷結(jié)束。
hw.next() // { value: 'hello', done: false }hw.next() // { value: 'world', done: false }hw.next() // { value: 'ending', done: true }hw.next() // { value: undefined, done: true } 復(fù)制代碼yield
yield 表達(dá)式就是暫停標(biāo)志,只能用在 Generator 函數(shù)里。 Generator 函數(shù)可以不使用 yield 表達(dá)式,這樣就變成一個(gè)單純的暫緩執(zhí)行函數(shù)。
function *slow(){console.log('調(diào)用next才執(zhí)行呀') } const gen = slow(); gen.next(); 復(fù)制代碼yield 可以接受 next 方法的參數(shù)作為上一個(gè) yield 表達(dá)式的返回值。
function *foo(x) {var y = x * (yield);return y; } const it = foo(6); it.next(); // 啟動(dòng)foo() // {value: undefined, done: false}it.next(7) // {value: 42, done: true} 復(fù)制代碼第一次使用next方法時(shí),傳遞參數(shù)是無(wú)效的。
for...of循環(huán)
使用for...of循環(huán),可以自動(dòng)遍歷 Generator 函數(shù)生成的迭代器對(duì)象,此時(shí)不需要調(diào)用 next 方法。
function *foo() {yield 1;yield 2;yield 3;yield 4;yield 5;return 6; } // 到6時(shí)done:true,不會(huì)進(jìn)入循環(huán)體內(nèi)執(zhí)行 for (let v of foo()) {console.log(v); } // 1 2 3 4 5 復(fù)制代碼for...of 循環(huán)在每次迭代中自動(dòng)調(diào)用 next() ,不會(huì)給 next 傳值,并且在接收到 done:true 之后自動(dòng)停止(不包含此時(shí)返回的對(duì)象)。
對(duì)于一些迭代器總是返回 done:false 的,需要加一個(gè) break 條件,防止死循環(huán)。
我們也可以手動(dòng)實(shí)現(xiàn)迭代器循環(huán):
let it = foo(); // 這種的有點(diǎn)就是可以向next傳遞參數(shù) for(let ret; (ret=it.next()) && !ret.done;) {console.log(ret.value) } 復(fù)制代碼Generator + Promise
我們先看下基于Promise 的實(shí)現(xiàn)方法:
function getData() {return request('http://xxx.com') } getData().then((res)=> {console.log(res)},()=>{console.log('fail')}) 復(fù)制代碼結(jié)合Generator使用:
function getData() {return request('http://xxx.com') } function *main(){try {const text = yield getData();console.log(text)} catch (error) {console.log(error)} } 復(fù)制代碼執(zhí)行main方法如下:
const it = main(); const p = it.next().value; p.then((text)=>{console.log(text) },(err)=>{console.log(err) }) 復(fù)制代碼盡管 Generator 函數(shù)將異步操作簡(jiǎn)化,但是執(zhí)行的流程管理很不方便(需要手動(dòng)調(diào)用 next 執(zhí)行),有更好的方式嗎?肯定是有的。
co
co 是TJ 大神發(fā)布的一個(gè) Generator 函數(shù)包裝工具,用于自動(dòng)執(zhí)行 Generator 函數(shù)。
co 模塊可以讓我們不用編寫 next 進(jìn)行迭代,就會(huì)自動(dòng)執(zhí)行:
const co = require('co');function getData() {return request('http://xxx.com') } const gen = function *(){const text = yield getData();console.log(text) }co(gen).then(()=>{console.log('gen執(zhí)行完成') }) 復(fù)制代碼co函數(shù)返回一個(gè)Promise對(duì)象,等到 Generator 函數(shù)執(zhí)行結(jié)束,就會(huì)輸出一行提示。
async & await
ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),它就是 Generator 函數(shù)的語(yǔ)法糖。
Node V7.6+已經(jīng)原生支持 async 了。 Koa2 也使用 async 替代之前的 Generator 版本。
基本用法
async function fetchImg(url) {const realUrl = await getMainUrl(url);const result = await downloadImg(realUrl);return result; }fetchImg('https://detail.1688.com/offer/555302162390.html').then((result) => {console.log(result) }) 復(fù)制代碼和 Generator 函數(shù)對(duì)比,async函數(shù)就是將 Generator 函數(shù)的星號(hào)(*)替換成async,將yield替換成await。其功能和 co 類似,自動(dòng)執(zhí)行。
async函數(shù)返回一個(gè) Promise 對(duì)象,可以使用then方法添加回調(diào)函數(shù)。 async函數(shù)內(nèi)部return語(yǔ)句返回的值,會(huì)成為then方法回調(diào)函數(shù)的參數(shù)。 只有async函數(shù)內(nèi)部的異步操作執(zhí)行完,才會(huì)執(zhí)行then方法指定的回調(diào)函數(shù)。
await后面是一個(gè) Promise 對(duì)象。如果不是,會(huì)被轉(zhuǎn)成一個(gè)立即resolve的 Promise 對(duì)象。
async function f() {return await 123; }f().then(v => console.log(v)) // 123 復(fù)制代碼await 必須在 async 函數(shù)中執(zhí)行!
實(shí)例:按順序完成異步操作
講一下一個(gè)可能會(huì)遇到的場(chǎng)景:經(jīng)常遇到一組異步操作,需要按照順序完成。比如,依次根據(jù)圖片url下載圖片,按照讀取的順序輸出結(jié)果。
一個(gè)async的實(shí)現(xiàn):
async function downInOrder(urls, path, win) {for(const url of urls) {try {await downloadImg(url, path)win.send('logger', `圖片 ${url} 下載完成`)} catch (error) {win.send('logger', `圖片 ${url} 下載出錯(cuò)`)}} } 復(fù)制代碼上述這種實(shí)現(xiàn),代碼確實(shí)簡(jiǎn)化了,但是效率很差,需要一個(gè)操作完成,才能進(jìn)行下一個(gè)操作(下載圖片),不能并發(fā)執(zhí)行。
并發(fā)執(zhí)行,摘自我的一個(gè)半成品1688pic :
async function downInOrder(urls, path, win) {// 并發(fā)執(zhí)行const imgPromises = urls.map(async url => {try {const resp = await downloadImg(url, path);return `圖片 ${url} 下載完成`;} catch (error) {return `圖片 ${url} 下載出錯(cuò)`;}})// 按順序輸出for (const imgPromise of imgPromises) {win.send('logger', await imgPromise);} } 復(fù)制代碼上面代碼中,雖然map方法的參數(shù)是async函數(shù),但它是并發(fā)執(zhí)行的,因?yàn)橹挥?/span>async函數(shù)內(nèi)部是繼發(fā)執(zhí)行,外部不受影響。后面的for..of循環(huán)內(nèi)部使用了await,因此實(shí)現(xiàn)了按順序輸出。
這塊基本參考的是阮一峰老師的教程
總結(jié)
使用 async / await, 可以通過(guò)編寫形似同步的代碼來(lái)處理異步流程, 提高代碼的簡(jiǎn)潔性和可讀性。
參考文檔:
- es6入門
- 你不知道的JavaScript(中卷)
轉(zhuǎn)載于:https://juejin.im/post/5b0ea519518825154b147924
總結(jié)
以上是生活随笔為你收集整理的JS之Promise的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 梦到猫死了有什么预兆
- 下一篇: JS 正则 钱