js異步從入門到放棄(二)- 傳統的異步實現方案

前言

上一篇文章介紹了js異步的底層基礎--Event Loop模型,本文將介紹JS中傳統的幾種異步操做實現的模式。jquery

正文

1.回調函數(callback)

回調函數是異步的最基本實現方式。es6

// 例子:回調函數
const f1 = (callback) => setTimeout(()=>{
  console.log('f1')  // 自身要執行的函數內容
  callback()
},1000)

const f2 = () =>{ console.log('f2') }
f1(f2)
  • 思路:將回調函數做爲參數傳入主函數,執行完主函數內容以後,執行回調函數
  • 優勢:簡單粗暴、容易理解
  • 缺點:ajax

    • 代碼耦合度過高,不利於代碼維護
    • 有多層回調的狀況下,容易引發回調地獄
    • 通常回調的觸發點只有一個,例如fs.readFile等函數,只提供傳入一個回調函數,若是想觸發2個回調函數,就只能再用一個函數把這兩個函數包起來
// 例子1:回調地獄,依次執行f1,f2,f3...
const f1 = (callback) => setTimeout(()=>{
  console.log('f1')
  callback()
},1000)


const f2 = (callback) =>setTimeout(()=>{
  console.log('f2')
  callback()
},1000)
...
// 假設還有f3,f4...fn都是相似的函數,那麼就要不斷的把每一個函數寫成相似的形式,而後使用下面的形式調用:
f1(f2(f3(f4)))  


// 例子2:若是想給`fs.readFile`執行2個回調函數callback1,callback2
// 必須先包起來
const callback3 = ()=>{
    callback1
    callback2
}
fs.readFile(filename,[encoding],callback3)

2.事件監聽(Listener)

事件監聽的含義是:採用事件驅動模式,讓任務的執行不取決於代碼的順序,而取決於某個事件是否發生。先給出實現的效果:編程

const f1 = () => setTimeout(()=>{
  console.log('f1') // 函數體

  f1.trigger('done') // 執行完函數體部分 觸發done事件
},1000)
f1.on('done',f2) // 綁定done事件回調函數
f1()
// 一秒後輸出 f1,再過一秒後輸出f2

接下來手動實現一下上面的例子,體會一下這種方案的原理:設計模式

const f1 = () => setTimeout(()=>{
  console.log('f1') // 函數體

  f1.trigger('done') // 執行完函數體部分 觸發done事件
},1000)

/*----------------核心代碼start--------------------------------*/
// listeners 用於存儲f1函數各類各樣的事件類型和對應的處理函數
f1.listeners = {}
// on方法用於綁定監聽函數,type表示監聽的事件類型,callback表示對應的處理函數
f1.on = function (type,callback){
    if(!this.listeners[type]){
        this.listeners[type] = []
    }
    this.listeners[type].push(callback) //用數組存放 由於一個事件可能綁定多個監聽函數
}

// trigger方法用於觸發監聽函數 type表示監聽的事件類型
f1.trigger = function (type){
    if(this.listeners&&this.listeners[type]){
        // 依次執行綁定的函數
        for(let i = 0;i < this.listeners[type].length;i++){
            const  fn = this.listeners[type][i]
            fn()
        }
    }
}
/*----------------核心代碼end--------------------------------*/
const f2 = () =>setTimeout(()=>{
  console.log('f2')
},1000)
const f3 = () =>{ console.log('f3') }

f1.on('done',f2) // 綁定done事件回調函數
f1.on('done',f3) // 多個回調

f1()
// 一秒後輸出 f1, f3,再一秒後輸出f2

核心原理:數組

  1. listeners對象儲存要監聽的事件類型和對應的函數;
  2. 調用on方法時,往listeners中對應的事件類型添加回調函數;
  3. 調用trigger方法時,檢查listeners中對應的事件,若是存在回調函數,則依次執行;

和回調相比,代碼上的區別只是把原先執行callback的地方,換成了執行對應監聽事件的回調函數。可是從模式上看,變成了事件驅動模型promise

  • 優勢:避免了直接使用回調的高耦合問題,能夠綁定多個回調函數
  • 缺點:由事件驅動,不容易看出執行的主流程

3.發佈/訂閱模式(Publish/Subscribe)

在剛剛事件監聽的例子中,咱們改造了f1,使它擁有了添加監聽函數和觸發事件的功能,若是咱們把這部分功能移到另一個全局對象上實現,就成了發佈訂閱者模式異步

// 消息中心對象
const Message = {
  listeners:{}
}

// subscribe方法用於添加訂閱者 相似事件監聽中的on方法 裏面的代碼徹底一致
Message.subscribe = function (type,callback){
    if(!this.listeners[type]){
        this.listeners[type] = []
    }
    this.listeners[type].push(callback) //用數組存放 由於一個事件可能綁定多個監聽函數
}

// publish方法用於通知消息中心發佈特定的消息 相似事件監聽中的trigger 裏面的代碼徹底一致
Message.publish = function (type){
    if(this.listeners&&this.listeners[type]){
        // 依次執行綁定的函數
        for(let i = 0;i < this.listeners[type].length;i++){
            const  fn = this.listeners[type][i]
            fn()
        }
    }
}

const f2 = () =>setTimeout(()=>{
  console.log('f2')
},1000)

const f3 = () => console.log('f3')

Message.subscribe('done',f2) // f2函數 訂閱了done信號
Message.subscribe('done',f3) // f3函數 訂閱了done信號
const f1 = () => setTimeout(()=>{
  console.log('f1') 
  Message.publish('done')  // 消息中心發出done信號
},1000)
f1() 
// 執行結果和上面徹底同樣

若是認真看的話會發現,這裏的代碼和上一個例子幾乎沒有區別,僅僅是:async

  1. 建立了一個Message全局對象,而且listeners移到該對象
  2. on方法更名爲subscribe方法,而且移到Message對象上
  3. trigger方法更名爲publish,而且移到Message對象上

這麼作有意義嗎?固然有。異步編程

  • 在事件監聽模式中,消息傳遞路線:被監聽函數f1與監聽函數f2直接交流
  • 在發佈/訂閱模式中,是發佈者f1和消息中心交流,訂閱者f2也和消息中心交流

如圖:
對比2種模式
消息中心的做用正如它的名字--承擔了消息中轉的功能,全部發布者和訂閱器都只和它進行消息傳遞。有這個對象的存在,能夠更方便的查看全局的消息訂閱狀況。

實質上,這也是設計模式中,觀察者模式和發佈/訂閱者模式的區別。

4.Promise

Promise 是異步編程的一種解決方案,它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。

注意,只是在es6原生提供了Promise對象,不表明Promise的設計是在es6纔出現的。最典型的,當咱們還在使用jquery$.ajax時,已經使用$.ajax().then().catch()時,就已經用到了Promise對象。所以這個也歸爲傳統異步實現。

關於Promise詳細內容,建議你們學習阮一峯老師的ES6教程,本文只介紹異步相關的核心內容

接下來一樣地,用js模擬實現一個簡單的Promise對象。

首先分析Promise的要點:

  1. 構造函數接受一個函數爲參數,而且要接受resolve(reject)方法
  2. 能夠經過resolvereject方法改變狀態:resolve使狀態從pending(進行中)變成、fulfilled(已成功);reject使狀態變成rejected(已失敗)
  3. then方法用於註冊回調函數,而且返回值必須爲Promise對象,這樣才能實現鏈式調用(鏈式調用是指p.then().then().then()這樣的形式)

根據上述分析,實現一個有thenresolve方法的簡單Promise對象:

// 例子:手動實現簡單Promise

function MyPromise(fn){
  this.status = 'pending' 
  this.resolves =[] //存放成功執行後的回調函數
  return fn(this.resolve.bind(this))// 這裏必須bind,不然this對象會根據執行上下文改變
}

// then方法用於添加註冊回調函數
MyPromise.prototype.then = function(fn){
  // 註冊回調函數 並返回Promise.
  this.resolves.push(fn)
  return this
}

// resolve用於變動狀態 而且觸發回調函數,實際上resolve能夠接受參數 這裏簡單實現就先忽略
MyPromise.prototype.resolve = function(){
  this.status = 'fulfilled' 
  if(this.resolves.length===0){
    return 
  }
  // 依次執行回調函數 並清空
  for(i=0;i<this.resolves.length;i++){
    const fn = this.resolves[i]
    fn()
  }
  this.resolves = [] //清空
  return this
}

// 使用寫好的MyPromise作實驗
const f1 = new MyPromise(resolve=>{
  setTimeout(()=>{
    console.log('f1 開始運行')
    resolve()
  },1000)
})

f1.then(()=>{
  setTimeout(()=>{
    console.log('f1的第一個then')
  },3000)
})

// 一個小思考,下面函數的執行輸出是什麼?
f1.then(()=>{
  setTimeout(()=>{
    console.log('f1的第一個then')
  },3000)
}).then(()=>{
  setTimeout(()=>{
    console.log('f1的第二個then')
  },1000)
})

以上就是Promise的核心思路。

總結

本文針對傳統的幾種異步實現方案作了說明。而ES6中新的異步處理方案Generatorasync/await會在後面補充。


若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞一杯咖啡~

相關文章
相關標籤/搜索