Javascript 異步編程

所謂"異步",簡單說就是一個任務(wù)分成兩段,先執(zhí)行第一段绿饵,然后轉(zhuǎn)而執(zhí)行其他任務(wù)娃闲,當(dāng)?shù)谝欢斡辛藞?zhí)行結(jié)果之后,再回過頭執(zhí)行第二段围来。JavaScript采用異步編程原因有兩點跺涤,一是JavaScript是單線程,二是為了提高CPU的利用率监透。在提高CPU的利用率的同時也提高了開發(fā)難度桶错,尤其是在代碼的可讀性上。

console.log(1);

setTimeout(function () {
  console.log(2);
});

console.log(3);
JavaScript異步執(zhí)行示意圖

callback

最開始我們在處理異步的時候胀蛮,采用的是callback回調(diào)函數(shù)的方式

asyncFunction(function(value){
    // todo
})

在一般簡單的情況下院刁,這種方式是完全夠用的,但是如果碰到稍微復(fù)雜的場景粪狼,就有些力不從心退腥,例如當(dāng)異步嵌套過多的時候。

回調(diào)金字塔

但是當(dāng)我們的異步操作比較多鸳玩,而且都依賴于上一步的異步的執(zhí)行結(jié)果阅虫,那么我們就會產(chǎn)生回調(diào)金字塔,難于閱讀

step1(function (value1) {
    step2(function(value2) {
        step3(function(value3) {
            step4(function(value4) {
                // Do something with value4
            });
        });
    });
});

當(dāng)然為了改進這種層層嵌套的寫法不跟,我們有幾種方式
1 命名函數(shù)

function fun1 (params) {
  // todo
  asyncFunction(fun2);
}

function fun2 (params) {
  // todo
  asyncFunction(fun3)
}

function fun3 (params) {
  // todo
  asyncFunction(fun4)
}

function fun4 (params) {
  // todo
}

asyncFunction(fun1)

2 基于事件消息機制的寫法

eventbus.on("init", function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});
 
eventbus.on("ACompleted", function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});
 
eventbus.on("BCompleted", function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});
 
eventbus.on("CCompleted", function(){
    // do something when all operation completed
});

當(dāng)然也可以利用模塊化來處理颓帝,使得代碼易于閱讀。以上這三種方式都只是在代碼的可讀性上面做了改進窝革,但是并沒有解決另外一個問題就是異常捕獲购城。

錯誤棧

function a () {
    b();
}

function b () {
    c();
}

function c () {
    d();
}

function d () {
    throw new Error('出錯啦');
}

a();
Node錯誤打印

從上面的圖我們可以看到有一個比較清晰的錯誤棧信息,a調(diào)用b - b調(diào)用c - c調(diào)用d 虐译,在d中拋出了一個異常瘪板。也就是說在JavaScript中在執(zhí)行一個函數(shù)的時候首先會壓入執(zhí)行棧中,執(zhí)行完畢后會移除執(zhí)行棧漆诽,F(xiàn)ILO的結(jié)構(gòu)侮攀。我們可以很方便的從錯誤信息中定位到出錯的地方。

function a() {
    b();
}

function b() {
    c(cb);
}

function c(callback) {
    setTimeout(callback, 0)
}

function cb() {
    throw new Error('出錯啦');
}

a();
包含異步的錯誤棧

從上圖我們可以看到只打印出了是在一個setTimeout中的回調(diào)函數(shù)中出現(xiàn)了異常厢拭,執(zhí)行順序是跟蹤不到的兰英。

異常捕獲

回調(diào)函數(shù)中的異常是不能夠捕捉到的,因為是異步的供鸠,我們只能在回調(diào)函數(shù)中使用try catch捕獲畦贸,也就是我注釋的部分。

function a() {
    setTimeout(function () {
        // try{
            throw new Error('出錯啦');
        // } catch (e) {
        
        // }
        
    }, 0);
}

try {
    a();
} catch (e) {
    console.log('捕捉到異常啦,好高興哦');
}

但是try catch只能捕捉到同步的錯誤薄坏,不過在回調(diào)中也有一些比較好的錯誤處理模式趋厉,例如error-first的代碼風(fēng)格約定,這種風(fēng)格在node.js中廣泛被使用 胶坠。

function foo(cb) {
  setTimeout(() => {
    try {
      func();
      cb(null, params);
    } catch (error) {
      cb(error);
    }
    
  }, 0);
}

foo(function(error, value){
    if(error){
        // todo
    }
    // todo
});

但是這么做也很容易陷入惡魔金字塔中君账。

Promise

規(guī)范簡述

  • promise 是一個擁有 then 方法的對象或函數(shù)。
  • 一個promise有三種狀態(tài) pending, rejected, resolved 狀態(tài)一旦確定就不能改變涵但,且只能夠由pending狀態(tài)變成rejected或者resolved狀態(tài)杈绸,reject和resolved狀態(tài)不能相互轉(zhuǎn)換。
  • 當(dāng)promise執(zhí)行成功時矮瘟,調(diào)用then方法的第一個回調(diào)函數(shù)瞳脓,失敗時調(diào)用第二個回調(diào)函數(shù)。
  • promise實例會有一個then方法澈侠,這個then方法必須返回一個新的promise劫侧。

規(guī)范更多細節(jié)請看這里

基本用法

// 異步操作放在Promise構(gòu)造器中
const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

// 得到異步結(jié)果之后的操作
promise1.then(value => {
  console.log(value, 'world');
}, error =>{
  console.log(error, 'unhappy')
});

異步代碼,同步寫法

asyncFun()
    .then(cb)
    .then(cb)
    .then(cb)

promise以這種鏈?zhǔn)綄懛ㄉ诳校鉀Q了回調(diào)函數(shù)處理多重異步嵌套帶來的回調(diào)地獄問題烧栋,使代碼更加利于閱讀,當(dāng)然本質(zhì)還是使用回調(diào)函數(shù)拳球。

異常捕獲

前面說過如果在異步的callback函數(shù)中也有一個異常审姓,那么是捕獲不到的,原因就是回調(diào)函數(shù)是異步執(zhí)行的祝峻。我們看看promise是怎么解決這個問題的魔吐。

asyncFun(1).then(function (value) {
    throw new Error('出錯啦');
}, function (value) {
    console.error(value);
}).then(function (value) {

}, function (result) {
  console.log('有錯誤', result);
});

其實是promise的then方法中,已經(jīng)自動幫我們try catch了這個回調(diào)函數(shù)莱找,實現(xiàn)大致如下酬姆。

Promise.prototype.then = function(cb) {
    try {
        cb()
    } catch (e) {
       // todo
       reject(e)
    }
}

then方法中拋出的異常會被下一個級聯(lián)的then方法的第二個參數(shù)捕獲到(前提是有),那么如果最后一個then中也有異常怎么辦奥溺。

Promise.prototype.done = function (resolve, reject) {
    this.then(resolve, reject).catch(function (reason) {
        setTimeout(() => {
           throw reason;
        }, 0);
    });
};
 asyncFun(1).then(function (value) {
     throw new Error('then resolve回調(diào)出錯啦');
 }).catch(function (error) {
     console.error(error);
     throw new Error('catch回調(diào)出錯啦');
 }).done((reslove, reject) => {});

我們可以加一個done方法辞色,這個方法并不會返回promise對象,所以在此之后并不能級聯(lián)浮定,done方法最后會把異常拋到全局相满,這樣就可以被全局的異常處理函數(shù)捕獲或者中斷線程。這也是promise的一種最佳實踐策略桦卒,當(dāng)然這個done方法并沒有被ES6實現(xiàn)立美,所以我們在不適用第三方Promise開源庫的情況下就只能自己來實現(xiàn)了。為什么需要這個done方法闸盔。

const asyncFun = function (value) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(value);
    }, 0);
  })
};


asyncFun(1).then(function (value) {
  throw new Error('then resolve回調(diào)出錯啦');
});

(node:6312) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: then resolve回調(diào)出錯啦
(node:6312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code
我們可以看到JavaScript線程只是報了一個警告,并沒有中止線程琳省,如果是一個嚴(yán)重錯誤如果不及時中止線程迎吵,可能會造成損失躲撰。

局限

promise有一個局限就是不能夠中止promise鏈,例如當(dāng)promise鏈中某一個環(huán)節(jié)出現(xiàn)錯誤之后击费,已經(jīng)沒有了繼續(xù)往下執(zhí)行的必要性拢蛋,但是promise并沒有提供原生的取消的方式,我們可以看到即使在前面已經(jīng)拋出異常蔫巩,但是promise鏈并不會停止谆棱。雖然我們可以利用返回一個處于pending狀態(tài)的promise來中止promise鏈。

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
});

promise1.then((value) => {
    throw new Error('出錯啦!');
}).then(value => {
    console.log(value);
}, error=> {
    console.log(error.message);
    return result;
}).then(function () {
    console.log('DJL簫氏');
});

特殊場景

  • 當(dāng)我們的一個任務(wù)依賴于多個異步任務(wù)圆仔,那么我們可以使用Promise.all
  • 當(dāng)我們的任務(wù)依賴于多個異步任務(wù)中的任意一個垃瞧,至于是誰無所謂,Promise.race

上面所說的都是ES6的promise實現(xiàn)坪郭,實際上功能是比較少个从,而且還有一些不足的,所以還有很多開源promise的實現(xiàn)庫歪沃,像q.js等等嗦锐,它們提供了更多的語法糖,也有了更多的適應(yīng)場景沪曙。

核心代碼

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

當(dāng)調(diào)用then的時候奕污,把所有的回調(diào)函數(shù)存在一個隊列中,當(dāng)調(diào)用resolve方法后液走,依次將隊列中的回調(diào)函數(shù)取出來執(zhí)行

var ref = function (value) {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};

這一段代碼實現(xiàn)的級聯(lián)的功能碳默,采用了遞歸。如果傳遞的是一個promise那么就會直接返回這個promise育灸,但是如果傳遞的是一個值腻窒,那么會將這個值包裝成一個promise。

generator

基本用法

function * gen (x) {
    const y = yield x + 2;
    // console.log(y);  // 猜猜會打印出什么值
}

const g = gen(1);
console.log('first', g.next());  //first { value: 3, done: false }
console.log('second', g.next()); // second { value: undefined, done: true }

通俗的理解一下就是yield關(guān)鍵字會交出函數(shù)的執(zhí)行權(quán)磅崭,next方法會交回執(zhí)行權(quán)儿子,yield會把generator中yield后面的執(zhí)行結(jié)果,帶到函數(shù)外面砸喻,而next方法會把外面的數(shù)據(jù)返回給generator中yield左邊的變量柔逼。這樣就實現(xiàn)了數(shù)據(jù)的雙向流動。

generator實現(xiàn)異步編程

我們來看generator如何是如何來實現(xiàn)一個異步編程(*)

const fs = require('fs');

function * gen() {
    try {
        const file = yield fs.readFile;
        console.log(file.toString());
    } catch(e) {
        console.log('捕獲到異常', e);
    }
}

// 執(zhí)行器
const g = gen();

g.next().value('./config1.json', function (error, value) {
  if (error) {
    g.throw('文件不存在');
  }
  g.next(value);
});

那么我們next中的參數(shù)就會是上一個yield函數(shù)的返回結(jié)果割岛,可以看到在generator函數(shù)中的代碼感覺是同步的愉适,但是要想執(zhí)行這個看似同步的代碼,過程卻很復(fù)雜癣漆,也就是流程管理很復(fù)雜维咸。那么我們可以借用TJ大神寫的co。

generator 配合 co

下面來看看如何使用:

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
const co = require('co');

function * gen(path) {
    try {
        const file = yield readFile('./basic.use1.js');
        console.log(file.toString());
    } catch(e) {
        console.log('出錯啦');
    }
}

co(gen());

我們看到使用co這個執(zhí)行器配合generator和promise會非常方便,非常類似同步寫法癌蓖,而且異步中的錯誤也能很容易被try catch到瞬哼。這里之所以要使用utils.promisify這個工具函數(shù)將普通的異步函數(shù)轉(zhuǎn)換成一個promise,是因為co may only yield a chunk, promise, generator, array, or object租副。使用co 配合generator最大的一個好處就是錯誤可以try catch 到坐慰。

async/await

先來看一段async/await的異步寫法

const fs = require('fs');
const utils = require('util');
const readFile = utils.promisify(fs.readFile);
async function readJsonFile() {
    try {
        const file = await readFile('../generator/config.json');
        console.log(file.toString());
    } catch (e) {
        console.log('出錯啦');
    }

}

readJsonFile();

我們可以看到async/await的寫法十分類似于generator,實際上async/await就是generator的一個語法糖用僧,只不過內(nèi)置了一個執(zhí)行器结胀。并且當(dāng)在執(zhí)行過程中出現(xiàn)異常,就會停止繼續(xù)執(zhí)行责循。當(dāng)然await后面必須接一個promise糟港,而且node版本必須要>=7.6.0才可以使用,當(dāng)然低版本也可以采用babel沼死。

補充

在開發(fā)過程中我們常常手頭會同時有幾個項目着逐,那么node的版本要求很有可能是不同的,那么我們就需要安裝不同版本的node意蛀,并且管理這些不同的版本耸别,這里推薦使用nvm下載好nvm县钥,安裝秀姐,使用nvm list 查看node版本列表。使用nvm use 版本號 進行版本切換若贮。

在Node.js中捕獲漏網(wǎng)之魚

process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})

在瀏覽器環(huán)境中捕獲漏網(wǎng)之魚

window.addEventListener('onrejectionhandled', (event: any) => {
    console.error('onrejectionhandled', event)
})

參考文章

Promise中文迷你書
剖析Promise內(nèi)部結(jié)構(gòu)省有,一步一步實現(xiàn)一個完整的、能通過所有Test case的Promise類
深入理解Promise實現(xiàn)細節(jié)
DJL簫氏的個人博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谴麦,一起剝皮案震驚了整個濱河市蠢沿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匾效,老刑警劉巖舷蟀,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異面哼,居然都是意外死亡野宜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門魔策,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匈子,“玉大人,你說我怎么就攤上這事闯袒』⒍兀” “怎么了游岳?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長其徙。 經(jīng)常有香客問我吭历,道長,這世上最難降的妖魔是什么擂橘? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮摩骨,結(jié)果婚禮上通贞,老公的妹妹穿的比我還像新娘。我一直安慰自己恼五,他們只是感情好昌罩,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著灾馒,像睡著了一般茎用。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上睬罗,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天轨功,我揣著相機與錄音,去河邊找鬼容达。 笑死古涧,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的花盐。 我是一名探鬼主播羡滑,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼算芯!你這毒婦竟也來了柒昏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤熙揍,失蹤者是張志新(化名)和其女友劉穎职祷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诈嘿,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡堪旧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奖亚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淳梦。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖昔字,靈堂內(nèi)的尸體忽然破棺而出爆袍,到底是詐尸還是另有隱情首繁,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布陨囊,位于F島的核電站弦疮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜘醋。R本人自食惡果不足惜胁塞,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望压语。 院中可真熱鬧啸罢,春花似錦、人聲如沸胎食。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厕怜。三九已至衩匣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粥航,已是汗流浹背琅捏。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留递雀,地道東北人午绳。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像映之,于是被迫代替她去往敵國和親拦焚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容