1. 程式人生 > 實用技巧 >JS——非同步程式設計的六種解決方案

JS——非同步程式設計的六種解決方案

將需要在非同步任務後執行的操作,作為引數傳入到非同步操作中,當非同步操作執行完成後,呼叫該引數執行後面的操作

ajax(url,()=>{})

回撥函式簡單,容易理解和實現;但回撥函式的缺點就是,容易寫出回撥地獄

多個非同步操作需要規定執行順序時產生回撥地獄

回撥地獄導致程式碼不容易閱讀和維護,各個部分高度耦合,使得程式結構混亂,流程那一追蹤

ajax(url,()=>{
    //處理邏輯
    ajax(url1,()=>{
        //處理邏輯
        aja(url2,()=>{
            //處理邏輯
        })
    })
})
  • 事件監聽

    非同步任務的執行不取決於程式碼的順序,而是取決於某個事件是否發生,事件驅動

    f1.on('done',f2)
    //f1 繫結事件 done,只有當f1發生done事件的時候才執行f2
    function f1(){
        setTimeout(function(){
            //……
            f1.trigger('done')	//觸發done事件,f2立即執行
        }, 1000)
    }
    

    這種方式容易理解,可以繫結多個事件,每個事件可以指定多個回撥函式,而且可以“去耦合”,有利於實現模組化。缺點就是整個程式都會變成事件驅動型,執行流程變得很不清晰

  • 釋出訂閱

    假定存在一個"訊號中心",某個任務執行完成,就想訊號中心"釋出(publish)"一個訊號,其他任務可以向訊號中心"訂閱(subscribe)"這個訊號,從而知道什麼時候自己可以執行。這就是釋出訂閱模式,又稱"觀察者模式"

    jQuery.subscribe('done',f2)
    //f2 向訂閱中心訂閱了 done 訊號
    function f1(){
        setTimeout(function(){
            //……
            jQuery.publish('done')	//執行完成後向訂閱中心釋出done訊號
        },1000)
    }
    
    jQuery.unsubscribe('done',f2)	//f2執行完成後可以取消訂閱
    

    這種方式與"事件監聽類似",但明顯後者優於前者。因為可以通過檢視"訊息中心",瞭解存在多少訊號,每個訊號有多少訂閱者,從而監聽程式的執行

  • Promise

    ES6引入的非同步程式設計解決方案

    graph LR a(回撥函式)-- 複雜非同步問題 -->b(回撥地獄) b-->c(程式碼難以閱讀和維護) c-->d(Promise)
    /*##回撥函式非同步解決方案*/
    setTimeout(()=>{
        console.log('hello world');
        setTimeout(()=>{
            console.log('hello vueJs');
            setTimeout(()=>{
                console.log('hello Promise');   //回撥函式產生的回撥地獄
            },1000)
        },1000)
    }, 1000)
    /*##Promise非同步解決方案*/
    new Promise((resolve, reject)=>{
        //第一次非同步請求
        setTimeout(()=>{
            //成功的時候呼叫resolve
            resolve('hello world')   //通過resolve函式將結果返回到外部 then 處理
        }, 1000)
    }).then((data)=>{
        //處理第一次非同步請求
        console.log(data);
        //第二次非同步請求
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                resolve()
            },1000)
        })
    }).then(()=>{
        //處理第二次非同步請求
        console.log('hello vueJs');
        //第三次請求
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                resolve()
            },1000)
        })
    }).then(()=>{
        //處理第三次非同步請求
        console.log('hello Promise');
    })
    
    
    • Promise 怎麼使用

      1. 將非同步操作放進 Promise 中

      2. new Promise(回撥函式)

        new -> 建構函式(1.儲存一些狀態資訊,2.執行傳入的引數(即回撥函式))

      3. 回撥函式:(resolve,reject)=>{非同步操作}

      4. resolve 和 reject 本身又是函式

      5. 通過 resolve 和 reject 將資料返回到外部 .then() .catch處理 --> 鏈式程式設計

        可以正確和錯誤的回撥都在 then() 中處理

        new Promise((resolve,reject)=>{
            setTimeout(()=>{
                //遇到錯誤呼叫reject,將錯誤返回到外部 catch 處理
                reject('hello error')
            },1000)
        }).then(data=>{
            console.log(data);
        },err=>{
            console.log(err);
        })
        
    • Promise 的三種狀態

      pending: 等待狀態,比如正在進行的網路請求,或定時器沒有到時間
      fulfill: 滿足狀態,主動回撥 resolve 時,就處於滿足狀態,並且回撥 .then()
      reject: 拒絕狀態,主動回撥 reject 時,就處於拒絕狀態,並且回撥 .catch()

    • Promise的鏈式呼叫

      graph LR a(返回新的Promise物件)-->b(返回Promise.resolve) b-->c(直接返回資料)
      /*最開始的鏈式呼叫*/
      new Promise((resolve,reject)=>{
          setTimeout(()=>{
              resolve('hello1')
          })
      }).then((data)=>{
          console.log(data,"第一層資料處理");
          return new Promise(resolve => {
              resolve(data+'1')
          })
      }).then(data=>{
          console.log(data,'第二層資料處理');
          return new Promise(resolve => {
              resolve(data+'1')
          })
      }).then(data=>{
          console.log(data,'第三層資料處理')
      })
      
      
      /*簡化new的鏈式呼叫*/
      new Promise((resolve,reject)=>{
          setTimeout(()=>{
              resolve('hello1')
          })
      }).then((data)=>{
          console.log(data,"第一層資料處理");
          // return Promise.resolve(data+'1')
          return Promise.reject('error')  //丟擲錯誤資訊的簡化方式
      }).then(data=>{
          console.log(data,'第二層資料處理');
          return Promise.resolve(data+'1')
      }).then(data=>{
          console.log(data,'第三層資料處理')
      }).catch(err=>{
          console.log(err);
      })
      
      
      /*最終簡化版*/
      new Promise((resolve,reject)=>{
          setTimeout(()=>{
              resolve('hello1')
          })
      }).then((data)=>{
          console.log(data,"第一層資料處理");
          // return data+'1'
          throw 'error'   //丟擲錯誤的簡寫方式
      }).then(data=>{
          console.log(data,'第二層資料處理');
          return data+'1'
      }).then(data=>{
          console.log(data,'第三層資料處理')
      }).catch(err=>{
          console.log(err);
      })
      
    • Promise.all

      當一個任務需要多個非同步任務的結果才能執行時

      /*1. 通過回撥函式實現*/
      let isResult1 = false
      let isResult2 = false
      $ajax({
          url: '',
          success: function () {
              console.log('結果1');
              isResult1 = true
              handleResult()
          }
      })
      $ajax({
          url: '',
          success: function () {
              console.log('結果2');
              isResult2 = true
              handleResult()
          }
      })
      function handleResult() {
          if(isResult1 && isResult2){
              //邏輯處理
          }
      }
      
      /*2. Promise的all方法,同時進行多個非同步操作*/
      Promise.all([
          new Promise((resolve,reject)=>{
              $ajax({
                  url: 'url1',
                  success: function (data) {
                      resolve(data)
                  }
              })
          }),
          new Promise((resolve,reject)=>{
              $ajax({
                  url: 'url2',
                  success: function (data) {
                      resolve(data)
                  }
              })
          }),
      ]).then(results=>{
          results[0]  //第一個非同步操作返回的結果
          results[1]  //第二個非同步操作返回的結果
      })
      
  • 生成器 Generator/yield

    Generator與函式很像,但定義時多個 * 號,並且除了 return 以外,還可以用 yield 返回多次

    function* foo(x){
        yield x+1
        yield x+2
        return x+3
    }
    let f = foo(3);
    f.next()    //{value: 4, done: false}
    f.next()    //{value: 5, done: false}
    f.next()    //{value: 6, done: true}
    f.next()    //{value: undefined, done: true}
    //也可以直接用迴圈地跌 generator 物件
    for(var x fo foo(3)){
        console.log(x)
    }
    

    generator 函式的特性很適合用來處理非同步操作因為它的每一步yield都是按順序的

    function *fetch() {
        yield ajax(url, () => {})
        yield ajax(url1, () => {})
        yield ajax(url2, () => {})
    }
    let it = fetch()
    let result1 = it.next()
    let result2 = it.next()
    let result3 = it.next()
    
  • Aysnc/Await

    Aysnc 函式是 Generator 的優化

    Aysnc 是基於 Promise 實現的,Aysnc 返回一個 Promise 物件

    • Aysnc 對 Generator 的改進

      1. 內建執行器

        Generator 函式執行必須依靠執行器,所以才有了 co 函式庫,而 aysnc 函式只帶執行器。也就是說 async 函式的執行與普通函式的執行一模一樣,只要一行

      2. 更廣適用性

        yield 命令後面只能是 Thunk 函式或者 Promise 物件,而 async 函式的 await 命令後面,可以跟 Promise 物件和 原始型別的值(數值,字串和布林值,但這時等同於同步操作)

      3. 更好的語義

        async 和 await,比起 * 號和 yield ,語義更清楚了, async 表示函式裡有非同步操作, await 表示緊跟在後面的表示式需要等待結果

    • 缺點

      因為 await 將非同步程式碼改造成了同步程式碼,如果多個非同步程式碼沒有依賴性卻使用了 await 會導致效能的降低,程式碼沒有依賴性的話,完全可以使用 Promise.all 的方式