深入淺出koa洋蔥模型


關於洋蔥模型很多人都理解,並且絕大多數人都知道要想保證洋蔥模型必須要使用async 和await 

那么問題來了async和 await 是 用來解決異步編程的,那么當我們調用的下一個中間件不存在異步的時候,是否還需要使用async和 await

答案是肯定的,以至於現在很多人只要是寫中間件必用async 和 await 那么你是否知道它的運行機制和底層原理的 一個合格的開發人員是不是要做到知其然還要知其所以然呢?

我們就拿最為簡單的全局異常處理來舉例,在異步編程模型中全局異常處理因為函數執行時壓棧與出棧隊列的關系,往往有些抽象

async function fun() {
  try {
    await fun1();
  } catch (error) {
    console.log(5555);
  }
}

async function fun1() {
  await fun2();
}

async function fun2() {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('error'));
    }, 0);
  });
}

fun();

result:5555

上述代碼中fun2執行時后續執行棧中並沒有同步任務 

因此Promise中的異步任何將會執行並且拋出一個異常

async fun2會返回一個Promise並且默認執行reject

async fun1會處於出棧狀態並且接受到fun2的Promise

因為await的求值特性 所以會執行Promose中的reject再次將錯誤拋出 並且由fun1包裝為Promise返回給fun

fun執行Promise中reject拋出錯誤,因為函數狀態隊列全部出棧,所以錯誤將被立即拋出 由try catch捕捉

那么問題來了我們可以發現fun1中並沒有異步操作,它唯一做的一件事就是捕捉到了fun2返回Promise 並且再度將這個錯誤拋出 返回給了fun

那么我們為什么要給fun加await呢?讓fun1使用await讓它去和fun2壓棧不好嗎?

其實在這個案例中確實,fun中不使用await異步解決方案,並不影響我們捕捉到這個錯誤

但回到問題的本身,我們說的是要讓koa保證洋蔥模型,簡單修改下代碼

async function fun() {
  fun1().catch((err) => {
    console.log(55555);
  });

  console.log('I do it first');
}

async function fun1() {
  await fun2();
}

async function fun2() {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('error'));
    }, 0);
  });
}

fun();

result:I do it first

result:5555

前文已經說過了async會將拋出的錯誤用一個Promise進行包裝,而await的求值特性會執行reject從而拋出錯誤

那么修改后的代碼我們不再使用await,就需要手動catch這個錯誤了,但這並不影響它本身捕捉這個錯誤不是嗎?

但如果你是一個koa玩家,這個時候大概率一定明白了,是的

洋蔥模型要保證執行順序,在fun1,fun2全部執行完畢后才可以調用I do it first

但是很明顯結果並不是,這就是為什么即便下一個中間件不存在異步編程我們也要使用async的原因

至於原因,是因為node畢竟是基於V8引擎,而其本身單線程的特性,在遇到await時會對其所在的函數進行壓棧先去執行執行棧中的同步任務

這也是應有之意吧,所以在我們給fun加上await之后,我們就強制要求它進行壓棧,必須等fun2 fun1處理完畢后再行出棧

那么也就保證了洋蔥模型,至於node的運行原理,event loop 事件隊列這一些,我相信是一個JS玩家的基本素質了吧

好的那么再加一層吧,看看能不能捕捉到,是的捕捉到了依然是層層執行,拋出錯誤,包裝為Promise,是不是很有魅力呢?但是問題依然不能保證洋蔥模型

 

async function fun() {
  add().catch((err) => {
    console.log(55555);
  });

  console.log('I do it first');
}

async function add() {
  await fun1();
}

async function fun1() {
  await fun2();
}

async function fun2() {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('error'));
    }, 0);
  });
}

fun();

我們看到代碼中除了fun2中存在異步編程其他都沒有,但是必須要使用async和await才能保證錯誤層層遞進,當然了函數出棧時本身是不受錯誤影響的

但有一個問題請不要忽略,那就是如果你不對當前函數進行壓棧,那么當它執行到fun2時發現是異步編程對其壓棧

那么fun1和add不進行壓棧那么它們就會瞬間執行完畢,從而你在對fun進行壓棧它依然捕捉不到錯誤

因為當add執行完畢時,fun就已經出棧了,但是add並沒有捕捉到錯誤,從而也就不可能拋出錯誤了好的再次修改代碼舉下例

async function fun() {
  try {
    await add();
  } catch (error) {
    console.log(2222);
  }

  console.log('I do it first');
}

async function add() {
  fun1();
}

async function fun1() {
  await fun2();
}

async function fun2() {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('error'));
    }, 0);
  });
}

fun();

這個時候add並沒有使用await進行壓棧,那么我們來推演一下執行過程,fun調用add,add調用fun1 

fun1調用fun2,當執行棧到這里時,發現存在await異步編程,對fun2進行壓棧,並且返回一個pending狀態的Promise

此時fun1因為fun2壓棧也會進行壓棧,並且再度拋出pending狀態的Promise,這個時候add拿到了fun1拋出的Promise

如果這個時候對add拿到的Promise就行catch那么將會fun2出棧時會執行reject拋出錯誤,fun1因此也就能拿到錯誤並且執行

這個時候add自然能夠攔截錯誤,但問題是我們需要讓fun攔截錯誤,所以又回到了問題本身必須給使用await對add強行壓棧

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM