# 程式30天挑戰 > 為了讓自己累積能夠獨立做出來的自信於是開始了30天挑戰 > 連續30天每天push一點點,重點不是【夠強】而是一直有進步 > 筆記會以教朋友寫程式的口吻撰寫,歡迎大家參考與指教 > 這份筆記希望讓跟我一樣懷疑自己能不能轉職的人,也能獲得相信自己的骨氣 --- ## 🗓️ 程式 30 天挑戰進度 | 日期 | Day | 主題 | | ----- | ----- | ----------------------------------- | | 04/21 | Day 1 | 開發環境建立 | | 04/22 | Day 2 | 主頁路由按鈕+複習useState,做計數器 | | 04/23 | Day3 | 設計資料結構、陣列產生按鈕 | | 04/24 | Day4 | 互動功能進階(計算機) | | 04/25 | Day5 | 計算機UI設計 | | 04/26 | Day6 | 面試題練習(受控元件vs不受控元件) | | 04/27 | Day7 | 小總整理日 (補齊Day5、6筆記) | | 04/28 | Day8 | 計算機功能 | | 04/29 | Day9 | 支援鍵盤輸入 | | 04/30 | Day10 | 休息日 | | 05/01 | Day11 | JAVA倒敘練習 | | 05/03 | Day12 | 日語會話練習 | | 05/04 | Day13 | 小整理日(計算機功能筆記) | | | | | --- ## day1 安裝程式+專案初始化 下載[vs code](https://code.visualstudio.com/)和[node.js](https://nodejs.org/zh-tw/download) node.js跑完安裝程序後在終端機輸入`node -v npm -v`如果出現版本號即表示安裝成功 vs code安裝好後可以安裝套件改成中文版(減少心理壓力 哈哈) 安裝教學可參考[卡斯伯老師的筆記](https://www.casper.tw/development/2019/12/01/vscode-chinese/#google_vignette) 安裝好之後就可以來將檔案初始化啦~ **step1.** 打開你的終端機 並且輸入 `npm create vite@latest` 安裝vite **step2.** 這次專案的基本資訊是選擇 React + JavaScript,然後依照指示繼續(就一路 Enter~) **step3.** 輸入 **`cd 專案資料夾、npm install、npm run dev`** 打開伺服器看環境是否建立成功 **step4.** 🎯小任務時間:將預設內容清除,寫一句專屬於你的歡迎詞 **step5.** 若伺服器畫面內容有跟著更動,就完成囉!! 今天也是很棒的一天 🚩**今天的程式碼統整:** | 功能 | 程式碼 | 打在哪? | | ---------- | --------------------------------------- | ------- | | 確認node.js成功安裝 | node -v npm -v | 終端機 | | 安裝vite | npm create vite@latest | 終端機 | | 打開伺服器 | cd 專案資料夾、npm install、npm run dev | 終端機 | 🔔 **給不小心出錯的我們** 1. node.js 不要直接從官網首頁下載,會變成壓縮檔很麻煩,從下載頁面(我上面貼的連結)下載會最順利,另外下載任何程式檔案時盡量都下載LTS版本比較不會出問題 1. 下載好之後要重新打開終端機再下`node -v npm -v` 才會吃到新的資料 1. 不小心輸錯指令的時候只要重輸一次就好囉,不過vite現在沒有支援回上一步的功能,所以不小心輸錯就要重新下`npm create vite@latest`囉 > 🧠 **今天的心得**:原本以為從安裝程式開始建置環境會很麻煩,沒想到很快就完成了,超級讚! > > 🕐**明天目標**:複習useState,製作計數器 --- ## day2 主頁路由按鈕+計數器練習 > 為了讓後續挑戰可以快速從主頁按鈕進入今天先做了路由按鈕 ### day2-1 路由按鈕 **step1.** 在終端機輸入 **npm i react-router-dom** 下載路由套件 **step2.** 設置元件 **src>days>Day2.jsx** **step3.** 在days資料夾下新增index.jsx統一匯出所有元件 **step4.** 設置路由 新增 **src>routes>index.jsx** ``` import App from "../App"; import { Day2 } from "../days/index.jsx"; import { createBrowserRouter, createHashRouter } from "react-router-dom"; const routes = [{ path: "/", element: <App />, }, { path: "/day2", element: <Day2 /> }]; {/* 判斷是否為開發狀態決定使用哪一種路由*/} const router = import.meta.env.MODE === "development" ? createBrowserRouter(routes) : createHashRouter(routes); export default router; ``` **step5.** 在 main.jsx **import { RouterProvider } from 'react-router-dom'**,並將原本的<App/> 改為 **<RouterProvider router={router}></RouterProvider>** **step6.** 在App.jsx **import Outlet, useNavigate**,並使用useNavigate設定按鈕路由, ``` {/* 記得要import */} import { Outlet, useNavigate } from 'react-router-dom' import './App.css' function App() { {/* 宣告useNavigate方法 */} const navigate = useNavigate(); return ( <> <h1>React30天挑戰!</h1> <button type="button" onClick={()=>navigate('/day2') }>day2</button> {/* 要記得加上()=> 不然會直接執行變成無窮迴圈喔 */} <Outlet></Outlet> </> ) } export default App ``` 完成以上6步驟,就設定好day2的路由啦🎉 🔔 **給不小心出錯的我們** 1. 開始設定路由前,先確定有下載過**react-router-dom** 避免電腦找不到而報錯 2. Copilot 很好用,但它有時會「忘了幫你 import」~看到報錯先別慌,第一步就是檢查 import! 如:Outlet, useNavigate ...等 3. import.meta.env.MODE 的MODE 一定要記得全大寫(固定寫法) --- ### day2-2 計數器 ### 🧪 畫面預期效果 - 顯示當前數字與點擊次數 - 三個按鈕(+1、+10、+20)可以點擊加數字 - 數字超過 100 後顯示達成訊息 **step1.** ``` //宣告 前面為變數,後面是控制方法,useState要記得import const [number,setNumber] = useState(0); const [count,setCount] = useState(0); ``` **step2.** ``` return ( <> <h1>Day 2 計數器練習</h1> {/*文字顯示 當前加總的數字及點擊總次數 */} <p>目前的數字{number}</p> <p>目前點擊次數{count}</p> {/* 點擊按鈕時讓原本的數字+n(自己設定) ,count+1(多點擊一次)*/} <button onClick={()=>{setNumber(number+1);setCount(count+1)}}>按我+1</button> <button onClick={()=>{setNumber(number+10);setCount(count+1)}}>按我+10</button> <button onClick={()=>{setNumber(number+20);setCount(count+1)}}>按我+20</button> </> {/*當number>100時出現此文字*/} {number >100 && <p>你花了{count}次,讓數字大於100</p>} ) ``` **step3.** 將重複的方法抽出來做成函式 ``` const handleClick = (num)=>{ setNumber(number+num); setCount(count+1); } //button寫法改為,10為自訂數字 <button onClick={()=>{handleClick(10)}}> ``` 完整版: ``` import { useState } from "react"; export default function Day2() { const [number,setNumber] = useState(0); const [count,setCount] = useState(0); const handleClick = (num)=>{ setNumber(number+num); setCount(count+1); } return ( <> <h1>Day 2 計數器練習</h1> <p>目前的數字{number}</p> <p>目前點擊次數{count}</p> <button onClick={()=>{handleClick(1)}}>按我+1</button> <button onClick={()=>{handleClick(10)}}>按我+10</button> <button onClick={()=>{handleClick(20)}}>按我+20</button> {number >100 && <p>你花了{count}次,讓數字大於100</p>} </> ) } ``` 🔔 **給不小心出錯的我們** 1. onClick{()=>},()=>一定要寫 避免造成無窮迴圈而報錯 2. 改成函式寫法時()內記得填入數字,否則電腦會不知道要加多少而出錯 > 🧠 **今天的心得**:一開始連hook使用時機都看得霧煞煞沒想到現在可以輕鬆自在的用useState寫出計數器了! 回頭看才發現自己原來也走了很遠了! > > 🕐**明天目標**: > - 設計資料結構(例如:day、日期、完成狀態) > - 使用 `.map()` 或其他陣列方法來產生多顆按鈕 **day3新增功能:** 自訂數字 ``` const [customNum,setCustomNum] = useState(0); <input type="number" name="customNum" value={customNum} min="1" onChange={(e)=>setCustomNum(Number(e.target.value))} /> <button onClick={()=>handleClick(customNum)}>確定</button> ``` --- ## day3 使用陣列資料產生按鈕 **step1.** 宣告資料結構(放在App.jsx) ``` const daysData=[ {day:1,label:"Day1:環境建置",path:"" }, {day:2,label:"Day2:計數器練習",path:"/day2"}, {day:3,label:"Day3:陣列產生按鈕",path:""}, {/*後續可再新增*/} ]; ``` **step2.** 使用.map產生按鈕 ``` {/*如果該物件沒有path(!day.path) 則讓按鈕disabled 並且顯示"這一天沒有頁面"*/} {daysData.map((day)=> <button key={day.day} onClick={()=>navigate(day.path)} disabled={!day.path} style={{ margin: '4px', opacity: !day.path ? 0.5 : 1, cursor: !day.path ? 'not-allowed' : 'pointer' }} title={!day.path ? "這一天沒有頁面" : `Day ${day.day}`}>{day.label}</button>)} ``` 🔔 **給不小心出錯的我們** 1. 就算該物件沒有path,也要打上空字串"" 2. disabled很好用 寫法可以記一下**disabled={!day.path}** 1. 要在字串的地方放變數要使用樣板字面值 **`Day ${day.day}`** 1. key要記得使用唯一值 > 🧠 **今天的心得**:今天建立資料也建立得很快,並且透過.map去產生按鈕可以快速生成很方便,是個很好用的方法 > > 🕐**明天目標**: > - 使用者輸入數字後可以「加」或「減」 > - 按下按鈕後清空輸入框 > - 防呆:輸入為空或 0 時不能按 > - Reset 重置所有數字與點擊次數 > - 顯示使用者目前進行了幾次互動 --- ## day4 計算機 **step1.** 設定初始值 ``` const [num1, setNum1] = useState(0); const [num2, setNum2] = useState(0); const [result, setResult] = useState(0); const calculate = [ { id: 1, sign: "+", label: "加" }, { id: 2, sign: "-", label: "減" }, { id: 3, sign: "*", label: "乘" }, { id: 4, sign: "/", label: "除" }, ]; ``` **step2.** 處理運算的方法 ``` const handleCalculate = (num1, num2, sign) => { num1 = Number(num1); num2 = Number(num2); switch (sign) { case "+": return num1 + num2; case "-": return num1 - num2; case "*": return num1 * num2; case "/": return num1 / num2; default: return 0; } } ``` **step3.** 設定兩個數字input ``` <h1>計算機</h1> <input type="number" name="num1" value={num1} onChange={(e) => { setNum1(e.target.value) }} style={{ width: '50px', }} /> <input type="number" name="num2" value={num2} onChange={(e) => { setNum2(e.target.value) }} style={{ width: '50px', }} /> ``` **step4.** 使用.map陣列方法產生按鈕 ``` {calculate.map((item) => <button type="button" key={item.id} onClick={() => item.sign !== "/" ? setResult(handleCalculate(num1,num2,item.sign)) : setResult(num2 ? handleCalculate(num1,num2,item.sign) : "除數不得為0")} > {item.label}</button> )} ``` **step5.** 結果顯示與重置 ``` <p>結果{result}</p> <button type="button" onClick={()=>setResult(0)}>Reset</button> ``` 🔔 **給不小心出錯的我們** 1. 寫新元件的時候要記得 **return**還有到index匯出、新增路由 2. 三元運算子很好用! 可以省去複雜的if、else結構 > 🧠 **今天的心得**:三元運算子很好用 > > 🕐**明天目標**: > - 改善畫面設計 > - 連續加總功能 --- ## Day5 計算機介面 **step1.** 使用陣列存放按鈕 ``` //使用二維陣列建立數字鍵盤,每個子陣列都是一行按鈕 const numbers =[ [7,8,9], [4,5,6], [1,2,3], [0,"c","="] ] //運算符按鈕 const operators = [ {index:0 ,sign:"+"}, {index:1 ,sign:"-"}, {index:2 ,sign:"*"}, {index:3 ,sign:"/"}, ] ``` **step2.** 使用巢狀.map()產生數字鍵盤 ``` {/* 使用巢狀.map()產生數字鍵盤,外層處理行,內層處理每顆按鈕 */} {numbers.map((row,rowIndex)=>( <div key={rowIndex} className="d-flex gap-1"> {row.map((label)=>( <button type="button" className="btn btn-secondary m-1" key={label} >{label}</button> ))} </div> ))} ``` **step3.** 使用.map()產生運算符按鈕 ``` { operators.map((sign)=>( <button type="button" className="btn btn-secondary" key={sign.index}>{sign.sign}</button> ))} ``` **step4.** 加上flex排版(完整版程式碼) > 最外層從 flex-column 換成 **flex-row**, > 才能讓數字區和運算子區並排 ``` <><h1>Day5 計算機進階</h1> {/* 最上面顯示輸入與結果的地方 */} <div className="input" >{input}</div> <br /> {/* 使用巢狀.map()產生數字鍵盤,外層處理行,內層處理每顆按鈕 */} <div className="d-flex flex-row align-items-center justify-content-center gap-2 mt-1"> <div className="d-flex flex-column"> {numbers.map((row, rowIndex) => ( <div key={rowIndex} className="d-flex gap-1"> {row.map((label) => ( <button type="button" className="btn btn-secondary m-1" key={label} >{label}</button> ))} </div> ))} </div> {/* 使用.map()產生運算子按鈕 */} <div className="d-flex flex-column gap-2"> {operators.map((sign) => ( <button type="button" className="btn btn-secondary" key={sign.index}>{sign.sign}</button> ))} </div> </div> </> ``` 🔔 **給不小心出錯的我們** 1. 巢狀.map()略為複雜要再多看幾次 2. flex排版要多留意排版方法 > 🧠 **今天的心得**:感覺這幾天寫下來對.map()的方法更清楚了,flex真的是一個神奇的東西,原本排不出想要的樣式,但多調整一下又完成了 > ![螢幕擷取畫面 2025-04-26 001426](https://hackmd.io/_uploads/S1HwJCo1gx.png) --- ## Day6 面試題練習(受控元件vs非受控元件) > 在此感謝我的AI助手小暖,陪我無壓力的練習面試題 **受控元件:** 使用 useState 即時偵測使用者輸入的內容。 但因為使用者每打一次字就會觸發一次 render, 當輸入速度很快、或者表單規模較大時,容易造成網頁效能負擔。 可以透過 debounce 或 throttle 的方式來延遲或限制 re-render 的條件與頻率,提升效能。 **範例:** ``` // 受控元件例子-使用useState控制 const [value, setValue] = useState(""); <input value={value} onChange={(e) => setValue(e.target.value)} /> ``` **非受控元件:** 通常會使用在整合第三方套件,或希望降低 re-render 次數的場景。 不會即時偵測輸入內容,而是透過 useRef 直接取得 DOM 的資料, 等到需要的時候一次性讀取。 **範例:** ``` // 非受控元件例子-使用useRef控制 const inputRef = useRef(); <input ref={inputRef} /> ``` 延伸參考: [iT邦幫忙](https://ithelp.ithome.com.tw/articles/10297302#:~:text=%E9%9D%9E%E5%8F%97%E6%8E%A7%E5%85%83%E4%BB%B6%20%E7%9A%84%E5%84%AA%E9%BB%9E%E6%98%AF%E9%9D%9E%E5%B8%B8%E7%B4%94%E7%B2%B9%20%28%E4%B8%8D%E6%B7%BB%E5%8A%A0%E4%BB%BB%E4%BD%95%E5%A4%96%E4%BE%86%E7%89%A9%E8%B3%AA%29%EF%BC%8C%E5%8F%AA%E8%A6%81%E6%8A%8A%E5%80%BC%E4%B9%96%E4%B9%96%E6%94%BE%E5%9C%A8%E8%87%AA%E5%B7%B1%E8%BA%AB%E4%B8%8A%EF%BC%8C%E4%B8%8D%E7%94%A8%E9%9A%A8%E6%99%82%E6%9B%B4%E6%96%B0%E5%88%B0%20state%20%E4%B8%8A%EF%BC%8C%E5%9B%A0%E6%AD%A4%20%E6%95%B4%E9%AB%94%E6%95%88%E8%83%BD%E4%BE%86%E8%AA%AA%E6%98%AF%E8%BC%83%E5%A5%BD%E7%9A%84%E3%80%82,%E5%8F%97%E6%8E%A7%E5%85%83%E4%BB%B6%20%E7%9A%84%E5%84%AA%E9%BB%9E%E6%98%AF%EF%BC%8C%20%E5%B0%8D%E6%96%BC%E8%A1%A8%E5%96%AE%E7%9A%84%E6%8E%8C%E6%8E%A7%E8%83%BD%E5%8A%9B%E6%8F%90%E9%AB%98%EF%BC%8C%E5%9B%A0%E7%82%BA%E9%9A%A8%E6%99%82%E5%8F%AF%E4%BB%A5%E7%9F%A5%E9%81%93%E6%AF%8F%E5%80%8B%E6%AC%84%E4%BD%8D%E5%8D%B3%E6%99%82%E7%9A%84%E6%95%B8%E5%80%BC%EF%BC%8C%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%81%9A%E5%88%B0%E5%83%8F%E6%98%AF%E3%80%8CA%20%E6%AC%84%E4%BD%8D%E5%A6%82%E6%9E%9C%E9%81%B8%E4%BA%86%E4%BB%80%E9%BA%BC%EF%BC%8CB%20%E6%AC%84%E4%BD%8D%E5%B0%B1%E6%80%8E%E6%A8%A3%E3%80%8D%E9%80%99%E7%A8%AE%20UI%20%E6%96%B9%E9%9D%A2%E7%9A%84%E4%BA%A4%E4%BA%92%E9%97%9C%E4%BF%82%E3%80%82)、[React文件](https://zh-hant.legacy.reactjs.org/docs/uncontrolled-components.html) > 🧠 **今天的心得**:發現自己還有很多理論上的知識沒有理解的很透徹,之後要再多花一些時間認真準備面試題 --- Day7: 小總整理日 今日內容:整理並補齊 Day5、Day6 的筆記。 > 🧠 **今天的心得**:雖然今天沒有練習新技能,也沒有推進新的挑戰,但我喜歡這樣用自己的步調,一步一步穩穩前進的感覺。 比起一次爆衝、後繼無力,慢慢累積的成就感,反而更讓我想繼續走下去。 --- ### Day8 計算機功能 > 上次只有做出計算機按鈕但無法點擊進行運算,今天我們要做的事情是讓計算機可以真的運作起來 **step1.** 先宣告input、isResult的useState存放狀態 ``` const [input, setInput] = useState(""); const [isResult, setIsResult] = useState(false); ``` **step2.** 分別撰寫四則運算的函式 (add、substract、multiply、divide ) ``` const add = (a, b) => a + b; const substract = (a, b) => a - b; const multiply = (a, b) => a * b; const divide = (a, b) => { if (b === 0) { //除式要多判斷除數不得為0 //如果除數為0,則顯示錯誤訊息,並返回"錯誤" alert("除數不能為0"); return "錯誤"; } return a / b; }; ``` **step3.** 建立函式尋找運算符 (findOperator) > 用.find去尋找第一個符合 『input裡包含的運算符』這個條件的資料並回傳 ``` // 回傳找到的第一個符號的 sign 屬性(如果沒有則為 undefined) const findOperator = (input) => { return operators.find((op) => input.includes(op.sign))?.sign; }; ``` **step4.** 建立函式處理運算過程 (handlePress) ``` //將數字、運算符傳入計算函式,並根據運算符進行計算 //最後返回計算結果 const handleCalculate = (num1, num2, operator) => { switch (operator) { case "+": return add(num1, num2); case "-": return substract(num1, num2); case "*": return multiply(num1, num2); case "/": return divide(num1, num2); default: return 0; } }; ``` **step4.** 建立函式處理運算事件邏輯 **step4-1.** 宣告運算符陣列,並取得最後一個字元 ``` const signs = ["+", "-", "*", "/"]; const lastChar = input[input.length - 1]; ``` **step4-2.** 按下等號時進行運算 ``` if (label === "=") { //使用findOperator函式尋找運算符 //如果沒有找到運算符,則顯示錯誤訊息 const operator = findOperator(input); if (!operator) { alert("請輸入正確算式"); return; } //如果找到運算符,則將字串分割成兩個數字 //並將兩個數字與運算符傳進入handleCalculate函式進行計算,並儲存成result const [num1, num2] = input.split(operator); const result = handleCalculate(Number(num1), Number(num2), operator); //處理除數為0的情況,在divide函式中我們有將除數為0的情況回傳為 "錯誤" //如果resuult為"錯誤",則跳出錯誤提示 if (result === "錯誤") { alert("除數不能為0"); //尋找運算符在字串中的最後一個index位置 const operatorIndex = input.lastIndexOf(operator); //並將運算符前的字串取出,將input重新設置為這個字串 //為了保留之前正確輸入的數字,只清除運算符與之後的錯誤部分 const newInput = input.slice(0, operatorIndex); setInput(newInput); //跳出 if (label === "=")這段判斷式 return; } //將計算設為字串,設為input值 setInput(result.toString()); //設置isResult為true,表示計算結果已經顯示 setIsResult(true); //跳出 if (label === "=")這段判斷式 return; } ``` **step4-3.** 數字處理 ``` //檢查目前按下的是不是數字/\d/為正規表達式的數字表示方法 if (/\d/.test(label)) { //如果是數字鍵且已有結果,並且最後的字元不是運算符 if (isResult && !signs.includes(lastChar)) { // 計算結果後重新開始 setInput(label); } else { //否則將數字繼續接在 input 後面 setInput(prev => prev + label); } //將isResult設為false,表示還沒產生結果 setIsResult(false); return; } ``` **step4-4.** 運算符處理 ``` //判斷目前按的鍵是不是運算符 if (signs.includes(label)) { //如果input的最後一個字元是運算符號,就會將最後一個字元刪除,並換成目前按下的鍵 if (signs.includes(lastChar)) { setInput(prev => prev.slice(0, -1) + label); } else { //如果最後一個字元不是運算符,則會直接將目前按下的鍵加在input後面 setInput(prev => prev + label); } //將isResult設為false,表示還沒產生結果 setIsResult(false); return; } ``` **step4-5.** 處理清空鍵與倒退鍵 ``` // 清除輸入 //如果目前按下的是"c"或"C",則會將input清空,並將isResult設為false if (label === "c" || label === "C") { setInput(""); setIsResult(false); return; } // 倒退鍵 //如果目前按下的是 "←",則會清除最後一個字元 if (label === "←") { setInput(prev => prev.slice(0, -1)); setIsResult(false); return; } ``` ### **完整版** ``` const handlePress = (label) => { const signs = ["+", "-", "*", "/"]; const lastChar = input[input.length - 1]; if (label === "=") { const operator = findOperator(input); if (!operator) { alert("請輸入正確算式"); return; } const [num1, num2] = input.split(operator); const result = handleCalculate(Number(num1), Number(num2), operator); if (result === "錯誤") { alert("除數不能為0"); const operatorIndex = input.lastIndexOf(operator); const newInput = input.slice(0, operatorIndex); setInput(newInput); return; } setInput(result.toString()); setIsResult(true); return; } if (/\d/.test(label)) { if (isResult && !signs.includes(lastChar)) { setInput(label); } else { setInput(prev => prev + label); } setIsResult(false); return; } if (signs.includes(label)) { if (signs.includes(lastChar)) { setInput(prev => prev.slice(0, -1) + label); } else { setInput(prev => prev + label); } setIsResult(false); return; } if (label === "c" || label === "C") { setInput(""); setIsResult(false); return; } if (label === "←") { setInput(prev => prev.slice(0, -1)); setIsResult(false); return; } }; ``` > 🧠 **今天的心得**:一開始只是因為compilot的自動提示一起興起寫了計算機的程式,沒想到比想像中還複雜很多,但也因此學到了不少,像是處理陣列、字串、判斷式等等的知識 --- ### Day9 計算機(支援鍵盤) > 繼承Day8內容,再新增支援鍵盤輸入功能 **step1.** 使用useEffect處理按鍵事件 ``` useEffect(() => { const handleKeyDown = (e) => { const key = e.key; if (/\d/.test(key)) { // 如果是數字 0-9 handlePress(key); } else if (["+", "-", "*", "/"].includes(key)) { handlePress(key); } else if (key === "Enter") { handlePress("="); } else if (key.toLowerCase() === "c") { handlePress("c"); } else if (key === "Backspace") { handlePress("←"); // 自訂倒退鍵(我們下一步會處理這個) } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [handlePress]); ``` **step1-1.** 撰寫handleKeyDown函式 ``` const handleKeyDown = (e) => { const key = e.key; if (/\d/.test(key)) { // 如果是數字 0-9 handlePress(key); } else if (["+", "-", "*", "/"].includes(key)) { handlePress(key); } else if (key === "Enter") { handlePress("="); } else if (key.toLowerCase() === "c") { handlePress("c"); } else if (key === "Backspace") { handlePress("←"); // 自訂倒退鍵 } }; ``` 這段程式會根據使用者按下的鍵盤按鍵內容(e.key),對應呼叫 handlePress: **step1-2.** 加入監聽事件 ``` window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; ``` 當按下鍵盤時觸發handleKeyDown函式 並在處理完後使用`return`將其移除避免佔用記憶體空間 SPA(單頁應用)中切換頁面時,也可以避免監聽未移除產生錯頁邏輯 **step1-3.** 當handlePress函式更新,就要觸發這段useEffect `[handlePress]` useEffect([...]) 中的陣列是依賴清單。 當裡面的變數(這裡是handlePress)變動時,重新執行uesEffect的邏輯 確保最新的變數被正確綁定,跟著變數進行更新,不會用到舊邏輯 **step2.** 在handlePress加上useCallBack > 為了避免每次re-render都產生一份新的函式,所以使用useCallback 將它記憶起來。 > 只有當useCallBack裡的依賴變數改變時函式才會被重新建立 ``` const handlePress = useCallback((label) => { //原始函式內容,參照day5 },[input, isResult, operators]); ``` * useCallback 適合記憶「函式」 * 若該函式會被 useEffect 或其他 hook 使用,就應該透過 useCallback 穩定引用 * 依賴陣列中一樣是放入在useCallback中有用到的變數條件 **step3.** 使用useMemo記住運算子的陣列值 ``` const operators =useMemo(()=> [ { index: 0, sign: "+" }, { index: 1, sign: "-" }, { index: 2, sign: "*" }, { index: 3, sign: "/" }, { index: 4, sign: "←" }, ] ,[] ) ``` 因為operators是一組固定不變的值,如果直接放在元件裡面,每次 re-render 都會被重新建立,導致依賴它的函式(這裡是handlePress)會不斷被重建,因此必須要使用useMemo記憶起來,確保他在整個生命週期中都不會變 > useMemo 是記憶「資料」,useCallback 是記憶「函式」,兩者常搭配使用來穩定 React 組件的邏輯與效能。 > 🧠 今天的心得:當初只是照著做沒想到有那麼多眉眉角角,隨著做完筆記也跟著疏通了一些觀念。