沐鳴註冊平台_編寫高質量可維護的代碼——異步優化

前言

在現在前端開發中,異步操作的頻次已經越來越高了,特別對於數據接口請求和定時器的使用,使得我們不得不關注異步在業務中碰到的場景,以及對異步的優化。錯誤的異步處理可能會帶來很多問題,諸如頁面渲染、重複加載等問題。

下面我們就先簡單的從 JavaScript 中有大致的哪幾種異步類型為切入點,然後再列舉一些業務中我們會碰到的場景來逐個分析下,我們該如何解決。

異步實現種類

首先關於異步實現的方式上大致有如下幾種:

callback

callback 即回調函數。這傢伙出現很早很早了,他其實是處理異步的基本方法。並且回調的概念不單單出現在JavaScript,你也會在 Java 或者 C# 等後端語言中也能找到他的影子。

回調函數簡單的說其實就是給另外一個寄主函數作為傳參的函數。在寄主函數執行完成或者執行到特定階段之後觸發調用回調函數並執行,然後把執行結果再返回給寄主函數的過程。

比如我們熟悉的 setTimeout 或者 react 中的 setState 的第二個方法都是以回調函數方式去解決異步的實現。

setTimeout(() => {
   //等待0.2s之後再做具體的業務操作
   this.doSomething();
}, 200);
this.setState({
  count: res.count,
}, () => {
  //在更新完count之後再做具體的業務操作
  this.doSomething();
});

Promise

Promise 是個好東西,有了它之後我們可以對異步進行很多操作,並且可以把異步以鏈式的方式進行操作。

其實在 JQuery 中的 deferred 和它就有點像,都是採用回調函數的解決方案,都可以做鏈式調用,但是在Promise 中增加了錯誤的 catch 方法可以更加方便的處理異常場景,並且它內置狀態(resolve, reject,pending),狀態只能由 pending 變為另外兩種的其中一種,且改變后不可逆也不可再度修改。

let promise = new Promise((resolve, reject) => { 
  reject("對不起,你不是我的菜");
});
promise.then((data) => {
console.log('第一次success' + data);
  return '第一次success' + data
},(error) => {
console.log(error) }
).then((data2) => {
  console.log('第二次success' + data2);
},(error2) => { 
  console.log(error2) }
).catch((e) => {
  console.log('抓到錯誤啦' + e);
});

await/async

await/async 其實是 Promise 的一種升級版本,使用 await/async 調用異步的時候是從上到下,順序執行,就像在寫同步代碼一樣,這更加的符合我們編寫代碼的習慣和思維邏輯,所以容易理解。 整體代碼邏輯也會更加的清晰。

async function asyncDemoFn() {
  const data1 = await getData1();
  const data2 = await getData2(data1);
  const data3 =  await getData3(data2);
  console.log(data3)
}
await asyncDemoFn()

generator

generator 中文名叫構造器,是 ES6 中的一個新東西,我相信很多人在現實的代碼中很少能接觸到它,所以它相對而言對大家來說還是比較晦澀,但是這傢伙還是很強的,簡單來說它能控制異步調用,並且其實是一個狀態機。

function* foo() {
  for (let i = 1; i <= 3; i++) {
    let x = yield `等我一下唄,i = ${i}`;
    console.log(x);
  }
}
setTimeout(() => {
  console.log('終於輪到我了');
}, 1);
var a = foo();
console.log(a); // foo {<closed>}
var b = a.next();
console.log(b); // {value: "等我一下唄,i = 1", done: false}
var c = a.next();
console.log(c); // {value: "等我一下唄,i = 2", done: false}
var d = a.next();
console.log(d); // {value: "等我一下唄,i = 3", done: false}
var e = a.next();
console.log(e); // {value: undefined, done: true}
// 終於輪到我了

上面代碼的函數 foo 是一個協程,它的厲害的地方就是 yield 命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield 命令是異步兩個階段的分界線。

協程遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除 yield 命令,簡直一模一樣。

再來個有點貼近點場景方式來使用下 generator。比如現在在頁面中我們需要自動的執行 checkAuth 和checkAddress 檢查,我們就用 generator 的方式去實現自動檢查上述兩異步檢查。

const checkAuth = () => {
    return new Promise((resolve)=>{
        setTimeout(()=>{
           resolve('checkAuth1') 
        },1000)
    })
}
const checkAddress = () => {
    return new Promise((resolve)=>{
        setTimeout(()=>{
            resolve('checkAddress2')
        },2000)
    })
}
var steps = [checkAuth,checkAddress]
function* foo(checkList) {
  for (let i = 0; i < checkList.length; i++) {
    let x = yield checkList[i]();
    console.log(x);
  }
}
var stepsGen = foo(steps)
var run = async (gen)=>{
    var isFinnish = false
    do{
       const {done,value} = gen.next()
       console.log('done:',done)
       console.log('value:',value)
       const result = await value
       console.log('result:',result)
       
       isFinnish = done
    }while(!isFinnish)
    console.log('isFinnish:',isFinnish)
}
run(stepsGen)

種類對比

從時間維度從早到晚:callback,promise, generator,await/async

await/async 是目前對於異步的終極形式

callback 讓我們有了基本的方式去處理異步情況,Promise 告別了 callback 的回調地獄並且增加 resolve,reject 和 catch 等方法讓我們能處理不同的情況,generator 增加了對於異步的可操作性,類似一個狀態機可暫時停住多個異步的執行,然後在合適的時候繼續執行剩餘的異步調用,await/async 讓異步調用更加語義化,並且自動執行異步

異步業務中碰到的場景

回調地獄

在使用回調函數的時候我們可能會有這樣的場景,B 需要在 A 的返回之後再繼續調用,所以在這樣有先後關係的時候就存在了一個叫回調地獄的問題了。

getData1().then((resData1) => {
  getData2(resData1).then((resData2) => {
  getData3(resData2).then((resData3)=>{
    console.log('resData3:', resData3)
    })
  });
});

碰到這樣的情況我們可以試着用 await/async 方式去解這種有多個深層嵌套的問題。

async function asyncDemoFn2() {
  const resData1 = await getData1();
  const resData2 = await getData2(resData1);
  const resData3 =  await getData3(resData2);
  console.log(resData3)
}
await asyncDemoFn2()

異步循環

在業務中我們最最經常碰到的就是其實還是存在多個異步調用的順序問題,大致上可以分為如下幾種:

并行執行

在并行執行的時候,我們可以直接使用 Promise 的 all 方法

Promise.all([getData1(),getData2(),getData3()]).then(res={
    console.log('res:',res)
})

順序執行

在順序執行中,我們可以有如下的兩種方式去做

使用 async/await 配合 for

const sources = [getData1,getData2,getData3]
async function promiseQueue() {
  console.log('開始');
  for (let targetSource in sources) {
    await targetSource();
  }
  console.log('完成');
};
promiseQueue()

使用 async/await 配合 while

//getData1,getData2,getData3 都為promise對象
const sources = [getData1,getData2,getData3]
async function promiseQueue() {
  let index = 0
  console.log('開始');
  while(index >=0 && index < sources.length){
    await targetSource();
    index++
  }
  console.log('完成');
};
promiseQueue()

使用 async/await 配合 reduce

//getData1,getData2,getData3 都為promise對象
const sources = [getData1,getData2,getData3]
sources.reduce(async (previousValue, currentValue)=>{
  await previousValue
  return currentValue()
},Promise.resolve())

使用遞歸

const sources = [getData1,getData2,getData3]
function promiseQueue(list , index = 0) {
  const len = list.length
  console.log('開始');
  if(index >= 0 && index < len){
    list[index]().then(()=>{
      promiseQueue(list, index+1)      
    })
  }
  console.log('完成');
}
promiseQueue(sources)

結尾

今天只是關於異步的普通使用場景的討論,並且做了些簡單的例子。其實關於異步的使用還有很多很多複雜的使用場景。更多的奇思妙想正等着你。

參考文獻

JS 異步編程六種方案

Async/Await替代Promise的6個理由

Javascript異步編程的4種方法

本文來自:https://zoo.team/article/asynchronization-optimizing

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/9855