JS中的異步操作

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)還是有很多不同的。

  1. 整體模型的不同:發(fā)布訂閱模式是靠信息池作為發(fā)布者和訂閱者的中轉(zhuǎn)站的,訂閱者訂閱的是信息池中的某個信息;而觀察者模式是直接將訂閱者訂閱到發(fā)布者內(nèi)部的,目標對象需要負責維護觀察者,也就是觀察者模式中訂閱者是依賴發(fā)布者的。

  2. 觸發(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ù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

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

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,325評論 5 22
  • 本文首發(fā)在個人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云閱讀 1,701評論 0 3
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,746評論 0 5
  • 一.非阻塞和異步 借用知乎用戶嚴肅的回答在此總結(jié)下,同步和異步是針對消息通信機制,同步代表一個client發(fā)出一個...
    Daniel_adu閱讀 1,841評論 0 8
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大...
    HetfieldJoe閱讀 6,387評論 9 19