學習知識要善於思考,思考,再思考。 —— 愛因斯坦前端
曾幾什麼時候,咱們的代碼是這樣的,爲了拿到回調的結果,不得不callback hell
,這種環環相扣的代碼能夠說是至關噁心了git
let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})
複製代碼
終於,咱們的蓋世英雄
出現了,他身披金甲聖衣、駕着七彩祥雲。好吧打岔兒了,沒錯他就是咱們的Promise
,那讓咱們來看看用了Promise
以後,上面的代碼會變成什麼樣吧es6
let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})
複製代碼
如上所示github
真的是很方便,有木有?意中人能夠說是Swag
到變形了。那麼言歸正傳,咱們怎麼才能本身寫一個這麼Swag
的解決異步神器呢?npm
首先咱們要知道本身手寫一個Promise
,應該怎麼去寫,誰來告訴咱們怎麼寫,須要遵循什麼樣的規則。固然這些你都不用擔憂,其實業界都是經過一個規則指標來生產Promise
的。讓咱們來看看是什麼東西。傳送門☞Promise/A+數組
咱們先聲明一個類,叫作Promise
,裏面是構造函數。若是es6還有問題的能夠去阮大大的博客上學習一下(傳送門☞es6)promise
class Promise{
constructor(executor){
//控制狀態,使用了一次以後,接下來的都不被使用
this.status = 'pendding'
this.value = undefined
this.reason = undefined
//定義resolve函數
let resolve = (data)=>{
//這裏pendding,主要是爲了防止executor中調用了兩次resovle或reject方法,而咱們只調用一次
if(this.status==='pendding'){
this.status = 'resolve'
this.value = data
}
}
//定義reject函數
let reject = (data)=>{
if(this.status==='pendding'){
this.status = 'reject'
this.reason = data
}
}
//executor方法可能會拋出異常,須要捕獲
try{
//將resolve和reject函數給使用者
executor(resolve,reject)
}catch(e){
//若是在函數中拋出異常則將它注入reject中
reject(e)
}
}
}
複製代碼
那麼接下來我會分析上面代碼的做用,原理前端工程師
executor:
這是實例Promise
對象時在構造器中傳入的參數,通常是一個function(resolve,reject){}
status:``Promise
的狀態,一開始是默認的pendding狀態,每當調用道resolve和reject方法時,就會改變其值,在後面的then方法中會用到value:
resolve回調成功後,調用resolve方法裏面的參數值reason:
reject回調成功後,調用reject方法裏面的參數值resolve:
聲明resolve方法在構造器內,經過傳入的executor方法傳入其中,用以給使用者回調reject:
聲明reject方法在構造器內,經過傳入的executor方法傳入其中,用以給使用者回調then方法是Promise
中最爲重要的方法,他的用法你們都應該已經知道,就是將Promise
中的resolve或者reject的結果拿到,那麼咱們就能知道這裏的then方法須要兩個參數,成功回調和失敗回調,上代碼!異步
then(onFufilled,onRejected){
if(this.status === 'resolve'){
onFufilled(this.value)
}
if(this.status === 'reject'){
onRejected(this.reason)
}
}
複製代碼
這裏主要作了將構造器中resolve和reject的結果傳入onFufilled
和onRejected
中,注意這兩個是使用者傳入的參數,是個方法。因此你覺得這麼簡單就完了?要想更Swag
的應對各類場景,咱們必須得再完善。繼續往下走!async
以前咱們只是處理了同步狀況下的Promise,簡而言之全部操做都沒有異步的成分在內。那麼若是是異步該怎麼辦?
最先處理異步的方法就是callback,就至關於我讓你幫我掃地,我會在給你發起任務時給你一個手機,以後我作本身的事情去,不用等你,等你掃完地就會打手機給我,誒,我就知道了地掃完了。這個手機就是callback,回調函數。
首先咱們須要改一下構造器裏的代碼,分別添加兩個回調函數的數組,分別對應成功回調和失敗回調。他們的做用是當成功執行resolve或reject時,執行callback。
//存放成功回調的函數
this.onResolvedCallbacks = []
//存放失敗回調的函數
this.onRejectedCallbacks = []
let resolve = (data)=>{
if(this.status==='pendding'){
this.status = 'resolve'
this.value = data
//監聽回調函數
this.onResolvedCallbacks.forEach(fn=>fn())
}
}
let reject = (data)=>{
if(this.status==='pendding'){
this.status = 'reject'
this.reason = data
this.onRejectedCallbacks.forEach(fn=>fn())
}
}
複製代碼
而後是then須要多加一個狀態判斷,當Promise中是異步操做時,須要在咱們以前定義的回調函數數組中添加一個回調函數。
if(this.status === 'pendding'){
this.onResolvedCallbacks.push(()=>{
// to do....
let x = onFufilled(this.value)
resolvePromise(promise2,x,resolve,reject)
})
this.onRejectedCallbacks.push(()=>{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
})
}
複製代碼
ok!大功告成,異步已經解決了
這也是
Promise
中的重頭戲,我來介紹一下,咱們在用Promise的時候可能會發現,當then函數中return了一個值,咱們能夠繼續then下去,不過是什麼值,都能在下一個then中獲取,還有,當咱們不在then中放入參數,例:promise.then().then()
,那麼其後面的then依舊能夠獲得以前then返回的值,可能你如今想很迷惑。讓我來解開你心中的憂愁,follow me。
then(onFufilled,onRejected){
//解決onFufilled,onRejected沒有傳值的問題
onFufilled = typeof onFufilled === 'function'?onFufilled:y=>y
//由於錯誤的值要讓後面訪問到,因此這裏也要跑出個錯誤,否則會在以後then的resolve中捕獲
onRejected = typeof onRejected === 'function'?onRejected:err=>{ throw err ;}
//聲明一個promise對象
let promise2
if(this.status === 'resolve'){
//由於在.then以後又是一個promise對象,因此這裏確定要返回一個promise對象
promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
//由於穿透值的緣故,在默認的跑出一個error後,不能再用下一個的reject來接受,只能經過try,catch
try{
//由於有的時候須要判斷then中的方法是否返回一個promise對象,因此須要判斷
//若是返回值爲promise對象,則須要取出結果看成promise2的resolve結果
//若是不是,直接做爲promise2的resolve結果
let x = onFufilled(this.value)
//抽離出一個公共方法來判斷他們是否爲promise對象
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
if(this.status === 'reject'){
promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
try{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
if(this.status === 'pendding'){
promise2 = new Promise((resolve,reject)=>{
this.onResolvedCallbacks.push(()=>{
// to do....
setTimeout(()=>{
try{
let x = onFufilled(this.value)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
this.onRejectedCallbacks.push(()=>{
setTimeout(()=>{
try{
let x = onRejected(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
})
}
return promise2
}
複製代碼
一會兒多了不少方法,不用怕,我會一一解釋
Promise
?:首先咱們要注意的一點是,then有返回值,then了以後還能在then,那就說明以前的then返回的必然是個Promise
。setTimeout
?:由於Promise自己是一個異步方法,屬於微任務一列,必須得在執行棧執行完了在去取他的值,因此全部的返回值都得包一層異步setTimeout。resolvePromise
是什麼?:這實際上是官方Promise/A+的需求。由於你的then能夠返回任何職,固然包括Promise
對象,而若是是Promise
對象,咱們就須要將他拆解,直到它不是一個Promise
對象,取其中的值。那就讓咱們來看看這個
resolvePromise
到底長啥樣。
function resolvePromise(promise2,x,resolve,reject){
//判斷x和promise2之間的關係
//由於promise2是上一個promise.then後的返回結果,因此若是相同,會致使下面的.then會是同一個promise2,一直都是,沒有盡頭
if(x === promise2){//至關於promise.then以後return了本身,由於then會等待return後的promise,致使本身等待本身,一直處於等待
return reject(new TypeError('循環引用'))
}
//若是x不是null,是對象或者方法
if(x !== null && (typeof x === 'object' || typeof x === 'function')){
//爲了判斷resolve過的就不用再reject了,(好比有reject和resolve的時候)
let called
try{//防止then出現異常,Object.defineProperty
let then = x.then//取x的then方法可能會取到{then:{}},並無執行
if(typeof then === 'function'){
//咱們就認爲他是promise,call他,由於then方法中的this來自本身的promise對象
then.call(x,y=>{//第一個參數是將x這個promise方法做爲this指向,後兩個參數分別爲成功失敗回調
if(called) return;
called = true
//由於可能promise中還有promise,因此須要遞歸
resolvePromise(promise2,y,resolve,reject)
},err=>{
if(called) return;
called = true
//一次錯誤就直接返回
reject(err)
})
}else{
//若是是個普通對象就直接返回resolve做爲結果
resolve(x)
}
}catch(e){
if(called) return;
called = true
reject(e)
}
}else{
//這裏返回的是非函數,非對象的值,就直接放在promise2的resolve中做爲結果
resolve(x)
}
}
複製代碼
它的做用是用來將onFufilled的返回值進行判斷取值處理,把最後得到的值放入最外面那層的
Promise
的resolve函數中。
promise2
(then函數返回的Promise對象),x
(onFufilled函數的返回值),resolve、reject
(最外層的Promise上的resolve和reject)。promise2
和x
?:首先在Promise/A+中寫了須要判斷這二者若是相等,須要拋出異常,我就來解釋一下爲何,若是這二者相等,咱們能夠看下下面的例子,第一次p2是p1.then出來的結果是個Promise
對象,這個Promise
對象在被建立的時候調用了resolvePromise(promise2,x,resolve,reject)函數,又由於x等於其自己,是個Promise
,就須要then方法遞歸它,直到他不是Promise
對象,可是x(p2)的結果還在等待,他卻想執行本身的then方法,就會致使等待。let p1 = new Promise((resolve,reject)=>{
resolve()
})
let p2 = p1.then(d=>{
return p2
})
複製代碼
resolvePromise
函數已經resolve或者reject了,那就不須要在執行下面的resolce或者reject。resolvePromise
函數?:相信細心的人已經發現了,我這裏使用了遞歸調用法,首先這是Promise/A+中要求的,其次是業務場景的需求,當咱們碰到那種Promise的resolve裏的Promise的resolve裏又包了一個Promise的話,就須要遞歸取值,直到x不是Promise對象。咱們如今已經基本完成了Promise的then方法,那麼如今咱們須要看看他的其餘方法。
相信你們都知道catch這個方法是用來捕獲Promise中的reject的值,也就是至關於then方法中的onRejected回調函數,那麼問題就解決了。咱們來看代碼。
//catch方法
catch(onRejected){
return this.then(null,onRejected)
}
複製代碼
該方法是掛在Promise原型上的方法。當咱們調用catch傳callback的時候,就至關因而調用了then方法。
你們必定都看到過Promise.resolve()、Promise.reject()
這兩種用法,它們的做用其實就是返回一個Promise對象,咱們來實現一下。
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
})
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
})
}
複製代碼
這兩個方法是直接能夠經過class調用的,原理就是返回一個內部是resolve或reject的Promise對象。
all方法能夠說是Promise中很經常使用的方法了,它的做用就是將一個數組的Promise對象放在其中,當所有resolve的時候就會執行then方法,當有一個reject的時候就會執行catch,而且他們的結果也是按着數組中的順序來排放的,那麼咱們來實現一下。
//all方法(獲取全部的promise,都執行then,把結果放到數組,一塊兒返回)
Promise.all = function(promises){
let arr = []
let i = 0
function processData(index,data){
arr[index] = data
i++
if(i == promises.length){
resolve(arr)
}
}
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data)
},reject)
}
})
}
複製代碼
其原理就是將參數中的數組取出遍歷,每當執行成功都會執行
processData
方法,processData
方法就是用來記錄每一個Promise的值和它對應的下標,當執行的次數等於數組長度時就會執行resolve,把arr的值給then。這裏會有一個坑,若是你是經過arr數組的長度來判斷他是否應該resolve的話就會出錯,爲何呢?由於js數組的特性,致使若是先出來的是1位置上的值進arr,那麼0位置上也會多一個空的值,因此不合理。
race方法雖然不經常使用,可是在Promise方法中也是一個能用得上的方法,它的做用是將一個Promise
數組放入race中,哪一個先執行完,race就直接執行完,並從then中取值。咱們來實現一下吧。
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
}
})
}
複製代碼
原理你們應該看懂了,很簡單,就是遍歷數組執行Promise,若是有一個
Promise
執行成功就resolve。
語法糖這三個字你們必定很熟悉,做爲一個很Swag的前端工程師,對async/await這對兄弟確定很熟悉,沒錯他們就是generator的語法糖。而咱們這裏要講的語法糖是Promise的。
//promise語法糖 也用來測試
Promise.deferred = Promise.defer = function(){
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
複製代碼
什麼做用呢?看下面代碼你就知道了
let fs = require('fs')
let Promise = require('./promises')
//Promise上的語法糖,爲了防止嵌套,方便調用
//壞處 錯誤處理不方便
function read(){
let defer = Promise.defer()
fs.readFile('./1.txt','utf8',(err,data)=>{
if(err)defer.reject(err)
defer.resolve(data)
})
return defer.Promise
}
複製代碼
沒錯,咱們能夠方便的去調用他語法糖defer中的
Promise
對象。那麼它還有沒有另外的方法呢?答案是有的。咱們須要在全局上安裝promises-aplus-tests插件npm i promises-aplus-tests -g
,再輸入promises-aplus-tests [js文件名] 便可驗證你的Promise的規範。
今天咱們就作了一個屬於本身的Promise
項目,理解裏面的源碼,方法原理,但願你們都有收穫。固然有什麼意見你們均可以在評論區評論,peace and love。