JS中的非同步操作(轉)
JS中非同步程式設計的方法有:
- 回撥函式
- 事件監聽
- 釋出/訂閱
- promise
- generator(ES6)
- async/await(ES7)
回撥函式
回撥是非同步程式設計中最基礎的方法。舉例一個簡單的回撥:在f1執行完之後再執行f2
var func1=function(callback){ console.log(1); (callback && typeof(callback)==='function') && callback(); } func1(func2); var func2=function(){ console.log(2); }
非同步回撥中最常見的形式可能就是Ajax了:
$.ajax({
url:"/getmsg",
type: 'GET',
dataType: 'json',
success: function(ret) {
if (ret && ret.status) {
//
}
},
error: function(xhr) {
//
}
})
事件監聽
通過事件機制,實現程式碼的解耦。js處理DOM互動就是採用的事件機制,我們這兒只是實現一些自定義的事件而已。JS中已經很好的支援了自定義事件,如:
//新建一個事件
var event=new Event('Popup::Show');
//dispatch the event
elem1.dispatchEvent(event)
//listen for this event
elem2.addEventListener('Popup::Show',function(msg){},false)
釋出-訂閱模式
在系統中存在一個"訊號中心",當某個任務執行完成後向訊號中心"釋出"(publish)一個訊號,其他任務可以向訊號中心"訂閱"(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。簡單實現如下:
//釋出-訂閱 //有個訊息池,存放所有訊息 let pubsub = {}; (function(myObj) { topics = {} subId = -1; //釋出者接受引數(訊息名稱,引數) myObj.publish = function(topic, msg) { //如果釋出的該訊息沒有訂閱者,直接返回 if (!topics[topic]) { return } //對該訊息的所有訂閱者,遍歷去執行各自的回撥函式 let subs = topics[topic] subs.forEach(function(sub) { sub.func(topic, msg) }) } //訂閱者接受引數:(訊息名稱,回撥函式) 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) { //找到了,從訂閱者的陣列中去掉該訂閱者 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')
其中儲存訊息的結構用json可以表示為:
topics = {
topic1: [{ token: 1, func: callback1 }, { token: 2, func: callback2 }],
topic2: [{ token: 3, func: callback3 }, { token: 4, func: callback4 }],
topic3: []
}
訊息池的結構是釋出訂閱模式與事件監聽模式的最大區別。當然,每個訊息也可以看做是一個個的事件,topics物件就相當於一個事件處理中心,每個事件都有各自的訂閱者。所以事件監聽其實就是釋出訂閱模式的一個簡化版本。而釋出訂閱模式的優點就是我們可以檢視訊息中心的資訊,瞭解有多少訊號,每個訊號有多少訂閱者。
再說一說觀察者模式
很多情況下,我們都將觀察者模式和釋出-訂閱模式混為一談,因為都可用來進行非同步通訊,實現程式碼的解耦,而不再細究其不同,但是內部實現還是有很多不同的。
-
整體模型的不同:釋出訂閱模式是靠資訊池作為釋出者和訂閱者的中轉站的,訂閱者訂閱的是資訊池中的某個資訊;而觀察者模式是直接將訂閱者訂閱到釋出者內部的,目標物件需要負責維護觀察者,也就是觀察者模式中訂閱者是依賴釋出者的。
-
觸發回撥的方式不同:釋出-訂閱模式中,訂閱者通過監聽特定訊息來觸發回撥;而觀察者模式是釋出者暴露一個介面(方法),當目標物件發生變化時呼叫此介面,以保持自身狀態的及時改變。
觀察者模式很好的應用是MVC架構,當資料模型更新時,檢視也發生變化。從資料模型中將檢視解耦出來,從而減少了依賴。但是當觀察者數量上升時,效能會有顯著下降。我們同樣可以自己實現:
//觀察者模式
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
為解決回撥函式噩夢而提出的寫法,將回調函式的橫向載入變成縱向載入。
- 物件狀態不受外界影響。三種狀態:pending,resolved,rejected。只有非同步操作的結果才能改變狀態
- 狀態一旦改變,就不會再變。
用Promise物件實現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)
})
再舉一個需要多層回撥的例子:假設每個步驟都是非同步,並且依賴上一個步驟的結果,使用setTimeout來模擬非同步操作。
//輸入n,表示該函式執行時間,結果為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物件,將輸出的結果作為下一步新的輸入:
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();
//輸出結果為
step1 with 300
step2 with 500
step3 with 700
result is 900
dolt: 1516.713ms
實際耗時跟我們計算的延遲時間300+500+700=1500ms差不多。但是對於長的鏈式操作來說,看起來是一堆then方法的堆砌,程式碼冗餘,語義也不清楚,而且還是靠著箭頭函式才使得程式碼略微簡短一些。Promise還有一個痛點,就是傳遞引數太麻煩,尤其是需要傳遞多引數的情況下。
Generator函式
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}
呼叫generator函式返回的是內部的指標物件,呼叫next方法就會移動內部指標。Generator函式之所以能被用來處理非同步操作,因為它可以暫停執行和恢復執行、函式體內外的資料交換和錯誤處理機制。
針對前面多工的例子,使用generator實現:
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 函式的執行必須靠執行器
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函式基於Generator又做了幾點改進:
- 內建執行器,將Generator函式和自動執行器進一步包裝。
- 語義更清楚,async表示函式中有非同步操作,await表示等待著緊跟在後邊的表示式的結果。
- 適用性更廣泛,await後面可以跟promise物件和原始型別的值(Generator中不支援)
很多人都認為這是非同步程式設計的終極解決方案,由此評價就可知道該方法有多優秀了。它基於Promise使用async/await來優化then鏈的呼叫,其實也是Generator函式的語法糖。 async 會將其後的函式(函式表示式或 Lambda)的返回值封裝成一個 Promise 物件,而 await 會等待這個 Promise 完成,並將其 resolve 的結果返回出來。
await得到的就是返回值,其內部已經執行promise中resolve方法,然後將結果返回。使用async/await的方式重寫前面的回撥任務:
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外掛可以很好的轉義。另外await只能用在async函式中,否則會報錯。
作者:RichardBillion 連結:https://www.jianshu.com/p/6f91e7696b91 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。