iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
0
Modern Web

先你一步的菜鳥 - 從 0 開始的前端網頁設計系列 第 13

Day-13 非同步 和 同步 - async/await & promise 物件了解 & callback function 和 IIFEs

  • 分享至 

  • xImage
  •  

在講這篇的主題之前,先來聊聊超商店員好了。

不覺得他們根本就超猛嗎,除了基本業務以外,還可以代繳費、代收包裹、宅配寄件、做你的假掰咖啡、打掃、煮關東煮、茶葉蛋、就連指路、敦親睦鄰都快劃進工作內容了。

這樣的人只領基本時薪,這該死的資本主義,讓我們舉起人民的法....

https://ithelp.ithome.com.tw/upload/images/20200913/20123396tF5uzBU6Vi.jpg

讓我們假設,有一天你家附近所有的超商店員集體職業倦怠,他再也不管你上班前有多想端著你的city cafe、不管你多想快一點帶著你的微波食品回家看電視、不管你打翻了甚麼要他趕快去清,他一次只想做一件事

你可以想像整個超商的情況會怎樣。

在這之前他可以在幫前一個客人結帳後微波他的食物,在過程中順手按下咖啡機,然後用其他人網購的清掃機器人幫那位弟弟清理他灑出來的牛奶,順便在幫你結帳的同時回答旁邊的阿伯中港路就是台灣大道。

現在他在微波完之前不會離開那台機器,就算有多少人因為地上那攤牛奶滑倒,旁邊的阿伯多想上街抗議更名回中港路,那個想要咖啡的客人因為咖啡因沒有及時攝取開始狂啃櫃台旁的咖啡豆-這些都不甘他的事,你只能等待他做完上一件事。

好的,拉回來現實,幸虧全台灣的超商店員意志堅強,目前還沒有這種慘案發生,一起心懷敬意的為他們致上誠摯的感謝一秒鐘。

非同步

而超商店員,就是非同步最典型的例子,他可以非同步處理每一件事,不用等待上一件事到底完成了沒有,只要呼叫到甚麼,就馬上執行,舉例來說像這樣:

  function clean(){
    console.log('clean start')
    setTimeout(()=>console.log('clean done'),1000)
  }
  function cafe(){
    console.log('cafe start')
    setTimeout(()=>console.log('cafe done'),1000)
  }
  function checkOut(){
    console.log('checkOut start')
    setTimeout(()=>console.log('checkOut done'),1000)
  }
  function microwave(){
    console.log('microwave start')
    setTimeout(()=>console.log('microwave done'),1000)
  }
  function pointTheWay(){
    console.log('point start')
    setTimeout(()=>console.log('point done'),1000)
  }
  function superman(){
    microwave()
    cafe()
    clean()
    checkOut()
    pointTheWay()
  }
  superman()

可以看到每件事情我們都預設一秒鐘執行時間,如果是同步處理的話,應該是 something strat、something done … and so on。但非同步處理的話會長這樣。

https://ithelp.ithome.com.tw/upload/images/20200913/20123396iRiL7dOMou.png

我們不需要等待事情結束才能開始下一件事情,這就是非同步。

但有時候,總是會需要等某一件事完成才能做下一件,那這時候要怎麼處理呢。

剛剛說到超商店員(以下簡稱超人)迫於無奈只能拆了客人的包裹來清掃店內,但為了不被客訴,所以要確保把包裹裝回去再做其他事。

這時候就可以使用 async / await 處理,可是 async / await 終究是包裝了 promise 的語法糖,所以我們必須先知道 promise 是甚麼,再來了解 async / await 是怎麼幫我們處理問題的 。

promise

promise 就是承諾,跟字面上的意思差不多。

以上面的例子來說,使用 promise 就像是超人使用過後會包裝回去的動作。

但是使用的過程一定會有很多意外,有可能清掃機器人在清掃那攤牛奶的時候光榮殉職了,這樣超人就慘了。

所以 promise 是一個最終一定會回傳結果的動作,我們可以依照最後傳過來的結果決定使用哪個動作:

  const clean = () => {
    return new Promise((resolve,reject) => {
	   //機器人清掃中
	   //完成=>回傳 result
      if(result==='succeed'){
        resolve()   //成功之後做
      }
      else{
        reject()    //失敗之後做
      }
    });
  };

可以看到 promise 擁有兩個函數 resolve 和 reject 當作參數,只要等到結果回傳回來,就可以執行這兩個 callback function。

註:promise 函數默認命名是這樣,但你也可以自己決定名稱。

callback function

總算有個機會可以補到底甚麼是 callback function 了 XD,雖然說有點晚了,更早講的話效果應該會更好的,不過跟著這系列一直講一直看,應該隱約都有一點感覺了,但還是簡單的解釋一下。

callback function 就像是被條件執行的動作,通常都是執行某個動作之後附加在裡面的動作,以上面我們講的超人動作式例:

  function clean(){
    console.log('clean start')
    setTimeout(()=>console.log('clean done'),1000)
  }
  function cafe(){...}
  function checkOut(){...}
  function microwave(){...}
  function pointTheWay(){...}

  function superman(){
    microwave()
    cafe()
    clean()
    checkOut()
    pointTheWay()
  }
  superman()

clean()、café()、checkout()、microwave()、pointTheWay() 這些都是 superman()的 callbacke function。

而 setTimeout() 則是 clean() 的 callback function。

應該有聽過 callback hell 或是 波動拳難題等等的事吧,這些都跟 callback 有關,礙於篇幅,這邊就不多做解釋了,雖然我這樣補完有點混,不過其實站上有不少講解 callback 的好文,這邊就做個簡單解釋就好。


好的回到正題,當我們得知結果之後,就可以依照情況來處理狀況了。

promise 依照結果執行 reslove / reject 之後,回傳 fulfilled(已實現) / rejected(已拒絕)作為狀態。

而我們就可以依照是 fulfilled / rejected 執行動作,而這個動作就是 then / catch。

then

then 可以同時接受當狀態是 fulfilled 或 rejected 時的 callback function:

  const clean = () => {
    return new Promise((resolve,reject) => {
	   //機器人清掃中
	   //完成=>回傳 result
      if(result==='succeed'){
        resolve()   //成功之後做
      }
      else{
        reject()    //失敗之後做
      }
    });
  };

  clean.then(
    (fulfilledsValue) => {/*do when resolve*/},
    (rejectedsValue) => {/*do when reject*/}
  );

catch

catch 只能處理狀態是 rejected 時的狀況,所以雖然 then 可以處理兩種狀況,可是為了 code 的直覺性,會把 rejected 的狀態給 catch 處理,fulfilled 交給then。

  clean
  .then((fulfilledsValue) => {/*do when resolve*/})
  .catch((rejectedsValue) => {/*do when reject*/})

promise 使用 then / catch 執行後續的動作,被稱為鏈結,我們可以使用 return 執行下一個 promise 物件:

  clean
  .then((fulfilledsValue) => {/*do when resolve*/})
  .catch((rejectedsValue) => {/*do when reject*/})

我們來實作看看鏈結吧,預設流程是:清掃機器人沒壞 => 重新包裝 => 手賤再玩一次 => 然後再包回去 =>預設失敗(給出失敗原因):

  const clean = (result) => {
    return new Promise((resolve, reject) => {
      if (result !== true) {
        reject(console.log("clean reject:壞了")); //失敗之後做
      }
      else{
        resolve(console.log("clean resolve:沒壞")); //成功之後做
      }
    });
  };

  const rePack = (result) => {
    return new Promise((resolve, reject) => {
      if (result !== true) {
        reject(console.log("repack reject : 你再給我搓手一次試試看")); //失敗之後做
      }
      else{
        resolve(console.log("repack resolve:卑鄙修復術" )); //成功之後做
      }
    });
  };

  async function superman() {
    clean(true)
      .then(() => {return rePack(true,);})
      .then(() => {return clean(false);})
      .then(() => {return rePack(true);}) //因為上一個 then 使狀態變成 rejected 所以會跳過這個直接執行 catch
      .catch(() => {console.log("error :" + Error)});
  }

  superman();

結果會變成這樣

https://ithelp.ithome.com.tw/upload/images/20200913/20123396vrEzxJ0mpl.png

註:這邊可以發現如果鏈結中一旦有一個 promise 回傳了 rejected state 就會馬上跳出,直接執行 catch。

finally

其實還有最後一個函數可以加入鍊結,finally 通常用來確認這串鍊結結束與否,也可以用來關閉伺服器連結,處理一些狀態參數,有點像 unMount 的用法。

註:promise 還有其他的函數可以用,不過這不是本篇的重點所以就不一一講解

以上就是 promise 的方法實作,老實說語法還是稍嫌複雜,如果後續執行的程序很多的話,會導致鏈結不斷的擴大,形成另一種的 callback hell。

async / await

作為 promise 的語法糖,肯定比 promise 更易懂易讀。

  • async 可以定義在任何一個 function 前面,宣告這是一個非同步的 function 且將會回傳一個 promise 物件。

  • await 只能使用在 async 宣告的 function 裡面,代表說只有當這個非同步的動作完成後,才會繼續執行這個 function 後續的動作。

在大致上了解了 async / await 的用法之後,我們就按著先前超人處理事情的邏輯來改寫吧:

流程:微波食品=>做咖啡=>清潔=>等待把掃地機器人掃完才裝回去=>結帳=>指路。

>註:為了節省版面這邊就把 promise 的 reject 實作部分省略,並且加上 setTimeout 感受一下非同步的感覺。

  function cafe() {
    console.log("cafe start");
    setTimeout(() => console.log("cafe done"), 1000);
  }

  function checkOut() {
    console.log("checkOut start");
    setTimeout(() => console.log("checkOut done"), 1000);
  }

  function microwave() {
    console.log("microwave start");
    setTimeout(() => console.log("microwave done"), 1000);
  }

  function pointTheWay() {
    console.log("point start");
    setTimeout(() => console.log("point done"), 1000);
  }

  const clean = () => {
    console.log('clean start')
    return new Promise((resolve) => {
        setTimeout(resolve(console.log("clean resolve:沒壞")) ,1000);
    });
  };

  const rePack = (result) => {
    return new Promise((resolve) => {
      resolve(console.log("repack resolve:卑鄙修復術" ));
    });
  };

  (async function superman(data){
    console.log(data)
    microwave()
    cafe()
    await clean()
    await rePack()
    checkOut()
    pointTheWay()
  })('捶倒資本主義的高牆!')

這邊雖然說沒有跟鏈結一樣多的程序,但可以很清處的看到,不用再使用 then / catch 這種煩死人的東西了,整個畫面乾淨很多。

現在就讓我們來看看有沒有跟預想的一樣吧!

https://ithelp.ithome.com.tw/upload/images/20200913/201233960UH7CQfvg7.png

可以看到雖然每個動作都有預設一秒的短暫延遲,但還是如我們預想的,在機器人掃完地之前其他動作都暫停,直到真正完成了裝回去這個動作才繼續其他的動作。

恩?你說畫面參雜了奇怪的東西?

沒有錯,突如其來的教學就跟 youtube 忽然插入的廣告一樣讓人措手不及。

https://ithelp.ithome.com.tw/upload/images/20200913/20123396iPY38eHipG.png

可以注意到 superman 這個 function,他沒有額外的呼叫就直接執行了,這是怎麼做的呢!?

  (async function superman(data){
    console.log(data)
    microwave()
    cafe()
    await clean()
    await rePack()
    checkOut()
    pointTheWay()
  })('捶倒資本主義的高牆!')

IIFEs(Immediately Invoked Functions Expressions)

IIFEs 也就是立即執行函數,還記我們在 Day-12 講過的 function expressions 嗎,IIFEs 就是他的其中一種。

只要在任何一個 expression 後面加上 (),就可以變成 IIFEs,不過因為 superman 本身是 function declaration,所以需要用 () 包住整個function 才能使用。

IIFEs 可以在定義 function 的同時執行 function,更可以在後面的 () 後面加入參數並立即傳進去。

就可以變成這樣了

https://ithelp.ithome.com.tw/upload/images/20200913/201233960AiCWOduyp.png

當然 IIFEs 還有很多種運用技巧,如果後面的實作部分有代到的話就會詳細解釋。

接下來,我們來學今天最後也是最屌超級屌的使用方法。

我們回想一下今天的整個故事流程:

超人在幫前一個客人結帳後微波他的食物,在過程中順手按下咖啡機,然後用其他人網購的清掃機器人幫那位弟弟清理他灑出來的牛奶,順便在幫你結帳的同時回答旁邊的阿伯中港路就是台灣大道。

因為超人實在太忙了,忙到他其實沒有發現-其實這些麻煩的業務,都是那位阿伯要求的,然後那個小孩,也是阿伯的孫子。

https://ithelp.ithome.com.tw/upload/images/20200913/20123396g2DH17TRne.jpg

這就代表超人不能一件一件處理了,他必須全部處理好再一次性地跟阿伯說。

這時候該怎麼辦呢,超人想起今天跟他交接早班的那位晚班同仁跟他說的話。

--
時間回到早上

超人帶著滿滿的朝氣走進店裡,看向遠處緩緩升起的太陽。

璀璨的朝陽暖暖的曬在他的臉上,他知道,今天還有很多的迷途羔羊需要他的幫助,需要像這樣的溫暖。

只有每天都帶著這樣崇高的理想,才能說服他這個金錢至上的社會還是有救的。

不過,做著這樣偉大的工作,到底幾時才能不領最低薪資呢。

大概還要很久吧,超人自嘲的笑了笑,收起稍微低落的情緒,看到準備交班同事正緩步向他走來,正準備大聲的鼓勵他。

「趴...趴密思喔」同事氣咽聲絲的說著。

「你在講甚麼阿」超人疑惑

「趴...趴密思喔!!!!」同事仰天大喊,接著倒地不起...。

整個世界凝重了起來,只剩下遠處疾駛而來的救護車,閃爍著紅藍色的光芒,照耀在昨天大量的進貨上。

--

超人重回憶中醒來,此時的他理解了同事的話

Promise.all([])

好了,就不鬼扯了,當需要處理多個非同步事件時 Promise.all 方法會等待所有的 Promise 都完成( reslove ),或是發生了第一起失敗( rejected )案件時執行下一步,值得注意的是 Promise.all 也會回傳 promise 物件,所以也可以使用 then / catch 處理接下了的事件。

我們把上面所有的非同步事件都創建成 promise 事件,並同時回傳吧。

  function cafe() {
    return new Promise((resolve) => {
      setTimeout(() => resolve("cafe dobe"), 1000);
    });
  }
  function checkOut() {
    return new Promise((resolve) => {
      setTimeout(() => resolve("checkOut done"), 2000);
    });
  }
  function microwave() {
    return new Promise((resolve) => {
      setTimeout(() => resolve("microwave done"), 3000);
    });
  }
  function pointTheWay() {
    return new Promise((resolve) => {
      setTimeout(() => resolve("point done"), 1000);
    });
  }
  function clean() {
    return new Promise((resolve) => {
      setTimeout(resolve("clean done"), 2000);
    });
  }
  function rePack() {
    return new Promise((resolve) => {
      setTimeout(resolve("repack done"), 3000);
    });
  }
  (async function superman(data) {
    try {
      console.log(data);
      const first = microwave();
      const second = cafe();
      const third = clean();
      const fourth = rePack();
      const fifth = checkOut();
      const sixth = pointTheWay();
      const allPromise = await Promise.all([first,second,third,fourth,fifth,sixth,]);
      console.log(allPromise);
    } catch (error) {
      console.log(error);
    }
  })("捶倒資本主義的高牆!");

我們來看畫面如何

https://ithelp.ithome.com.tw/upload/images/20200913/20123396OqhltG356V.png

可以看到,就算時間不一樣他還是同時回傳回來了。

「小夥子,幹得不錯」 那位土裡土氣的阿伯語氣乍然一變,瞬間高大上了起來

「明天開始,去總公司報到吧,我幫你留了一個好位子」

阿伯拿著他的咖啡和維波食品,牽著他的孫子上了一台勞斯萊斯後,伴隨著震耳欲聾的引擎聲消失在轉角。

只留下超人錯愕的臉龐,他轉移視線,看向他左手嶄新的 BMW 鑰匙。

「資本主義萬歲!」


嗨我是 Chris,其實因該看的出來這篇講得很趕,很多觀念跟舉例都是一撇而過,不像以前那樣可以解釋的很詳細,其實這是因為最近寫文的節奏太慢了,在計劃裡早就已經開始帶專案了,為了不讓最後沒有一個好的結尾,所以選擇寫得快一點,希望各位能體諒。

今天講到了 關於同步和非同步的一些相關概念,和一些以前埋很久沒講得坑,希望不會導致篇幅太長,明天將慢慢開始進入系列專案,我們明天見!


上一篇
Day-12 hosting & Lexical Scope & function expression/declaration 雜談總集
下一篇
Day-14 從需求的角度來看-把這隻筆賣給我
系列文
先你一步的菜鳥 - 從 0 開始的前端網頁設計31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言