前言:你們好,我叫邵威儒,你們都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程序猿的坑,從大學買的第一本vb和自學vb,我就與編程結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟件,至今進入了前端領域,看到很多朋友都寫文章分享,本身也弄一個玩玩,如下文章純屬我的理解,便於記錄學習,確定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享我的對技術的通俗理解,共同成長!javascript
後續我會陸陸續續更新javascript方面,儘可能把javascript這個學習路徑體系都寫一下
包括前端所經常使用的es六、angular、react、vue、nodejs、koa、express、公衆號等等
都會從淺到深,從入門開始逐步寫,但願能讓你們有所收穫,也但願你們關注我~前端
源碼地址:github.com/iamswr/prom…
文章列表:juejin.cn/user/372040…vue
Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/java
JavaScript做爲單線程語言,其特色也是其缺陷,特色就是不用處理多線程引起的佔用資源、衝突啪啦啪啦等,缺陷就是同一時間,只能作一件事情,那麼會存在一個問題,網絡傳輸是有延遲的,好比A發一條信息到B服務器,在B服務器還沒返回信息給A時,那麼A就會一直在等待接收信息,會形成頁面的假死,那麼該怎麼辦?俗話說得好,程序猿改變世界,因而乎出現了異步的概念,我會分如下幾點,去講述我對前端異步的理解:node
回調函數被認爲是一種高級函數,一種被做爲參數傳遞給另外一個函數(在這稱做"otherFunction")的高級函數,回調函數會在otherFunction內被調用(或執行)。回調函數的本質是一種模式(一種解決常見問題的模式),所以回調函數也被稱爲回調模式。react
是否是看起來一頭懵逼,到底什麼是回調函數?有什麼做用?我對回調函數理解,從真正意義上讓我忽然間恍然大悟的,就是當初研究jQuery底層源碼的時候,咱們看如下一段代碼:jquery
<img src='../a.jpg'></img> <img src='../b.jpg'></img> <img src='../c.jpg'></img> $("img").attr("title",function(index,attr){ console.log(index) // 依次返回0 1 2 }); 複製代碼
在attr方法的第二個參數,傳入了一個function,而該函數,會依次獲取$('img')
的DOM對象對應index和attr,咱們能夠在該function裏,寫咱們須要的業務邏輯,那麼這樣有什麼好處呢?git
個人理解是,假如我要封裝一個庫,造一個輪子,那麼要考慮到通用性和複用性而且提供一個途徑,讓使用者任意發揮想象寫業務邏輯,而且把相關可能使用到的參數,都傳給使用者。es6
假設,咱們如今有一個需求,要寫一個判斷類型的方法,常見的方法有如下幾種:github
- typeOf // 簡單的數據類型判斷,棧區 - instanceof // 複雜的數據類型,堆區 - constructor // 複雜的數據類型,主要是用在繼承的改寫指向的構造函數,不多用於判斷類型 - Object.prototype.toString.call() // 絕大多數庫底層都是使用該方式,返回值如[object String] 複製代碼
首先,咱們寫一個isType方法
function isType(content,type,fn){ // 類型判斷 let t = Object.prototype.toString .call(content) .replace(/\[object\s|\]/g,'') // 判斷完成後,執行傳入的callback函數 fn(type,t) } 複製代碼
如今咱們要判斷一個值的類型,而後拿到這個類型,最終執行咱們須要作的事情
isType('hello swr','String',function(type,t){ // 做爲參數傳入的函數,接收isType函數內的fn中type和t這兩個參數 console.log(type === t) // true }) 複製代碼
那麼問題就出現了,好比咱們使用node.js的時候,進行文件讀取操做時,想獲取的值是一種嵌套依賴關係時,會出現什麼問題呢?
目錄結構: - iamswr - A.txt - B.txt - C.txt 其中 A.txt文件裏的內容爲字符串B.txt B.txt文件裏的內容爲字符串C.txt C.txt文件裏的內容爲字符串'hello swr' 那麼當咱們想獲取到'hello swr',會遇到什麼問題呢?請看下面的代碼 let fs = require('fs') fs.readFile('A.txt','utf8',function(err,data){ // 此時回調函數data值爲'B.txt' fs.readFile(data,'utf8',function(err,data){ // 此時回調函數data值爲'C.txt' fs.readFile(data,'utf8',function(err,data){ console.log(data) // 'hello swr' }) }) }) 複製代碼
以上這個例子若是嵌套依賴層次更高一些,那代碼變得十分難維護以及難閱讀,咱們在企業開發當中,常常會遇到想獲得的數據,是經過嵌套依賴的關係,最終纔得到須要的數據,陷入了回調地獄,而es6中,promise解決了這個讓前端頭疼的問題,後面我會詳細講promise,下面咱們先了解一下閉包、高階函數。
我我的理解,閉包其實是一種函數,因此閉包技術也是函數技術的一種;閉包能作的事情函數幾乎都能作,閉包有最大的兩個用處,一個是能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。
在javascript中,若是一個對象不被引用了,那麼這個對象會被GC回收,不然則一直保留在內存中,那麼利用這個特色,配合閉包使用,有如下幾個優勢:封閉做用域、保存做用域、做用域鏈條。
不污染全局變量,當團隊協做時,好比A大佬,封裝了jQuery庫,而jQuery庫內是有大量變量,若是不使用閉包,則jQuery庫內的變量會污染整個項目,甚至和其餘團員的變量有衝突
外部沒法獲取閉包內的變量,封閉了做用域 (function(){ var str = 'hello swr' console.log(str) // 'hello swr' })() console.log(str) // 報錯 咱們用原生js來寫代碼的時候,會存在一個問題, 好比有5個button標籤 var btns = document.getElementsByTagName('button'); for(var i=0; i< btns.length; i++){ var btn = btns[i]; btn.onclick = function () { alert('點擊了第' + i + '個按鈕'); } } 不管咱們點擊哪一個button,都是彈出'點擊了第5個按鈕', 由於btn.onclick事件是異步觸發的,當事件被觸發時, for循環早已經結束,此時變量I的值已是5, 全部onclick事件函數從內到外查找變量i時,查找到的值老是5。 能夠經過封閉做用域把每次循環的i值都封閉起來, 當時間函數順着做用域鏈從內到外查找變量i時, 會先找到被封閉在閉包環境中的i, 若是有5個按鈕, 則i的值就是0,1,2,3,4 var btns = document.getElementsByTagName('button'); for(var i=0; i< btns.length; i++){ (function (i) { var btn = btns[i]; btn.onclick = function () { alert('點擊了第' + i + '個按鈕'); } })(i); } 複製代碼
咱們知道,在es6以前,只有函數是有做用域的說法,在es6出現了,則有了塊級做用域的說法,好比
(function person(){ var name = '邵威儒' console.log(name) // '邵威儒' })() console.log(name) // 報錯 複製代碼
在函數外部,是訪問不了內部的name,這就是做用域。 在es6出了一個新的概念,就是塊級做用域
{ let name = '邵威儒' console.log(name) // '邵威儒' } console.log(name) // 報錯 複製代碼
效果和閉包同樣
函數嵌套函數,那麼內部的那個函數將造成做用域閉包。簡單的說,這種閉包可以達到的好處就是讓指令可以綁定一些全局數據去運行,優勢是全局數據隱藏化、 將數據綁定在指令上運行,讓指令再也不依賴全局數據。
function plus(num){ ++num return function(){ console.log(num) } } let toPlus = plus(5) 此時toPlus實際上爲 function(){ console.log(num) } 而這個num實際上就是plus函數內做用域的num,此時咱們沒法從外部修改num,並且把plus函數內的數據隱藏化,將數據綁定在toPlus上運行。 複製代碼
好比說,咱們實際開發中會遇到一個問題,就是某個函數,要等多個異步執行完畢後才執行,這種狀況怎麼作呢?
通常會想到如下這個辦法
let fs = require('fs') let arr = [] fs.readFile('./a.txt','utf8',function(err,data){ arr.push(data) // 假設data爲'hello' }) fs.readFile('./b.txt','utf8',function(err,data){ arr.push(data) // 假設data爲'swr' }) console.log(arr) // 咱們但願打印出來是['hello','swr']或['swr','hello'],可是打印出來的倒是[] 這是爲何呢? 是由於javascript執行原理,是先執行同步,再執行異步的,而fs.readFile方法屬於異步方法,因此還沒執行完畢,就已經執行了console.log(arr)了 複製代碼
對於這種並不是依賴嵌套獲取,咱們稱爲「同步」獲取,此同步非異步同步的那個同步,特別是這種異步請求的數據,獲取到的時間前後順序不一樣,那咱們該如何實現「同步」獲取呢?
let fs = require('fs') function after(times,callback){ let arr = [] return function(data){ arr.push(data) if(--times === 0){ callback(arr) } } } let fn = after(2,function(arr){ console.log(arr) // 當fn執行兩次後,則會執行該回調函數 }) fs.readFile('./a.txt','utf8',function(err,data){ fn(data) // 假設data爲'hello' }) fs.readFile('./b.txt','uft8',function(err,data)=>{ fn(data) // 假設data爲'swr' }) 最終當2個fs.readFile讀取完畢後,執行了fn()達到2次時,則會打印出['hello','swr']或者['swr','hello'] 複製代碼
雖然以上的方式,實現了咱們須要的需求,可是問題來了,難道咱們每一次都要特地寫一個after函數嗎?其實還有一個概念,叫作發佈訂閱,訂閱就相似你收藏了這個電臺,而發佈,則是這個電臺向全部收藏了本電臺的粉絲進行廣播,看下面代碼
let fs = require('fs') let event = { arr:[], // 存須要執行的函數 result:[], // 存結果 on(fn){ // 訂閱 this.arr.push(fn) }, emit(data){ // 發佈 this.result.push(data) this.arr.forEach(fn=>fn(this.result)) } } event.on(function(data){ if(data.length === 2){ console.log(data) // ['hello','swr'] 或者 ['swr','hello'] } }) fs.readFile('./a.txt','utf8',(err,data)=>{ event.emit(data) // data爲'hello' }) fs.readFile('./b.txt','utf8',(err,data)=>{ event.emit(data) // data爲'swr' }) 當兩個fs.readFile讀取完成,而且在其回調函數內執行了event.emit,最終會打印出['hello','swr'] 或者 ['swr','hello'] 複製代碼
囉囉嗦嗦說了那麼多,主要是想你們瞭解一下回調函數以及閉包,由於這概念和promise的緊密關聯的,promise部分我主要是想和你們根據promiseAplus規範,逐步手寫一個promise的底層實現方式。
首先,promise怎麼理解?我在知乎上看到一篇比較通俗易懂的小故事,你們能夠看看,zhuanlan.zhihu.com/p/19622332
早上,老爸說:「兒子,天氣如何?」 每週一早上,老爸問兒子下午的天氣狀況,兒子能夠到自家房子旁邊小山上使用望遠鏡來觀看。兒子在出發時許諾(Promise)老爸(會通知老爸天氣狀況)。 此刻,老爸決定,若是天氣不錯,明天就出去捕魚,不然就不去。並且若是兒子沒法得到天氣預報的話,也不去捕魚。 30分鐘左右,兒子回來了,每週的結局都不同。
結局A:成功得到了(retrieved)天氣預報,晴天 :) 兒子成功獲取了天氣預報,天空晴朗,陽光明媚!承諾(Promise)兌現了(resolved),因而老爸決定開始爲週日的捕魚作準備。
結局B:一樣成功得到了天氣預報,雨天:( 兒子成功得到了天氣預報,只不過是烏雲密佈,要下雨。承諾(Promise)兌現了(resolved),只是老爸決定呆在家裏,由於天氣很糟糕。
結局C:無法得到天氣預報:-/ 出了問題,兒子無法得知天氣預報,由於霧很大,就算站在小山上也沒法看清。兒子沒辦法對象他離開時許下的諾言, promise was rejected!老爸決定留下來,這並不值得冒險。
首先咱們要了解PromiseA+規範 promisesaplus.com/
// Promise構造函數的第一個參數爲executor let promise = new Promise(function(resolve,reject){ console.log('我是會被當即執行的喲') }) // promise的實例都有then方法 promise.then(()=>{ // 成功的回調 },()=>{ // 失敗的回調 }) 複製代碼
// 默認時爲pending態,既不會走成功的回調也不會走失敗的回調 promise.then(()=>{ console.log('success1') },()=>{ console.log('error1') }) console.log('2') 在這段代碼中,只會打印出'2',由於promise一直處於pending態,不會走then後的回調函數 複製代碼
let promise = new Promise(function(resolve,reject){ console.log('1') resolve() // 更改pending狀態爲resolved }) promise.then(()=>{ console.log('success1') },()=>{ console.log('error1') }) console.log('2') 此時輸出順序爲'1' -> '2' -> 'success1' 複製代碼
let promise = new Promise(function(resolve,reject){ console.log('1') setTimeout(()=>{ // 異步行爲 resolve() // 更改狀態爲成功 },1000) }) promise.then(()=>{ console.log("success1") }) promise.then(()=>{ console.log('success2') }) console.log("2") 此時輸出順序爲'1' -> '2' -> 'success1' -> 'success2' 複製代碼
let promise = new Promise(function(resolve,reject){ throw new Error('出錯了') // 拋出錯誤 }) promise.then(()=>{ console.log('success1') },()=>{ console.log('error1') }) 此時輸出爲 'error1' 複製代碼
下面代碼部分和源碼實現部分要結合來看
// ----- 代碼部分 // 1.executor默認在new的時候會自動執行 // 成功和失敗的視乎能夠傳遞參數 let promise = new Promise((resolve,reject)=>{ // 6.resolve、reject函數對應源碼實現部分的resolve、reject函數 resolve('hello swr') // 11.執行resolve }) // 7.Promise的實例都有then方法 promise.then((data)=>{ // 8.成功的回調函數 },(err)=>{ // 9.失敗的回調函數 }) 複製代碼
// ----- 源碼實現部分 // 2.聲明一個Promise構造函數 function Promise(executor){ let self = this self.value = undefined self.reason = undefined // 12.由於value和reason值須要在Promise實例方法then中使用,因此把這兩個值,賦給new出來的實例 function resolve(value){ // 3.聲明一個resolve函數 self.value = value // 13.當調用了resolve而且傳參數時,則把這value值賦予self.value } function reject(reason){ // 4.聲明一個reject函數 self.reason = reason // 13.當調用了reject而且傳參數時,則把這reason值賦予self.reason } executor(resolve,reject) // 5.把resolve、reject函數傳到executor } // 由於Promise的實例都有then方法,那麼意味着then方法是在Promise的原型對象中的方法 // 10.對應上面成功的回調函數onFulfilled以及失敗的回調函數onRejected Promise.prototype.then = function(onFulfilled,onRejected){ } module.exports = Promise // 把Promise暴露出去 複製代碼
此時,咱們會發現,如何去判斷調用resolve仍是reject呢? 這個時候咱們在內部應該維護一個狀態,而咱們以前說過了Promise有三種狀態,分別爲pending、resolved、rejected,那麼咱們接着看下面的代碼。
// ----- 代碼部分 let promise = new Promise((resolve,reject)=>{ resolve('hello swr') // 5.暫時忽略此行 resolve('看看同時執行resolve和reject會發生什麼?') // 5.此行執行resovle reject('看看同時執行resolve和reject會發生什麼?') // 5.此行執行reject }) promise.then((data)=>{ console.log('success:' + data) // 5.當調用了resolve函數,則輸出success:hello swr },(err)=>{ }) 複製代碼
// ----- 源碼實現部分 function Promise(executor){ let self = this self.value = undefined self.reason = undefined self.status = 'pending' // 1.在內部維護一個status狀態 function resolve(value){ self.value = value self.status = 'resolved' // 2.當調用了resolve時,更改狀態爲resolved } function reject(reason){ self.reason = reason self.status = 'rejected' // 2.當調用了reject時,更改狀態爲rejected } executor(resolve,reject) } Promise.prototype.then = function(onFulfilled,onRejected){ let self = this // 3.當咱們在then中,執行了成功或者失敗的回調函數時,首先要判斷目前處於什麼狀態 if(self.status === 'resolved'){ onFulfilled(self.value) // 4.當調用了resolve函數後,會執行成功的回調函數,而且把resolve中傳遞的值,傳遞給成功的回調函數 } if(self.status === 'rejected'){ onRejected(self.reason) // 4.當調用了reject函數後,會執行成功的回調函數,而且把reject中傳遞的值,傳遞給失敗的回調函數 } } module.exports = Promise 複製代碼
當咱們在上面5中同時執行resolve和reject,會發現都可以執行,那麼就違背了狀態只能更改一次的原則了,下面咱們來解決這個問題。
// ----- 代碼部分 let promise = new Promise((resolve,reject)=>{ resolve('看看同時執行resolve和reject會發生什麼?') // 1. 此時執行resolve和reject reject('看看同時執行resolve和reject會發生什麼?') // 3.此時即便調用reject,由於resolve已經調用了一次,從pending更改成resolve,因此在第一次調用後,屢次調用也不會生效 // 4.以上resolve、reject暫時忽略掉,咱們考慮一個狀況,當promise拋出錯誤時,怎麼去處理呢? throw new Error('出錯啦') }) promise.then((data)=>{ console.log('success:' + data) },(err)=>{ }) 複製代碼
// ----- 源碼實現部分 function Promise(executor){ let self = this self.value = undefined self.reason = undefined self.status = 'pending' function resolve(value){ if(self.status === 'pending'){ // 2.此時新增一個狀態判斷,當狀態爲pending的時候才能執行 self.value = value self.status = 'resolved' } } function reject(reason){ if(self.status === 'pending'){ // 2.此時新增一個狀態判斷,當狀態爲pending的時候才能執行 self.reason = reason self.status = 'rejected' } } // 5.當咱們在執行executor時,內部拋出錯誤的時候,能夠利用try catch來處理這個問題 try{ executor(resolve,reject) }catch(error){ reject(error) } } Promise.prototype.then = function(onFulfilled,onRejected){ let self = this if(self.status === 'resolved'){ onFulfilled(self.value) } if(self.status === 'rejected'){ onRejected(self.reason) } } module.exports = Promise 複製代碼
這樣咱們就解決了屢次調用,只認第一次的更改狀態,而且當拋出錯誤時,使用try catch來處理,那麼接下來,咱們想一下,目前咱們都是new一個Promise,而後調用then,這整個流程,彷彿沒任何問題,可是,如今問題出現了,若是此時resolve或者reject是處於setTimeout(()=>{resolve()},3000)中,即處於異步中,當咱們new一個Promise時,不會立刻執行異步代碼,而是直接執行了promise.then這個函數,而此時由於self.status的狀態依然是處於pending,因此不會執行resolve或者reject,當同步代碼執行完畢後,執行異步代碼時,更改了狀態爲resolved或者rejected時,此時then方法已經執行完畢了,不會再次執行then的方法,那麼此時咱們該如何處理?
還存在一個問題,就是上面所說的,同一個promise的實例能夠then屢次,成功時會調用全部的成功方法,失敗時會調用全部的失敗方法,那這個又該如何處理呢?
能夠利用咱們前面所說的發佈訂閱的思路來解決,如今咱們看下面代碼。
// ----- 代碼部分 let promise = new Promise((resolve,reject)=>{ setTimeout(()=>{ // 1.此時resolve處於異步 resolve('hello swr') },3000) }) promise.then((data)=>{ // 多個then console.log('success1:' + data) },(err)=>{ }) promise.then((data)=>{ // 多個then console.log('success2:' + data) },(err)=>{ }) 複製代碼
// ----- 源碼實現部分 function Promise(executor){ let self = this self.value = undefined self.reason = undefined self.status = 'pending' self.onResolvedCallbacks = [] // 2.可能new Promise中會有異步的操做,此時咱們把異步操做時,執行的then函數的成功回調,統一保存在該數組中 self.onRejectedCallbacks = [] // 2.可能new Promise中會有異步的操做,此時咱們把異步操做時,執行的then函數的失敗回調,統一保存在該數組中 function resolve(value){ if(self.status === 'pending'){ self.value = value self.status = 'resolved' // 4.當調用resolve時,把該數組中存放的成功回調都執行一遍,若是是異步,則會把成功的回調都存到該數組裏了,若是是異步,則沒存到。 self.onResolvedCallbacks.forEach(fn=>fn()) } } function reject(reason){ if(self.status === 'pending'){ self.reason = reason self.status = 'rejected' // 4.當調用reject時,把該數組中存放的失敗回調都執行一遍,若是是異步,則會把成功的回調都存到該數組裏了,若是是異步,則沒存到。 self.onRejectedCallbacks.forEach(fn=>fn()) } } try{ executor(resolve,reject) }catch(error){ reject(error) } } Promise.prototype.then = function(onFulfilled,onRejected){ let self = this if(self.status === 'resolved'){ onFulfilled(self.value) } if(self.status === 'rejected'){ onRejected(self.reason) } // 3.當new Promise中有resolve、reject處於異步中,執行then的時候,狀態爲pending, if(self.status === 'pending'){ self.onResolvedCallbacks.push(()=>{ onFulfilled(self.value) }) // 3. 把成功的回調函數,存到該數組中,這樣寫的好處,就是把參數傳進去,不須要未來遍歷onResolvedCallbacks時,再傳參 self.onRejectedCallbacks.push(()=>{ onRejected(self.reason) }) // 3. 把失敗的回調函數,存到該數組中,這樣寫的好處,就是把參數傳進去,不須要未來遍歷onRejectedCallbacks時,再傳參 } } module.exports = Promise 複製代碼
到此爲止,咱們簡版的Promise實現得差很少了,小夥伴們能夠對着代碼敲一下,感覺一下,體會一下。
其實Promise的核心在於鏈式調用,Promise主要是解決2個問題:
首先,好比回調地獄怎麼解決呢?那麼咱們來看下面的代碼,而且改成promise。
// 回調函數 let fs = require('fs') fs.readFile('./a.txt','utf8',(err,data)=>{ // 往fs.readFile方法傳遞了第三個爲函數的參數 if(err){ console.log(err) return } console.log(data) }) // 改寫爲Promise let fs = require('fs') function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve() }) }) } read('./a.txt','utf8').then((data)=>{ // 在這裏則再也不須要傳回調函數進去,而是採用then來達到鏈式調用 console.log(data) },(err)=>{ console.log(err) }) // 這樣看好像Promise也沒什麼優點,那麼接下來咱們對比一下 // 假設有3個文件 // - 1.txt 文本內容爲'2.txt' // - 2.txt 文本內容爲'3.txt' // - 3.txt 文本內容爲'hello swr' // 用回調函數 fs.readFile('./1.txt','utf8',(err,data)=>{ fs.readFile(data,'utf8',(err,data)=>{ fs.readFile(data,'utf8',(err,data)=>{ console.log(data) // hello swr }) }) }) // 用Promise read('./1.txt','utf8') .then((data)=>{ // 1.若是一個promise執行完後,返回的仍是一個promise, // 會把這個promise的執行結果會傳遞給下一次then中 return read(data,'utf8') }) .then((data)=>{ return read(data,'utf8') }) .then((data)=>{ // 2.若是在then中返回的不是一個promise, // 而是一個普通值,會將這個普通值做爲下次then的成功的結果 return data.split('').reverse().join('') }) .then((data)=>{ console.log(data) // rws olleh // 3.若是當前then中失敗了,會走下一個then的失敗回調 throw new Error('出錯') }) .then(null,(err)=>{ console.log(err) // Error:出錯 報錯了 // 4.若是在then中不返回值,雖然沒有顯式返回, // 可是默認是返回undefined,是屬於普通值,依然會把這個普通值傳到 // 下一個then的成功回調中 }) .then((data)=>{ console.log(data) // undefined }) 複製代碼
從上面能夠看得出,改寫爲Promise的代碼,更好閱讀和維護,從用Promise方式能夠得出結論:
// 若是在then中拋出錯誤,會怎樣呢? // 情景一,會被下一個then中的失敗回調捕獲 read('./1.txt','utf8') .then((data)=>{ throw new Error('出錯了') }) .then(null,(err)=>{ console.log(err) // Error:出錯了 報錯 }) // 情景二,若是沒有被失敗的回調捕獲,拋出錯誤最終會變成異常 read('./1.txt','utf8') .then((data)=>{ throw new Error('出錯了') }) // 情景三,若是沒有被失敗的回調捕獲,那麼最終會被catch捕獲到 read('./1.txt','utf8') .then((data)=>{ throw new Error('出錯了') }) .then((data)=>{ }) .catch((err)=>{ console.log(err) // Error:出錯了 報錯 }) // 情景四,若是被失敗的回調捕獲了,那麼不會被catch捕獲到 read('./1.txt','utf8') .then((data)=>{ throw new Error('出錯了') }) .then(null,(err)=>{ console.log(err) // Error:出錯了 報錯 }) .catch((err)=>{ console.log(err) // 不會執行到這裏 }) 複製代碼
jquery的鏈式調用,是經過其內部執行完後return this,返回自身這個對象,達到鏈式調用的目的,那爲何Promise不採用這種方式呢?
咱們能夠看如下代碼,感覺一下。
let promise = new Promise((resolve,reject)=>{ resolve() // 執行resolve,使狀態從pending變爲resolved }) let promise2 = promise.then(()=>{ throw new Error() // 拋出錯誤 return this // 返回自身 }) // 那麼我在promise2中,調then,那麼它會執行失敗的回調嗎?答案是不會的。 // 由於咱們不可能讓狀態既成功又失敗的 // promise成功了,若是返回this,那不能走向失敗 promise2.then(()=>{ console.log('來到這裏了') },()=>{ console.log('會來到這裏嗎?') }) // 此時then中返回自身後,promise2其實就是promise,而咱們想達到 // 的是把當前的then返回後,傳到下一個then中,可是咱們這樣返回this, // 其實會變得很矛盾,由於狀態已經從pending變爲resolved,不可能又從resolved變成rejected的 // 因此得出結論,返回的必須是一個新的promise,由於promise成功後不能再走失敗 // 只能建立一個新的promise再執行業務邏輯,返回同一個promise的話,就不能既成功又失敗 複製代碼
// ----- 代碼部分 let promise = new Promise((resolve,reject)=>{ resolve() }) // 2.返回的值爲promise2 爲何這樣規定呢?這是promiseA+規範規定的,咱們要遵循 let promise2 = promise.then((data)=>{ return x // 1.then中的返回值x多是普通值也多是promise,而且傳給下一個then }).then((data)=>{ console.log(data) // x的值 }) 複製代碼
// ----- 源碼實現部分 function Promise(executor){ let self = this self.value = undefined self.reason = undefined self.status = 'pending' self.onResolvedCallbacks = [] self.onRejectedCallbacks = [] function resolve(value){ if(self.status === 'pending'){ self.value = value self.status = 'resolved' self.onResolvedCallbacks.forEach(fn=>fn()) } } function reject(reason){ if(self.status === 'pending'){ self.reason = reason self.status = 'rejected' self.onRejectedCallbacks.forEach(fn=>fn()) } } try{ executor(resolve,reject) }catch(error){ reject(error) } } Promise.prototype.then = function(onFulfilled,onRejected){ let self = this let promise2 // 3.上面講promise鏈式調用時,已經說了返回的是一個新的promise對象,那麼咱們聲明一個新的promise // 4.那麼咱們new一個新的promise,而且把如下代碼放到promise中 let promise2 = new Promise((resolve,reject)=>{ if(self.status === 'resolved'){ // 7.當執行成功回調的時候,可能會出現異常,那麼就把這個異常做爲promise2的錯誤的結果 try{ let x = onFulfilled(self.value) // 6.這裏的x,就是上面then中執行完返回的結果,咱們在這裏聲明一個x用來接收 // 8.根據promiseA+規範,咱們應該提供一個函數來處理promise2 // 我我的的理解是,then中無論是成功回調仍是失敗回調,其返回 // 值,有多是promise,也有多是普通值,也有多是拋出錯誤 // 那麼咱們就須要一個函數來處理這幾種不一樣的狀況 // 這個函數咱們聲明爲resolvePromise吧 resolvePromise(promise2,x,resolve,reject) // 9. 這裏的promise2就是當前的promise2,x則是執行then中成功回調後返回的結果,若是是成功則調promise2的resolve,失敗則調reject }catch(e){ reject(e) // 注意:這裏的reject是這個promise2的reject } } if(self.status === 'rejected'){ // 同6-7步 try{ let x = onRejected(self.reason) // 同8-9 resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } } if(self.status === 'pending'){ self.onResolvedCallbacks.push(()=>{ // 同6-7步 try{ let x = onFulfilled(self.value) // 同8-9 resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } }) self.onRejectedCallbacks.push(()=>{ // 同6-7步 try{ let x = onRejected(self.reason) // 同8-9 resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } }) } }) return promise2 // 5.在jquery中是return this,可是在promise中,則是返回一個新的promise對象 } module.exports = Promise 複製代碼
接下來咱們寫一下resolvePromise這個函數,整個Promise最核心的部分就是在這裏
// ----- 代碼部分 let promise = new Promise((resolve,reject)=>{ resolve() }) let promise2 = promise.then((data)=>{ return x }).then((data)=>{ console.log(data) }) // 2.咱們在resolvePromise函數中,在原生狀況下,若是傳參的時候,promise2和x是同一個對象會發生什麼呢? let promise = new Promise((resolve,reject)=>{ resolve() }) let promise2 = promise.then(()=>{ return promise2 // 2.1報錯 UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise> // 報錯的意思是,陷入了死循環,那怎麼理解呢? // promise2的成功或失敗是要取決於promise中then的返回結果,而返回的倒是promi2本身 // 這樣就陷入死循環了,promise2是依賴於promise的then返回的結果, // 而then返回的結果是promise2,而then中的promise2,既不是成功也不是失敗,不能本身等於本身 }) // 7.當取一個對象上的屬性,可能存在報異常的狀況,怎麼理解呢? // 由於這個方法有可能不是本身寫的,可能別人搞惡做劇亂寫的,看如下代碼。 let obj = {} // 給obj對象定義一個then方法,當咱們去obj對象中調用then方法時 // 就會執行裏面的get,而get則是拋出異常 Object.defineProperty(obj,'then',{ get(){ throw new Error() } }) // 10.爲何要用call呢?解決了什麼問題?看一下如下代碼 首先咱們執行 promise.then(()=>{ console.log(this) // 此時this是指向該promise的,對象的方法中this是指向這個對象的 }) 可是咱們在下面經過let then = promise.then,來判斷是否promise,是否會異常 當咱們執行then時,裏面的this仍是會指向這個promise嗎?答案是不必定的, 由於此時then,若是在全局下執行,指向的可能就是window了,因此爲了讓this的 指向正確,咱們須要經過 then.call(promise),來把then的this指向promise 複製代碼
// ----- 源碼實現部分 function Promise(executor){ let self = this self.value = undefined self.reason = undefined self.status = 'pending' self.onResolvedCallbacks = [] self.onRejectedCallbacks = [] function resolve(value){ if(self.status === 'pending'){ self.value = value self.status = 'resolved' self.onResolvedCallbacks.forEach(fn=>fn()) } } function reject(reason){ if(self.status === 'pending'){ self.reason = reason self.status = 'rejected' self.onRejectedCallbacks.forEach(fn=>fn()) } } try{ executor(resolve,reject) }catch(error){ reject(error) } } // 1.聲明一個resolvePromise函數 // 這個函數很是核心,全部的promise都遵循這個規範,全部的promise能夠通用, /** * * @param {*} promise2 then的返回值,返回新的promise * @param {*} x then中成功函數或者失敗函數的返回值 * @param {*} resolve promise2的resolve * @param {*} reject promise2的reject */ function resolvePromise(promise2,x,resolve,reject){ // 3.從2中咱們能夠得出,本身不能等於本身 // 當promise2和x是同一個對象的時候,則走reject if(promise2 === x){ return reject(new TypeError('Chaining cycle detected for promise')) } // 4.由於then中的返回值能夠爲promise,當x爲對象或者函數,纔有可能返回的是promise let called if(x !== null && (typeof x === 'object' || typeof x === 'function')){ // 8.從第7步,能夠看出爲何會存在拋出異常的可能,因此使用try catch處理 try{ // 6.由於當x爲promise的話,是存在then方法的 // 可是咱們取一個對象上的屬性,也有可能出現異常,咱們能夠看一下第7步 let then = x.then // 9.咱們爲何在這裏用call呢?解決了什麼問題呢?能夠看上面的第10步 // x可能仍是個promise,那麼就讓這個promise執行 // 可是仍是存在一個惡做劇的狀況,就是{then:{}} // 此時須要新增一個判斷then是否函數 if(typeof === 'function'){ then.call(x,(y)=>{ // y是返回promise後的成功結果 // 一開始咱們在這裏寫的是resolve(y),可是考慮到一點 // 這個y,有可能仍是一個promise, // 也就是說resolve(new Promise(...)) // 因此涉及到遞歸,咱們把resolve(y)改爲如下 // 12.限制既調resolve,也調reject if(called) return called = true resolvePromise(promise2,y,resolve,reject) // 這樣的話,代碼會一直遞歸,取到最後一層promise // 11.這裏有一種狀況,就是不能既調成功也調失敗,只能挑一次, // 可是咱們前面不是處理過這個狀況了嗎? // 理論上是這樣的,可是咱們前面也說了,resolvePromise這個函數 // 是全部promise通用的,也能夠是別人寫的promise,若是別人 // 的promise可能既會調resolve也會調reject,那麼就會出問題了,因此咱們接下來要 // 作一下限制,這個咱們寫在第12步 },(err)=>{ // err是返回promise後的失敗結果 if(called) return called = true reject(err) }) }else{ resolve(x) // 若是then不是函數的話,那麼則是普通對象,直接走resolve成功 } }catch(e){ // 當出現異常則直接走reject失敗 if(called) return called = true reject(e) } }else{ // 5.x爲一個常量,則是走resolve成功 resolve(x) } } Promise.prototype.then = function(onFulfilled,onRejected){ // onFulfilled、onRejected是可選參數 onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val; onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err} let self = this let promise2 let promise2 = new Promise((resolve,reject)=>{ if(self.status === 'resolved'){ // 13.根據promiseA+規範,onFulfilled或onRejected必須 // 被調用不是當前的上下文,then方法是異步的 setTimeout(()=>{ try{ let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } },0) } if(self.status === 'rejected'){ // 同13 setTimeout(()=>{ try{ let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } },0) } if(self.status === 'pending'){ self.onResolvedCallbacks.push(()=>{ // 同13 setTimeout(()=>{ try{ let x = onFulfilled(self.value) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } },0) }) self.onRejectedCallbacks.push(()=>{ // 同13 setTimeout(()=>{ try{ let x = onRejected(self.reason) resolvePromise(promise2,x,resolve,reject) }catch(e){ reject(e) } },0) }) } }) return promise2 } // 14.到目前爲止,根據promiseA+規範的代碼寫得差很少了,咱們能夠經過測試代碼來測試咱們是否寫得正確,下面咱們寫一段測試代碼 Promise.defer = Promise.deferred = function(){ let dfd = {} dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve dfd.reject = reject }) return dfd } // 14.接下來咱們要安裝一個插件,npm install promises-aplus-test -g module.exports = Promise 複製代碼
// 完整代碼 也順便帶你們理順一下 function Promise(executor) { let self = this; self.value = undefined; // 成功的值 self.reason = undefined; // 失敗的值 self.status = 'pending'; // 目前promise的狀態pending self.onResolvedCallbacks = []; // 可能new Promise的時候會存在異步操做,把成功和失敗的回調保存起來 self.onRejectedCallbacks = []; function resolve(value) { // 把狀態更改成成功 if (self.status === 'pending') { // 只有在pending的狀態才能轉爲成功態 self.value = value; self.status = 'resolved'; self.onResolvedCallbacks.forEach(fn => fn()); // 把new Promise時異步操做,存在的成功回調保存起來 } } function reject(reason) { // 把狀態更改成失敗 if (self.status === 'pending') { // 只有在pending的狀態才能轉爲失敗態 self.reason = reason; self.status = 'rejected'; self.onRejectedCallbacks.forEach(fn => fn()); // 把new Promise時異步操做,存在的失敗回調保存起來 } } try { // 在new Promise的時候,當即執行的函數,稱爲執行器 executor(resolve, reject); } catch (e) { // 若是執行executor拋出錯誤,則會走失敗reject reject(e); } } // 這個函數爲核心,全部的promise都遵循這個規範 // 主要是處理then中返回的值x和promise2的關係 function resolvePromise(promise2,x,resolve,reject){ // 當promise2和then返回的值x爲同一個對象時,變成了本身等本身,會陷入死循環 if(promise2 === x){ return reject(new TypeError('Chaining cycle')); } let called; // x多是一個promise也多是一個普通值 if(x!==null && (typeof x=== 'object' || typeof x === 'function')){ try{ let then = x.then; if(typeof then === 'function'){ then.call(x,y=>{ if(called) return; called = true; resolvePromise(promise2,y,resolve,reject); },err=>{ if(called) return; called = true; reject(err); }); }else{ resolve(x); } }catch(e){ if(called) return; called = true; reject(e); } }else{ resolve(x); } } // then調用的時候,都是屬於異步,是一個微任務 // 微任務會比宏任務先執行 // onFulfilled爲成功的回調,onRejected爲失敗的回調 Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val; onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err} let self = this; let promise2; // 上面講了,promise和jquery的區別,promise不能單純返回自身, // 而是每次都是返回一個新的promise,才能夠實現鏈式調用, // 由於同一個promise的pending resolve reject只能更改一次 promise2 = new Promise((resolve, reject) => { if (self.status === 'resolved') { // 爲何要加setTimeout? // 首先是promiseA+規範要求的 // 其次是你們寫的代碼,有的是同步,有的是異步 // 因此爲了更加統一,就使用爲setTimeout變爲異步了,保持一致性 setTimeout(()=>{ try { // 上面executor雖然使用try catch捕捉錯誤 // 可是在異步中,不必定可以捕捉,因此在這裏 // 用try catch捕捉 let x = onFulfilled(self.value); // 在then中,返回值多是一個promise,因此 // 須要resolvePromise對返回值進行判斷 resolvePromise(promise2,x,resolve,reject); } catch (e) { reject(e); } },0) } if (self.status === 'rejected') { setTimeout(()=>{ try { let x = onRejected(self.reason); resolvePromise(promise2,x,resolve,reject); } catch (e) { reject(e); } },0) } if (self.status === 'pending') { self.onResolvedCallbacks.push(() => { setTimeout(()=>{ try { let x = onFulfilled(self.value); resolvePromise(promise2,x,resolve,reject); } catch (e) { reject(e); } },0) }); self.onRejectedCallbacks.push(() => { setTimeout(()=>{ try { let x = onRejected(self.reason); resolvePromise(promise2,x,resolve,reject); } catch (e) { reject(e); } },0) }); } }); return promise2 } Promise.defer = Promise.deferred = function(){ let dfd = {}; dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }) return dfd; } module.exports = Promise; 複製代碼
執行promises-aplus-tests promise.js
複製代碼
到此爲止,咱們已經寫了一個符合promiseA+規範的promise了,你們能夠好好多看幾回。
Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason); }) } Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ resolve(value); }) } 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 processData(index,data){ arr[index] = data; if(++i == promises.length){ resolve(arr); } } for(let i = 0;i<promises.length;i++){ promises[i].then(data=>{ processData(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.resolve使用 Promise.resolve('hello swr').then((data)=>{ // 直接把成功的值傳遞給下一個then console.log(data) // hello swr }) // 那麼Promise.resolve內部是怎麼實現的呢? Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ // 在內部new一個Promise對象 resolve(value) }) } // 同理,Promise.reject內部也是相似實現的 Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) }) } 複製代碼
// 原生Promise的catch使用 Promise.reject('hello swr').catch((e)=>{ console.log(e) // hello swr }) // 上面這段代碼至關於下面這段代碼 Promise.reject('hello swr').then(null,(e)=>{ // then裏直接走了失敗的回調 console.log(e) // hello swr }) // 內部實現 Promise.prototype.catch = function(onRejected){ return this.then(null,onRejected) // 至關於then裏的成功回調只傳個null } 複製代碼
// 原生Promise.all的使用 // 假設1.txt內容爲hello 2.txt內容爲swr let fs = require('fs') function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) }) } Promise.all([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{ console.log(data) // 所有讀取成功後返回 ['hello','swr'] // 須要注意的是,當其中某個失敗的話,則會走失敗的回調函數 }) // 內部實現 Promise.all = function(promises){ // promises 是一個數組 return new Promise((resolve,reject)=>{ let arr = [] let i = 0 function processData(index,data){ arr[index] = data // 5.咱們能用arr.length === promises.length來判斷請求是否所有完成嗎? // 答案是不行的,假設arr[2] = 'hello swr' // 那麼打印這個arr,將是[empty × 2, "hello swr"], // 此時數組長度也是爲3,而數組arr[0] arr[1]則爲空 // 那麼換成如下的辦法 if(++i === promises.length){ // 6.利用i自增來判斷是否都成功執行 resolve(arr) // 此時arr 爲['hello','swr'] } } for(let i = 0;i < promises.length;i++){ // 1.在此處遍歷執行 promises[i].then((data)=>{ // 2.data是成功後返回的結果 processData(i,data) // 4.由於Promise.all最終返回的是一個數組成員按照順序排序的數組 // 並且異步執行,返回並不必定按照順序 // 因此須要傳當前的i },reject) // 3.若是其中有一個失敗的話,則調用reject } }) } 複製代碼
// 原生Promise.race的使用 // 一個成功就走成功的回調,一個失敗就走失敗的回調 Promise.race([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{ console.log(data) // 可能返回 'hello' 也可能返回 'swr' 看哪一個返回快就用哪一個做爲結果 }) // 內部實現 Promise.race = function(promises){ // promises 是一個數組 return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ promises[i].then(resolve,reject) // 和上面Promise.all有點相似 } }) } 複製代碼
這個語法糖能夠簡化一些操做,好比
let fs = require('fs') // 寫法一: function read(filePath,encoding){ // 這裏的new Promise依然是傳遞了一個executor回調函數 // 咱們該怎樣減小回調函數嵌套呢? return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) }) } // 寫法二: // 這樣的寫法減小了一層回調函數的嵌套 function read(filePath,encoding){ let dfd = Promise.defer() fs.readFile(filePath,encoding,(err,data)=>{ if(err) dfd.reject(err) dfd.resolve(data) }) return dfd.promise } read('./1.txt','utf8').then((data)=>{ console.log(data) }) 複製代碼
結尾:第一次寫,都是想到哪寫到哪,請你們多多諒解~ 也但願對你們有所幫助,promise的源碼實現,我最大的收穫並非怎麼實現promise,而是編程思惟,你們能夠多多往深裏想想,也但願你們能夠和我進行交流,共同進步