JS 非同步發展流程 —— 非同步歷史

Desmonddai583發表於2018-03-13

非同步介紹

非同步簡單來說就是做一件事時,做到一半可能需要等待系統或服務處理之後才會得到響應和結果,此時可以轉去做另一件事,等到獲得響應之後在去執行剩下一半的事情。反之同步就是一直等到響應然後接著做事,中間不會跳去做別的事。

非同步發展史

非同步發展史可以簡單歸納為: callback -> promise -> generator + co -> async+await(語法糖)

1. Callback

callback也就是我們經常聽到的回撥函式,它會在我們非同步任務執行並收到結果響應後出發,舉個簡單的例子:

let fs = require('fs');
fs.readFile('./2.promise/1.txt', 'utf8', function(err, data) {
    fs.readFile(data, 'utf8', function(err,data) {
        console.log(data);
    });
});
複製程式碼

(這裡有個點就是非同步並不支援try/catch,只有同步方可)

2. Promise

callback雖然幫我們解決了非同步問題,但是它仍有一些不足,首先,試想如果上面的巢狀一多,在程式碼上看起來就會很亂,之後回來修改邏輯時就會很難入手,一旦修改了一個,它巢狀的回撥函式也要跟著改,第二個問題就是無法合併兩個或多個非同步的結果,譬如以下例子:

fs.readFile('./2.promise/1.txt', 'utf8', function(err,data) { 
});
fs.readFile('./2.promise/2.txt', 'utf8', function(err, data) { 
    console.log(data);
});
複製程式碼

Promise的引入就解決了以上這些問題,首先來看下Promise的簡單用法:

let p = new Promise(function(resolve, reject) {
    resolve(100);
});

p.then(function(data) {
    console.log('data', data);
}, function(err) {
    console.log('err', err);
});
複製程式碼

可以看到Promis通過then的鏈式呼叫解決了巢狀回撥的問題,在用法上Promise的建構函式會接受一個executor函式,這個函式帶有兩個引數resolve和reject,兩個引數背後其實就是兩個函式,而通過Promise建構函式建立出來的物件會儲存一個status屬性,resolve會做的事就是將這個屬性從初始化的pending轉為resolved,而reject則是轉為rejected,同時兩個函式都可以接受一個引數,作為之後then中回撥函式的引數傳入,那麼在then方法中我們可以看到它接收兩個引數,第一個就是成功resolved之後會呼叫的回撥函式,第二個就是rejected的回撥函式。

這裡注意的是,只要狀態轉為resolved或rejected之中的其中一個,那麼當前promise物件就不能再轉變狀態了。之後不管調resolve還是reject都會被忽略。

另外,上面所說Promise是可以支援鏈式呼叫的,所以then是可以多次呼叫的,但是因為剛剛所說狀態不可轉變的問題,所以鏈式呼叫每次then返回的不是當前的Promise物件而是一個新的Promise物件,那麼第2次then的狀態又是怎麼決定的呢,第一次then中無論是成功的回撥還是失敗的回撥只要返回了結果就會走下一個then中的成功,如果有錯誤走下一個then的失敗。

接下來介紹一些其他Promise的使用方法:

- Promise.all

const p = Promise.all([p1, p2, p3]);
複製程式碼

上面程式碼中,Promise.all方法接受一個陣列作為引數,p1、p2、p3都是Promise例項,如果不是,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理(Promise.all方法的引數可以不是陣列,但必須具有Iterator介面,且返回的每個成員都是 Promise例項)

p的狀態由p1、p2、p3決定,分成兩種情況.

(1)只有p1、p2、p3的狀態都變成fullfilled,p的狀態才會變成fullfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式.

(2)只要p1、p2、p3之中有一個被reject,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。

- Promise.race

const p = Promise.race([p1, p2, p3]);
複製程式碼

上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。

Promise.race方法的引數與Promise.all方法一樣,如果不是Promise例項,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise例項,再進一步處理。

- Promise.resolve

將現有物件轉為Promise物件,該例項的狀態為resolved.

- Promise.reject

將現有物件轉為Promise物件,該例項的狀態為rejected.

- Promise.prototype.catch

catch方法其實就是.then(null, rejection)的別名

關於Promise的實現規範有興趣的可以去看一下,接下來介紹一些現有的封裝了Promise的庫,在本文最後我們也會一起來完成一個Promise庫的實現:

1. Q庫

可以通過npm install q安裝使用

Q.fcall(function() {
    return 100;
}).then(function(data) {
    console.log(data);
})
複製程式碼

這裡的fcall其實就類似於Promis.resolve方法

function read(url) {
    return new Promise(function(resolve, reject) {
        require('fs').readFile(url, 'utf8', function(err, data) {
            if (err) reject(err);
            resolve(data);
        });
    });
}

let Q = require('q');
Q.all([read('./2.promise/1.txt'), read('./2.promise/2.txt')]).then(function ([a1, a2]) {
    console.log(a1, a2);
});
複製程式碼

同樣它也有類似Promise.all這樣的方法

let Q = require('q');
function read(url) {
    let defer = Q.defer();
    require('fs').readFile(url, 'utf8', function(err, data) {
        if (err) defer.reject(err);
        defer.resolve(data);
    })
    return defer.promise;
}
read('./2.promise/1.txt').then(function(data) {
    console.log(data);
});
複製程式碼

同樣也可以使用defer這樣一個語法糖(關於defer會在之後的Promise實現中解釋)

2. Bluebird庫

可以通過npm install bluebird安裝

它有兩個比較亮點的方法,promisify和promisifyAll

let fs = require('fs');
let bluebird = require('bluebird');

let read = bluebird.promisify(fs.readFile);
bluebird.promisifyAll(fs);
fs.readFileAsync('./2.promise/1.txt', 'utf8').then(function(data) {
    console.log(data);
});
複製程式碼

所做的就是將傳入的方法全部轉成返回是Promise物件的新函式

背後實現原理其實也很簡單

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])
        }
    })
}
複製程式碼

3. Generator

關於generato的使用和原理這裡就不再贅述,大家可以去參考這裡,這裡就給個簡單的例子:

// genrator函式要用* 來比標識,yield(暫停 產出)
// 它會將函式分割出好多個部分,呼叫一次next就會繼續向下執行
// 返回結果是一個迭代器 迭代器有一個next方法
// yield後面跟著的是value的值
// yield等號前面的是我們當前呼叫next傳進來的值
// 第一次next傳值是無效的
function* read() {
    console.log(1);
    let a = yield 'zf';
    console.log(a);
    let b = yield 9;
    console.log(b);
    return b;
}

let it = read();
console.log(it.next('213')); // {value:'zf',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}
複製程式碼

generator與Promise的搭配使用例子如下

let bluebird = require('bluebird');
let fs = require('fs');

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

function* r() {
    let content1 = yield read('./2.promise/1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
let it = r();
it.next().value.then(function(data) { // 2.txt
    it.next(data).value.then(function(data) {
        console.log(it.next(data).value);
    });
})
複製程式碼

當然我們可以看到呼叫時還是得這樣不停的巢狀去獲取值,所以這裡需要引入另一個叫co的庫,那麼使用方法就可以變成如下:

let co = require('co');
let bluebird = require('bluebird');
let fs = require('fs');

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

function* r() {
    let content1 = yield read('./2.promise/1.txt', 'utf8');
    let content2 = yield read(content1, 'utf8');
    return content2;
}
co(r()).then(function(data) {
    console.log(data)
})
複製程式碼

co背後實現的原理其實也不復雜:

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();
    });
}
複製程式碼

4. async+await

async+await就是目前為至,非同步的最佳解決方案,它同時解決了

  1. 回撥地獄
  2. 併發執行非同步,在同一時刻同步返回結果 Promise.all
  3. 返回值的問題
  4. 可以實現程式碼的try/catch;

示例程式碼:

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

// 用async來修飾函式,aysnc需要配await, await只能接promise
// async和await(語法糖) === co + generator
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)
    }
}

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

相關文章