幾種js異步編程方法

前言

JS需要異步處理的地方實在是比較多羹奉,比如定時器/ajax/io操作等等,在當今前端技術日新月異的情況下约计,異步編程成了核心技能之一诀拭,在這里我只是羅列一下幾種我用過的異步編程方式并稍加對比。本次編寫的代碼全部在node 7+版本中運行

同步和異步

首先我們要弄清同步和異步到底是個什么玩意兒煤蚌,其實我的理解就是他們對代碼的“執(zhí)行順序”控制程度不一樣耕挨。為什么這樣說呢?因為同步在一段代碼調用之后尉桩,是不管有沒有結果返回的筒占,立馬就執(zhí)行到下一步去了。而異步蜘犁,是會等待那個調用的赋铝,直到返回了結果再往下執(zhí)行。
舉個例子:假設有個搶紅包的調用,它是需要一段時間才能滿足搶紅包結束的

var result = function(){
    if(搶紅包結束) return 5
}
console.log(result())

如果是同步沽瘦,這段代碼就不管result的死活了直接往下走革骨,輸出undefined,如果寫成異步風格的代碼析恋,那就不一樣了良哲。

回調函數(shù)

在前端的遠古時代,回調是處理異步的不二選擇助隧,為什么筑凫,因為它的寫法簡單滑沧,沒有多余的api。就拿剛剛那個搶紅包的例子來說巍实,我用一個定時器替代它:

var result = function(){
    setTimeout(()=>{
        return 5;
    },1000)
}
console.log(result())

用回調函數(shù)處理怎么弄呢滓技?很簡單,讓result的參數(shù)為一個回調函數(shù)就可以了,于是代碼變成下面這樣

var result = function(callback){
    setTimeout(()=>{
        callback(5)
    },1000)
}
result(console.log)

現(xiàn)在我們用一個真實的io調用替代搶紅包棚潦,新建一個numbers.txt令漂,在里面寫若干個紅包金額,代碼如下:

const fs = require('fs');

const readFileAsArray = function (file, cb) {
    fs.readFile(file, (err, data) => {
        if (err) return cb(err);
        const lines = data.toString().trim().split('\n');
        cb(null, lines);
    })
}

readFileAsArray('./numbers.txt', (err, lines) => {
    if (err) throw err;
    const numbers = lines.map(Number);
    console.log(`分別搶到了${numbers}塊紅包`);
})

代碼輸出為:

>分別搶到了10,11,12,13,14,15塊紅包

從代碼中我們可以看到,定義了一個readFileAsArray函數(shù)丸边,傳兩個參:文件名和回調函數(shù)叠必,然后調用這個函數(shù),把回調函數(shù)寫入第二個參數(shù)里妹窖,就可以控制代碼執(zhí)行順序了纬朝。
不過,回調的缺點就是寫多了骄呼,層層嵌套共苛,又會造成回調地獄的坑爹情況,代碼變得難以維護和閱讀蜓萄。所以我們需要更好的解決辦法隅茎。

Promise

借用ydjs的一句話:Promise實現(xiàn)了控制反轉。什么意思呢绕德?原來這個順序的控制是在代碼那邊而不是程序員控制,現(xiàn)在有了Promise摊阀,控制權就由人來掌握了耻蛇,通過一系列Promise的方法如then/catch/all/race等控制異步流程。<a >Promise文檔</a>
還是剛剛那個搶紅包的例子胞此,這次用Promise來寫就是這樣的:

const fs = require('fs');

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\n');
            resolve(lines);
        })
    })
}

readFileAsArray('./numbers.txt').then(
    lines => {
        const numbers = lines.map(Number);
        console.log(`分別搶到了${numbers}塊紅包`);
    }
).catch(error => console.error(error));

結果和使用回調函數(shù)一樣臣咖,但是在這里已經把控制權交給了程序員,代碼也變得更好理解漱牵。雖然Promise有單值/不可取消等缺點夺蛇,不過在現(xiàn)在大部分的情況下實現(xiàn)異步還是夠用的。想深入了解的朋友可以去看看《你不知道的JS》中卷第三章酣胀。

await/async

Promise的api太多了刁赦,有沒有簡化的辦法呢?答案是肯定有的闻镶,ES7推出了一個語法糖:await/async甚脉,它的內部封裝了Promise和Generator的組合使用方式,至于Generator是什么铆农,這里不再贅述牺氨,有興趣的朋友們可以去自行研究。
于是,剛剛那段代碼就變成了:

const fs = require('fs');

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\n');
            resolve(lines);
        })
    })
}

async function result() {
    try {
        const lines = await readFileAsArray('./numbers.txt');
        const numbers = lines.map(Number);
        console.log(`分別搶到了${numbers}塊紅包`);
    } catch (err) {
        console.log("await出錯猴凹!");        
        console.log(err);
    }
}

result();

這樣做的結果是不是讓代碼可讀性更高了夷狰!而且也屏蔽了Promise和Generator的細節(jié)。

event

另一個實現(xiàn)異步的方式是event郊霎,回調(promise沼头、await/async)和event的關系就像計劃經濟和市場經濟一樣,一個是人為的強制性的控制歹篓,一個是根據需求和供給這只看不見的手控制瘫证。
還是同一個例子,用event寫就是這樣:

const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
    executeAsy(asyncFunc, args) {
        this.emit("開始");
        console.time('執(zhí)行耗時');
        asyncFunc(args, (err, data) => {
            if (err) return this.emit('error', err);
            this.emit('data', data);
            console.timeEnd('執(zhí)行耗時');
            this.emit("結束");
        });
    }
}

const myEventEmitter = new MyEventEmitter();

myEventEmitter.on('開始', () => {
    console.log('開始執(zhí)行了');
})
myEventEmitter.on('data', (data) => {
    console.log(`分別搶到了${data}塊紅包`);
})
myEventEmitter.on('結束', () => {
    console.log('結束執(zhí)行了');
})
myEventEmitter.on('error', (err) => {
    console.error(err);
})

myEventEmitter.executeAsy(fs.readFile, './numbers.txt');

這種事件驅動非常靈活庄撮,也不刻意去控制代碼的順序背捌,一旦有事件的供給(emit),它就會立刻消費事件(on)洞斯,不過正是因為這樣毡庆,它的缺點也很明顯:讓程序的執(zhí)行流程很不清晰。

event+promise+await/async

純粹的計劃經濟也不好烙如,純粹的市場經濟也不好么抗。好的方式是什么?當然是結合起來啦亚铁!
所以就有了結合event和promise的寫法:

const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
    async executeAsy(asyncFunc, args) {
        this.emit("開始");
        try {
            console.time('執(zhí)行耗時');
            const data = await asyncFunc(args);
            this.emit('data', data);
            console.timeEnd('執(zhí)行耗時');
            this.emit('結束');
        } catch (err) {
            console.log("出錯了!");
            this.emit('error', err);
        }

    }
}

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\r\n');
            resolve(lines);
        })
    })
}
const myEventEmitter = new MyEventEmitter();

myEventEmitter.on('開始', () => {
    console.log('開始執(zhí)行了');
})
myEventEmitter.on('data', (data) => {
    console.log(`分別搶到了${data}塊紅包`);
})
myEventEmitter.on('結束', () => {
    console.log('結束執(zhí)行了');
})
myEventEmitter.on('error', (err) => {
    console.error(err);
})

myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');

這種結合的方式基本上可以應付現(xiàn)今的異步場景了蝇刀,缺點嘛。徘溢。吞琐。就是代碼量比較多

rxjs

js越發(fā)壯大,jser們終于站起來了然爆,看著其他語言使用著rx這個強大的工具站粟,我們怎么能少,一種大一統(tǒng)管理異步的方案:rxjs就這樣來到了世上曾雕。
簡單介紹下rxjs和異步的關系:它可以把數(shù)據轉化成一股流奴烙,無論這個數(shù)據是同步得到的還是異步得到的,是單值還是多值剖张。
比如用Rx.Observable.of來包裝單值同步數(shù)據切诀,
用Rx.Observable.of來包裝單值同步數(shù)據,
用Rx.Observable.fromPromise來包裝單值異步數(shù)據搔弄,
以及用Rx.Observable.fromEvent來包裝多值異步數(shù)據:

const fs = require('fs');
const Rx = require('rxjs');
const EventEmitter = require('events');

class MyEventEmitter extends EventEmitter {
    async executeAsy(asyncFunc, args) {
        this.emit("開始");
        try {
            console.time('執(zhí)行耗時');
            const data = await asyncFunc(args);
            this.emit('data', data);
            console.timeEnd('執(zhí)行耗時');
            this.emit('結束');
        } catch (err) {
            console.log("出錯了!");
            this.emit('error', err);
        }

    }
}

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\r\n');
            resolve(lines);
        })
    })
}
const myEventEmitter = new MyEventEmitter();

myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');

let dataObservable = Rx.Observable.fromEvent(myEventEmitter, 'data')

let subscription = dataObservable.subscribe((data) => {
    console.log(`分別搶到了${data}塊紅包`);
}, err => {
    console.error(err);
}, compelete => {
    console.info("compelete!");
})

rxjs還有很多重要的概念趾牧,比如生產者Observe和消費者Observable、推拉模型肯污、各種方便的操作符和函數(shù)式編程等等

關于異步的未來展望

ES8已經著手Observable和Observe的實現(xiàn)了翘单,node也在著手異步生命周期鉤子Async Hooks來方便程序們來調試異步程序吨枉,我相信,未來js的異步編程會變得越來越容易哄芜,功能也會越來越強大~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末貌亭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子认臊,更是在濱河造成了極大的恐慌圃庭,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件失晴,死亡現(xiàn)場離奇詭異剧腻,居然都是意外死亡,警方通過查閱死者的電腦和手機涂屁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門书在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拆又,你說我怎么就攤上這事儒旬。” “怎么了帖族?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵栈源,是天一觀的道長。 經常有香客問我竖般,道長甚垦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任涣雕,我火速辦了婚禮艰亮,結果婚禮上,老公的妹妹穿的比我還像新娘胞谭。我一直安慰自己垃杖,他們只是感情好男杈,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布丈屹。 她就那樣靜靜地躺著,像睡著了一般伶棒。 火紅的嫁衣襯著肌膚如雪旺垒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天肤无,我揣著相機與錄音先蒋,去河邊找鬼。 笑死宛渐,一個胖子當著我的面吹牛竞漾,可吹牛的內容都是我干的眯搭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼业岁,長吁一口氣:“原來是場噩夢啊……” “哼鳞仙!你這毒婦竟也來了?” 一聲冷哼從身側響起笔时,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤棍好,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后允耿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體借笙,經...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年较锡,在試婚紗的時候發(fā)現(xiàn)自己被綠了业稼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡念链,死狀恐怖盼忌,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情掂墓,我是刑警寧澤谦纱,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站君编,受9級特大地震影響跨嘉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜吃嘿,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一祠乃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兑燥,春花似錦亮瓷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挣饥,卻和暖如春除师,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扔枫。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工汛聚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人短荐。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓倚舀,卻偏偏與公主長得像叹哭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子痕貌,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容