雖然今年已經18年,可是今天仍是要繼續聊聊ES6的東西,ES6已通過去幾年,但是咱們對於ES6的語法到底是掌握了什麼程度,是瞭解?會用?仍是精通?相信你們和我同樣都對本身有着一個提高的心,對於新玩具可不能僅僅瞭解,對於其中的思想纔是最吸引人的,因此接下來會經過一篇文章,來讓你們對於Promise
這個玩具作到精通的程度!!!npm
打開一瓶冰闊落~~~編程
Promise
是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6將其寫進了語言標準,統一了用法,原生提供了Promise
對象。
嗝~promise
首先,咱們經過字面能夠看出來Pormise
是一種解決方案,並且還有兩種傳統的解決方案·回調函數
和事件
,ok,那麼咱們就來先聊聊這兩種方案。app
回調函數想必你們都不陌生,就是咱們常見的把一個函數當作參數傳遞給另一個函數,在知足了必定的條件以後再去執行回調,好比咱們想要實現一個在三秒後去計算1到5的和,那麼:dom
// 求和函數 function sum () { return eval([...arguments].join('+')) } // 三秒後執行函數 function asycnGetSum (callback) { setTimeout(function(){ var result = callback(1,2,3,4,5); console.log(result) },3000) } asyncGetSum(sum);
這樣的實現就是回調函數,可是若是我要實如今一段動畫,動畫的執行過程是小球先向右移動100px,而後再向下移動100px,在向左移動100px,每段動畫持續時間都是3s.異步
dom.animate({left:'100px'},3000,'linear',function(){ dom.animate({top:'100px'},3000,'linear',function(){ dom.animate({left:'0px'},3000,'linear',function(){ console.log('動畫 done') }) }) })
這樣就會看到造成了一個回調嵌套,也就是咱們常說的回調地獄
,致使代碼可讀性十分差。async
事件處理就是jQuery
中的on
綁定事件和trigger
觸發事件,其實就是咱們常見的發佈訂閱模式,當我訂閱了一個事件,那麼我就是訂閱者,若是發佈者發佈了數據以後,那麼我就要收到相應的通知。異步編程
// 定義一個發佈中心 let publishCenter = { subscribeArrays:{}, // 定義一個訂閱者回調函數callback subscribe:function(key,callback){ // 增長訂閱者 if(!this.subscribeArrays[key]){ this.subscribeArrays[key] = []; } this.subscribeArrays[key].push(callback) }, publish:function(){ //發佈 第一個參數是key let params = [...arguments]; let key = params.shift(); let callbacks = this.subscribeArrays[key]; if(!callbacks || callbacks.length === 0){ // 若是沒人訂閱 那麼就返回 return false } for( let i = 0 ; i < callbacks.length; i++ ){ callbacks[i].apply( this, params ); } } }; // 訂閱 一個wantWatermelon事件 publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')}) //觸發wantWatermelon事件 好咯 能夠看到 恰西瓜咯 publishCenter.publish('wantWatermelon')
恰西瓜中~~~函數
嗝~ok,吃完咱們進入正題,看到上面異步編程如此如此如此麻煩,對於我這種頭大用戶,固然是拒絕的啊,還好咱們有Pormise
(Pormise
大法好),下面咱們就來經過實現一個Promise
去更深的瞭解Promise
的原理,首先咱們瞭解一下PromiseA+
,它是一種規範,用來約束你們寫的Promise
方法的,爲了讓你們寫的Promise
杜絕一些錯誤,按照咱們所指望的流程來走,所以就出現了PromiseA+
規範。測試
咱們根據PromiseA+
文檔來一步一步的看Promise
有什麼特色。
首先咱們看文檔的2.1節,題目是Promise states,也就是說講的是Promise
的狀態,那麼都說了些什麼呢,咱們來看一哈:
- 一個promise只有三種狀態,pending態,fulfilled態(完成態),rejected(拒絕態)
- 當promise處於pending態時,可能轉化成fulfilled或者rejected
- 一旦promise的狀態改爲了fulfilled後,狀態就不能再改變了,而且須要提供一個不可變的value
- 一旦promise的狀態改爲了rejected後,狀態就不能再改變了,而且須要提供一個不可變的reason
ok,那麼咱們就開始寫咱們本身的Promise
,咱們先看看一段正常Promise
的寫法
// 成功或者失敗是須要提供一個value或者reason let promise1 = new Promise((resolve,rejected)=>{ // 能夠發現 當咱們new Promise的時候這句話是同步執行的 也就是說當咱們初始化一個promise的時候 內部的回調函數(一般咱們叫作執行器executor)會當即執行 console.log('hahahha'); // promise內部支持異步 setTimeout(function(){ resolve(123); },100) // throw new Error('error') 咱們也能夠在執行器內部直接拋出一個錯誤 這時promise會直接變成rejected態 })
根據咱們上面的代碼還有PromiseA+規範中的狀態說明,咱們能夠知道Promise
已經有了下面幾個特色
promise
有三種狀態 默認pending
態 pending
能夠變成fulfilled
(成功態)或者rejected
(失敗態),而一旦轉變以後就不能在變成其餘值了promise
內部有一個value
用來存儲成功態的結果promise
內部有一個reason
用來存儲失敗態的緣由promise
接受一個executor
函數,這個函數有兩個參數,一個是resolve
方法,一個是reject
方法,當執行resolve
時,promise
狀態改變爲fulfilled
,執行reject
時,promise
狀態改變爲rejected
new Promise
執行的時候內部的executor
函數執行promise
內部支持異步改變狀態promise
內部支持拋出異常,那麼該promise
的狀態直接改爲rejected
咱們接下來繼續看PromiseA+文檔:
promise
必需要有一個then
方法,用來訪問它當前的value
或者是reason
- 該方法接受兩個參數
onFulfilled
(成功回掉函數),onRejected
(失敗回調函數)promise.then(onFulfilled, onRejected)
- 這兩個參數都是可選參數,若是發現這兩個參數不是函數類型的話,那麼就忽略 好比
promise.then().then(data=>console.log(data),err=>console.log(err))
就能夠造成一個值穿透onFulfilled
必須在promise
狀態改爲fulfilled
以後改爲調用,而且呢promise
內部的value
值是這個函數的參數,並且這個函數不能重複調用onRejected
必須在promise
狀態改爲rejected
以後改爲調用,而且呢promise
內部的reason
值是這個函數的參數,並且這個函數不能重複調用onFulfilled
和onRejected
這兩個方法必需要在當前執行棧的上下文執行完畢後再調用,其實就是事件循環中的微任務(setTimeout
是宏任務,有必定的差別)onFulfilled
和onRejected
這兩個方法必須經過函數調用,也就是說 他們倆不是經過this.onFulfilled()
或者this.onRejected()
調用,直接onFulfilled()
或者onRejected()
then
方法能夠在一個promise
上屢次調用,也就是咱們常見的鏈式調用- 若是當前
promise
的狀態改爲了fulfilled
那麼就要按照順序依次執行then
方法中的onFulfilled
回調- 若是當前
promise
的狀態改爲了rejected
那麼就要按照順序依次執行then
方法中的onRejected
回調then
方法必須返回一個promise
(接下來咱們會把這個promise
稱作promise2
),相似於promise2 = promise1.then(onFulfilled, onRejected);
- 若是呢
onFulfilled()
或者onRejected()
任一一個返回一個值x
,那麼就要去執行resolvePromise
這個函數中去(這個函數是用來處理返回值x
遇到的各類值,而後根據這些值去決定咱們剛剛then
方法中onFulfilled()
或者onRejected()
這兩個回調返回的promise2
的狀態)- 若是咱們在
then
中執行onFulfilled()
或者onRejected()
方法時產生了異常,那麼就將promise2
用異常的緣由e
去reject
- 若是
onFulfilled
或者onRejected
不是函數,而且promise
的狀態已經改爲了fulfilled
或者rejected
,那麼就用一樣的value
或者reason
去更新promise2
的狀態(其實這一條和第三條一個道理,也就是值得穿透問題)
好吧,咱們總結了這麼多規範特色,那麼咱們就用這些先來練練手
/** * 實現一個PromiseA+ * @description 實現一個簡要的promise * @param {Function} executor 執行器 * @author Leslie */ function Promise(executor){ let self = this; self.status = 'pending'; // 存儲promise狀態 pending fulfilled rejected. self.value = undefined; // 存儲成功後的值 self.reason = undefined; // 記錄失敗的緣由 self.onfulfilledCallbacks = []; // 異步時候收集成功回調 self.onrejectedCallbacks = []; // 異步時候收集失敗回調 function resolve(value){ if(self.status === 'pending'){ self.status = 'fulfilled';// resolve的時候改變promise的狀態 self.value = value;//修改爲功的值 // 異步執行後 調用resolve 再把存儲的then中的成功回調函數執行一遍 self.onfulfilledCallbacks.forEach(element => { element() }); } } function reject(reason){ if(self.status === 'pending'){ self.status = 'rejected';// reject的時候改變promise的狀態 self.reason = reason; // 修改失敗的緣由 // 異步執行後 調用reject 再把存儲的then中的失敗回調函數執行一遍 self.onrejectedCallbacks.forEach(element => { element() }); } } // 若是執行器中拋出異常 那麼就把promise的狀態用這個異常reject掉 try { //執行 執行器 executor(resolve,reject); } catch (error) { reject(error) } } Promise.prototype.then = function(onfulfilled,onrejected){ // onfulfilled then方法中的成功回調 // onrejected then方法中的失敗回調 let self = this; // 若是onfulfilled不是函數 那麼就用默認的函數替代 以便達到值穿透 onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val; // 若是onrejected不是函數 那麼就用默認的函數替代 以便達到值穿透 onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err} let promise2 = new Promise((resolve,reject)=>{ if(self.status === 'fulfilled'){ // 加入setTimeout 模擬異步 // 若是調用then的時候promise 的狀態已經變成了fulfilled 那麼就調用成功回調 而且傳遞參數爲 成功的value setTimeout(function(){ // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由 try { // x 是執行成功回調的結果 let x = onfulfilled(self.value); // 調用resolvePromise函數 根據x的值 來決定promise2的狀態 resolvePromise(promise2,x,resolve,reject); } catch (error) { reject(error) } },0) } if(self.status === 'rejected'){ // 加入setTimeout 模擬異步 // 若是調用then的時候promise 的狀態已經變成了rejected 那麼就調用失敗回調 而且傳遞參數爲 失敗的reason setTimeout(function(){ // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由 try { // x 是執行失敗回調的結果 let x = onrejected(self.reason); // 調用resolvePromise函數 根據x的值 來決定promise2的狀態 resolvePromise(promise2,x,resolve,reject); } catch (error) { reject(error) } },0) } if(self.status === 'pending'){ //若是調用then的時候promise的狀態仍是pending,說明promsie執行器內部的resolve或者reject是異步執行的,那麼就須要先把then方法中的成功回調和失敗回調存儲襲來,等待promise的狀態改爲fulfilled或者rejected時候再按順序執行相關回調 self.onfulfilledCallbacks.push(()=>{ //setTimeout模擬異步 setTimeout(function(){ // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由 try { // x 是執行成功回調的結果 let x = onfulfilled(self.value) // 調用resolvePromise函數 根據x的值 來決定promise2的狀態 resolvePromise(promise2,x,resolve,reject); } catch (error) { reject(error) } },0) }) self.onrejectedCallbacks.push(()=>{ //setTimeout模擬異步 setTimeout(function(){ // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由 try { // x 是執行失敗回調的結果 let x = onrejected(self.reason) // 調用resolvePromise函數 根據x的值 來決定promise2的狀態 resolvePromise(promise2,x,resolve,reject); } catch (error) { reject(error) } },0) }) } }) return promise2; }
一鼓作氣,是否是以爲以前總結出的特色十分有效,對着特色十分順暢的就擼完了代碼~
那麼就讓咱們接着來看看promiseA+文檔裏還有些什麼內容吧
resolvePromise
這個函數呢會決定promise2
用什麼樣的狀態,若是x
是一個普通值,那麼就直接採用x
,若是x
是一個promise
那麼就將這個promise
的狀態當成是promise2
的狀態- 判斷若是
x
和promise2
是一個對象,即promise2 === x
,那麼就陷入了循環調用,這時候promise2
就會以一個TypeError
爲reason
轉化爲rejected
- 若是
x
是一個promise
,那麼promise2
就採用x
的狀態,用和x
相同的value
去resolve
,或者用和x
相同的reason
去reject
- 若是
x
是一個對象或者是函數 那麼就先執行let then = x.then
- 若是
x
不是一個對象或者函數 那麼就resolve
這個x
- 若是在執行上面的語句中報錯了,那麼就用這個錯誤緣由去
reject
promise2
- 若是
then
是一個函數,那麼就執行then.call(x,resolveCallback,rejectCallback)
- 若是
then
不是一個函數,那麼就resolve
這個x
- 若是
x
是fulfilled
態 那麼就會走resolveCallback
這個函數,這時候就默認把成功的value
做爲參數y
傳遞給resolveCallback
,即y=>resolvePromise(promise2,y)
,繼續調用resolvePromise
這個函數 確保 返回值是一個普通值而不是promise
- 若是
x
是rejected
態 那麼就把這個失敗的緣由reason
做爲promise2
的失敗緣由reject
出去- 若是
resolveCallback
,rejectCallback
這兩個函數已經被調用了,或者屢次被相同的參數調用,那麼就確保只調第一次,剩下的都忽略掉- 若是調用
then
拋出異常了,而且若是resolveCallback
,rejectCallback
這兩個函數已經被調用了,那麼就忽略這個異常,不然就用這個異常做爲promise2
的reject
緣由
咱們又又又又又又總結了這麼多,好吧不說了總結多少就開擼吧。
/** * 用來處理then方法返回結果包裝成promise 方便鏈式調用 * @param {*} promise2 then方法執行產生的promise 方便鏈式調用 * @param {*} x then方法執行完成功回調或者失敗回調後的result * @param {*} resolve 返回的promise的resolve方法 用來更改promise最後的狀態 * @param {*} reject 返回的promise的reject方法 用來更改promise最後的狀態 */ function resolvePromise(promise2,x,resolve,reject){ // 首先判斷x和promise2是不是同一引用 若是是 那麼就用一個類型錯誤做爲Promise2的失敗緣由reject if( promise2 === x) return reject(new TypeError('typeError:大佬,你循環引用了!')); // called 用來記錄promise2的狀態改變,一旦發生改變了 就不容許 再改爲其餘狀態 let called; if( x !== null && ( typeof x === 'object' || typeof x === 'function')){ // 若是x是一個對象或者函數 那麼他就有多是promise 須要注意 null typeof也是 object 因此須要排除掉 //先得到x中的then 若是這一步發生異常了,那麼就直接把異常緣由reject掉 try { let then = x.then;//防止別人瞎寫報錯 if(typeof then === 'function'){ //若是then是個函數 那麼就調用then 而且把成功回調和失敗回調傳進去,若是x是一個promise 而且最終狀態時成功,那麼就會執行成功的回調,若是失敗就會執行失敗的回調若是失敗了,就把失敗的緣由reject出去,作爲promise2的失敗緣由,若是成功了那麼成功的value時y,這個y有可能仍然是promise,因此須要遞歸調用resolvePromise這個方法 直達返回值不是一個promise then.call(x,y => { if(called) return; called = true; resolvePromise(promise2,y,resolve,reject) }, error=>{ if(called) return called = true; reject(error) }) }else{ resolve(x) } } catch (error) { if(called) return called = true; reject(error) } }else{ // 若是是一個普通值 那麼就直接把x做爲promise2的成功value resolve掉 resolve(x) } }
finnnnnnnnnally,咱們終於經過咱們的不懈努力實現了一個基於PromiseA+規範的Promise
!
最後呢爲了完美,咱們還要在這個promise
上實現Promise.resolve
,Promise.reject
,以及catch
,Promise.all
和Promise.race
這些方法。
Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ resolve(value) }) } Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) }) } Promise.prototype.catch = function(onRejected){ return this.then(null,onRejected) } Promise.all = function(promises){ return new Promise((resolve,reject)=>{ let arr = []; let i = 0; function getResult(index,value){ arr[index] = value; if(++i == promises.length) { resolve(arr) } } for(let i = 0;i<promises.length;i++){ promises[i].then(data=>{ getResult(i,data) },reject) } }) } Promise.race = function(promises){ return new Promise((resolve,reject)=>{ for(let i = 0 ; i < promises.length ; i++){ promises[i].then(resolve,reject) } }) }
恰完西瓜來口糖,語法糖是爲了讓咱們書寫promise的時候可以更加的快速,因此作了一層改變,咱們來看一個例子,好比當咱們封裝一個異步讀取圖片的寬高函數
// 原來的方式 let getImgWidthHeight = function(imgUrl){ return new Promise((resolve,reject)=>{ let img = new Image(); img.onload = function(){ resolve(img.width+'-'+img.height) } img.onerror = function(e){ reject(e) } img.src = imgUrl; }) }
是否是以爲怎麼寫起來有點舒服但又有點不舒服,好像我每次都要去寫執行器啊!爲何!好的,沒有爲何,既然不舒服 咱們就改!
// 實現一個promise的語法糖 Promise.defer = Promise.deferred = function (){ let dfd = {}; dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }) return dfd }
有了上面的語法糖咱們再看一下那個圖片的函數怎麼寫
let newGetImgWidthHeight = function(imgUrl){ let dfd = Promise.defer(); let img = new Image(); img.onload = function(){ dfd.resolve(img.width+'-'+img.height) } img.onerror = function(e){ dfd.reject(e) } img.url = imgUrl; return dfd.promise }
是否是發現咱們少了一層函數嵌套,呼 得勁
npm install promises-aplus-tests -g
既然咱們都說了咱們是遵循promiseA+規範的,那至少要拿出點證據來是否是,否則是否是說服不了你們,那麼咱們就用promises-aplus-tests這個包來檢測咱們寫的promise
究竟怎麼樣呢!安裝完成以後來跑一下咱們的promise
最終跑出來咱們所有經過測試!酷!晚餐再加個雞腿~