# 程式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真的是一個神奇的東西,原本排不出想要的樣式,但多調整一下又完成了
>

---
## 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 組件的邏輯與效能。
> 🧠 今天的心得:當初只是照著做沒想到有那麼多眉眉角角,隨著做完筆記也跟著疏通了一些觀念。