javascript
JS之Promise
開胃菜,先做如下思考:
- 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(請求參數依賴上一次請求的結果)中經??梢猿霈F這種現象。 當然實際情況,我們不會那樣寫(什么?你就是這樣,趕緊改),而是會使用函數封裝一下再調用:
$.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) 回調
方法
- 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 實例。
只有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.reject() 將現有對象轉為 Promise 對象,Promise.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(中卷)
轉載于:https://juejin.im/post/5b0ea519518825154b147924
總結
以上是生活随笔為你收集整理的JS之Promise的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦到猫死了有什么预兆
- 下一篇: JS 正则 钱