Javascript Promise學習過程總結

GetFullStack發表於2018-03-16

Callback函式

JS早期實現非同步都是採用回撥方式callback來實現非同步任務的。包括事件回撥,setTimeout,setInterval, Ajax等,採取這樣的方式寫程式碼,功能實現沒有問題,但是不夠優雅清晰,容易出現回撥地獄。

Promise

Promise A plus 中文規範 英文規範

實現過程(一)

同步基礎框架

new Promise(function(resovle, reject){
    
})
複製程式碼

從上程式碼可以看出建構函式接受一個函式。

const PENDING = "pending"; //等待
const FULFILLED = "fulfilled"; //已完成
const REJECTED = "rejected"; // 已拒絕

function Promise(executor) {
    let self = this;
    self.status = PENDING;

    self.value;
    self.reason;


    function resolve(value) {
        if (self.status === PENDING) {
            self.status = FULFILLED;
            self.value = value;
        }
    }

    function reject(reason) {
        if (self.status === PENDING) {
            self.status = REJECTED;
            self.reason = reason;
        }
    }
    try { // 規範提到,執行器拋異常會reject
        executor(resolve, reject);
    } catch(e) {
        reject(e)
    }
}
複製程式碼

then方法實現

Promise.prototype.then = function (onFulfilled, onRjected) {
    let self = this;
    /**
     * onFulfilled 和 onRejected 都是可選引數。
     * 如果 onFulfilled 不是函式,其必須被忽略
     * 如果 onRejected 不是函式,其必須被忽略
     */
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) {
        return value;
    };
    onRjected = typeof onRjected === 'function' ? onRjected : function(reason) {
        throw reason;
    }
    
    if (self.status === FULFILLED) {
        onFulfilled(self.value);
    }
    if (self.status === REJECTED) {
        onRjected(self.reason);
    }
}
複製程式碼

實現過程(二)

非同步呼叫實現

promise例項可以多次then,當成功後會將then中的成功方法按順序執行,可以先將then中的成功的回撥和失敗的回撥存到陣列內,當成功時呼叫成功或失敗的陣列。

// 新增then的回撥佇列
self.onResolvedCallbacks = [];
self.onRejectedCallbacks = [];
複製程式碼
// 成功時候呼叫佇列裡的handler
self.onResolvedCallbacks.forEach(function (fn, index, array) {
    fn()
});
複製程式碼
// 失敗時候呼叫佇列裡的handler
self.onRejectedCallbacks.forEach(function (fn, index, array) {
    fn()
})
複製程式碼

實現過程(三)

當鏈式呼叫then()時候,jquery能實現鏈式呼叫靠的就是返回this,但是promise不能返回this,promise實現鏈式呼叫靠的是返回一個新的promise。 而且then()方法可以什麼都不穿,實現穿透呼叫下一個then()...

Promise.prototype.then = function (onFulfilled, onRjected) {
    let self = this;

    let promise2 = null; //返回的promise

    // 我們的程式碼可以在then中什麼都不傳 promise中值的穿透
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
        return value;
    };
    
    onRjected = typeof onRjected === 'function' ? onRjected : function (reason) {
        throw reason;
    }

    if (self.status === FULFILLED) {
        promise2 = new Promise(function (onFulfilled2, onRjected2) {
            let result = onFulfilled(self.value);
            onFulfilled2(result);
        });
    }
    
    if (self.status === REJECTED) {
        promise2 = new Promise(function (onFulfilled2, onRjected2) {
            let reason = onRjected(self.reason);
            onRjected2(reason);
        });

    }

    if (self.status === PENDING) {
        promise2 = new Promise(function (resolve, reject) {
            self.onResolvedCallbacks.push(function () {
                let value = onFulfilled(self.value);
                resolve(value);
            });

            self.onRejectedCallbacks.push(function () {
                let reason = onRjected(self.reason);
                reject(reason);
            })
        });
    }
    return promise2;
}
複製程式碼

實現過程(四)

如果then中無論是成功的回撥還是失敗的回撥,只要返回了結果(return xxx)就會走下一個then中的成功,如果有錯誤(throw new Error)就走下一個then的失敗。

如果第一個promise返回一個普通值,會進到下一次then的成功的回撥,如果第一個promise返回了一個promise,需要等待返回的promise執行後的結果傳遞給下一次then中。

function resolvePromise(promise2, x, resolve, reject) {
    // 有可能這裡返回的x是別人的promise
    // 儘可能允許其他亂寫
    // 返回的結果和promise是同一個那麼永遠不會成功和失敗
    if (promise2 === x) {
        throw new Error('不能反悔自己')
    }

    let called;
    // 判斷Promise型別
    if (typeof x !== 'object' && (typeof x === 'object' || typeof x === 'function')) {
        try {


            let then = x.then;
            // promise 可以是一個函式。函式上有一個then方法。
            if (typeof then === 'function') {
                then.call(x, function (y) {
                    if (called) return;
                    called = true;
                    // y可能還是一個promise,在去解析直到返回的是一個普通值
                    resolvePromise(promise2, y, resolve, reject);
                }, function (err) {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                // 不是函式,又是一個promise
                resolve(x)
            }


        } catch (err) {
            if (called) return
            called = true;
            reject(err);
        }

    } else {
        //一般型別返回
        //3.如果then中無論是成功的回撥還是失敗的回撥只要返回了結果就會走下一個then中的成功,
        resolve(x);
    }
}
複製程式碼
Promise.prototype.then = function (onFulfilled, onRjected) {
    let self = this;

    let promise2 = null;

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
        return value;
    };
    onRjected = typeof onRjected === 'function' ? onRjected : function (reason) {
        throw reason;
    }


    if (self.status === FULFILLED) {
        promise2 = new Promise(function (onFulfilled2, onRjected2) {
            setTimeout(function () { //9.promise規範中要求,所有的onFufiled和onRjected都需要非同步執行,setTimeout
                try {
                    let result = onFulfilled(self.value);
                    resolvePromise(promise2, result, onFulfilled, onRjected);
                } catch (err) {
                    onRjected2(err);//如果有錯誤走下一個then的失敗

                }
            });

        });
    }
    if (self.status === REJECTED) {
        promise2 = new Promise(function (onFulfilled2, onRjected2) {
            setTimeout(function () { //9.promise規範中要求,所有的onFufiled和onRjected都需要非同步執行,setTimeout
                try {
                    let reason = onRjected(self.reason);
                    resolvePromise(promise2, reason, onFulfilled, onRjected);
                } catch (err) {
                    onRjected2(err);//如果有錯誤走下一個then的失敗

                }
            });
        });

    }

    if (self.status === PENDING) {
        promise2 = new Promise(function (resolve, reject) {
            self.onResolvedCallbacks.push(function () {
                setTimeout(function () { //9.promise規範中要求,所有的onFufiled和onRjected都需要非同步執行,setTimeout
                    try {
                        let value = onFulfilled(self.value);
                        resolvePromise(promise2, value, resolve, reject);
                    } catch (err) {
                        reject(err); //如果有錯誤走下一個then的失敗
                    }
                });
            });

            self.onRejectedCallbacks.push(function () {
                setTimeout(function () { //9.promise規範中要求,所有的onFufiled和onRjected都需要非同步執行,setTimeout
                    try {
                        let reason = onRjected(self.reason);
                        resolvePromise(promise2, reason, resolve, reject);
                    } catch (err) {
                        reject(err);//如果有錯誤走下一個then的失敗

                    }
                });

            })
        });

    }
    return promise2;
}
複製程式碼

實現過程(五)

實現catch, all, race, resolve, reject, defer這些事例方法。

// 捕獲錯誤的方法
// 相當於不要then的第一個成功回撥
Promise.prototype.catch = function (callback) {
    return this.then(null, callback)
};

Promise.prototype.all = function (promises) {
    return new Promise(function (resolve, reject) {
        let returns = [];
        let index = 0;

        function processData(i, data) {
            returns[i] = data;
            if (++index === promises.length) {
                resolve(returns)
            }
        }

        for (let i = 0; i <= promises.length; i++) {
            promises[i].then(function (data) {
                processData(i, data)
            }, reject);
        }
    })

};

Promise.prototype.race = function (promises) {
    return new Promise(function (resolve, reject) {
        promises.forEach(function (value) {
            value.then(resolve, reject);
        })
    })
};

Promise.resolve = function (value) {
    return new Promise(function (resolve, reject) {
        resolve(value)
    });
};

Promise.reject = function (reason) {
    return new Promise(function (resolve, reject) {
        reject(reason)
    })
};

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    return new Promise(function (resolve, reject) {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
};
複製程式碼

Genernator

Genernator函式要用* 來比標識,yield(暫停 產出),它會將函式分割出好多個部分,呼叫一次next就會繼續向下執行,返回結果是一個迭代器,迭代器有一個next方法,yield後面跟著的是value的值,yield等號前面的是我們當前呼叫next傳進來的值,第一次next傳值是無效的。

function* read() {
    console.log('init');
    let a = yield '第一波湯圓';
    console.log(a);
    let b = yield '第二波湯圓'
    console.log(b);
    return b;
}
let it = read();
console.log(it.next('給我湯圓'));
// init
// {value:'第一波湯圓',done:false}
console.log(it.next('我還要湯圓')); 
// 我還要湯圓
// {value:'第二波湯圓',done:false}
console.log(it.next('夠了不要了'));
// 夠了不要了
// {value:‘第二波湯圓’,done:true}
console.log(it.next('還有麼?'));
// {value:undefined,done:true}
複製程式碼

可以看到,當呼叫read()時候,程式僅僅返回一個迭代器。函式內程式碼並沒有執行。

提一次呼叫next('給我湯圓'),後函式執行了,如下內容:

console.log('init');
yield '第一波湯圓';
// 所以答應出來
// init
// 並且返回{value:'第一波湯圓',done:false}
// 注意,第一次傳入的'給我湯圓' 函式並沒有接收處理。
複製程式碼

第二次呼叫console.log(it.next('我還要湯圓')),程式執行了如下內容:

let a = '我還要湯圓';
console.log(a);
yield '第二波湯圓'
// 我還要湯圓
// {value:'第二波湯圓',done:false}
// 將傳入參宿賦值給a,並列印出來,然後返回yield後的'第二波湯圓'
複製程式碼

第三次呼叫console.log(it.next('夠了不要了')),程式執行了如下內容:

let b = '夠了不要了'
console.log(b);
return b;
// 夠了不要了
// {value:‘夠了不要了’, done:true}
// 將傳入參宿賦值給b,並列印出來,然後返回b的'夠了不要了',這是迭代結束,返回的done就變為true。
複製程式碼

繼續呼叫console.log(it.next('還有麼?')),程式沒有迭代內容了,所以直接返回

// {value:undefined, done:true}
複製程式碼

Generator主要和Promise搭配使用

let bluebird = require('bluebird'); // bluebird見相關函式庫介紹
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);
function* gRead() {
    let content1 = yield read('./1.txt', 'utf8');
    // content1 存放的是content2的檔案地址
    let content2 = yield read(content1, 'utf8');
    return content2;
}
// 接下來我要一次一次的呼叫next才能得到我要的content2的內容,麻煩
複製程式碼

於是與co庫

// co庫 npm install co 可以自動的將generator進行迭代
let co = require('co');
co(gRead()).then(function (data) {
    console.log(data)
})
// 這樣就可以一次拿到結果
複製程式碼

可是co庫是在麼實現的呢?讓我們來模擬一個co庫

function co(it) {
    // 必須返回一個promise
    return new Promise(function (resolve, reject) {
        function next(d) { // 第二次遞迴進來就有值了。
            let { value, done } = it.next(d);
            // value 是一個promise,第一次呼叫傳遞值無用。
            if (!done) {
                // 如果沒有遍歷結束
                value.then(function (data) {
                // content2的promise執行,並傳入讀取結果。
                    next(data)
                }, reject)
            } else {
                resolve(value); 
            }
        }
        next();
    });
}
複製程式碼

可以看出co庫就是遞迴調動了迭代器裡的每一個next(),直到done===true。

async-await

async-await可以理解為co + generator的語法糖。用async 來修飾函式,aysnc需要配await,await只能promise。

async function aRead(){
    try{
        let content1 = await read('./1.txt','utf8');
        let content2 = await read(content1,'utf8');
        return content2;
    }catch(e){ // 如果出錯會catch
        console.log('err',e)
    }
}
複製程式碼

函式呼叫和promise一樣,如下:

// async函式返回的是promise,
aRead().then(function (data) {
    console.log('print', data);
}, function (err) {
    console.log('error', err);
});
複製程式碼

async/await解決了哪些問題呢?

  • 回撥地獄。
  • 併發執行非同步,在同一時刻同步返回結果 Promise.all。
  • 解決了返回值的問題,yield返回的要靠呼叫函式傳入。
  • 可以實現程式碼的try/catch。

相關函式庫

co

bluebird

/ blueBird
// npm install bluebird
let fs = require('fs');
let bluebird = require('bluebird');
function promisify(fn) { // promise化 將回撥函式在內部進行處理
    return function (...args) {
        return new Promise(function (resolve, reject) {
            fn(...args, function (err, data) {
                if (err) reject(err);
                resolve(data);
            })
        })
    }
}
function promisifyAll(obj) {
    Object.keys(obj).forEach(key => { // es5將物件轉化成陣列的方法
        if (typeof obj[key] === 'function') {
            obj[key + 'Async'] = promisify(obj[key])
        }
    })
}
promisifyAll(fs); // 將所有的方法全部增加一個promise化
fs.readFileAsync('./1.txt', 'utf8').then(function (data) {
    console.log(data);
});
複製程式碼

promises-aplus-tests

相關文章