1. 程式人生 > >JS中的非同步操作(轉)

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物件就相當於一個事件處理中心,每個事件都有各自的訂閱者。所以事件監聽其實就是釋出訂閱模式的一個簡化版本。而釋出訂閱模式的優點就是我們可以檢視訊息中心的資訊,瞭解有多少訊號,每個訊號有多少訂閱者。

再說一說觀察者模式

很多情況下,我們都將觀察者模式和釋出-訂閱模式混為一談,因為都可用來進行非同步通訊,實現程式碼的解耦,而不再細究其不同,但是內部實現還是有很多不同的。

  1. 整體模型的不同:釋出訂閱模式是靠資訊池作為釋出者和訂閱者的中轉站的,訂閱者訂閱的是資訊池中的某個資訊;而觀察者模式是直接將訂閱者訂閱到釋出者內部的,目標物件需要負責維護觀察者,也就是觀察者模式中訂閱者是依賴釋出者的。

  2. 觸發回撥的方式不同:釋出-訂閱模式中,訂閱者通過監聽特定訊息來觸發回撥;而觀察者模式是釋出者暴露一個介面(方法),當目標物件發生變化時呼叫此介面,以保持自身狀態的及時改變。

觀察者模式很好的應用是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 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。