前言 本文使用 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 => s === first); 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 => s === first); 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 => props.onAllReelsStop (reels)); machine.on ('win' , reels => props.onWin (reels)); machine.on ('lose' , reels => props.onLose (reels)); machine.on ('jackpot' , reels => props.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 >
啟動專案。
程式碼