我想在你身上,作春天在櫻桃樹身上作的事情。——巴勃羅·聶魯達node
首先來看看promise的用法,從名字能夠看出它是個構造函數,因此咱們得new它,獲得一個Promise實例p,咱們打印p看看jquery
let p = new Promise
console.log(p) // TypeError: Promise resolver undefined is not a function
複製代碼
報錯信息告訴咱們,Promise須要一些參數,這裏須要一個函數(咱們叫它執行器)做爲參數,該函數有兩個參數————resolve和reject,這兩個參數也是函數(由js引擎提供),咱們能夠在Promise內部調用,當異步操做成功時,調用resolve,不然reject。git
let p =new Promise(function(resolve, reject){
if(/* 異步操做成功 */){
resolve(data)
}else{
reject(err)
}
})
複製代碼
如今咱們須要知道一個重要概念,Promise是有「狀態」的,分別是pending(等待態)、fulfilled(成功態)、rejected(失敗態),pending能夠轉換爲fulfilled或rejected,但fulfilled和rejected不可相互轉化。程序員
resolve方法能夠將pending轉爲fulfilled,reject方法能夠將pending轉爲rejected。github
經過給Promise示例上的then方法傳遞兩個函數做爲參數,能夠提供改變狀態時的回調,第一個函數是成功的回調,第二個則是失敗的回調。ajax
p.then(function(data){ // resolve方法會將參數傳進成功的回調
console.log(data)
}, function(err){ // reject方法會將失敗的信息傳進失敗的回調
console.log(err)
})
複製代碼
舉個栗子
let p = new Promise(function(resolve, reject){
setTimeout(function(){
let num = Math.random()
if (num > 0.5) {
resolve(num)
}else{
reject(num)
}
}, 1000)
})
p.then(function(num){
console.log('大於0.5的數字:', num)
},function(num){
console.log('小於等於0.5的數字', num)
})
// 運行第一次:小於等於0.5的數字 0.166162996031475
// 運行第二次:大於0.5的數字: 0.6591451548308984
...
複製代碼
在Promise執行器中咱們進行了一次異步操做,並在咱們以爲合適的時候調用成功或失敗的回調函數,並拿到了想要的數據以進行下一步操做編程
除此以外,每個then方法都會返回一個新的Promise實例(不是原來那個),讓then方法支持鏈式調用,並能夠經過返回值將參數傳遞給下一個then數組
p.then(function(num){
return num
},function(num){
return num
}).then(function(num){
console.log('大於0.5的數字:', num)
},function(num){
console.log('小於等於0.5的數字', num)
})
複製代碼
catch方法等同於.then(null, reject),能夠直接指定失敗的回調(支持接收上一個then發生的錯誤)promise
這多是個頗有用的方法,它能夠統一處理多個Promise緩存
Promise.all能將多個Promise實例包裝成一個Promise實例
let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
// 三個都成功則成功
}, function(){
// 只要有失敗,則失敗
})
複製代碼
這個組合後的Promise實例和普通實例同樣,有三種狀態,這裏有組成它的幾個小Promise的狀態決定 :
一、當Promise1, Promise2, Promise3的狀態都爲成功態,則p爲成功態; 二、當Promise1, Promise2, Promise3中有任意一個爲失敗態,則p爲失敗態;
與all方法相似,也能夠講多個Promise實例包裝成一個新的Promise實例
不一樣的是,all時大Promise的狀態由多個小Promise共同決定,而race時由第一個轉變狀態的小Promise的狀態決定,第一個是成功態,則轉成功態,第一個失敗態,則轉失敗態
能夠生成一個成功的Promise
Promise.resolve('成功')等同於new Promise(function(resolve){resolve('成功')})
能夠生成一個失敗的Promise
Promise.reject('出錯了')等同於new Promise((resolve, reject) => reject('出錯了'))
以jquery的ajax爲例(@1.5.0版本之前,後來jquery也引入了Promise的概念),看看從前咱們是如何解決異步問題的。
$.get('url', {data: data}, function(result){
console.log('成功', result)// 成功的回調,result爲異步拿到的數據
});
複製代碼
看起來還能夠?
想象一個場景,當咱們須要發送多個異步請求,而請求之間相互關聯相互依賴,沒有請求1就不會有請求2,沒有請求2就不會有請求3........
這時咱們須要這樣寫
$.get('url', {data: data}, function(result1){
$.get('url', {data: result1}, function(result2){
$.get('url', {data: result2}, function(result3){
$.get('url', {data: result3}, function(result4){
......
$.get('url', {data: resultn}, function(resultn+1){
console.log('成功')
}
}
}
}
});
複製代碼
這樣的話,咱們就掉入了傳說中的回調地獄,萬劫不復,不能自拔。
這種代碼,難以維護和調試,一旦出現bug,牽一髮而動全身。
下面咱們看看Promise是如何解決的,咱們以node中的fs訪問文件舉例
先建立三個相互依賴的txt文件
1.txt的內容:
2.txt
複製代碼
2.txt的內容:
3.txt
複製代碼
3.txt的內容:
完成
複製代碼
js代碼:
let readFile = require('fs').readFile; // 加載node內置模塊fs 利用readFile方法異步訪問文件
function getFile(url){ // 建立一個讀取文件方法
return new Promise(function(resolve, reject){ // 返回一個Promise對象
readFile(url, 'utf8', function(err,data){ // 讀取文件
resolve(data) // 調用成功的方法
})
})
}
getFile('1.txt').then(function(data){ // then方法進行鏈式調用
console.log(data) // 2.txt
return getFile(data) //拿到了第一次的內容用來請求第二次
}).then(function(data){
console.log(data) // 3.txt
return getFile(data) //拿到了第二次的內容用來請求第三次
}).then(function(data){
console.log(data) // 完成
})
複製代碼
(這裏咱們先沒必要搞懂代碼,下面會介紹具體用法)
看起來多了幾行代碼[尷尬],但咱們經過建立一個讀取函數返回一個Promise對象,再利用Promise自帶的.then方法,將嵌套的異步代碼弄得看起來像同步同樣,這樣的話,出現問題能夠輕易的調試和修改。
接下來是本文的重頭戲,根據PromiseA+(Promise的官方標準)動手實現一個180行左右代碼的promise,功能可實現多數(then catch all race resolve reject),這裏會將的比較詳細,一步一步理清思路。
根據使用方法咱們能夠知道,Promise是一個須要接受一個執行器的構造函數,執行器提供兩個方法,內部有狀態機制,原型鏈上有then方法。
開始擼:
// myPromise
function Promise(executor){ //executor是一個執行器(函數)
let _this = this // 先緩存this以避免後面指針混亂
_this.status = 'pending' // 默認狀態爲等待態
_this.value = undefined // 成功時要傳遞給成功回調的數據,默認undefined
_this.reason = undefined // 失敗時要傳遞給失敗回調的緣由,默認undefined
function resolve(value) { // 內置一個resolve方法,接收成功狀態數據
// 上面說了,只有pending能夠轉爲其餘狀態,因此這裏要判斷一下
if (_this.status === 'pending') {
_this.status = 'resolved' // 當調用resolve時要將狀態改成成功態
_this.value = value // 保存成功時傳進來的數據
}
}
function reject(reason) { // 內置一個reject方法,失敗狀態時接收緣由
if (_this.status === 'pending') { // 和resolve同理
_this.status = 'rejected' // 轉爲失敗態
_this.reason = reason // 保存失敗緣由
}
}
executor(resolve, reject) // 執行執行器函數,並將兩個方法傳入
}
// then方法接收兩個參數,分別是成功和失敗的回調,這裏咱們命名爲onFulfilled和onRjected
Promise.prototype.then = function(onFulfilled, onRjected){
let _this = this; // 依然緩存this
if(_this.status === 'resolved'){ // 判斷當前Promise的狀態
onFulfilled(_this.value) // 若是是成功態,固然是要執行用戶傳遞的成功回調,並把數據傳進去
}
if(_this.status === 'rejected'){ // 同理
onRjected(_this.reason)
}
}
module.exports = Promise // 導出模塊,不然別的文件無法使用
複製代碼
注意:上面代碼的命名不是隨便起的,像onFulfilled和onRjected,是嚴格按照Promise/A+規範走的,不信你看圖
這樣咱們就實現了第一步,能夠建立Promise實例並使用then方法了,測試一下
let Promise = require('./myPromise') // 引入模塊
let p = new Promise(function(resolve, reject){
resolve('test')
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失敗', err)
})
// 成功 test
複製代碼
再試試reject
let Promise = require('./myPromise') // 引入模塊
let p = new Promise(function(resolve, reject){
reject('test')
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失敗', err)
})
// 失敗 test
複製代碼
看起來不錯,但回調函數是當即執行的,沒法進行異步操做,好比這樣是不行的
let p = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(100)
}, 1000)
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失敗', err)
})
// 不會輸出任何代碼
複製代碼
緣由是咱們在then函數中只對成功態和失敗態進行了判斷,而實例被new時,執行器中的代碼會當即執行,但setTimeout中的代碼將稍後執行,也就是說,then方法執行時,Promise的狀態沒有被改變依然是pending態,因此咱們要對pending態也作判斷,而因爲代碼多是異步的,那麼咱們就要想辦法把回調函數進行緩存,而且,then方法是能夠屢次使用的,因此要能存多個回調,那麼這裏咱們用一個數組。
在實例上掛兩個參數
_this.onResolvedCallbacks = []; // 存放then成功的回調
_this.onRejectedCallbacks = []; // 存放then失敗的回調
複製代碼
then方法加一個pending時的判斷
if(_this.status === 'pending'){
// 每一次then時,若是是等待態,就把回調函數push進數組中,何時改變狀態何時再執行
_this.onResolvedCallbacks.push(function(){ // 這裏用一個函數包起來,是爲了後面加入新的邏輯進去
onFulfilled(_this.value)
})
_this.onRejectedCallbacks.push(function(){ // 同理
onRjected(_this.reason)
})
}
複製代碼
下一步要分別在resolve和reject方法里加入執行數組中存放的函數的方法,修改一下上面的resolve和reject方法
function resolve(value) {
if (_this.status === 'pending') {
_this.status = 'resolved'
_this.value = value
_this.onResolvedCallbacks.forEach(function(fn){ // 當成功的函數被調用時,以前緩存的回調函數會被一一調用
fn()
})
}
}
function reject(reason) {
if (_this.status === 'pending') {
_this.status = 'rejected'
_this.reason = reason
_this.onRejectedCallbacks.forEach(function(fn){// 當失敗的函數被調用時,以前緩存的回調函數會被一一調用
fn()
})
}
}
複製代碼
如今能夠執行異步任務了,也能夠屢次then了,一個窮人版Promise就完成了,
上面的代碼雖然能用,但經不起考驗,真正的Promise若是在實例中拋出錯誤,應該走reject:
new Promise(function(resolve, reject){
throw new Error('錯誤')
}).then(function(){
},function(err){
console.log('錯誤:', err)
})
// 錯誤: Error: 錯誤
複製代碼
咱們實現一下,思路很簡單,在執行器執行時進行try catch
try{
executor(resolve, reject)
}catch(e){ // 若是捕獲發生異常,直接調失敗,並把參數穿進去
reject(e)
}
複製代碼
上面說過了,then能夠鏈式調用,也是這一點讓Promise十分好用,固然這部分源碼也比較複雜
咱們知道jquery實現鏈式調用是return了一個this,但Promise不行,爲何不行?
正宗的Promise是這樣的套路:
let p1 = new Promise(function(resolve, reject){
resolve()
})
let p2 = p1.then(function(data){ //這是p1的成功回調,此時p1是成功態
throw new Error('錯誤') // 若是這裏拋出錯誤,p2應是失敗態
})
p2.then(function(){
},function(err){
console.log(err)
})
// Error: 錯誤
複製代碼
若是返回的是this,那麼p2跟p1相同,固狀態也相同,但上面說了,Promise的成功態和失敗態不能相互轉換,那就不會獲得p1成功而p2失敗的效果,而其實是可能發生這種狀況的。
因此Promise的then方法實現鏈式調用的原理是:返回一個新的Promise
在then方法中先定義一個新的Promise,取名爲promise2(官方規定的),而後在三種狀態下分別用promise2包裝一下,在調用onFulfilled時用一個變量x(規定的)接收返回值,trycatch一下代碼,沒錯就調resolve傳入x,有錯就調reject傳入錯誤,最後再把promise2給return出去,就能夠進行鏈式調用了,,,,可是!
// 改動then
let promise2;
if (_this.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
// 能夠湊合用,可是是有不少問題的
try {
let x = onFulfilled(_this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
}
if (_this.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
// 能夠湊合用,可是是有不少問題的
try {
let x = onRjected(_this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
}
if(_this.status === 'pending'){
promise2 = new Promise(function (resolve, rejec
_this.onResolvedCallbacks.push(function(){
// 能夠湊合用,可是是有不少問題的
try {
let x = onFulfilled(_this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
_this.onRejectedCallbacks.push(function(){
// 能夠湊合用,可是是有不少問題的
try {
let x = onRjected(_this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
})
}
return promise2
複製代碼
這裏我先解釋一下x的做用再說爲何不行,x是用來接收上一次then的返回值,好比這樣
let p = new Promise(function(resolve, reject){
resolve(data)
})
p.then(function(data){
return xxx // 這裏返回一個值
}, function(){
}).then(function(data){
console.log // 這裏會接收到xxx
}, function(){
})
// 以上代碼中第一次then的返回值就是源碼內第一次調用onRjected的返回值,能夠用一個x來接收
複製代碼
接下來講問題,上面這樣看起來是符合邏輯的,而且也確實能夠鏈式調用並接受到,但咱們在寫庫,庫就要經得起考驗,把容錯性提到最高,要接受使用者各類新(cao)奇(dan)操做,所謂有容nai大。可能性以下:
一、前一次then返回一個普通值,字符串數組對象這些東西,都沒問題,只需傳給下一個then,剛纔的方法就夠用。
二、前一次then返回的是一個Promise,是正常的操做,也是Promise提供的語法糖,咱們要想辦法判斷到底返回的是啥。
三、前一次then返回的是一個Promise,其中有異步操做,也是理所固然的,那咱們就要等待他的狀態改變,再進行下面的處理。
四、前一次then返回的是本身自己這個Promise
var p1 = p.then(function(){// 這裏得用var,let因爲做用域的緣由會報錯undefined
return p1
})
複製代碼
五、前一次then返回的是一個別人本身隨便寫的Promise,這個Promise多是個有then的普通對象,好比{then:'哈哈哈'},也有可能在then裏故意拋錯(這種蛋疼的操做咱們也要考慮進去)。好比他這樣寫
let promise = {}
Object.defineProperty(promise,'then',{
value: function(){
throw new Error('報錯氣死你')
}
})
// 若是返回這東西,咱們再去調then方法就確定會報錯了
複製代碼
六、調resolve的時候再傳一個Promise下去,咱們還得處理這個Promise。
p.then(function(data) {
return new Promise(function(resolve, reject) {
resolve(new Promise(function(resolve,reject){
resolve(1111)
}))
})
})
複製代碼
七、可能既調resolve又調reject,得忽略後一個。
八、光then,裏面啥也不寫。
。。
稍等,我先吐一會。。。
好了我們調整心情繼續擼,其實這一系列的問題,不少都是相關的,只要根據規範,均可以順利解決,接上面的代碼,先幹三件事
一、問題7是最好解決的,若是沒傳resolve和reject,咱們就給他一個。
二、官方規範規定了一件事
簡單說就是爲免在測試中出問題onFulfilled和onRejected要異步執行,咱們就讓他異步執行
三、問題1-7,咱們能夠採起統一的以爲方案,定義一個函數來判斷和處理這一系列的狀況,官方給出了一個叫作resolvePromise的函數
再看then方法
Promise.prototype.then = function (onFulfilled, onRjected) {
//成功和失敗默認不傳給一個函數,解決了問題8
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
return value;
}
onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
throw err;
}
let _this = this;
let promise2; //返回的promise
if (_this.status === 'resolved') {
promise2 = new Promise(function (resolve, reject) {
// 當成功或者失敗執行時有異常那麼返回的promise應該處於失敗狀態
setTimeout(function () {// 根據規範讓那倆傢伙異步執行
try {
let x = onFulfilled(_this.value);//這裏解釋過了
// 寫一個方法統一處理問題1-7
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (_this.status === 'rejected') {
promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try {
let x = onRjected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
})
}
if (_this.status === 'pending') {
promise2 = new Promise(function (resolve, reject) {
_this.onResolvedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
})
});
_this.onRejectedCallbacks.push(function () {
setTimeout(function () {
try {
let x = onRjected(_this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
複製代碼
接下來看看resolvePromise該怎麼寫
function resolvePromise(promise2, x, resolve, reject) {
// 接受四個參數,新Promise、返回值,成功和失敗的回調
// 有可能這裏返回的x是別人的promise
// 儘量容許其餘亂寫
if (promise2 === x) { //這裏應該報一個類型錯誤,來解決問題4
return reject(new TypeError('循環引用了'))
}
// 看x是否是一個promise,promise應該是一個對象
let called; // 表示是否調用過成功或者失敗,用來解決問題7
//下面判斷上一次then返回的是普通值仍是函數,來解決問題一、2
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 多是promise {},看這個對象中是否有then方法,若是有then我就認爲他是promise了
try {
let then = x.then;// 保存一下x的then方法
if (typeof then === 'function') {
// 成功
//這裏的y也是官方規範,若是仍是promise,能夠當下一次的x使用
//用call方法修改指針爲x,不然this指向window
then.call(x, function (y) {
if (called) return //若是調用過就return掉
called = true
// y可能仍是一個promise,在去解析直到返回的是一個普通值
resolvePromise(promise2, y, resolve, reject)//遞歸調用,解決了問題6
}, function (err) { //失敗
if (called) return
called = true
reject(err);
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true;
reject(e);
}
} else { // 說明是一個普通值1
resolve(x); // 表示成功了
}
}
複製代碼
PromiseA+提供了測試庫promises-aplus-tests,github上明確講解了使用方法 公開一個適配器接口:
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise(function (resolve, reject) {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd
}
複製代碼
用命令行: promises-aplus-tests myPromise.js
通過一系列測試獲得結果
872 passing (18s)
複製代碼
證實了咱們的promise是徹底符合規範的!
除了最重要的then方法,Promise還有不少方法,但都不難,這裏一次性介紹一遍
// 捕獲錯誤的方法,在原型上有catch方法,返回一個沒有resolve的then結果便可
Promise.prototype.catch = function (callback) {
return this.then(null, callback)
}
// 解析所有方法,接收一個Promise數組promises,返回新的Promise,遍歷數組,都完成再resolve
Promise.all = function (promises) {
//promises是一個promise的數組
return new Promise(function (resolve, reject) {
let arr = []; //arr是最終返回值的結果
let i = 0; // 表示成功了多少次
function processData(index, y) {
arr[index] = y;
if (++i === promises.length) {
resolve(arr);
}
}
for (let i = 0; i < promises.length; i++) {
promises[i].then(function (y) {
processData(i, y)
}, reject)
}
})
}
// 只要有一個promise成功了 就算成功。若是第一個失敗了就失敗了
Promise.race = function (promises) {
return new Promise(function (resolve, reject) {
for (var i = 0; i < promises.length; i++) {
promises[i].then(resolve,reject)
}
})
}
// 生成一個成功的promise
Promise.resolve = function(value){
return new Promise(function(resolve,reject){
resolve(value);
})
}
// 生成一個失敗的promise
Promise.reject = function(reason){
return new Promise(function(resolve,reject){
reject(reason);
})
}
複製代碼
結語:Promise是異步的較好的解決方案之一,經過對源碼的解析,對Promise甚至js異步都有了深入的理解。Promise已經誕生好久了,若是你還不瞭解它,那你已經很落後了,抓緊時間上車。程序世界一日千里,做爲程序員,要主動擁抱變化。
歡迎加個人我的微信深刻交流