1. 程式人生 > >js非同步發展歷史與Promise原理分析

js非同步發展歷史與Promise原理分析

關於非同步

所謂 “非同步”,簡單說就是一個任務不是連續完成的,可以理解成該任務被人為分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。

比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。

相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。

簡單的說同步就是大家排隊工作,非同步就是大家同時工作。如果你還不太明白非同步與同步,多看看JS基礎的文章。

非同步的發展歷史

1.CallBack寫法

回撥意為“回撥函式”,即非同步操作執行完後觸發執行的函式,例如:

$.get("http://api.xxxx.com/xxx",callback);

當請求完成時就會觸發回撥函式。

回撥可以完成非同步操作,但是經歷過JQuery的時代的人應該都對某一種需求折磨過,舉個例子:專案要求前端AJAX請求後端介面列表型別名稱,然後在用型別名稱AJAX請求列表ID,在用ID請求列表具體內容,最後程式碼大概是這樣的

$.ajax({
    url: "type",
    data:1,
    success: function (a) {
        $.ajax({
            url: "list"
, data:a, success: function (b) { $.ajax({ url: "content", data:b, success: function (c) { console.log(c) } }) } }) } })

這是是單純的巢狀程式碼,如若再加上業務程式碼,程式碼可讀性可想而知,如果是開發起來還好,但是後期的維護和修改的難度足以讓人瘋掉。這就是那個JQuery的時代的“回撥地獄”問題。

2.Promise

為了解決“回撥地獄”問題,提出了無極物件,並且後來加入了ES6標準,無極物件簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise是一個物件,從它可以獲取非同步操作的訊息。提供統一的API,各種非同步操作都可以用同樣的方法進行處理。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve和reject。它們是兩個函式,由JavaScript引擎提供,不用自己部署。

解決函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從待定變為已解決),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從待決變為被拒絕),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

無極例項生成以後,可以用那麼方法分別指定解決狀態和拒收狀態的回撥函式。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

那麼方法可以接受兩個回撥函式作為引數。第一個回撥函式是無極物件的狀態變為解決時呼叫,第二個回撥函式是無極物件的狀態變為拒絕時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受無極物件傳出的值作為引數。

這樣採用Promise,解決“回撥地獄”問題,比如連續讀取多個檔案,寫法如下。

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

可見這種寫法要比回撥寫法直觀的多。但是,有沒有更好的寫法呢?

3.Generator函式

Genrator函式要用*來比標識,yield關鍵字表示暫停。將函式分割出好多個部分,呼叫一次next就會繼續向下執行。返回結果是一個迭代器,迭代器有一個下一個方法。

function* read() {
    console.log(1);
    let a = yield '123';
    console.log(a);
    let b = yield 9
    console.log(b);
    return b;
}
let it = read();
console.log(it.next('213')); // {value:'123',done:false}
console.log(it.next('100')); // {value:9,done:false}
console.log(it.next('200')); // {value:200,done:true}
console.log(it.next('200')); // {value:200,done:true}

收率後面跟著的是值的值,產率等號前面的是我們當前呼叫下一個傳進來的值,並且第一次下傳值是無效的。

處理非同步的時候發生器和無極搭配使用

let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);//將readFile轉為Promise物件的例項
function* r() {
    let content1 = yield read('./2.promise/1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}

這樣看起來是我們想要的樣子,但是隻寫成這樣還不行,想得到R()的結果還要對函式進行處理

function co(it) {
    return new Promise(function (resolve, reject) {
        function next(d) {
            let { value, done } = it.next(d);
            if (!done) {
                value.then(function (data) { // 2,txt
                    next(data)
                }, reject)
            } else {
                resolve(value);
            }
        }
        next();
    });
}
co(r()).then(function (data) {
    console.log(data)//得到r()的執行結果
})

這樣的處理方式顯然很麻煩,並不是我們想要,我們想要直觀的寫起來就就像同步函式,而且簡便的方式處理非同步。有這樣的方法嗎?

4.async,等待函式

ES2017標準引入了非同步函式,使得非同步操作變得更加方便。

async函式是什麼?一句話,它就是Generator函式的語法糖。

let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);

async function r(){
    try{
        let content1 = await read('./2.promise/100.txt','utf8');
        let content2 = await read(content1,'utf8');
        return content2;
    }catch(e){ // 如果出錯會catch
        console.log('err',e)
    }
}

一比較就會發現,非同步函式就是將發生函式的星號(*)替換成非同步,將產生替換成await,僅此而已。

非同步函式返回的是承諾

r().then(function(data){
    console.log(data);
},function(err){

})

至此,非同步的await函式已經可以我們滿意,以後會不會出現更優秀的方案?以我們廣大程式群體的創造力,相信一定會有的。

無極原理分析

非同步-AWAIT函式其實只是發生器函式的語法糖,而發電機函式的實現方式也是要基於無極,所以我們隊無極的實現原理進行分析。

無極物件有以下幾種狀態:

  • pending:初始狀態,既不是fulfilled也不是拒絕。
  • 履行:成功的操作。
  • 被拒絕:失敗的操作。

在上面瞭解了承諾的基本用法後,我們先將承諾的框架搭起來

function Promise(executor) { // executor是一個執行函式
    let self = this;
    self.status = 'pending';
    self.value = undefined; // 預設成功的值
    self.reason = undefined; // 預設失敗的原因
    self.onResolvedCallbacks = []; // 存放then成功的回撥
    self.onRejectedCallbacks = []; // 存放then失敗的回撥
    function resolve(value) { // 成功狀態
        
    }
    function reject(reason) { // 失敗狀態
        
    }
    try {
        executor(resolve, reject)
    } catch (e) { // 捕獲的時候發生異常,就直接失敗了
        reject(e);
    }
}

Promise.prototype.then = function (onFulfilled, onRjected) {
//then方法
})

接下來當呼叫成功狀態的決心的時候,會改變狀態,執行回撥函式:

function resolve(value) { // 成功狀態
        if (self.status === 'pending') {
            self.status = 'resolved';
            self.value = value;
            self.onResolvedCallbacks.forEach(function (fn) {
                fn();
            });
        }
    }

拒絕函式同理

function reject(reason) { // 失敗狀態
        if (self.status === 'pending') {
            self.status = 'rejected';
            self.reason = reason;
            self.onRejectedCallbacks.forEach(function (fn) {
                fn();
            })
        }
    }

接下來我們完成然後函式

Promise.prototype.then = function (onFulfilled, onRjected) {
    let self = this;
    let promise2; //返回的promise
    if (self.status === 'resolved') {
        promise2 = new Promise(function (resolve, reject) {
            
        })
    }
    if (self.status === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            
        })
    }
    // 當呼叫then時可能沒成功 也沒失敗
    if (self.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {

        })
    }
    return promise2;
}

承諾允許鏈式呼叫,所以要返回一個新的承諾物件promise2

Promise.prototype.then = function (onFulfilled, onRjected) {
    //成功和失敗預設不穿給一個函式
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
        return value;
    }
    onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
        throw err;
    }
    let self = this;
    let promise2; //返回的promise
    if (self.status === 'resolved') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onFulfilled(self.value);
                    // x可能是別人promise,寫一個方法統一處理
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (self.status === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRjected(self.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    
                    reject(e);
                }
            })

        })
    }
    // 當呼叫then時可能沒成功 也沒失敗
    if (self.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {
            // 此時沒有resolve 也沒有reject
            self.onResolvedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e)
                    }
                })
            });
            self.onRejectedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onRjected(self.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                })
            });
        })
    }
    return promise2;
}

在promise2內部定義一個變數X為回撥函式的返回值,由於返回值可能會有多種可能的情況,所以我們定義一個resolvePromise函式統一處理

返回值可以分為

  • 諾言回報自己(報錯迴圈引用)
  • 返回承諾物件(根據承諾物件呼叫成功或失敗回撥函式)
  • 返回普通值(呼叫成功回撥函式傳入返回值)
  • 報錯(呼叫失敗回到傳入錯誤)
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) { 
        return reject(new TypeError('迴圈引用了'))
    }
    // 判定x是不是一個promise,promise應該是一個物件
    let called; // 表示是否呼叫過成功或者失敗
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        try { // {then:1}
            let then = x.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 {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e);
        }
    } else { // 說明是一個普通值
        resolve(x); // 呼叫成功回撥
    }
}

如果返回值為物件或函式,且有那麼方法,那我們就認為是一個承諾物件,去呼叫這個承諾進行遞迴,直到返回普通值呼叫成功回撥。

最後,再加上一個捕方法,很簡單

Promise.prototype.catch = function (callback) {
    return this.then(null, callback)
}

這些就是承諾的主要功能的原理,附上完整程式碼

function Promise(executor) { // executor是一個執行函式
    let self = this;
    self.status = 'pending';
    self.value = undefined; // 預設成功的值
    self.reason = undefined; // 預設失敗的原因
    self.onResolvedCallbacks = []; // 存放then成功的回撥
    self.onRejectedCallbacks = []; // 存放then失敗的回撥
    function resolve(value) { // 成功狀態
        if (self.status === 'pending') {
            self.status = 'resolved';
            self.value = value;
            self.onResolvedCallbacks.forEach(function (fn) {
                fn();
            });
        }
    }
    function reject(reason) { // 失敗狀態
        if (self.status === 'pending') {
            self.status = 'rejected';
            self.reason = reason;
            self.onRejectedCallbacks.forEach(function (fn) {
                fn();
            })
        }
    }
    try {
        executor(resolve, reject)
    } catch (e) { 
        reject(e);
    }
}
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) { 
        return reject(new TypeError('迴圈引用了'))
    }
    let called; 
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        try { 
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, function (y) {
                    if (called) return
                    called = true
                    resolvePromise(promise2, y, resolve, reject)
                }, function (err) { //失敗
                    if (called) return
                    called = true
                    reject(err);
                })
            } else {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e);
        }
    } else { 
        resolve(x); 
    }
}
Promise.prototype.then = function (onFulfilled, onRjected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
        return value;
    }
    onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
        throw err;
    }
    let self = this;
    let promise2; 
    if (self.status === 'resolved') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (self.status === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRjected(self.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    
                    reject(e);
                }
            })

        })
    }

    if (self.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {
            self.onResolvedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e)
                    }
                })
            });
            self.onRejectedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onRjected(self.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                })
            });
        })
    }
    return promise2;
}

Promise.prototype.catch = function (callback) {
    return this.then(null, callback)
}