前言 本文使用 OOP 封裝邏輯,練習多種事件設計,透過「老虎機」範例理解事件驅動程式設計的核心概念。
簡介 程式的流程不靠單一線性執行,而是由物件狀態或外部行為觸發事件,事件觸發相應回呼(處理函式)。
核心概念 
事件(Event):物件狀態改變或操作完成的訊號 
回呼(Callback/Handler):對事件的響應函式 
主動或被動觸發:
主動:呼叫函式執行動作 
被動:事件發生時自動執行回呼 
 
 
例子:
按鈕點擊:onClick 
倒數計時器:onTick 
 
 
 
事件驅動是 callback 的進階抽象,用於多事件、多訂閱者的情境,降低耦合。
簡單練習 以下做一個簡單的「倒數計時」練習,理解「事件」和「回呼」的概念。
建立非同步函式 建立 playground/countdown.js 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  countdown  = (seconds ) => {  let  remaining = seconds;   const  timer = setInterval (() =>  {     remaining--;     if  (remaining <= 0 ) {       console .log ('⏰ Time up!' );       clearInterval (timer);     }   }, 1000 ); }; countdown (3 );
執行腳本。
1 node playground/countdown.js 
經過 3 秒,輸出如下:
提供回呼方法 如果想要在倒數計時結束時通知,就要做一個叫 onTimeUp 的 callback 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const  countdown  = (seconds, onTimeUp ) => {  let  remaining = seconds;   const  timer = setInterval (() =>  {     remaining--;     if  (remaining <= 0 ) {       console .log ('⏰ Time up!' );       clearInterval (timer);       if  (onTimeUp) onTimeUp ();     }   }, 1000 ); }; countdown (3 , () =>  {  console .log ('🎉 Countdown finished!' ); }); 
執行腳本。
1 node playground/countdown.js 
經過 3 秒,輸出如下:
1 2 ⏰ Time up! 🎉 Countdown finished! 
實現中途通知 如果想要進一步在每一秒時通知,就要再做一個 onTick 的 callback 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const  countdown  = ({ seconds, onTick, onTimeUp } ) => {  let  remaining = seconds;   const  timer = setInterval (() =>  {     remaining--;     if  (remaining > 0 ) {       if  (onTick) onTick (remaining);     }     if  (remaining <= 0 ) {       clearInterval (timer);       console .log ('⏰ Time up!' );       if  (onTimeUp) onTimeUp ();     }   }, 1000 ); }; countdown ({  seconds : 3 ,   onTick : (remaining ) =>  console .log (`⏳ ${remaining}  seconds remaining...` ),   onTimeUp : () =>  console .log ('🎉 Countdown finished!' ), }); 
執行腳本。
1 node playground/countdown.js 
經過 3 秒,輸出如下:
1 2 3 4 ⏳ 2 seconds remaining... ⏳ 1 seconds remaining... ⏰ Time up! 🎉 Countdown finished! 
事件/回呼 vs async/await 建立 playground/countdownAsync.js 檔。
1 2 3 4 5 6 7 8 9 const  delay  = (ms ) => new  Promise (resolve  =>setTimeout (resolve, ms));const  countdownAsync  = async  (ms ) => {  await  delay (ms);   console .log ('⏰ Time up!' ); }; await  countdownAsync (3000 );console .log ('🎉 Countdown finished!' );
執行腳本。
1 node playground/countdownAsync.js 
經過 3 秒,輸出如下:
1 2 ⏰ Time up! 🎉 Countdown finished! 
特性 
回呼 / 事件 
async/await 
 
 
支援多 handler 
✅ 
❌(需額外設計) 
 
每秒 tick / 中途通知 
✅ 
❌(需額外迴圈和 await) 
 
寫法線性可讀 
❌(callback 可能巢狀) 
✅ 
 
建立專案 建立專案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 npm create vite@latest > npx > create-vite │ ◇  Project name: │  event-driven-slot-machine │ ◇  Select a framework: │  Vue │ ◇  Select a variant: │  JavaScript │ ◇  Scaffolding project in  
實作類別 屬性指派式 把事件 handler 直接指派給物件屬性。
特點:
舊式 DOM API / 早期 JS 常見:像 element.onclick = ...。 
每個事件只保留一個 handler,新的指派會覆蓋舊的 handler。 
簡單易懂,但缺乏多重訂閱能力。 
 
建立 lib/OldSlotMachine.js 檔案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class  OldSlotMachine  {  constructor (reels = 3 , symbols = ['7️⃣' , '🍒' , '🍋' , '🍊' , '🍇' , '🍌' , '🍓' , '🔔' ] ) {     this .reels  = reels;             this .symbols  = symbols;         this .currentReels  = Array (this .reels ).fill (null );          this .onReset  = null ;     this .onSpinStart  = null ;     this .onReelStop  = null ;     this .onAllReelsStop  = null ;     this .onWin  = null ;     this .onLose  = null ;     this .onJackpot  = null ;   }      reset (     this .currentReels  = Array (this .reels ).fill (null );     if  (this .onReset ) this .onReset ();   }      spinReel (     const  idx = Math .floor (Math .random () * this .symbols .length );     return  this .symbols [idx];   }      spin (     if  (this .onSpinStart ) this .onSpinStart ();     this .currentReels  = [];          for  (let  i = 0 ; i < this .reels ; i++) {       const  symbol = this .spinReel ();       this .currentReels .push (symbol);       if  (this .onReelStop ) this .onReelStop (i + 1 , symbol);     }     if  (this .onAllReelsStop ) this .onAllReelsStop (this .currentReels );          const  [first] = this .currentReels ;     const  win = this .currentReels .every (s  =>     if  (!win) {       if  (this .onLose ) this .onLose (this .currentReels );       return ;     }     if  (first === '7️⃣' ) {       if  (this .onJackpot ) this .onJackpot (this .currentReels );       return ;     }     if  (this .onWin ) this .onWin (this .currentReels );   } } export  default  OldSlotMachine ;
建立 lib/demoOldSlotMachine.js 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  OldSlotMachine  from  './OldSlotMachine.js' ;const  machine = new  OldSlotMachine ();machine.onSpinStart  = () =>  console .log ('Pull the lever! Start spinning!' ); machine.onReelStop  = (i, symbol ) =>  console .log (`Reel ${i}  stopped: ${symbol} ` ); machine.onAllReelsStop  = reels  =>console .log ('All reels stopped!' ); machine.onWin  = reels  =>console .log (`You win! Reels: ${reels} ` ); machine.onLose  = reels  =>console .log (`You lose! Reels: ${reels} ` ); machine.onJackpot  = reels  =>console .log (`JACKPOT!!! Reels: ${reels} ` ); machine.onReset  = () =>  console .log ('Reels have been reset!' ); machine.reset (); machine.spin (); 
執行腳本。
1 node lib/demoOldSlotMachine.js 
輸出結果如下:
1 2 3 4 5 6 7 Reels have been reset! Pull the lever! Start spinning! Reel 1 stopped: 🍊 Reel 2 stopped: 🍒 Reel 3 stopped: 🍌 All reels stopped! You lose! Reels: 🍊,🍒,🍌 
註冊函式式 呼叫一個方法,將 handler 註冊到事件列表中。
特點:
可以註冊多個 handler,互不覆蓋。 
類似 DOM 現代 API 的 addEventListener。 
適合 OOP 封裝的事件驅動架構。 
 
建立 lib/SlotMachine.js 檔案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class  SlotMachine  {  constructor (reels = 3 , symbols = ['7️⃣' , '🍒' , '🍋' , '🍊' , '🍇' , '🍌' , '🍓' , '🔔' ] ) {     this .reels  = reels;             this .symbols  = symbols;         this .events  = {};             }      on (eventName, callback ) {     if  (!this .events [eventName]) this .events [eventName] = [];     this .events [eventName].push (callback);   }      emit (eventName, ...args ) {     (this .events [eventName] || []).forEach (cb  =>cb (...args));   }      reset (     this .currentReels  = Array (this .reels ).fill (null );     this .emit ('reset' );   }      spinReel (     const  idx = Math .floor (Math .random () * this .symbols .length );     return  this .symbols [idx];   }      spin (     this .emit ('spinStart' );     this .currentReels  = [];          for  (let  i = 0 ; i < this .reels ; i++) {       const  symbol = this .spinReel ();       this .currentReels .push (symbol);       this .emit ('reelStop' , i + 1 , symbol);     }     this .emit ('allReelsStop' , this .currentReels );          const  [first] = this .currentReels ;     const  win = this .currentReels .every (s  =>     if  (!win) {       this .emit ('lose' , this .currentReels );       return ;     }     if  (first === '7️⃣' ) {       this .emit ('jackpot' , this .currentReels );       return ;     }     this .emit ('win' , this .currentReels );   } } export  default  SlotMachine ;
建立 lib/demoSlotMachine.js 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  SlotMachine  from  './SlotMachine.js' ;const  machine = new  SlotMachine ();machine.on ('spinStart' , () =>  console .log ('Pull the lever! Start spinning!' )); machine.on ('reelStop' , (i, symbol ) =>  console .log (`Reel ${i}  stopped: ${symbol} ` )); machine.on ('allReelsStop' , reels  =>console .log ('All reels stopped!' )); machine.on ('win' , reels  =>console .log (`You win! Reels: ${reels} ` )); machine.on ('lose' , reels  =>console .log (`You lose! Reels: ${reels} ` )); machine.on ('jackpot' , reels  =>console .log (`JACKPOT!!! Reels: ${reels} ` )); machine.on ('reset' , () =>  console .log ('Reels have been reset!' )); machine.reset (); machine.spin (); 
執行腳本。
1 node lib/demoSlotMachine.js 
輸出結果如下:
1 2 3 4 5 6 7 Reels have been reset! Pull the lever! Start spinning! Reel 1 stopped: 🍓 Reel 2 stopped: 7️⃣ Reel 3 stopped: 🍇 All reels stopped! You lose! Reels: 🍓,7️⃣,🍇 
方法鏈 如果希望可以連續註冊多個事件回呼,可以讓 on 方法回傳自身(this),以實現方法鏈(method chaining)。
1 2 3 4 5 6 7 on (eventName, callback ) {  if  (!this .events [eventName]) this .events [eventName] = [];   this .events [eventName].push (callback);   return  this ; } 
實作介面 建立 src/components/SlotMachine.vue 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 <script  setup > import  SlotMachine  from  '../../lib/SlotMachine' ;const  props = defineProps ({  reels : {     type : Number ,     default : undefined ,   },   symbols : {     type : Array ,     default : undefined ,   },   onSpinStart : {     type : Function ,     default : () =>  {},   },   onReelStop : {     type : Function ,     default : () =>  {},   },   onAllReelsStop : {     type : Function ,     default : () =>  {},   },   onWin : {     type : Function ,     default : () =>  {},   },   onLose : {     type : Function ,     default : () =>  {},   },   onJackpot : {     type : Function ,     default : () =>  {},   },   onReset : {     type : Function ,     default : () =>  {},   }, }); const  machine = new  SlotMachine (props.reels , props.symbols );machine.on ('spinStart' , () =>  props.onSpinStart ); machine.on ('reelStop' , (i, symbol ) =>  props.onReelStop (i, symbol)); machine.on ('allReelsStop' , reels  =>onAllReelsStop (reels)); machine.on ('win' , reels  =>onWin (reels)); machine.on ('lose' , reels  =>onLose (reels)); machine.on ('jackpot' , reels  =>onJackpot (reels)); machine.on ('reset' , () =>  props.onReset ()); const  handleClick  = (  machine.reset ();   machine.spin (); }; </script > <template >   <button       @click ="handleClick"    >     Spin   </button >  </template > 
建立 src/components/SlotMachineBasic.vue 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <script  setup > import  { ref } from  'vue' ;import  SlotMachine  from  './SlotMachine.vue' ;const  resultRef = ref ();const  prizeRef = ref ();const  awardPrize  = (amount ) => {};const  playAnimation  = (const  notifyLeaderboard  = (const  handleWin  = reels => {  const  amount = 100 ;   awardPrize (amount);   notifyLeaderboard ();   resultRef.value .textContent  = `You win! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; const  handleLose  = reels => {  const  amount = 0 ;   resultRef.value .textContent  = `You lose! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; const  handleJackpot  = reels => {  const  amount = 1000 ;   awardPrize (amount);   notifyLeaderboard ();   playAnimation ();   resultRef.value .textContent  = `JACKPOT!!! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; </script > <template >   <div  class ="card" >      <h3 > The Pro Slot Machine</h3 >      <SlotMachine         :reels ="3"        :symbols ="['7️⃣', '🍒', '🍋', '🍊', '🍇']"        :on-win ="handleWin"        :on-lose ="handleLose"        :on-jackpot ="handleJackpot"      />     <h4 >        <span  ref ="resultRef" >   </span >        <br >        <span  ref ="prizeRef"  style ="margin-left: 0.5em" >   </span >      </h4 >    </div >  </template > 
建立 src/components/SlotMachinePro.vue 檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <script  setup > import  { ref } from  'vue' ;import  SlotMachine  from  './SlotMachine.vue' ;const  resultRef = ref ();const  prizeRef = ref ();const  awardPrize  = (amount ) => {};const  playAnimation  = (const  notifyLeaderboard  = (const  handleWin  = reels => {  const  amount = 500 ;   awardPrize (amount);   notifyLeaderboard ();   resultRef.value .textContent  = `You win! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; const  handleLose  = reels => {  const  amount = 0 ;   resultRef.value .textContent  = `You lose! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; const  handleJackpot  = reels => {  const  amount = 5000 ;   awardPrize (amount);   notifyLeaderboard ();   playAnimation ();   resultRef.value .textContent  = `JACKPOT!!! Reels: ${reels} ` ;   prizeRef.value .textContent  = `+${amount}  coins` ; }; </script > <template >   <div  class ="card" >      <h3 > The Pro Slot Machine</h3 >      <SlotMachine         :reels ="5"        :symbols ="['7️⃣', '🍒', '🍋', '🍊', '🍇', '🍌', '🍓']"        :on-win ="handleWin"        :on-lose ="handleLose"        :on-jackpot ="handleJackpot"      />     <h4 >        <span  ref ="resultRef" >   </span >        <br >        <span  ref ="prizeRef"  style ="margin-left: 0.5em" >   </span >      </h4 >    </div >  </template > 
修改 src/App.vue 檔。
1 2 3 4 5 6 7 8 9 10 <script  setup > import  SlotMachineBasic  from  './components/SlotMachineBasic.vue' ;import  SlotMachinePro  from  './components/SlotMachinePro.vue' ;</script > <template >   <SlotMachineBasic  />    <SlotMachinePro  />  </template > 
啟動專案。
程式碼