JS中異步編程的方法有:
- 回調(diào)函數(shù)
- 事件監(jiān)聽
- 發(fā)布/訂閱
- promise
- generator(ES6)
- async/await(ES7)
回調(diào)函數(shù)
回調(diào)是異步編程中最基礎的方法。舉例一個簡單的回調(diào):在f1執(zhí)行完之后再執(zhí)行f2
var func1=function(callback){
console.log(1);
(callback && typeof(callback)==='function') && callback();
}
func1(func2);
var func2=function(){
console.log(2);
}
異步回調(diào)中最常見的形式可能就是Ajax了:
$.ajax({
url:"/getmsg",
type: 'GET',
dataType: 'json',
success: function(ret) {
if (ret && ret.status) {
//
}
},
error: function(xhr) {
//
}
})
事件監(jiān)聽
通過事件機制,實現(xiàn)代碼的解耦。js處理DOM交互就是采用的事件機制,我們這兒只是實現(xiàn)一些自定義的事件而已。JS中已經(jīng)很好的支持了自定義事件,如:
//新建一個事件
var event=new Event('Popup::Show');
//dispatch the event
elem1.dispatchEvent(event)
//listen for this event
elem2.addEventListener('Popup::Show',function(msg){},false)
發(fā)布-訂閱模式
在系統(tǒng)中存在一個"信號中心",當某個任務執(zhí)行完成后向信號中心"發(fā)布"(publish)一個信號,其他任務可以向信號中心"訂閱"(subscribe)這個信號,從而知道什么時候自己可以開始執(zhí)行。簡單實現(xiàn)如下:
//發(fā)布-訂閱
//有個消息池,存放所有消息
let pubsub = {};
(function(myObj) {
topics = {}
subId = -1;
//發(fā)布者接受參數(shù)(消息名稱,參數(shù))
myObj.publish = function(topic, msg) {
//如果發(fā)布的該消息沒有訂閱者,直接返回
if (!topics[topic]) {
return
}
//對該消息的所有訂閱者,遍歷去執(zhí)行各自的回調(diào)函數(shù)
let subs = topics[topic]
subs.forEach(function(sub) {
sub.func(topic, msg)
})
}
//訂閱者接受參數(shù):(消息名稱,回調(diào)函數(shù))
myObj.subscribe = function(topic, func) {
//如果訂閱的該事件還未定義,初始化
if (!topics[topic]) {
topics[topic] = []
}
//使用不同的token來作為訂閱者的索引
let token = (++subId).toString()
topics[topic].push({
token: token,
func: func
})
return token
}
myObj.unsubscribe = function(token) {
//對消息列表遍歷查找該token是哪個消息中的哪個訂閱者
for (let t in topics) {
//如果某個消息沒有訂閱者,直接返回
if (!topics[t]) {
return }
topics[t].forEach(function(sub,index) {
if (sub.token === token) {
//找到了,從訂閱者的數(shù)組中去掉該訂閱者
topics[t].splice(index, 1)
}
})
}
}
})(pubsub)
let sub1 = pubsub.subscribe('Msg::Name', function(topic, msg) {
console.log("event is :" + topic + "; data is :" + msg)
});
let sub2 = pubsub.subscribe('Msg::Name', function(topic, msg) {
console.log("this is another subscriber, data is :" + msg)
});
pubsub.publish('Msg::Name', '123')
pubsub.unsubscribe(sub2)
pubsub.publish('Msg::Name', '456')
其中存儲消息的結(jié)構(gòu)用json可以表示為:
topics = {
topic1: [{ token: 1, func: callback1 }, { token: 2, func: callback2 }],
topic2: [{ token: 3, func: callback3 }, { token: 4, func: callback4 }],
topic3: []
}
消息池的結(jié)構(gòu)是發(fā)布訂閱模式與事件監(jiān)聽模式的最大區(qū)別。當然,每個消息也可以看做是一個個的事件,topics對象就相當于一個事件處理中心,每個事件都有各自的訂閱者。所以事件監(jiān)聽其實就是發(fā)布訂閱模式的一個簡化版本。而發(fā)布訂閱模式的優(yōu)點就是我們可以查看消息中心的信息,了解有多少信號,每個信號有多少訂閱者。
再說一說觀察者模式
很多情況下,我們都將觀察者模式和發(fā)布-訂閱模式混為一談,因為都可用來進行異步通信,實現(xiàn)代碼的解耦,而不再細究其不同,但是內(nèi)部實現(xiàn)還是有很多不同的。
整體模型的不同:發(fā)布訂閱模式是靠信息池作為發(fā)布者和訂閱者的中轉(zhuǎn)站的,訂閱者訂閱的是信息池中的某個信息;而觀察者模式是直接將訂閱者訂閱到發(fā)布者內(nèi)部的,目標對象需要負責維護觀察者,也就是觀察者模式中訂閱者是依賴發(fā)布者的。
觸發(fā)回調(diào)的方式不同:發(fā)布-訂閱模式中,訂閱者通過監(jiān)聽特定消息來觸發(fā)回調(diào);而觀察者模式是發(fā)布者暴露一個接口(方法),當目標對象發(fā)生變化時調(diào)用此接口,以保持自身狀態(tài)的及時改變。
觀察者模式很好的應用是MVC架構(gòu),當數(shù)據(jù)模型更新時,視圖也發(fā)生變化。從數(shù)據(jù)模型中將視圖解耦出來,從而減少了依賴。但是當觀察者數(shù)量上升時,性能會有顯著下降。我們同樣可以自己實現(xiàn):
//觀察者模式
var Subject=function(){
this.observers=[];
}
Subject.prototype={
subscribe:function(observer){
this.observers.push(observer);
},
unsubscribe:function(observer){
var index=this.observers.indexOf(observer);
if (index>-1) {
this.observers.splice(index,1);
}
},
notify:function(observer,msg){
var index=this.observers.indexOf(observer);
if (index>-1) {
this.observers[index].notify(msg)
}
},
notifyAll:function(msg){
this.observers.forEach(function(observe,msg){
observe.notify(msg)
})
}
}
var Observer=function(){
return {
notify:function(msg){
console.log("received: "+msg);
}
}
}
var subject=new Subject();
var observer0=new Observer();
var observer1=new Observer();
var observer2=new Observer();
var observer3=new Observer();
subject.subscribe(observer0);
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.subscribe(observer3);
subject.notifyAll('all notified');
subject.notify(observer2,'asda');
promise
為解決回調(diào)函數(shù)噩夢而提出的寫法,將回調(diào)函數(shù)的橫向加載變成縱向加載。
- 對象狀態(tài)不受外界影響。三種狀態(tài):pending,resolved,rejected。只有異步操作的結(jié)果才能改變狀態(tài)
- 狀態(tài)一旦改變,就不會再變。
用Promise對象實現(xiàn)Ajax操作的例子
var getJSON=function(url){
var promise=new Promise(function(resolve,reject){
var client=new XMLHttpRequest();
client.open("GET",url);
client.onreadystatechange=handler;
client.responseType="json";
client.setRequestHeader("Accept","application/json");
client.send();
function handler(){
if(this.readyState!=4){
return;
}
if(this.status==200){
resolve(this.response);
}else{
reject(new Error(this.statusText));
}
}
});
return promise;
}
getJSON('/posts.json').then(function(json){
console.log('Contents: '+json);
},function(error){
console.error(error)
})
再舉一個需要多層回調(diào)的例子:假設每個步驟都是異步,并且依賴上一個步驟的結(jié)果,使用setTimeout來模擬異步操作。
//輸入n,表示該函數(shù)執(zhí)行時間,結(jié)果為n+200,并且用于下一步的輸入
function takeLongTime(n){
return new Promise(resolve=>{
setTimeout(()=>resolve(n+200),n)
})
}
function step1(n){
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n){
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n){
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
如果使用Promise的方式將其3個步驟處理為鏈式操作,每一步都返回一個promise對象,將輸出的結(jié)果作為下一步新的輸入:
function dolt(){
console.time('dolt');
const time1=300;
step1(time1)
.then(time2=>step2(time2))
.then(time3=>step3(time3))
.then(result=>{
console.log(`result is ${result}`);
console.timeEnd('dolt')
});
}
dolt();
//輸出結(jié)果為
step1 with 300
step2 with 500
step3 with 700
result is 900
dolt: 1516.713ms
實際耗時跟我們計算的延遲時間300+500+700=1500ms差不多。但是對于長的鏈式操作來說,看起來是一堆then方法的堆砌,代碼冗余,語義也不清楚,而且還是靠著箭頭函數(shù)才使得代碼略微簡短一些。Promise還有一個痛點,就是傳遞參數(shù)太麻煩,尤其是需要傳遞多參數(shù)的情況下。
Generator函數(shù)
generator是一個封裝的異步任務,在需要暫停的地方,使用yield語句注明。如
function* gen(x){
let y=yield x+2;
return y;
}
let g=gen(1);
g.next();
//返回 {value: 3, done: false}
g.next();
//返回 {value: undefined, done: true}
調(diào)用generator函數(shù)返回的是內(nèi)部的指針對象,調(diào)用next方法就會移動內(nèi)部指針。Generator函數(shù)之所以能被用來處理異步操作,因為它可以暫停執(zhí)行和恢復執(zhí)行、函數(shù)體內(nèi)外的數(shù)據(jù)交換和錯誤處理機制。
針對前面多任務的例子,使用generator實現(xiàn):
function* dolt(){
console.time('dolt');
const time1=300;
const time2=yield step1(time1);
const time3=yield step2(time2);
const result=yield step3(time3);
console.log(`result is ${result}`);
console.timeEnd('dolt');
}
但是 Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
spawn(dolt);
async/await
async函數(shù)基于Generator又做了幾點改進:
- 內(nèi)置執(zhí)行器,將Generator函數(shù)和自動執(zhí)行器進一步包裝。
- 語義更清楚,async表示函數(shù)中有異步操作,await表示等待著緊跟在后邊的表達式的結(jié)果。
- 適用性更廣泛,await后面可以跟promise對象和原始類型的值(Generator中不支持)
很多人都認為這是異步編程的終極解決方案,由此評價就可知道該方法有多優(yōu)秀了。它基于Promise使用async/await來優(yōu)化then鏈的調(diào)用,其實也是Generator函數(shù)的語法糖。 async 會將其后的函數(shù)(函數(shù)表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,并將其 resolve 的結(jié)果返回出來。
await得到的就是返回值,其內(nèi)部已經(jīng)執(zhí)行promise中resolve方法,然后將結(jié)果返回。使用async/await的方式重寫前面的回調(diào)任務:
async function dolt(){
console.time('dolt');
const time1=300;
const time2=await step1(time1);
const time3=await step2(time2);
const result=await step3(time3);
console.log(`result is ${result}`);
console.timeEnd('dolt');
}
dolt();
功能還很新,屬于ES7的語法,但使用Babel插件可以很好的轉(zhuǎn)義。另外await只能用在async函數(shù)中,否則會報錯。
【參考】
async 函數(shù)的含義和用法
JavaScript 異步編程解決方案筆記
用ES6 Generator替代回調(diào)函數(shù)