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