[{"data":1,"prerenderedAt":4135},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-17":1210},[4,23,35,46,57,68,80,91,102,114,126,138,149,160,171,182,193,204,215,226,237,248,258,268,278,288,298,308,319,330,341,354,368,380,390,400,411,421,431,441,451,461,471,481,491,501,511,521,531,541,551,561,571,581,591,601,611,621,631,641,651,661,675,691,704,719,731,742,752,762,772,782,792,802,812,822,832,842,852,862,871,881,891,901,911,921,931,941,951,961,971,980,990,1000,1010,1020,1036,1050,1062,1073,1083,1095,1107,1121,1134,1145,1156,1167,1180,1191,1201],{"path":5,"title":6,"description":7,"date":8,"updated":9,"tags":10,"categories":14,"draft":15,"wordCount":16,"readingTime":17,"canonical":18,"series":19},"\u002Fithome-2026-day-30","Day 30：Angular Router Providers 與生命週期管理，路由離開後 Service 會銷毀嗎？","平常在 Angular 中，我們可以透過 providers 決定 Service 的注入範圍。例如註冊在 Root，讓整個 Application 共用同一個實例；或註冊在 Component，讓 Service 隨著…","2026-10-14T14:46:39+08:00",null,[11,12,13],"iThome 鐵人賽","2026 鐵人賽","Angular",[11,13],false,7227,7,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424222",{"name":20,"url":21,"day":22},"2026 iThome 鐵人賽：Angular 22 Signal 進化論","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939",30,{"path":24,"title":25,"description":26,"date":27,"updated":9,"tags":28,"categories":29,"draft":15,"wordCount":30,"readingTime":31,"canonical":32,"series":33},"\u002Fithome-2026-day-29","Day 29：Angular 也有 VueUse？使用 Signality 整合常見 Browser API","在 Vue 生態系中，VueUse 提供許多 Composables，讓我們可以快速使用瀏覽器功能，例如監聽網路狀態、保存使用者設定，或偵測元素是否進入可視範圍。","2026-10-13T23:14:18+08:00",[11,12,13],[11,13],6387,6,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10424070",{"name":20,"url":21,"day":34},29,{"path":36,"title":37,"description":38,"date":39,"updated":9,"tags":40,"categories":41,"draft":15,"wordCount":42,"readingTime":31,"canonical":43,"series":44},"\u002Fithome-2026-day-28","Day 28：Angular 動態建立 Component，NgComponentOutlet 與 ViewContainerRef","平常在 Angular 中使用 Component，通常會直接在 Template 裡面宣告：","2026-10-12T17:27:41+08:00",[11,12,13],[11,13],6565,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423641",{"name":20,"url":21,"day":45},28,{"path":47,"title":48,"description":49,"date":50,"updated":9,"tags":51,"categories":52,"draft":15,"wordCount":53,"readingTime":17,"canonical":54,"series":55},"\u002Fithome-2026-day-27","Day 27：NgRx Store vs SignalStore，從 Action、Reducer 到直接更新 State","前幾天我們使用 NgRx SignalStore 實作購物車，透過 withState() 管理商品資料、withComputed() 計算總金額，再利用 withMethods() 與 patchState() 更新 …","2026-10-11T20:06:41+08:00",[11,12,13],[11,13],7308,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423362",{"name":20,"url":21,"day":56},27,{"path":58,"title":59,"description":60,"date":61,"updated":9,"tags":62,"categories":63,"draft":15,"wordCount":64,"readingTime":17,"canonical":65,"series":66},"\u002Fithome-2026-day-26","Day 26：SignalStore 與 Strategy Pattern，透過 DI 動態切換業務邏輯","昨天我們使用 Angular 原生 Signal 與 NgRx SignalStore 實作購物車，透過 State 管理商品資料，再利用 Computed 計算商品數量與總金額。","2026-10-10T22:01:59+08:00",[11,12,13],[11,13],7347,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10423116",{"name":20,"url":21,"day":67},26,{"path":69,"title":70,"description":71,"date":72,"updated":9,"tags":73,"categories":74,"draft":15,"wordCount":75,"readingTime":76,"canonical":77,"series":78},"\u002Fithome-2026-day-25","Day 25：Angular SignalStore，原生 Signal 不夠用嗎？","在 Angular 開發中，如果資料只會被單一 Component 使用，通常可以直接透過 signal() 管理。但當多個 Component 需要共用同一份資料時，就得考慮狀態應該放在哪裡。","2026-10-09T21:28:45+08:00",[11,12,13],[11,13],9117,9,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422784",{"name":20,"url":21,"day":79},25,{"path":81,"title":82,"description":83,"date":84,"updated":9,"tags":85,"categories":86,"draft":15,"wordCount":87,"readingTime":31,"canonical":88,"series":89},"\u002Fithome-2026-day-24","Day 24：SPA Router 為什麼不再只靠 History API？","平常使用 Angular Router 時，可以透過 routerLink 或 router.navigate() 切換不同頁面。","2026-10-08T14:30:47+08:00",[11,12,13],[11,13],6330,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422348",{"name":20,"url":21,"day":90},24,{"path":92,"title":93,"description":94,"date":95,"updated":9,"tags":96,"categories":97,"draft":15,"wordCount":98,"readingTime":17,"canonical":99,"series":100},"\u002Fithome-2026-day-23","Day 23：RouteReuseStrategy，切換路由一定要銷毀 Component 嗎？","在後台系統中，很常會看到頁籤式介面，例如：","2026-10-07T15:58:32+08:00",[11,12,13],[11,13],7361,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10422016",{"name":20,"url":21,"day":101},23,{"path":103,"title":104,"description":105,"date":106,"updated":9,"tags":107,"categories":108,"draft":15,"wordCount":109,"readingTime":110,"canonical":111,"series":112},"\u002Fithome-2026-day-22","Day 22：Angular Render Lifecycle，從 afterNextRender 到 DestroyRef","過去在 Angular 中，只要遇到初始化、View 建立或資源清理，通常會先想到 Lifecycle Hooks：","2026-10-06T23:36:23+08:00",[11,12,13],[11,13],8111,8,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421810",{"name":20,"url":21,"day":113},22,{"path":115,"title":116,"description":117,"date":118,"updated":9,"tags":119,"categories":120,"draft":15,"wordCount":121,"readingTime":122,"canonical":123,"series":124},"\u002Fithome-2026-day-21","Day 21：Angular Template 還適合呼叫 Method 嗎？從 Pipe、RxJS 到 computed","過去在寫 Angular 時，因為 Template 可以直接呼叫 Method，很容易在專案裡看到大量這樣的寫法。","2026-10-05T23:30:32+08:00",[11,12,13],[11,13],5431,5,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421425",{"name":20,"url":21,"day":125},21,{"path":127,"title":128,"description":129,"date":130,"updated":9,"tags":131,"categories":132,"draft":15,"wordCount":133,"readingTime":134,"canonical":135,"series":136},"\u002Fithome-2026-day-20","Day 20：Signal Forms 非同步驗證，validateHttp 與 validateAsync","前兩篇介紹了 Signal Forms 的基本驗證，以及需要參考其他欄位的 Cross-field Validation。","2026-10-04T20:56:17+08:00",[11,12,13],[11,13],4278,4,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420910",{"name":20,"url":21,"day":137},20,{"path":139,"title":140,"description":141,"date":142,"updated":9,"tags":143,"categories":144,"draft":15,"wordCount":145,"readingTime":134,"canonical":146,"series":147},"\u002Fithome-2026-day-19","Day 19：Signal Forms Cross-field Validation 與欄位更新時機","上一篇介紹 Signal Forms 時，已經看到可以透過 Schema 定義欄位驗證，例如 required()、email()、minLength()。","2026-10-03T23:18:26+08:00",[11,12,13],[11,13],3783,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420631",{"name":20,"url":21,"day":148},19,{"path":150,"title":151,"description":152,"date":153,"updated":9,"tags":154,"categories":155,"draft":15,"wordCount":156,"readingTime":31,"canonical":157,"series":158},"\u002Fithome-2026-day-18","Day 18：Angular Signal Forms 穩定了，跟我們用了這麼久的 Reactive Forms 到底差在哪？","在 Angular 中處理表單，很多專案使用的都是 Reactive Forms。","2026-10-02T23:28:36+08:00",[11,12,13],[11,13],5886,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10420249",{"name":20,"url":21,"day":159},18,{"path":161,"title":162,"description":163,"date":164,"updated":9,"tags":165,"categories":166,"draft":15,"wordCount":167,"readingTime":31,"canonical":168,"series":169},"\u002Fithome-2026-day-17","Day 17：effect() 不是拿來同步 Signal 的，Angular 的 Side Effect 該怎麼寫？","effect() 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。","2026-10-01T21:22:13+08:00",[11,12,13],[11,13],6473,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419779",{"name":20,"url":21,"day":170},17,{"path":172,"title":173,"description":174,"date":175,"updated":9,"tags":176,"categories":177,"draft":15,"wordCount":178,"readingTime":31,"canonical":179,"series":180},"\u002Fithome-2026-day-16","Day 16：即將登場的 Route Resources，Angular Router 為什麼需要新的資料載入方式？","過去在 Angular Router 中，如果希望進入 Component 前先完成資料載入，可以透過 Resolver，讓資料取得流程在 Component 初始化之前執行。","2026-09-30T23:12:05+08:00",[11,12,13],[11,13],6534,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419409",{"name":20,"url":21,"day":181},16,{"path":183,"title":184,"description":185,"date":186,"updated":9,"tags":187,"categories":188,"draft":15,"wordCount":189,"readingTime":17,"canonical":190,"series":191},"\u002Fithome-2026-day-15","Day 15：搞懂 Injection Context，從 inject()、runInInjectionContext() 到 injectAsync()","前面提到 Injection Context 時，可能會讓人聯想到先前介紹過的 Reactive Context。我一開始也有點搞混，因此花了一些時間重新理解 Angular 裡這些不同的執行環境。","2026-09-29T13:44:28+08:00",[11,12,13],[11,13],6986,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418752",{"name":20,"url":21,"day":192},15,{"path":194,"title":195,"description":196,"date":197,"updated":9,"tags":198,"categories":199,"draft":15,"wordCount":200,"readingTime":17,"canonical":201,"series":202},"\u002Fithome-2026-day-14","Day 14：Lazy Loading 不只一種，從 Router Preloading、@defer 到 injectAsync()","當 Client Side Rendering 的應用越來越大時，我們通常會開始思考，能不能把原本一次載入的內容拆得更小，等到真正需要時再下載，避免初始載入過多 JavaScript，拉長畫面呈現的時間。","2026-09-28T22:44:46+08:00",[11,12,13],[11,13],7674,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10418485",{"name":20,"url":21,"day":203},14,{"path":205,"title":206,"description":207,"date":208,"updated":9,"tags":209,"categories":210,"draft":15,"wordCount":211,"readingTime":110,"canonical":212,"series":213},"\u002Fithome-2026-day-13","Day 13：從 Router Event 到 Signal","路由狀態管理在前端開發中比較少被單獨拿出來討論，但在 Angular 裡，一次路由導航其實會經過不少階段，也會在過程中發出不同的 Router Event，例如 NavigationStart、NavigationEnd…","2026-09-27T20:37:55+08:00",[11,12,13],[11,13],7777,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417948",{"name":20,"url":21,"day":214},13,{"path":216,"title":217,"description":218,"date":219,"updated":9,"tags":220,"categories":221,"draft":15,"wordCount":222,"readingTime":122,"canonical":223,"series":224},"\u002Fithome-2026-day-12","Day 12：從 httpResource 到 TanStack Query，認識 Server State","前面我們花了不少時間討論，為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。","2026-09-26T23:35:12+08:00",[11,12,13],[11,13],5119,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417614",{"name":20,"url":21,"day":225},12,{"path":227,"title":228,"description":229,"date":230,"updated":9,"tags":231,"categories":232,"draft":15,"wordCount":233,"readingTime":122,"canonical":234,"series":235},"\u002Fithome-2026-day-11","Day 11：用 rxResource 串起 Observable 與 Resource","前面介紹了不少 Resource 到 httpResource() 的應用。不過在 Angular 開發中，Observable 與 RxJS 仍然是很常見的非同步資料處理方式，像是 switchMap、map、debo…","2026-09-25T22:42:04+08:00",[11,12,13],[11,13],4908,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10417143",{"name":20,"url":21,"day":236},11,{"path":238,"title":239,"description":240,"date":241,"updated":9,"tags":242,"categories":243,"draft":15,"wordCount":244,"readingTime":31,"canonical":245,"series":246},"\u002Fithome-2026-day-10","Day 10：linkedSignal 讓相依狀態也能被修改","昨天我們透過 httpResource() 搭配 chain() 處理兩個非同步資料之間的依賴。下游 Request 需要使用上游資料時，會等上游完成後再繼續；當 Request 的來源改變，Resource 的狀態也會…","2026-09-24T23:49:49+08:00",[11,12,13],[11,13],5623,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416746",{"name":20,"url":21,"day":247},10,{"path":249,"title":250,"description":251,"date":252,"updated":9,"tags":253,"categories":254,"draft":15,"wordCount":255,"readingTime":134,"canonical":256,"series":257},"\u002Fithome-2026-day-09","Day 9：httpResource 實務篇，統一回覆格式與 chain 相依請求","昨天我們透過 Orval 串起 API Contract，了解如何根據 OpenAPI 規格產生前端需要的型別與 API 呼叫方式。","2026-09-23T23:42:40+08:00",[11,12,13],[11,13],4358,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10416227",{"name":20,"url":21,"day":76},{"path":259,"title":260,"description":261,"date":262,"updated":9,"tags":263,"categories":264,"draft":15,"wordCount":265,"readingTime":122,"canonical":266,"series":267},"\u002Fithome-2026-day-08","Day 8：用 Orval 串起 Angular API Contract，同時產生 HttpClient、httpResource 與 Zod","前後端在對接 API 時，常見的問題之一，就是雙方對資料格式的定義逐漸不一致。","2026-09-22T23:48:12+08:00",[11,12,13],[11,13],4451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415694",{"name":20,"url":21,"day":110},{"path":269,"title":270,"description":271,"date":272,"updated":9,"tags":273,"categories":274,"draft":15,"wordCount":275,"readingTime":17,"canonical":276,"series":277},"\u002Fithome-2026-day-07","Day 7：從 Resource 到 httpResource，用 Signal 管理 HTTP 資料請求","昨天我們提到，Resource API 本身是一套通用的非同步資料模型，比較適合用在「讀取資料」這類情境，而不是新增、修改或刪除資料這種 Mutation 操作。","2026-09-21T22:13:18+08:00",[11,12,13],[11,13],6735,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10415038",{"name":20,"url":21,"day":17},{"path":279,"title":280,"description":281,"date":282,"updated":9,"tags":283,"categories":284,"draft":15,"wordCount":285,"readingTime":122,"canonical":286,"series":287},"\u002Fithome-2026-day-06","Day 6：Resource API，把非同步資料帶進 Signal","今天來看 Angular Signal 裡一個很重要的非同步資料 API：Resource API。","2026-09-20T19:25:37+08:00",[11,12,13],[11,13],5054,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10414377",{"name":20,"url":21,"day":31},{"path":289,"title":290,"description":291,"date":292,"updated":9,"tags":293,"categories":294,"draft":15,"wordCount":295,"readingTime":134,"canonical":296,"series":297},"\u002Fithome-2026-day-05","Day 5：從 Reactive Graph 到 View Tree，看 Angular 如何找到真正需要更新的畫面","前面我們已經知道，當 Signal 的值發生變化後，Angular 會透過 Reactive Graph 找到受到影響的 Consumer。","2026-09-19T20:19:47+08:00",[11,12,13],[11,13],4364,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413852",{"name":20,"url":21,"day":122},{"path":299,"title":300,"description":301,"date":302,"updated":9,"tags":303,"categories":304,"draft":15,"wordCount":305,"readingTime":122,"canonical":306,"series":307},"\u002Fithome-2026-day-04","Day 4：Angular 怎麼知道誰正在讀 Signal？從 Reactive Context 理解依賴追蹤","前面我們看過，當 Template 讀取 Signal 時，Angular 會記住「誰依賴了誰」；當 Signal 發生變化後，也會透過 Reactive Graph 通知相關的 Consumer。","2026-09-18T21:58:16+08:00",[11,12,13],[11,13],4894,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10413313",{"name":20,"url":21,"day":134},{"path":309,"title":310,"description":311,"date":312,"updated":9,"tags":313,"categories":314,"draft":15,"wordCount":315,"readingTime":122,"canonical":316,"series":317},"\u002Fithome-2026-day-03","Day 3：從 set()、update() 看懂 Angular Signal 的基本使用與更新機制","昨天我們先試著理解，在 Template 中使用 Signal() 時，Angular 是如何建立依賴關係，並在 Signal 發生變化後，知道哪些 View 可能受到影響。","2026-09-17T23:08:58+08:00",[11,12,13],[11,13],5451,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10412826",{"name":20,"url":21,"day":318},3,{"path":320,"title":321,"description":322,"date":323,"updated":9,"tags":324,"categories":325,"draft":15,"wordCount":326,"readingTime":134,"canonical":327,"series":328},"\u002Fithome-2026-day-02","Day 2：Angular Signal 是什麼？從 Change Detection 理解它的運作原理","傳統 Angular 在監測資料變化時，通常會搭配 zone.js。像是 DOM 事件、Promise、Timer、HTTP 請求這些非同步操作發生時，zone.js 會幫忙攔截。","2026-09-16T20:19:12+08:00",[11,12,13],[11,13],4276,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411944",{"name":20,"url":21,"day":329},2,{"path":331,"title":332,"description":333,"date":334,"updated":9,"tags":335,"categories":336,"draft":15,"wordCount":337,"readingTime":329,"canonical":338,"series":339},"\u002Fithome-2026-day-01","Day 1：為什麼想聊 Angular Signal？","相信講到前端框架，大家第一時間想到的通常會是 Vue、React，甚至也有人會推薦 Svelte 這類近年討論度很高的框架。但一講到 Angular，很多人的第一印象可能就像看到一座大城堡一樣：東西很多、規則很多，一時之…","2026-09-15T23:56:26+08:00",[11,12,13],[11,13],1478,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10411425",{"name":20,"url":21,"day":340},1,{"path":342,"title":343,"description":344,"date":345,"updated":9,"tags":346,"categories":351,"draft":15,"wordCount":353,"readingTime":134,"canonical":9,"series":9},"\u002FTS-Excess-Property-Checks","TypeScript 踩坑筆記：物件字面量的多餘屬性檢查機制","深入探討 TypeScript 的多餘屬性檢查（Excess Property Checks）機制，解析物件字面量與變數賦值的型別檢查差異，並提供實務開發中避免型別陷阱的最佳實踐方法。","2026-01-31T16:29:23+08:00",[347,348,349,350],"Excess Property Checks","Structural Typing","Type Safety","NestJS",[352],"TypeScript",3551,{"path":355,"title":356,"description":357,"date":358,"updated":9,"tags":359,"categories":361,"draft":15,"wordCount":363,"readingTime":329,"canonical":364,"series":365},"\u002Fithome-2025-day-30","Day 30：開發之旅完結 - 感謝與展望","終於來到鐵人賽的最後一天！這是我第二次參加鐵人賽，今年的主題是 LINE Bot 開發。這個主題源自於我參加的十二週讀書會分享，當時我發現大家對 LINE Bot 很感興趣，而我自己也一直很喜歡這個主題，於是早在六月就定…","2025-10-13T20:11:12+08:00",[11,360],"2025 鐵人賽",[11,362],"Line Bot",1751,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396850",{"name":366,"url":367,"day":22},"2025 iThome 鐵人賽：Line Bot × NestJS：30 天開發日記","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F8779",{"path":369,"title":370,"description":371,"date":372,"updated":9,"tags":373,"categories":376,"draft":15,"wordCount":377,"readingTime":31,"canonical":378,"series":379},"\u002Fithome-2025-day-29","Day 29：LIFF 會員註冊 - 信箱驗證與 Swagger 文件整合","延續 Day 28 會員註冊的流程，在實際的會員註冊場景中，電子信箱驗證是確保用戶身份真實性的重要環節。本篇將基於昨天的註冊流程，新增兩個核心 API：發送驗證信與驗證驗證碼，讓會員註冊功能更加完整且安全。","2025-10-12T23:15:00+08:00",[11,360,362,374,350,375],"LIFF","Swagger",[11,362],6089,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396512",{"name":366,"url":367,"day":34},{"path":381,"title":382,"description":383,"date":384,"updated":9,"tags":385,"categories":386,"draft":15,"wordCount":387,"readingTime":122,"canonical":388,"series":389},"\u002Fithome-2025-day-28","Day 28：LIFF 會員註冊 - ID Token 驗證與資料庫整合","今天的開發重點是建立會員卡系統，將 LIFF 前端與 NestJS 後端完整串接。","2025-10-11T22:57:29+08:00",[11,360,362,374,350],[11,362],5254,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10396146",{"name":366,"url":367,"day":45},{"path":391,"title":392,"description":393,"date":394,"updated":9,"tags":395,"categories":396,"draft":15,"wordCount":397,"readingTime":318,"canonical":398,"series":399},"\u002Fithome-2025-day-27","Day 27：群組事件與檔案訊息接收","今天主要補充 LINE Bot 在群組中的專屬特性，包括群組限定的 Webhook 事件，以及前面提到但尚未說明的第 7 種訊息類型 file 檔案訊息。","2025-10-10T21:01:52+08:00",[11,360,362,350],[11,362],2888,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395785",{"name":366,"url":367,"day":56},{"path":401,"title":402,"description":403,"date":404,"updated":9,"tags":405,"categories":407,"draft":15,"wordCount":408,"readingTime":122,"canonical":409,"series":410},"\u002Fithome-2025-day-26","Day 26：LINE Pay 線上付款","LINE Pay 提供兩種付款方式：線上付款與實體付款。實體付款主要採用店家出示條碼的模式，顧客掃描後即可輸入金額完成付款。這兩種付款方式都支援在沙盒(Sandbox)環境中進行測試。本文將著重說明線上付款的整合流程。","2025-10-09T23:39:32+08:00",[11,360,406],"Line Pay",[11,362],4876,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395481",{"name":366,"url":367,"day":67},{"path":412,"title":413,"description":414,"date":415,"updated":9,"tags":416,"categories":417,"draft":15,"wordCount":418,"readingTime":318,"canonical":419,"series":420},"\u002Fithome-2025-day-25","Day 25：優惠券設定與 Coupon Message 實作","LINE Official Account 的「優惠券」是提升顧客互動與忠誠度的重要功能。透過獎勵機制，不僅能吸引新客，更能有效促進回購。","2025-10-08T23:49:32+08:00",[11,360,362,350],[11,362],3207,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10395076",{"name":366,"url":367,"day":79},{"path":422,"title":423,"description":424,"date":425,"updated":9,"tags":426,"categories":427,"draft":15,"wordCount":428,"readingTime":134,"canonical":429,"series":430},"\u002Fithome-2025-day-24","Day 24：LIFF 刮刮樂實戰體驗","在了解 LIFF 的基礎設計流程後，本文將透過「刮刮樂」案例示範 LIFF API 的實際應用。從啟動時檢查權限與執行環境、確認用戶分享功能權限，到刮刮樂結束後發送訊息並關閉 LIFF，完整呈現 LIFF 在互動應用中的…","2025-10-07T22:36:58+08:00",[11,360,362,374],[11,362],4200,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394618",{"name":366,"url":367,"day":90},{"path":432,"title":433,"description":434,"date":435,"updated":9,"tags":436,"categories":437,"draft":15,"wordCount":438,"readingTime":134,"canonical":439,"series":440},"\u002Fithome-2025-day-23","Day 23：LIFF 網頁應用開發入門","在鐵人賽前半段，我們深入探討了 LINE Bot 的兩大核心功能：Richmenu(選單設置)與各種 Reply Message(回覆訊息)的訊息類型。然而在實務應用中，若要提供更豐富的互動體驗，Line 往往需要搭配 …","2025-10-06T23:06:57+08:00",[11,360,362,374],[11,362],4400,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10394035",{"name":366,"url":367,"day":101},{"path":442,"title":443,"description":444,"date":445,"updated":9,"tags":446,"categories":447,"draft":15,"wordCount":448,"readingTime":329,"canonical":449,"series":450},"\u002Fithome-2025-day-22","Day 22：Render 雲端平台部署 Nest 後端伺服器","今天的目標是將系統正式上線。經過這段時間的開發，我們已完成 Line Message API 的核心功能與日誌處理機制。","2025-10-05T22:52:11+08:00",[11,360,362,350],[11,362],1895,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393809",{"name":366,"url":367,"day":113},{"path":452,"title":453,"description":454,"date":455,"updated":9,"tags":456,"categories":457,"draft":15,"wordCount":458,"readingTime":318,"canonical":459,"series":460},"\u002Fithome-2025-day-21","Day 21：Nest Pino 整合 Grafana Loki 日誌","Loki 環境變數可參考 Day 20 文章的 Grafana Data Source settings 設定","2025-10-04T17:24:26+08:00",[11,360,362,350],[11,362],2740,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10393305",{"name":366,"url":367,"day":125},{"path":462,"title":463,"description":464,"date":465,"updated":9,"tags":466,"categories":467,"draft":15,"wordCount":468,"readingTime":318,"canonical":469,"series":470},"\u002Fithome-2025-day-20","Day 20：Grafana Loki 日誌服務 - 從申請到 API 串接測試","目前已經理解了 LINE Messaging API 的核心功能，包括如何接收訊息、串接第三方服務，以及發送多種類型的訊息。接下來，我們將把原本架設在 ngrok 的後端伺服器，透過 GitHub 自動化部署到 Rend…","2025-10-03T23:16:16+08:00",[11,360,362,350],[11,362],2461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392910",{"name":366,"url":367,"day":137},{"path":472,"title":473,"description":474,"date":475,"updated":9,"tags":476,"categories":477,"draft":15,"wordCount":478,"readingTime":318,"canonical":479,"series":480},"\u002Fithome-2025-day-19","Day 19：Flex Message Carousel 食譜分享","Flex Message 可以分成 bubble(單張卡片) 跟 carousel(多張卡片輪播) 兩種方式，昨天示範了 bubble 格式的電子個人名片後，今天主要展示 carousel 的應用，兩者的基本結構大致相同…","2025-10-02T16:06:17+08:00",[11,360,362,350],[11,362],2267,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10392256",{"name":366,"url":367,"day":148},{"path":482,"title":483,"description":484,"date":485,"updated":9,"tags":486,"categories":487,"draft":15,"wordCount":488,"readingTime":122,"canonical":489,"series":490},"\u002Fithome-2025-day-18","Day 18：Flex Message 設計個人名片","從基礎的文字互動開始，我們逐步探索了六大核心訊息類型的發送機制、Template Message 的結構化設計，以及 ImageMap Message 的互動式體驗。今天，我們將進入 LINE Bot 最具彈性的訊息格式…","2025-10-01T17:57:47+08:00",[11,360,362,350],[11,362],4468,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391723",{"name":366,"url":367,"day":159},{"path":492,"title":493,"description":494,"date":495,"updated":9,"tags":496,"categories":497,"draft":15,"wordCount":498,"readingTime":122,"canonical":499,"series":500},"\u002Fithome-2025-day-17","Day 17：ImageMap Message  從互動海報到影片嵌入","在 Day 16 文章中，我們使用 Cloudflare Workers 實作了圖片代理功能。到目前為止，我們已經完成以下準備工作:","2025-09-30T11:58:32+08:00",[11,360,362,350],[11,362],4807,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10391164",{"name":366,"url":367,"day":170},{"path":502,"title":503,"description":504,"date":505,"updated":9,"tags":506,"categories":507,"draft":15,"wordCount":508,"readingTime":134,"canonical":509,"series":510},"\u002Fithome-2025-day-16","Day 16：Cloudflare Workers 圖片代理實作","在 Day 15 文章中，我們處理了 ImageMap Message 的背景圖，接著我們遇到了圖片不能出現副檔名及動態路徑支援不同解析度的問題。今天主要會透過 Cloudflare Workers 來解決這兩個問題，讓…","2025-09-29T16:15:41+08:00",[11,360,362,350],[11,362],3412,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10390567",{"name":366,"url":367,"day":181},{"path":512,"title":513,"description":514,"date":515,"updated":9,"tags":516,"categories":517,"draft":15,"wordCount":518,"readingTime":134,"canonical":519,"series":520},"\u002Fithome-2025-day-15","Day 15：共通屬性處理優化及 Imagemap  圖片製作","針對 Day 14 的程式碼架構進行優化。由於 Sender 和 Quick Reply 功能在所有回覆訊息類型中都能使用，我們應該將這些共用屬性抽取為獨立函數進行統一處理。","2025-09-28T21:03:38+08:00",[11,360,362,350],[11,362],4033,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10389697",{"name":366,"url":367,"day":192},{"path":522,"title":523,"description":524,"date":525,"updated":9,"tags":526,"categories":527,"draft":15,"wordCount":528,"readingTime":31,"canonical":529,"series":530},"\u002Fithome-2025-day-14","Day 14：LINE Bot Template Message 訊息回覆","經過前幾天的學習，我們已經成功掌握了 LINE Bot 六大基礎訊息類型的發送技巧，為訊息互動奠定了穩固的基礎。今天，我們將踏入更具挑戰性的進階應用領域！","2025-09-27T15:54:06+08:00",[11,360,362,350],[11,362],6372,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388976",{"name":366,"url":367,"day":203},{"path":532,"title":533,"description":534,"date":535,"updated":9,"tags":536,"categories":537,"draft":15,"wordCount":538,"readingTime":122,"canonical":539,"series":540},"\u002Fithome-2025-day-13","Day 13：LINE Bot 客製化 Sender 與 Quick Reply 功能實作","LINE Bot 所有回覆訊息類型都具備「共用屬性」功能，這些屬性帶來兩項重要的互動體驗提升：","2025-09-26T11:41:34+08:00",[11,360,362,350],[11,362],4426,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10388037",{"name":366,"url":367,"day":214},{"path":542,"title":543,"description":544,"date":545,"updated":9,"tags":546,"categories":547,"draft":15,"wordCount":548,"readingTime":318,"canonical":549,"series":550},"\u002Fithome-2025-day-12","Day 12：LINE Bot Image、Video、Audio、Location 訊息回覆","Day 11 文章中，我們針對 LINE Bot 可以回覆的Text及Sticker基礎型別進行了封裝，不僅新增了 line-message.ts模組，還讓開發者可以用最少的參數產生 LINE Message 所需要的格…","2025-09-25T18:08:09+08:00",[11,360,362,350],[11,362],2977,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10387598",{"name":366,"url":367,"day":225},{"path":552,"title":553,"description":554,"date":555,"updated":9,"tags":556,"categories":557,"draft":15,"wordCount":558,"readingTime":31,"canonical":559,"series":560},"\u002Fithome-2025-day-11","Day 11：LINE Bot Text Emoji 與 Sticker 訊息回覆","目前我們的 LINE Bot 主要使用 Text 方式回覆訊息。今天將探討 Text 類型的 Emoji 應用，以及 Sticker 回覆方式，基於 Day 8 版本進行改寫。","2025-09-24T23:21:11+08:00",[11,360,350,362],[11,362],5659,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386993",{"name":366,"url":367,"day":236},{"path":562,"title":563,"description":564,"date":565,"updated":9,"tags":566,"categories":567,"draft":15,"wordCount":568,"readingTime":318,"canonical":569,"series":570},"\u002Fithome-2025-day-10","Day 10：LINE Rich Menu Switch Action 提升使用者體驗","在 Day 9 文章中，我們結合 LINE 平台的功能，將天氣服務與圖片上傳服務整合，打造出第一張 LINE 圖文選單，透過 URI action 的方式觸發平台提供的互動效果。接著，我們使用客製化選單製作出 9 格的客…","2025-09-23T15:02:57+08:00",[11,360,362,350],[11,362],2906,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10386165",{"name":366,"url":367,"day":247},{"path":572,"title":573,"description":574,"date":575,"updated":9,"tags":576,"categories":577,"draft":15,"wordCount":578,"readingTime":318,"canonical":579,"series":580},"\u002Fithome-2025-day-09","Day  9：打造 LINE Rich Menu - 後台設計 vs API 客製化","在 Day8 文章中，我們透過整合雲端服務 Cloudinary，學習如何處理使用者上傳的圖片檔案，並將其儲存至雲端。然而，LINE Bot 除了讓使用者輸入文字回覆的方式之外，更常見的是透過圖文選單(Rich Menu…","2025-09-22T21:54:25+08:00",[11,360,362,350],[11,362],3285,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10385658",{"name":366,"url":367,"day":76},{"path":582,"title":583,"description":584,"date":585,"updated":9,"tags":586,"categories":587,"draft":15,"wordCount":588,"readingTime":17,"canonical":589,"series":590},"\u002Fithome-2025-day-08","Day 8：Cloudinary 雲端圖片儲存整合","在 Day 6 中，我們透過整合 OpenWeatherMap API 與座標位置訊息學習了如何結合第三方服務。今天將探索 Cloudinary 雲端服務，解決 LINE 圖片資源會過期的問題，學習如何透過 LINE M…","2025-09-21T23:13:19+08:00",[11,360,362,350],[11,362],7215,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384968",{"name":366,"url":367,"day":110},{"path":592,"title":593,"description":594,"date":595,"updated":9,"tags":596,"categories":597,"draft":15,"wordCount":598,"readingTime":122,"canonical":599,"series":600},"\u002Fithome-2025-day-07","Day 7：Joi 環境變數統一驗證，重構 LINE Bot 天氣服務模組","在 Day 6 開發 LINE Bot 結合 OpenWeatherMap API 的過程中，我們採用私有方法將 API 請求處理與資料格式化整合在一起。今天將對 Day 6 的實作進行架構重構，主要目標是建立更完善的環…","2025-09-20T23:07:08+08:00",[11,360,362,350],[11,362],5157,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10384302",{"name":366,"url":367,"day":17},{"path":602,"title":603,"description":604,"date":605,"updated":9,"tags":606,"categories":607,"draft":15,"wordCount":608,"readingTime":134,"canonical":609,"series":610},"\u002Fithome-2025-day-06","Day 6：整合第三方 API 天氣查詢服務","Day 5 帶大家了解 LINE Bot 能接收使用者傳遞的「6+1」種訊息類型。之所以這樣強調，是因為 File 類型僅能在群組聊天中使用，這個限制我自己也經常忘記，所以習慣用「6+1」來提醒自己。","2025-09-19T21:07:47+08:00",[11,360,362,350],[11,362],3892,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10383636",{"name":366,"url":367,"day":31},{"path":612,"title":613,"description":614,"date":615,"updated":9,"tags":616,"categories":617,"draft":15,"wordCount":618,"readingTime":122,"canonical":619,"series":620},"\u002Fithome-2025-day-05","Day 5：整合 Pino Logger 提升 LINE Bot 專案的可維護性","看著專案逐漸成型，現在該開始重視日誌記錄的規劃了。在實際部署時，我們通常會區分 develop(開發環境)和 production(正式環境)兩種不同的執行環境，針對不同環境採用相應的日誌策略。","2025-09-18T23:30:00+08:00",[11,360,362,350],[11,362],4746,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10382298",{"name":366,"url":367,"day":122},{"path":622,"title":623,"description":624,"date":625,"updated":9,"tags":626,"categories":627,"draft":15,"wordCount":628,"readingTime":31,"canonical":629,"series":630},"\u002Fithome-2025-day-04","Day 4：掌握 LINE Bot 七種訊息類型與 NestJS 架構優化","目前我們都只使用 LINE Message Event 中的純文字(text)格式進行回覆，但除了純文字之外，LINE Bot 後端伺服器其實可以接收多種不同類型的訊息。今天就讓我們一起來深入了解這些訊息類型吧！","2025-09-17T22:53:37+08:00",[11,360,362,350],[11,362],5828,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381651",{"name":366,"url":367,"day":134},{"path":632,"title":633,"description":634,"date":635,"updated":9,"tags":636,"categories":637,"draft":15,"wordCount":638,"readingTime":122,"canonical":639,"series":640},"\u002Fithome-2025-day-03","Day 3：Webhook 簽章驗證與訊息回覆策略(Push & Reply)","上一篇帶大家透過「Webhook 模式」成功架設了專屬的 LINE Bot 代理人。相信大家在體驗基礎功能後，對於 LINE Bot Webhook 還能監聽哪些事件、支援哪些類型的訊息回覆，以及如何確保後端收到的訊息確…","2025-09-16T21:53:39+08:00",[11,360,362,350],[11,362],5360,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10381303",{"name":366,"url":367,"day":318},{"path":642,"title":643,"description":644,"date":645,"updated":9,"tags":646,"categories":647,"draft":15,"wordCount":648,"readingTime":31,"canonical":649,"series":650},"\u002Fithome-2025-day-02","Day 2：從 Webhook 到後端，打造你的專屬機器人","上一篇，帶大家透過「Chat 模式」，讓自身變成客服機器人的方式。這篇會從「Webhook 模式」切入，並以 NestJS 作為後端框架，教你如何在休息時間，也能讓後端伺服器當你的替身幫你解惑使用者的問題。","2025-09-15T21:20:49+08:00",[11,360,362,350],[11,362],6590,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10380524",{"name":366,"url":367,"day":329},{"path":652,"title":653,"description":654,"date":655,"updated":9,"tags":656,"categories":657,"draft":15,"wordCount":658,"readingTime":318,"canonical":659,"series":660},"\u002Fithome-2025-day-01","Day 1：Hello World LINE Bot","每個程式語言的開始通常都會以練習 Hello World！作為一個簡單的起手式，LINE Bot 也不意外。這一系列會著重在 LINE Bot 的相關應用，帶大家用輕鬆的方式一起建立屬於自己的 Line Bot 聊天機器…","2025-09-14T22:48:33+08:00",[11,360,362,350],[11,362],2723,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10379895",{"name":366,"url":367,"day":340},{"path":662,"title":663,"description":664,"date":665,"updated":9,"tags":666,"categories":671,"draft":15,"wordCount":674,"readingTime":134,"canonical":9,"series":9},"\u002Fdbdiagram-tool","關聯式資料庫設計必備 - dbdiagram.io ERD 繪圖工具","還在煩惱如何設計關聯式資料表實體關聯圖嗎？ 使用 dbdiagram.io 加快你與同事之間的距離","2024-11-05T20:18:24+08:00",[667,668,669,670],"ERD","dbdiagram.io","DBML","資料庫設計",[672,673],"Database","開發工具",3982,{"path":676,"title":677,"description":678,"date":679,"updated":9,"tags":680,"categories":687,"draft":15,"wordCount":690,"readingTime":122,"canonical":9,"series":9},"\u002FPostgreSQL-1","前端勇士大闖六角後端體驗營 - 初探 PostgreSQL DML 語法","初步踏入後端的世界，了解何謂後端以及資料庫常見語法 DDL、DML。","2024-10-30T22:43:49+08:00",[681,682,683,684,685,686],"PostgreSQL","SQL","DML","DDL","資料庫","六角學院",[672,688,689],"後端實戰","學習心得",5220,{"path":692,"title":693,"description":694,"date":695,"updated":9,"tags":696,"categories":700,"draft":15,"wordCount":703,"readingTime":329,"canonical":9,"series":9},"\u002Fcss-flex-1","CSS flex 軸線對齊技巧 - 預設屬性","CSS flex 排版技巧你都了解了嗎？回歸學習初心的認識 flex 預設屬性","2024-10-24T20:29:08+08:00",[697,698,699],"Flexbox","前端排版","切版技巧",[701,702],"CSS","前端開發",1856,{"path":705,"title":706,"description":707,"date":708,"updated":9,"tags":709,"categories":712,"draft":15,"wordCount":714,"readingTime":329,"canonical":715,"series":716},"\u002Fithome-2024-day-30","Vue 3 用實作帶你看過核心概念 - Day 30：新手村的鐵人證書 - 完賽心得","花了整整半年左右的時間，每天下班都很用心的準備範例或是看書，這一段旅程對我來說真的很重要。擔心、害怕跟不安的心情一直充斥自己心中，擔心寫錯或是寫不好，想讓自己再多準備久一點再開始，所以拖到中秋連假才開始，想下定決心讓自己…","2024-10-13T12:42:25+08:00",[11,710,711],"2024 鐵人賽","Vue 3",[11,713],"Vue",1381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10367206",{"name":717,"url":718,"day":22},"2024 iThome 鐵人賽：Vue 3 初學者：用實作帶你看過核心概念","https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F7800",{"path":720,"title":721,"description":722,"date":723,"updated":9,"tags":724,"categories":727,"draft":15,"wordCount":728,"readingTime":134,"canonical":729,"series":730},"\u002Fithome-2024-day-29","Vue 3 用實作帶你看過核心概念 - Day 29：Composables(組合式函數)","Composables是 Vue 3 中的一個重要概念，它是利用 Vue 的 Composition API 來創建和管理可重用的有狀態邏輯的函數。Composables 允許我們將組件邏輯抽象成可重用的函數，使得代碼更…","2024-10-12T15:07:55+08:00",[11,710,711,725,726],"Options API","Composition API",[11,713],4233,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366865",{"name":717,"url":718,"day":34},{"path":732,"title":733,"description":734,"date":735,"updated":9,"tags":736,"categories":738,"draft":15,"wordCount":739,"readingTime":134,"canonical":740,"series":741},"\u002Fithome-2024-day-28","Vue 3 用實作帶你看過核心概念 - Day 28：官方 Vue Router 的應用與技巧","路由設定方式，一般可以分成兩種：","2024-10-11T17:14:44+08:00",[11,710,711,725,737],"Vue Router",[11,713],4095,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366520",{"name":717,"url":718,"day":45},{"path":743,"title":744,"description":745,"date":746,"updated":9,"tags":747,"categories":748,"draft":15,"wordCount":749,"readingTime":122,"canonical":750,"series":751},"\u002Fithome-2024-day-27","Vue 3 用實作帶你看過核心概念 - Day 27：VeeValidate 表單驗證工具","在開發 Vue 應用程式時，表單驗證至關重要，它能確保使用者輸入的資料符合預期的格式和要求。今天來介紹一款專門用於 Vue 的表單驗證工具—VeeValidate v4。這個工具不僅能幫助開發者輕鬆實現表單驗證，還提供了…","2024-10-10T21:02:16+08:00",[11,710,711,725],[11,713],4461,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10366165",{"name":717,"url":718,"day":56},{"path":753,"title":754,"description":755,"date":756,"updated":9,"tags":757,"categories":758,"draft":15,"wordCount":759,"readingTime":318,"canonical":760,"series":761},"\u002Fithome-2024-day-26","Vue 3 用實作帶你看過核心概念 - Day 26：KeepAlive 與 Teleport 內置組件","預設情況下，切換組件會導致組件的創建及銷毀，無法保持組件內部的狀態。","2024-10-09T18:50:53+08:00",[11,710,711,725],[11,713],3158,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365724",{"name":717,"url":718,"day":67},{"path":763,"title":764,"description":765,"date":766,"updated":9,"tags":767,"categories":768,"draft":15,"wordCount":769,"readingTime":318,"canonical":770,"series":771},"\u002Fithome-2024-day-25","Vue 3 用實作帶你看過核心概念 - Day 25：自定義指令(Custom Directives)","Vue不僅提供了豐富的內置指令用於操作DOM元素，還允許開發者創建自定義指令。透過自定義指令，我們可以對DOM元素進行更細緻的控制，並在指令的不同生命週期階段執行特定的邏輯，從而實現更靈活的功能擴展。","2024-10-08T22:05:32+08:00",[11,710,711,725],[11,713],3116,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10365355",{"name":717,"url":718,"day":79},{"path":773,"title":774,"description":775,"date":776,"updated":9,"tags":777,"categories":778,"draft":15,"wordCount":779,"readingTime":329,"canonical":780,"series":781},"\u002Fithome-2024-day-24","Vue 3 用實作帶你看過核心概念 - Day 24：異步加載(Lazy loading)","在大型項目中，我們通常需要將應用拆分為更小的代碼模塊，並僅在需要時才從服務器加載相關組件，以減少初次加載的資源量。Vue 提供了 defineAsyncComponent 方法來實現組件的異步加載，這樣可以有效提高應用的…","2024-10-07T20:28:27+08:00",[11,710,711,725],[11,713],2171,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364861",{"name":717,"url":718,"day":90},{"path":783,"title":784,"description":785,"date":786,"updated":9,"tags":787,"categories":788,"draft":15,"wordCount":789,"readingTime":318,"canonical":790,"series":791},"\u002Fithome-2024-day-23","Vue 3 用實作帶你看過核心概念 - Day 23：依賴注入( Provide-Inject )","在Dayxx中提到過，父子組件之間的數據傳遞可以通過props來實現。然而，當祖孫層組件需要進行數據傳遞時，中間層級的組件可能並不需要這個props，但卻不得不為了使子組件獲取數據而被迫傳遞。","2024-10-06T15:14:51+08:00",[11,710,711,725],[11,713],3183,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10364252",{"name":717,"url":718,"day":101},{"path":793,"title":794,"description":795,"date":796,"updated":9,"tags":797,"categories":798,"draft":15,"wordCount":799,"readingTime":134,"canonical":800,"series":801},"\u002Fithome-2024-day-22","Vue 3 用實作帶你看過核心概念 - Day 22：v-slot 插槽使用","先前Day 18中提到父組件可以通過props將參數資料傳遞給子組件進行渲染，這樣可以有效地將資料處理與畫面渲染的工作分開。然而，當我們需要更加客製化的呈現內容，由父組件來控制子組件的部分顯示時，可以使用v-slot 指…","2024-10-05T12:32:51+08:00",[11,710,711,725],[11,713],4292,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363713",{"name":717,"url":718,"day":113},{"path":803,"title":804,"description":805,"date":806,"updated":9,"tags":807,"categories":808,"draft":15,"wordCount":809,"readingTime":329,"canonical":810,"series":811},"\u002Fithome-2024-day-21","Vue 3 用實作帶你看過核心概念 - Day 21： 組件透傳屬性(Attributes)的應用","當父組件向子組件傳遞屬性或監聽器，而這些屬性未在子組件中定義為props或emits時，它們會自動繼承並附加到子組件的根元素上，常見的屬性包括class、style、id等。這些屬性可以透過$attrs屬性訪問並進行操作…","2024-10-04T22:42:54+08:00",[11,710,711,725],[11,713],2146,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10363469",{"name":717,"url":718,"day":125},{"path":813,"title":814,"description":815,"date":816,"updated":9,"tags":817,"categories":818,"draft":15,"wordCount":819,"readingTime":122,"canonical":820,"series":821},"\u002Fithome-2024-day-20","Vue 3 用實作帶你看過核心概念 - Day 20：組件使用 v-model 雙向綁定","在Day 13中，我們介紹了表單元素（例如輸入框）如何處理數據雙向綁定。當需要將表單元素與一個響應式變數綁定時，v-model提供雙向綁定功能。這代表當畫面上表單元素的變動會即時影響Vue數據，同時當Vue數據的值改變時…","2024-10-03T23:40:48+08:00",[11,710,711,725],[11,713],5191,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362957",{"name":717,"url":718,"day":137},{"path":823,"title":824,"description":825,"date":826,"updated":9,"tags":827,"categories":828,"draft":15,"wordCount":829,"readingTime":318,"canonical":830,"series":831},"\u002Fithome-2024-day-19","Vue 3 用實作帶你看過核心概念 - Day 19：子組件向父組件發射事件 - emits","組件可以通過$emit(functionName, params)自定義事件名稱並傳遞參數。自定義事件名稱建議使用kebab-case來命名自定義事件名稱，以確保命名風格與HTML 屬性一致性。此外，自定義事件還支持事件…","2024-10-02T15:51:16+08:00",[11,710,711,725],[11,713],2877,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10362144",{"name":717,"url":718,"day":148},{"path":833,"title":834,"description":835,"date":836,"updated":9,"tags":837,"categories":838,"draft":15,"wordCount":839,"readingTime":31,"canonical":840,"series":841},"\u002Fithome-2024-day-18","Vue 3 用實作帶你看過核心概念 - Day 18：父組件向子組件傳值 - props","組件之間的傳值必須透過聲明 props 的方式，這樣可以避免與HTML的屬性(attribute)混淆。父組件傳遞參數給子組件的方式有以下兩種方式：","2024-10-01T22:28:48+08:00",[11,710,711,725],[11,713],5782,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361721",{"name":717,"url":718,"day":159},{"path":843,"title":844,"description":845,"date":846,"updated":9,"tags":847,"categories":848,"draft":15,"wordCount":849,"readingTime":318,"canonical":850,"series":851},"\u002Fithome-2024-day-17","Vue 3 用實作帶你看過核心概念 - Day 17：組件基礎 - 全局註冊與區域註冊","在 Day 5 我們提到可以將應用拆分為根組件和多個組件實例，並將功能模塊化。","2024-09-30T23:30:10+08:00",[11,710,711,725],[11,713],2954,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10361253",{"name":717,"url":718,"day":170},{"path":853,"title":854,"description":855,"date":856,"updated":9,"tags":857,"categories":858,"draft":15,"wordCount":859,"readingTime":122,"canonical":860,"series":861},"\u002Fithome-2024-day-16","Vue 3 用實作帶你看過核心概念 - Day 16：使用 ref 屬性引用模板元素","雖然 Vue 透過模板幫助我們處理了大部分的DOM 操作，但在某些情況下，仍然需要手動操作 DOM。例如，在使用第三方套件（如 Bootstrap 的 Modal）時，或者需要調用像video.play()和video.…","2024-09-29T16:25:45+08:00",[11,710,711,725],[11,713],4608,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10360419",{"name":717,"url":718,"day":181},{"path":863,"title":864,"description":865,"date":866,"updated":9,"tags":867,"categories":868,"draft":15,"wordCount":488,"readingTime":122,"canonical":869,"series":870},"\u002Fithome-2024-day-15","Vue 3 用實作帶你看過核心概念 - Day 15：監聽器(Watchers)","在響應式狀態變化時執行特定操作，需要使用watch 監聽器。這些操作可能包含副作用 (side effects)，如發送請求、調用 API，或更新 DOM 以外的系統狀態，亦或是處理自定義的邏輯。","2024-09-28T18:30:31+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359958",{"name":717,"url":718,"day":192},{"path":872,"title":873,"description":874,"date":875,"updated":9,"tags":876,"categories":877,"draft":15,"wordCount":878,"readingTime":134,"canonical":879,"series":880},"\u002Fithome-2024-day-14","Vue 3 用實作帶你看過核心概念 - Day 14：生命週期鉤子(Lifecycle Hooks)","在 Vue 中，當一個組件創建時，它會經歷一系列的流程，這些流程決定了組件從初始化到渲染，再到最終銷毀的過程。","2024-09-27T21:57:32+08:00",[11,710,711,725],[11,713],3780,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10359419",{"name":717,"url":718,"day":203},{"path":882,"title":883,"description":884,"date":885,"updated":9,"tags":886,"categories":887,"draft":15,"wordCount":888,"readingTime":318,"canonical":889,"series":890},"\u002Fithome-2024-day-13","Vue 3 用實作帶你看過核心概念 - Day 13：v-model 雙向綁定控制 - 常用於表單輸入","表單輸入是開發中非常常見的需求，原始方式通常需要手動使用v-bind綁定表單元素的值 (value)，並透過v-on綁定input事件來監聽輸入變化。","2024-09-26T21:49:25+08:00",[11,710,711,725],[11,713],3153,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358850",{"name":717,"url":718,"day":214},{"path":892,"title":893,"description":894,"date":895,"updated":9,"tags":896,"categories":897,"draft":15,"wordCount":898,"readingTime":134,"canonical":899,"series":900},"\u002Fithome-2024-day-12","Vue 3 用實作帶你看過核心概念 - Day 12：v-on 指令 - 事件處理","在 Vue 中，監聽 HTML DOM 元素的事件變化，可以使用v-on 指令將 JavaScript 事件處理器綁定到對應的DOM 事件上。這個指令的縮寫符號為@，例如：v-on:click，其縮寫方式為@click。","2024-09-25T21:42:59+08:00",[11,710,711,725],[11,713],3808,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10358241",{"name":717,"url":718,"day":225},{"path":902,"title":903,"description":904,"date":905,"updated":9,"tags":906,"categories":907,"draft":15,"wordCount":908,"readingTime":134,"canonical":909,"series":910},"\u002Fithome-2024-day-11","Vue 3 用實作帶你看過核心概念 - Day 11：v-for 指令的基本與進階應用","當數據類型是陣列或物件時，我們不會希望將重複的選項或元素直接硬編碼在模板中。這樣的寫法會導致代碼冗長且不具可維護性。","2024-09-24T23:11:25+08:00",[11,710,711,725],[11,713],4381,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357705",{"name":717,"url":718,"day":236},{"path":912,"title":913,"description":914,"date":915,"updated":9,"tags":916,"categories":917,"draft":15,"wordCount":918,"readingTime":318,"canonical":919,"series":920},"\u002Fithome-2024-day-10","Vue 3 用實作帶你看過核心概念 - Day 10：條件渲染 - v-if 與 v-show 的區別與應用","開發中遇到需要處理【如果是...就要...】的情境時，Vue 提供了 v-if 指令來幫助我們根據條件進行元素的渲染。這樣可以在特定條件滿足時動態地顯示或隱藏某些元素。","2024-09-23T23:39:24+08:00",[11,710,711,725],[11,713],2827,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10357160",{"name":717,"url":718,"day":247},{"path":922,"title":923,"description":924,"date":925,"updated":9,"tags":926,"categories":927,"draft":15,"wordCount":928,"readingTime":122,"canonical":929,"series":930},"\u002Fithome-2024-day-09","Vue 3 用實作帶你看過核心概念 - Day 9：Class 與 Style 的樣式綁定","v-bind 指令通常用於綁定 HTML 屬性，但在實際開發中，最常見的應用場景之一還是綁定class和 style這兩個 HTML 屬性。","2024-09-22T15:05:53+08:00",[11,710,711,725],[11,713],4630,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10356119",{"name":717,"url":718,"day":76},{"path":932,"title":933,"description":934,"date":935,"updated":9,"tags":936,"categories":937,"draft":15,"wordCount":938,"readingTime":134,"canonical":939,"series":940},"\u002Fithome-2024-day-08","Vue 3 用實作帶你看過核心概念 - Day 8：計算屬性應用與實踐","在 Day 6 在使用 Vue 的Mustache語法時，可以用來放置表達式。不過，這邊建議避免在 Mustache 中撰寫過於複雜的邏輯。這樣的建議主要有兩個原因：","2024-09-21T21:33:29+08:00",[11,710,711,725],[11,713],4060,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10355474",{"name":717,"url":718,"day":110},{"path":942,"title":943,"description":944,"date":945,"updated":9,"tags":946,"categories":947,"draft":15,"wordCount":948,"readingTime":122,"canonical":949,"series":950},"\u002Fithome-2024-day-07","Vue 3 用實作帶你看過核心概念 - Day 7： 響應式基礎 - Options API","前面提到了模板搭配指令及 Mustache 的用法。今天則是要介紹所謂透過getter跟setter取值跟賦值的方式在 JavaScript 是怎麼做到的，這部分會跟 Vue 本身的響應式基礎有很大相關。此外，我也會帶著…","2024-09-20T23:41:42+08:00",[11,710,711],[11,713],4407,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354781",{"name":717,"url":718,"day":17},{"path":952,"title":953,"description":954,"date":955,"updated":9,"tags":956,"categories":957,"draft":15,"wordCount":958,"readingTime":134,"canonical":959,"series":960},"\u002Fithome-2024-day-06","Vue 3 用實作帶你看過核心概念 - Day 6：數據綁定和模板(Template)","Vue 使用基於 HTML 的模板語法（Template Syntax），將 Vue 實例中的數據綁定到模板中進行顯示。根據綁定的類型不同，數據可以影響元素的內容、屬性或樣式。","2024-09-19T20:39:46+08:00",[11,710,711,725],[11,713],3402,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10354231",{"name":717,"url":718,"day":31},{"path":962,"title":963,"description":964,"date":965,"updated":9,"tags":966,"categories":967,"draft":15,"wordCount":968,"readingTime":318,"canonical":969,"series":970},"\u002Fithome-2024-day-05","Vue 3 用實作帶你看過核心概念 - Day 5：Options API vs Composition API","前面已經詳細說明了 Vue 的環境建置方式，接下來將逐步介紹 Vue 應用說明，並比較Options API與Composition API的撰寫風格。","2024-09-18T22:10:36+08:00",[11,710,711,725,726],[11,713],2902,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10353717",{"name":717,"url":718,"day":122},{"path":972,"title":973,"description":974,"date":975,"updated":9,"tags":976,"categories":977,"draft":15,"wordCount":658,"readingTime":318,"canonical":978,"series":979},"\u002Fithome-2024-day-04","Vue 3 用實作帶你看過核心概念 - Day 4：基於 Vite 建構工具的 Vue 專案結構解析","在 Day 3 的教學中，學習了如何使用 Vue 鷹架工具(Vue project scaffolding tool)來生成一個基於 Vite 環境的專案。今天，我將更進一步地解析這個專案的資料夾結構，幫助大家更好地理解…","2024-09-17T23:27:07+08:00",[11,710,711,725],[11,713],"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10352470",{"name":717,"url":718,"day":134},{"path":981,"title":982,"description":983,"date":984,"updated":9,"tags":985,"categories":986,"draft":15,"wordCount":987,"readingTime":329,"canonical":988,"series":989},"\u002Fithome-2024-day-03","Vue 3 用實作帶你看過核心概念 - Day 3：使用 Vite 建構工具搭建 Vue 專案","⭐ 需要特別注意，需要事先安裝Nodejs。若不確定是否已安裝 Nodejs 也可以開啟終端機，執行以下指令確認版本。","2024-09-16T23:29:45+08:00",[11,710,711,725],[11,713],2100,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10351398",{"name":717,"url":718,"day":318},{"path":991,"title":992,"description":993,"date":994,"updated":9,"tags":995,"categories":996,"draft":15,"wordCount":997,"readingTime":329,"canonical":998,"series":999},"\u002Fithome-2024-day-02","Vue 3 用實作帶你看過核心概念 - Day 2：命令式編程 vs 聲明式編程","還記得我們在使用純 HTML 和 JavaScript 時，如何實現按下按鈕來變更文字顏色嗎？讓我們一起回想一下什麼是命令式宣告。","2024-09-15T22:52:27+08:00",[11,710,711,725,726],[11,713],1881,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350459",{"name":717,"url":718,"day":329},{"path":1001,"title":1002,"description":1003,"date":1004,"updated":9,"tags":1005,"categories":1006,"draft":15,"wordCount":1007,"readingTime":340,"canonical":1008,"series":1009},"\u002Fithome-2024-day-01","Vue 3 用實作帶你看過核心概念 - Day 1：前言介紹","大家好，我是 Antonio(安東尼奧)，目前是一名 Angular 的新手工程師，同時，也是一名熱愛寫程式的工程師。去年參加六角學院的 Vue 直播班，在那時候就深深著迷著 Vue 的語言，自此這半年多的時間裡，我白天…","2024-09-14T17:32:19+08:00",[11,710,711],[11,713],988,"https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10350457",{"name":717,"url":718,"day":340},{"path":1011,"title":1012,"description":1013,"date":1014,"updated":9,"tags":1015,"categories":1017,"draft":15,"wordCount":1019,"readingTime":329,"canonical":9,"series":9},"\u002Fhexschool-experience","六角學院 - 軟體工程師體驗營","這麼多線上課程? 你不想知道什麼是最吸引我的線上學習課程嗎?","2024-05-31T21:40:05+08:00",[686,1016],"軟體工程師體驗營",[689,1018],"前端成長記",1109,{"path":1021,"title":1022,"description":1023,"date":1024,"updated":9,"tags":1025,"categories":1033,"draft":15,"wordCount":1035,"readingTime":318,"canonical":9,"series":9},"\u002FJS-10","JavaScript：var、let、const之間的差異解析","var、let及const是JavaScript中宣告變數的關鍵字，它們在作用域、對於for迴圈的影響、提升 (Hoisting) 的差異及重複宣告的處理上有所不同。","2023-12-10T23:53:30+08:00",[1026,1027,1028,1029,1030,1031,1032],"ES6","var","let","const","Hoisting","Scope","TDZ",[1034,702],"JavaScript",2873,{"path":1037,"title":1038,"description":1039,"date":1040,"updated":9,"tags":1041,"categories":1048,"draft":15,"wordCount":1049,"readingTime":122,"canonical":9,"series":9},"\u002FJS-9","JavaScript：陣列的常見操作方法","掌握 JavaScript 陣列操作！從forEach、map、filter、findIndex、reduce、sort，深入了解這些方法，使你的程式碼更具靈活性和效率！","2023-12-01T15:01:06+08:00",[1042,1043,1044,1045,1046,1047],"forEach","map","filter","reduce","sort","findIndex",[1034,702],4846,{"path":1051,"title":1052,"description":1053,"date":1054,"updated":9,"tags":1055,"categories":1060,"draft":15,"wordCount":1061,"readingTime":318,"canonical":9,"series":9},"\u002FJS-8","JavaScript：ES module 匯入與匯出","使用\u003C script >標籤讓瀏覽器中的網頁能夠使用 ES 模組功能時，你了解不同的匯入及匯出方法了嗎？","2023-11-30T15:28:23+08:00",[1026,1056,1057,1058,1059],"ES Module","CommonJS","Node.js","模組化開發",[1034,702],2913,{"path":1063,"title":1064,"description":1065,"date":1066,"updated":9,"tags":1067,"categories":1070,"draft":15,"wordCount":1072,"readingTime":134,"canonical":9,"series":9},"\u002FReact-2","React：JSX 是什麼？為何在 React 中使用 JSX？","透過 JSX 語法糖，你能夠在一次 JavaScript 撰寫中包含 HTML 與 CSS 的內容","2023-11-29T16:03:40+08:00",[1068,1034,1069],"JSX","Babel",[1071,702],"React",3595,{"path":1074,"title":1075,"description":1076,"date":1077,"updated":9,"tags":1078,"categories":1081,"draft":15,"wordCount":1082,"readingTime":329,"canonical":9,"series":9},"\u002FReact-1","React：Pure Function 的概念及其在 React 中的應用","探索 Pure Function 概念及其與 React 的相互關係。","2023-11-29T15:50:17+08:00",[1034,1079,1080],"Pure Function","Functional Programming",[1071],1368,{"path":1084,"title":1085,"description":1086,"date":1087,"updated":9,"tags":1088,"categories":1093,"draft":15,"wordCount":1094,"readingTime":122,"canonical":9,"series":9},"\u002FJS-6","JavaScript：深入理解Promise的原理及實際應用","透過Promise，我們能夠有效解決JavaScript中的異步事件，使得程式碼更為清晰和可讀。","2023-11-29T14:49:18+08:00",[1089,1090,1091,1092],"Promise","AsyncAwait","Asynchronous Programming","異步處理",[1034,702],5479,{"path":1096,"title":1097,"description":1098,"date":1099,"updated":9,"tags":1100,"categories":1105,"draft":15,"wordCount":1106,"readingTime":122,"canonical":9,"series":9},"\u002FJS-5","JavaScript：物件傳參考特性(Pass by Reference)","淺拷貝(shallow copy)與深拷貝(deep copy)在JavaScript中的應用與理解","2023-11-29T09:55:50+08:00",[1101,1102,1103,1104],"Pass by Reference","Shallow Copy","Deep Copy","傳值與傳參考",[1034,702],4544,{"path":1108,"title":1109,"description":1110,"date":1111,"updated":9,"tags":1112,"categories":1118,"draft":15,"wordCount":1120,"readingTime":329,"canonical":9,"series":9},"\u002FJS-4","JavaScript：注重關注點的分離","透過關注點分離的設計方式，可顯著提高系統整體的可維護性。","2023-11-29T09:44:29+08:00",[1113,1114,1115,1116,1117],"SoC","MVC","Design Patterns","Clean Code","Refactoring",[1034,1119],"Software Engineering",1977,{"path":1122,"title":1123,"description":1124,"date":1125,"updated":9,"tags":1126,"categories":1132,"draft":15,"wordCount":1133,"readingTime":329,"canonical":9,"series":9},"\u002FJS-3","JavaScript：掌握箭頭函式，優雅簡約的程式寫作","深入解析ES6箭頭函數：揭開與傳統函式的差異。","2023-11-29T01:02:36+08:00",[1127,1128,1129,1130,1131],"Arrow Function","this","Lexical Scoping","Syntactic Sugar","Callback Function",[1034,702],2142,{"path":1135,"title":1136,"description":1137,"date":1138,"updated":9,"tags":1139,"categories":1143,"draft":15,"wordCount":1144,"readingTime":329,"canonical":9,"series":9},"\u002FJS-2","JavaScript：深入了解This的指向與應用","探索JavaScript This 的指向：深入剖析判斷的技巧。","2023-11-28T22:22:45+08:00",[1128,1140,1131,1141,1142],"Execution Context","Binding","Fundamentals",[1034,702],1963,{"path":1146,"title":1147,"description":1148,"date":1149,"updated":9,"tags":1150,"categories":1154,"draft":15,"wordCount":1155,"readingTime":340,"canonical":9,"series":9},"\u002FJS-1","JavaScript：淺談ES6常見縮寫","JavaScript ES6縮寫技巧：輕鬆簡化程式碼，提升效率！。","2023-11-28T13:38:14+08:00",[1026,1151,1152,1153],"Shorthand","Spread Operator","Object",[1034,702],818,{"path":1157,"title":1158,"description":1159,"date":1160,"updated":9,"tags":1161,"categories":1165,"draft":15,"wordCount":1166,"readingTime":340,"canonical":9,"series":9},"\u002FEmmet-1","前端開發神器：使用Emmet提升網頁開發效率","本文將介紹如何利用Emmet來大幅提高前端開發效率，讓你的編碼速度如專家般飛快。","2023-10-31T14:40:54+08:00",[1162,1163,1164],"Emmet","VSCode","HTML",[702,673],865,{"path":1168,"title":1169,"description":1170,"date":1171,"updated":9,"tags":1172,"categories":1177,"draft":15,"wordCount":1179,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-4","建立自己的部落格：了解Hexo指令，更換next部落格主題","今天的你跟我都是Hexo指令指揮官，讓我們將部落格造型大改造一下吧!!!","2023-10-20T14:33:38+08:00",[1173,1174,1175,1176],"Hexo","NexT","git clone","Hexo指令",[1178],"Blog",979,{"path":1181,"title":1182,"description":1183,"date":1184,"updated":9,"tags":1185,"categories":1189,"draft":15,"wordCount":1190,"readingTime":329,"canonical":9,"series":9},"\u002Fhexo-github-3","建立自己的部落格：修改Hexo設定檔中Site，查看其變化","今天的你跟我都是部落格工程師，我們一起來將網頁做一些小小的改變，有一天匯集在一起就會讓你的部落格變的與眾不同。","2023-10-18T20:50:31+08:00",[1173,1163,1186,1187,1188],"Sublime Text","YAML","SEO",[1178],1798,{"path":1192,"title":1193,"description":1194,"date":1195,"updated":9,"tags":1196,"categories":1199,"draft":15,"wordCount":1200,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-2","建立自己的部落格：創建Hexo Blog初始的模板","踏出那關鍵的第一步，從白紙到完整的部落格，一同見證你的部落格誕生的奇妙時刻。","2023-10-17T15:59:37+08:00",[1173,1197,1198],"Hexo-cli","hexo init",[1178],941,{"path":1202,"title":1203,"description":1204,"date":1205,"updated":9,"tags":1206,"categories":1208,"draft":15,"wordCount":1209,"readingTime":340,"canonical":9,"series":9},"\u002Fhexo-github-1","建立自己的部落格：認識Hexo及所其需工具(Nodejs & Git)","打造屬於自己風格的部落格其實不難，讓我們一步步手把手教你如何實現這個目標。","2023-10-16T08:30:27+08:00",[1173,1058,1207],"Git",[1178],790,{"id":1211,"title":162,"body":1212,"canonical":168,"categories":4118,"date":164,"description":163,"draft":15,"extension":4119,"meta":4120,"navigation":1295,"ogImage":9,"path":161,"rawbody":4121,"readingTime":31,"robots":9,"schemaOrg":9,"seo":4122,"series":4123,"sitemap":4124,"slug":4131,"stem":4132,"tags":4133,"updated":9,"wordCount":167,"__hash__":4134},"posts\u002F2026-10-01-ithome-2026-day-17.md",{"type":1213,"value":1214,"toc":4103},"minimark",[1215,1223,1232,1237,1240,1512,1527,1533,1540,1544,1557,1789,1804,1825,1831,1840,1844,1849,1926,1952,1958,1961,2057,2078,2081,2085,2094,2197,2207,2328,2339,2345,2348,2439,2445,2449,2455,2522,2532,2539,2632,2644,2648,2654,2660,2713,2722,2728,2796,2807,2814,2918,2925,3008,3014,3021,3025,3030,3161,3173,3179,3231,3246,3250,3256,3397,3400,3403,3529,3536,3572,3586,3592,3596,3601,3688,3691,3817,3823,3828,3832,3835,3840,3843,3893,3896,3944,3947,3992,3995,4001,4004,4009,4012,4024,4028,4099],[1216,1217,1218,1222],"p",{},[1219,1220,1221],"code",{},"effect()"," 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。",[1216,1224,1225,1226,1228,1229,1231],{},"這些寫法雖然能動，但用多了會讓狀態之間的關係變得難追。這篇會從 ",[1219,1227,1221],{}," 的執行方式開始，整理相依追蹤、cleanup，以及哪些情況真的需要使用 ",[1219,1230,1221],{},"。",[1233,1234,1236],"h2",{"id":1235},"effect-的基本行為","effect() 的基本行為",[1216,1238,1239],{},"先看一個最簡單的例子：",[1241,1242,1247],"pre",{"className":1243,"code":1244,"language":1245,"meta":1246,"style":1246},"language-ts shiki shiki-themes github-light github-dark-dimmed","import {\n  Component,\n  effect,\n  signal,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cbutton (click)=\"increase()\">\n      {{ count() }}\n    \u003C\u002Fbutton>\n  `,\n})\nexport class AppComponent {\n  readonly count = signal(0);\n\n  constructor() {\n    effect(() => {\n      console.log('count:', this.count());\n    });\n  }\n\n  increase() {\n    this.count.update(\n      value => value + 1\n    );\n  }\n}\n","ts","",[1219,1248,1249,1261,1266,1271,1276,1291,1297,1309,1320,1328,1333,1338,1343,1350,1355,1369,1394,1398,1406,1419,1446,1451,1456,1460,1467,1481,1498,1503,1507],{"__ignoreMap":1246},[1250,1251,1253,1257],"span",{"class":1252,"line":340},"line",[1250,1254,1256],{"class":1255},"s7YZ4","import",[1250,1258,1260],{"class":1259},"sYgZi"," {\n",[1250,1262,1263],{"class":1252,"line":329},[1250,1264,1265],{"class":1259},"  Component,\n",[1250,1267,1268],{"class":1252,"line":318},[1250,1269,1270],{"class":1259},"  effect,\n",[1250,1272,1273],{"class":1252,"line":134},[1250,1274,1275],{"class":1259},"  signal,\n",[1250,1277,1278,1281,1284,1288],{"class":1252,"line":122},[1250,1279,1280],{"class":1259},"} ",[1250,1282,1283],{"class":1255},"from",[1250,1285,1287],{"class":1286},"s-HuK"," '@angular\u002Fcore'",[1250,1289,1290],{"class":1259},";\n",[1250,1292,1293],{"class":1252,"line":31},[1250,1294,1296],{"emptyLinePlaceholder":1295},true,"\n",[1250,1298,1299,1302,1306],{"class":1252,"line":17},[1250,1300,1301],{"class":1259},"@",[1250,1303,1305],{"class":1304},"sPO5f","Component",[1250,1307,1308],{"class":1259},"({\n",[1250,1310,1311,1314,1317],{"class":1252,"line":110},[1250,1312,1313],{"class":1259},"  selector: ",[1250,1315,1316],{"class":1286},"'app-root'",[1250,1318,1319],{"class":1259},",\n",[1250,1321,1322,1325],{"class":1252,"line":76},[1250,1323,1324],{"class":1259},"  template: ",[1250,1326,1327],{"class":1286},"`\n",[1250,1329,1330],{"class":1252,"line":247},[1250,1331,1332],{"class":1286},"    \u003Cbutton (click)=\"increase()\">\n",[1250,1334,1335],{"class":1252,"line":236},[1250,1336,1337],{"class":1286},"      {{ count() }}\n",[1250,1339,1340],{"class":1252,"line":225},[1250,1341,1342],{"class":1286},"    \u003C\u002Fbutton>\n",[1250,1344,1345,1348],{"class":1252,"line":214},[1250,1346,1347],{"class":1286},"  `",[1250,1349,1319],{"class":1259},[1250,1351,1352],{"class":1252,"line":203},[1250,1353,1354],{"class":1259},"})\n",[1250,1356,1357,1360,1363,1367],{"class":1252,"line":192},[1250,1358,1359],{"class":1255},"export",[1250,1361,1362],{"class":1255}," class",[1250,1364,1366],{"class":1365},"sOLd2"," AppComponent",[1250,1368,1260],{"class":1259},[1250,1370,1371,1374,1378,1381,1384,1387,1391],{"class":1252,"line":181},[1250,1372,1373],{"class":1255},"  readonly",[1250,1375,1377],{"class":1376},"stnAF"," count",[1250,1379,1380],{"class":1255}," =",[1250,1382,1383],{"class":1304}," signal",[1250,1385,1386],{"class":1259},"(",[1250,1388,1390],{"class":1389},"sviXB","0",[1250,1392,1393],{"class":1259},");\n",[1250,1395,1396],{"class":1252,"line":170},[1250,1397,1296],{"emptyLinePlaceholder":1295},[1250,1399,1400,1403],{"class":1252,"line":159},[1250,1401,1402],{"class":1255},"  constructor",[1250,1404,1405],{"class":1259},"() {\n",[1250,1407,1408,1411,1414,1417],{"class":1252,"line":148},[1250,1409,1410],{"class":1304},"    effect",[1250,1412,1413],{"class":1259},"(() ",[1250,1415,1416],{"class":1255},"=>",[1250,1418,1260],{"class":1259},[1250,1420,1421,1424,1427,1429,1432,1435,1437,1440,1443],{"class":1252,"line":137},[1250,1422,1423],{"class":1259},"      console.",[1250,1425,1426],{"class":1304},"log",[1250,1428,1386],{"class":1259},[1250,1430,1431],{"class":1286},"'count:'",[1250,1433,1434],{"class":1259},", ",[1250,1436,1128],{"class":1389},[1250,1438,1439],{"class":1259},".",[1250,1441,1442],{"class":1304},"count",[1250,1444,1445],{"class":1259},"());\n",[1250,1447,1448],{"class":1252,"line":125},[1250,1449,1450],{"class":1259},"    });\n",[1250,1452,1453],{"class":1252,"line":113},[1250,1454,1455],{"class":1259},"  }\n",[1250,1457,1458],{"class":1252,"line":101},[1250,1459,1296],{"emptyLinePlaceholder":1295},[1250,1461,1462,1465],{"class":1252,"line":90},[1250,1463,1464],{"class":1304},"  increase",[1250,1466,1405],{"class":1259},[1250,1468,1469,1472,1475,1478],{"class":1252,"line":79},[1250,1470,1471],{"class":1389},"    this",[1250,1473,1474],{"class":1259},".count.",[1250,1476,1477],{"class":1304},"update",[1250,1479,1480],{"class":1259},"(\n",[1250,1482,1483,1486,1489,1492,1495],{"class":1252,"line":67},[1250,1484,1485],{"class":1376},"      value",[1250,1487,1488],{"class":1255}," =>",[1250,1490,1491],{"class":1259}," value ",[1250,1493,1494],{"class":1255},"+",[1250,1496,1497],{"class":1389}," 1\n",[1250,1499,1500],{"class":1252,"line":56},[1250,1501,1502],{"class":1259},"    );\n",[1250,1504,1505],{"class":1252,"line":45},[1250,1506,1455],{"class":1259},[1250,1508,1509],{"class":1252,"line":34},[1250,1510,1511],{"class":1259},"}\n",[1216,1513,1514,1516,1517,1520,1521,1523,1524,1526],{},[1219,1515,1221],{}," 執行時，會記錄同步讀取過的 signal。上面的程式讀取了 ",[1219,1518,1519],{},"this.count()","，所以 ",[1219,1522,1442],{}," 會成為相依，之後只要它改變，Angular 就會再次安排 ",[1219,1525,1221],{}," 執行。",[1216,1528,1529,1530,1532],{},"和其他 signal API 一樣，",[1219,1531,1221],{}," 會根據執行時讀取到的 signal 建立相依。不過它的用途不是產生新的狀態，而是在相依改變後執行一段程式。",[1216,1534,1535],{},[1536,1537],"img",{"alt":1538,"src":1539},"computed() 推算新狀態，effect() 把狀態交給外部系統","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851412\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fcomputed-vs-effect.png",[1233,1541,1543],{"id":1542},"effect-不會在每次-set-時立刻執行","effect() 不會在每次 set() 時立刻執行",[1216,1545,1546,1547,1549,1550,1553,1554,1556],{},"第一次使用 ",[1219,1548,1221],{}," 時，很容易把它理解成 signal 每次呼叫 ",[1219,1551,1552],{},"set()","，",[1219,1555,1221],{}," 就會立刻執行，但實際上不是。",[1241,1558,1560],{"className":1243,"code":1559,"language":1245,"meta":1246,"style":1246},"import {\n  Component,\n  effect,\n  signal,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cp>num：{{ num() }}\u003C\u002Fp>\n  `,\n})\nexport class AppComponent {\n  readonly num = signal(2);\n\n  constructor() {\n    effect(() => {\n      console.log(this.num());\n    });\n\n    this.num.set(4);\n\n    setTimeout(() => {\n      this.num.set(8);\n    }, 1000);\n\n    this.num.set(12);\n  }\n}\n",[1219,1561,1562,1568,1572,1576,1580,1590,1594,1602,1610,1616,1621,1627,1631,1641,1659,1663,1669,1679,1696,1700,1704,1721,1725,1736,1752,1762,1766,1781,1785],{"__ignoreMap":1246},[1250,1563,1564,1566],{"class":1252,"line":340},[1250,1565,1256],{"class":1255},[1250,1567,1260],{"class":1259},[1250,1569,1570],{"class":1252,"line":329},[1250,1571,1265],{"class":1259},[1250,1573,1574],{"class":1252,"line":318},[1250,1575,1270],{"class":1259},[1250,1577,1578],{"class":1252,"line":134},[1250,1579,1275],{"class":1259},[1250,1581,1582,1584,1586,1588],{"class":1252,"line":122},[1250,1583,1280],{"class":1259},[1250,1585,1283],{"class":1255},[1250,1587,1287],{"class":1286},[1250,1589,1290],{"class":1259},[1250,1591,1592],{"class":1252,"line":31},[1250,1593,1296],{"emptyLinePlaceholder":1295},[1250,1595,1596,1598,1600],{"class":1252,"line":17},[1250,1597,1301],{"class":1259},[1250,1599,1305],{"class":1304},[1250,1601,1308],{"class":1259},[1250,1603,1604,1606,1608],{"class":1252,"line":110},[1250,1605,1313],{"class":1259},[1250,1607,1316],{"class":1286},[1250,1609,1319],{"class":1259},[1250,1611,1612,1614],{"class":1252,"line":76},[1250,1613,1324],{"class":1259},[1250,1615,1327],{"class":1286},[1250,1617,1618],{"class":1252,"line":247},[1250,1619,1620],{"class":1286},"    \u003Cp>num：{{ num() }}\u003C\u002Fp>\n",[1250,1622,1623,1625],{"class":1252,"line":236},[1250,1624,1347],{"class":1286},[1250,1626,1319],{"class":1259},[1250,1628,1629],{"class":1252,"line":225},[1250,1630,1354],{"class":1259},[1250,1632,1633,1635,1637,1639],{"class":1252,"line":214},[1250,1634,1359],{"class":1255},[1250,1636,1362],{"class":1255},[1250,1638,1366],{"class":1365},[1250,1640,1260],{"class":1259},[1250,1642,1643,1645,1648,1650,1652,1654,1657],{"class":1252,"line":203},[1250,1644,1373],{"class":1255},[1250,1646,1647],{"class":1376}," num",[1250,1649,1380],{"class":1255},[1250,1651,1383],{"class":1304},[1250,1653,1386],{"class":1259},[1250,1655,1656],{"class":1389},"2",[1250,1658,1393],{"class":1259},[1250,1660,1661],{"class":1252,"line":192},[1250,1662,1296],{"emptyLinePlaceholder":1295},[1250,1664,1665,1667],{"class":1252,"line":181},[1250,1666,1402],{"class":1255},[1250,1668,1405],{"class":1259},[1250,1670,1671,1673,1675,1677],{"class":1252,"line":170},[1250,1672,1410],{"class":1304},[1250,1674,1413],{"class":1259},[1250,1676,1416],{"class":1255},[1250,1678,1260],{"class":1259},[1250,1680,1681,1683,1685,1687,1689,1691,1694],{"class":1252,"line":159},[1250,1682,1423],{"class":1259},[1250,1684,1426],{"class":1304},[1250,1686,1386],{"class":1259},[1250,1688,1128],{"class":1389},[1250,1690,1439],{"class":1259},[1250,1692,1693],{"class":1304},"num",[1250,1695,1445],{"class":1259},[1250,1697,1698],{"class":1252,"line":148},[1250,1699,1450],{"class":1259},[1250,1701,1702],{"class":1252,"line":137},[1250,1703,1296],{"emptyLinePlaceholder":1295},[1250,1705,1706,1708,1711,1714,1716,1719],{"class":1252,"line":125},[1250,1707,1471],{"class":1389},[1250,1709,1710],{"class":1259},".num.",[1250,1712,1713],{"class":1304},"set",[1250,1715,1386],{"class":1259},[1250,1717,1718],{"class":1389},"4",[1250,1720,1393],{"class":1259},[1250,1722,1723],{"class":1252,"line":113},[1250,1724,1296],{"emptyLinePlaceholder":1295},[1250,1726,1727,1730,1732,1734],{"class":1252,"line":101},[1250,1728,1729],{"class":1304},"    setTimeout",[1250,1731,1413],{"class":1259},[1250,1733,1416],{"class":1255},[1250,1735,1260],{"class":1259},[1250,1737,1738,1741,1743,1745,1747,1750],{"class":1252,"line":90},[1250,1739,1740],{"class":1389},"      this",[1250,1742,1710],{"class":1259},[1250,1744,1713],{"class":1304},[1250,1746,1386],{"class":1259},[1250,1748,1749],{"class":1389},"8",[1250,1751,1393],{"class":1259},[1250,1753,1754,1757,1760],{"class":1252,"line":79},[1250,1755,1756],{"class":1259},"    }, ",[1250,1758,1759],{"class":1389},"1000",[1250,1761,1393],{"class":1259},[1250,1763,1764],{"class":1252,"line":67},[1250,1765,1296],{"emptyLinePlaceholder":1295},[1250,1767,1768,1770,1772,1774,1776,1779],{"class":1252,"line":56},[1250,1769,1471],{"class":1389},[1250,1771,1710],{"class":1259},[1250,1773,1713],{"class":1304},[1250,1775,1386],{"class":1259},[1250,1777,1778],{"class":1389},"12",[1250,1780,1393],{"class":1259},[1250,1782,1783],{"class":1252,"line":45},[1250,1784,1455],{"class":1259},[1250,1786,1787],{"class":1252,"line":34},[1250,1788,1511],{"class":1259},[1216,1790,1791,1792,1794,1795,1797,1798,1800,1801,1803],{},"執行後會先印出 ",[1219,1793,1778],{},"，一秒後再印出 ",[1219,1796,1749],{},"，中間的 ",[1219,1799,1656],{}," 和 ",[1219,1802,1718],{}," 不會依序出現。",[1216,1805,1806,1807,1809,1810,1812,1813,1800,1816,1819,1820,1822,1823,1231],{},"原因是 ",[1219,1808,1221],{}," 不會在每次 ",[1219,1811,1552],{}," 當下同步執行，而是由 Angular 排程。",[1219,1814,1815],{},"set(4)",[1219,1817,1818],{},"set(12)"," 都發生在 ",[1219,1821,1221],{}," 再次執行以前，所以等它真正執行時，讀到的已經是 ",[1219,1824,1778],{},[1216,1826,1827],{},[1536,1828],{"alt":1829,"src":1830},"effect() 不是每次 set() 都立刻執行，讀到的是執行當下的最新值","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851417\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Feffect-scheduling.png",[1216,1832,1833,1834,1836,1837,1839],{},"所以 ",[1219,1835,1221],{}," 看到的是執行當下的最新狀態，不是每一次發生過的變化。如果每個值都不能漏掉，例如操作事件、WebSocket 訊息，或需要保留完整變化順序的資料流，就不適合只靠 ",[1219,1838,1221],{},"，這類需求通常更適合使用 RxJS、Output 或其他事件機制。",[1233,1841,1843],{"id":1842},"effect-只追蹤同步讀取的-signal","effect() 只追蹤同步讀取的 signal",[1216,1845,1846,1848],{},[1219,1847,1221],{}," 的相依追蹤只會發生在同步執行期間。",[1241,1850,1852],{"className":1243,"code":1851,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  console.log(this.user());\n\n  setTimeout(() => {\n    console.log(this.theme());\n  });\n});\n",[1219,1853,1854,1865,1883,1887,1898,1916,1921],{"__ignoreMap":1246},[1250,1855,1856,1859,1861,1863],{"class":1252,"line":340},[1250,1857,1858],{"class":1304},"effect",[1250,1860,1413],{"class":1259},[1250,1862,1416],{"class":1255},[1250,1864,1260],{"class":1259},[1250,1866,1867,1870,1872,1874,1876,1878,1881],{"class":1252,"line":329},[1250,1868,1869],{"class":1259},"  console.",[1250,1871,1426],{"class":1304},[1250,1873,1386],{"class":1259},[1250,1875,1128],{"class":1389},[1250,1877,1439],{"class":1259},[1250,1879,1880],{"class":1304},"user",[1250,1882,1445],{"class":1259},[1250,1884,1885],{"class":1252,"line":318},[1250,1886,1296],{"emptyLinePlaceholder":1295},[1250,1888,1889,1892,1894,1896],{"class":1252,"line":134},[1250,1890,1891],{"class":1304},"  setTimeout",[1250,1893,1413],{"class":1259},[1250,1895,1416],{"class":1255},[1250,1897,1260],{"class":1259},[1250,1899,1900,1903,1905,1907,1909,1911,1914],{"class":1252,"line":122},[1250,1901,1902],{"class":1259},"    console.",[1250,1904,1426],{"class":1304},[1250,1906,1386],{"class":1259},[1250,1908,1128],{"class":1389},[1250,1910,1439],{"class":1259},[1250,1912,1913],{"class":1304},"theme",[1250,1915,1445],{"class":1259},[1250,1917,1918],{"class":1252,"line":31},[1250,1919,1920],{"class":1259},"  });\n",[1250,1922,1923],{"class":1252,"line":17},[1250,1924,1925],{"class":1259},"});\n",[1216,1927,1928,1929,1931,1932,1935,1936,1939,1940,1943,1944,1947,1948,1951],{},"這個 ",[1219,1930,1221],{}," 會追蹤同步讀取的 ",[1219,1933,1934],{},"this.user()","，但不會追蹤 ",[1219,1937,1938],{},"setTimeout()"," 裡的 ",[1219,1941,1942],{},"this.theme()","，因為 Callback 執行時，原本的 Reactive Context 已經結束。",[1219,1945,1946],{},"subscribe()"," 的 Callback 和 ",[1219,1949,1950],{},"await"," 之後的程式也是一樣。",[1216,1953,1954],{},[1536,1955],{"alt":1956,"src":1957},"只有同步讀取的 Signal 會成為相依，setTimeout()、subscribe()、await 之後都不會","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851421\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fsync-dependency-tracking.png",[1216,1959,1960],{},"例如下面這段程式：",[1241,1962,1964],{"className":1243,"code":1963,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  const id = this.userId();\n\n  this.userService\n    .getUser(id)\n    .subscribe(() => {\n      console.log(this.theme());\n    });\n});\n",[1219,1965,1966,1976,1997,2001,2009,2020,2033,2049,2053],{"__ignoreMap":1246},[1250,1967,1968,1970,1972,1974],{"class":1252,"line":340},[1250,1969,1858],{"class":1304},[1250,1971,1413],{"class":1259},[1250,1973,1416],{"class":1255},[1250,1975,1260],{"class":1259},[1250,1977,1978,1981,1984,1986,1989,1991,1994],{"class":1252,"line":329},[1250,1979,1980],{"class":1255},"  const",[1250,1982,1983],{"class":1389}," id",[1250,1985,1380],{"class":1255},[1250,1987,1988],{"class":1389}," this",[1250,1990,1439],{"class":1259},[1250,1992,1993],{"class":1304},"userId",[1250,1995,1996],{"class":1259},"();\n",[1250,1998,1999],{"class":1252,"line":318},[1250,2000,1296],{"emptyLinePlaceholder":1295},[1250,2002,2003,2006],{"class":1252,"line":134},[1250,2004,2005],{"class":1389},"  this",[1250,2007,2008],{"class":1259},".userService\n",[1250,2010,2011,2014,2017],{"class":1252,"line":122},[1250,2012,2013],{"class":1259},"    .",[1250,2015,2016],{"class":1304},"getUser",[1250,2018,2019],{"class":1259},"(id)\n",[1250,2021,2022,2024,2027,2029,2031],{"class":1252,"line":31},[1250,2023,2013],{"class":1259},[1250,2025,2026],{"class":1304},"subscribe",[1250,2028,1413],{"class":1259},[1250,2030,1416],{"class":1255},[1250,2032,1260],{"class":1259},[1250,2034,2035,2037,2039,2041,2043,2045,2047],{"class":1252,"line":17},[1250,2036,1423],{"class":1259},[1250,2038,1426],{"class":1304},[1250,2040,1386],{"class":1259},[1250,2042,1128],{"class":1389},[1250,2044,1439],{"class":1259},[1250,2046,1913],{"class":1304},[1250,2048,1445],{"class":1259},[1250,2050,2051],{"class":1252,"line":110},[1250,2052,1450],{"class":1259},[1250,2054,2055],{"class":1252,"line":76},[1250,2056,1925],{"class":1259},[1216,2058,2059,2062,2063,2065,2066,2068,2069,2071,2072,2074,2075,2077],{},[1219,2060,2061],{},"this.userId()"," 是在 ",[1219,2064,1221],{}," 本體同步執行時讀取，所以會成為相依；",[1219,2067,1942],{}," 則是在 ",[1219,2070,1946],{}," 的 Callback 裡讀取，因此 ",[1219,2073,1913],{}," 改變時不會讓 ",[1219,2076,1221],{}," 重新執行。",[1216,2079,2080],{},"這和前面介紹過的 Reactive Context 是同一件事：Angular 只會記錄同步執行期間讀取到的 signal。",[1233,2082,2084],{"id":2083},"effect-裡建立訂閱要記得-cleanup","effect() 裡建立訂閱要記得 cleanup",[1216,2086,2087,2088,2090,2091,2093],{},"如果在 ",[1219,2089,1221],{}," 裡建立 Subscription，就要注意 ",[1219,2092,1221],{}," 可能會重新執行。",[1241,2095,2097],{"className":1243,"code":2096,"language":1245,"meta":1246,"style":1246},"readonly form =\n  viewChild.required\u003CNgForm>('form');\n\neffect(() => {\n  this.form()\n    .form\n    .valueChanges\n    .subscribe(value => {\n      console.log(value);\n    });\n});\n",[1219,2098,2099,2107,2129,2133,2143,2155,2160,2165,2180,2189,2193],{"__ignoreMap":1246},[1250,2100,2101,2104],{"class":1252,"line":340},[1250,2102,2103],{"class":1259},"readonly form ",[1250,2105,2106],{"class":1255},"=\n",[1250,2108,2109,2112,2115,2118,2121,2124,2127],{"class":1252,"line":329},[1250,2110,2111],{"class":1259},"  viewChild.",[1250,2113,2114],{"class":1304},"required",[1250,2116,2117],{"class":1259},"\u003C",[1250,2119,2120],{"class":1365},"NgForm",[1250,2122,2123],{"class":1259},">(",[1250,2125,2126],{"class":1286},"'form'",[1250,2128,1393],{"class":1259},[1250,2130,2131],{"class":1252,"line":318},[1250,2132,1296],{"emptyLinePlaceholder":1295},[1250,2134,2135,2137,2139,2141],{"class":1252,"line":134},[1250,2136,1858],{"class":1304},[1250,2138,1413],{"class":1259},[1250,2140,1416],{"class":1255},[1250,2142,1260],{"class":1259},[1250,2144,2145,2147,2149,2152],{"class":1252,"line":122},[1250,2146,2005],{"class":1389},[1250,2148,1439],{"class":1259},[1250,2150,2151],{"class":1304},"form",[1250,2153,2154],{"class":1259},"()\n",[1250,2156,2157],{"class":1252,"line":31},[1250,2158,2159],{"class":1259},"    .form\n",[1250,2161,2162],{"class":1252,"line":17},[1250,2163,2164],{"class":1259},"    .valueChanges\n",[1250,2166,2167,2169,2171,2173,2176,2178],{"class":1252,"line":110},[1250,2168,2013],{"class":1259},[1250,2170,2026],{"class":1304},[1250,2172,1386],{"class":1259},[1250,2174,2175],{"class":1376},"value",[1250,2177,1488],{"class":1255},[1250,2179,1260],{"class":1259},[1250,2181,2182,2184,2186],{"class":1252,"line":76},[1250,2183,1423],{"class":1259},[1250,2185,1426],{"class":1304},[1250,2187,2188],{"class":1259},"(value);\n",[1250,2190,2191],{"class":1252,"line":247},[1250,2192,1450],{"class":1259},[1250,2194,2195],{"class":1252,"line":236},[1250,2196,1925],{"class":1259},[1216,2198,2199,2200,2202,2203,2206],{},"每次 ",[1219,2201,1221],{}," 重跑，都會建立新的 Subscription，而前一次建立的 Subscription 不會自動取消。這時可以透過 ",[1219,2204,2205],{},"onCleanup()"," 處理：",[1241,2208,2210],{"className":1243,"code":2209,"language":1245,"meta":1246,"style":1246},"effect((onCleanup) => {\n  const form = this.form();\n\n  const sub =\n    form.form.valueChanges.subscribe(\n      value => {\n        console.log(value);\n      }\n    );\n\n  onCleanup(() => {\n    sub.unsubscribe();\n  });\n});\n",[1219,2211,2212,2229,2246,2250,2260,2269,2277,2286,2291,2295,2299,2310,2320,2324],{"__ignoreMap":1246},[1250,2213,2214,2216,2219,2222,2225,2227],{"class":1252,"line":340},[1250,2215,1858],{"class":1304},[1250,2217,2218],{"class":1259},"((",[1250,2220,2221],{"class":1376},"onCleanup",[1250,2223,2224],{"class":1259},") ",[1250,2226,1416],{"class":1255},[1250,2228,1260],{"class":1259},[1250,2230,2231,2233,2236,2238,2240,2242,2244],{"class":1252,"line":329},[1250,2232,1980],{"class":1255},[1250,2234,2235],{"class":1389}," form",[1250,2237,1380],{"class":1255},[1250,2239,1988],{"class":1389},[1250,2241,1439],{"class":1259},[1250,2243,2151],{"class":1304},[1250,2245,1996],{"class":1259},[1250,2247,2248],{"class":1252,"line":318},[1250,2249,1296],{"emptyLinePlaceholder":1295},[1250,2251,2252,2254,2257],{"class":1252,"line":134},[1250,2253,1980],{"class":1255},[1250,2255,2256],{"class":1389}," sub",[1250,2258,2259],{"class":1255}," =\n",[1250,2261,2262,2265,2267],{"class":1252,"line":122},[1250,2263,2264],{"class":1259},"    form.form.valueChanges.",[1250,2266,2026],{"class":1304},[1250,2268,1480],{"class":1259},[1250,2270,2271,2273,2275],{"class":1252,"line":31},[1250,2272,1485],{"class":1376},[1250,2274,1488],{"class":1255},[1250,2276,1260],{"class":1259},[1250,2278,2279,2282,2284],{"class":1252,"line":17},[1250,2280,2281],{"class":1259},"        console.",[1250,2283,1426],{"class":1304},[1250,2285,2188],{"class":1259},[1250,2287,2288],{"class":1252,"line":110},[1250,2289,2290],{"class":1259},"      }\n",[1250,2292,2293],{"class":1252,"line":76},[1250,2294,1502],{"class":1259},[1250,2296,2297],{"class":1252,"line":247},[1250,2298,1296],{"emptyLinePlaceholder":1295},[1250,2300,2301,2304,2306,2308],{"class":1252,"line":236},[1250,2302,2303],{"class":1304},"  onCleanup",[1250,2305,1413],{"class":1259},[1250,2307,1416],{"class":1255},[1250,2309,1260],{"class":1259},[1250,2311,2312,2315,2318],{"class":1252,"line":225},[1250,2313,2314],{"class":1259},"    sub.",[1250,2316,2317],{"class":1304},"unsubscribe",[1250,2319,1996],{"class":1259},[1250,2321,2322],{"class":1252,"line":214},[1250,2323,1920],{"class":1259},[1250,2325,2326],{"class":1252,"line":203},[1250,2327,1925],{"class":1259},[1216,2329,2330,2332,2333,2335,2336,2338],{},[1219,2331,2205],{}," 會在下一次 ",[1219,2334,1221],{}," 執行前呼叫，",[1219,2337,1221],{}," 被銷毀時也會執行，因此可以用來清掉前一次建立的資源。",[1216,2340,2341],{},[1536,2342],{"alt":2343,"src":2344},"相依改變時先執行 onCleanup() 取消舊資源，再重新執行 effect()","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851414\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Feffect-cleanup-lifecycle.png",[1216,2346,2347],{},"除了 Observable，也可以用來清除 Timer：",[1241,2349,2351],{"className":1243,"code":2350,"language":1245,"meta":1246,"style":1246},"effect((onCleanup) => {\n  const id = setInterval(() => {\n    console.log(this.count());\n  }, 1000);\n\n  onCleanup(() => {\n    clearInterval(id);\n  });\n});\n",[1219,2352,2353,2367,2384,2400,2409,2413,2423,2431,2435],{"__ignoreMap":1246},[1250,2354,2355,2357,2359,2361,2363,2365],{"class":1252,"line":340},[1250,2356,1858],{"class":1304},[1250,2358,2218],{"class":1259},[1250,2360,2221],{"class":1376},[1250,2362,2224],{"class":1259},[1250,2364,1416],{"class":1255},[1250,2366,1260],{"class":1259},[1250,2368,2369,2371,2373,2375,2378,2380,2382],{"class":1252,"line":329},[1250,2370,1980],{"class":1255},[1250,2372,1983],{"class":1389},[1250,2374,1380],{"class":1255},[1250,2376,2377],{"class":1304}," setInterval",[1250,2379,1413],{"class":1259},[1250,2381,1416],{"class":1255},[1250,2383,1260],{"class":1259},[1250,2385,2386,2388,2390,2392,2394,2396,2398],{"class":1252,"line":318},[1250,2387,1902],{"class":1259},[1250,2389,1426],{"class":1304},[1250,2391,1386],{"class":1259},[1250,2393,1128],{"class":1389},[1250,2395,1439],{"class":1259},[1250,2397,1442],{"class":1304},[1250,2399,1445],{"class":1259},[1250,2401,2402,2405,2407],{"class":1252,"line":134},[1250,2403,2404],{"class":1259},"  }, ",[1250,2406,1759],{"class":1389},[1250,2408,1393],{"class":1259},[1250,2410,2411],{"class":1252,"line":122},[1250,2412,1296],{"emptyLinePlaceholder":1295},[1250,2414,2415,2417,2419,2421],{"class":1252,"line":31},[1250,2416,2303],{"class":1304},[1250,2418,1413],{"class":1259},[1250,2420,1416],{"class":1255},[1250,2422,1260],{"class":1259},[1250,2424,2425,2428],{"class":1252,"line":17},[1250,2426,2427],{"class":1304},"    clearInterval",[1250,2429,2430],{"class":1259},"(id);\n",[1250,2432,2433],{"class":1252,"line":110},[1250,2434,1920],{"class":1259},[1250,2436,2437],{"class":1252,"line":76},[1250,2438,1925],{"class":1259},[1216,2440,2441,2442,2444],{},"Angular 會管理 ",[1219,2443,1221],{}," 本身的生命週期，但裡面建立的 Subscription、Timer 或其他外部資源，仍然要處理各自的清理。",[1233,2446,2448],{"id":2447},"不想追蹤某個-signal可以使用-untracked","不想追蹤某個 signal，可以使用 untracked()",[1216,2450,2451,2452,2454],{},"有時候 ",[1219,2453,1221],{}," 裡需要讀取某個 signal，但不希望它成為觸發條件。",[1241,2456,2458],{"className":1243,"code":2457,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  console.log(\n    'user:',\n    this.user(),\n    'theme:',\n    this.theme()\n  );\n});\n",[1219,2459,2460,2470,2478,2485,2496,2503,2513,2518],{"__ignoreMap":1246},[1250,2461,2462,2464,2466,2468],{"class":1252,"line":340},[1250,2463,1858],{"class":1304},[1250,2465,1413],{"class":1259},[1250,2467,1416],{"class":1255},[1250,2469,1260],{"class":1259},[1250,2471,2472,2474,2476],{"class":1252,"line":329},[1250,2473,1869],{"class":1259},[1250,2475,1426],{"class":1304},[1250,2477,1480],{"class":1259},[1250,2479,2480,2483],{"class":1252,"line":318},[1250,2481,2482],{"class":1286},"    'user:'",[1250,2484,1319],{"class":1259},[1250,2486,2487,2489,2491,2493],{"class":1252,"line":134},[1250,2488,1471],{"class":1389},[1250,2490,1439],{"class":1259},[1250,2492,1880],{"class":1304},[1250,2494,2495],{"class":1259},"(),\n",[1250,2497,2498,2501],{"class":1252,"line":122},[1250,2499,2500],{"class":1286},"    'theme:'",[1250,2502,1319],{"class":1259},[1250,2504,2505,2507,2509,2511],{"class":1252,"line":31},[1250,2506,1471],{"class":1389},[1250,2508,1439],{"class":1259},[1250,2510,1913],{"class":1304},[1250,2512,2154],{"class":1259},[1250,2514,2515],{"class":1252,"line":17},[1250,2516,2517],{"class":1259},"  );\n",[1250,2519,2520],{"class":1252,"line":110},[1250,2521,1925],{"class":1259},[1216,2523,2524,2525,1800,2527,2529,2530,1231],{},"這裡的 ",[1219,2526,1880],{},[1219,2528,1913],{}," 都會成為相依。假設需求只是 User 改變時記錄 Log，並順便把目前的 Theme 一起印出來，就不希望 Theme 改變時也重新執行 ",[1219,2531,1221],{},[1216,2533,2534,2535,2538],{},"這時可以使用 ",[1219,2536,2537],{},"untracked()","：",[1241,2540,2542],{"className":1243,"code":2541,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  const user = this.user();\n  const theme = untracked(this.theme);\n\n  console.log(\n    'user:',\n    user,\n    'theme:',\n    theme\n  );\n});\n",[1219,2543,2544,2554,2571,2590,2594,2602,2608,2613,2619,2624,2628],{"__ignoreMap":1246},[1250,2545,2546,2548,2550,2552],{"class":1252,"line":340},[1250,2547,1858],{"class":1304},[1250,2549,1413],{"class":1259},[1250,2551,1416],{"class":1255},[1250,2553,1260],{"class":1259},[1250,2555,2556,2558,2561,2563,2565,2567,2569],{"class":1252,"line":329},[1250,2557,1980],{"class":1255},[1250,2559,2560],{"class":1389}," user",[1250,2562,1380],{"class":1255},[1250,2564,1988],{"class":1389},[1250,2566,1439],{"class":1259},[1250,2568,1880],{"class":1304},[1250,2570,1996],{"class":1259},[1250,2572,2573,2575,2578,2580,2583,2585,2587],{"class":1252,"line":318},[1250,2574,1980],{"class":1255},[1250,2576,2577],{"class":1389}," theme",[1250,2579,1380],{"class":1255},[1250,2581,2582],{"class":1304}," untracked",[1250,2584,1386],{"class":1259},[1250,2586,1128],{"class":1389},[1250,2588,2589],{"class":1259},".theme);\n",[1250,2591,2592],{"class":1252,"line":134},[1250,2593,1296],{"emptyLinePlaceholder":1295},[1250,2595,2596,2598,2600],{"class":1252,"line":122},[1250,2597,1869],{"class":1259},[1250,2599,1426],{"class":1304},[1250,2601,1480],{"class":1259},[1250,2603,2604,2606],{"class":1252,"line":31},[1250,2605,2482],{"class":1286},[1250,2607,1319],{"class":1259},[1250,2609,2610],{"class":1252,"line":17},[1250,2611,2612],{"class":1259},"    user,\n",[1250,2614,2615,2617],{"class":1252,"line":110},[1250,2616,2500],{"class":1286},[1250,2618,1319],{"class":1259},[1250,2620,2621],{"class":1252,"line":76},[1250,2622,2623],{"class":1259},"    theme\n",[1250,2625,2626],{"class":1252,"line":247},[1250,2627,2517],{"class":1259},[1250,2629,2630],{"class":1252,"line":236},[1250,2631,1925],{"class":1259},[1216,2633,2634,2635,2637,2638,2640,2641,2643],{},"這樣只有 ",[1219,2636,1934],{}," 會成為相依，而 ",[1219,2639,1913],{}," 雖然一樣可以讀取，但它的變化不會讓 ",[1219,2642,1221],{}," 再次執行。",[1233,2645,2647],{"id":2646},"expliciteffect-明確指定相依","explicitEffect 明確指定相依",[1216,2649,2650,2651,1231],{},"如果希望直接寫出哪些 signal 會觸發副作用，也可以使用 ngxtension 提供的 ",[1219,2652,2653],{},"explicitEffect()",[1216,2655,2656,2657,2659],{},"一般的 ",[1219,2658,1221],{}," 會依照 Callback 裡實際讀取的 signal 自動建立相依：",[1241,2661,2663],{"className":1243,"code":2662,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  console.log(\n    this.id(),\n    this.keyword()\n  );\n});\n",[1219,2664,2665,2675,2683,2694,2705,2709],{"__ignoreMap":1246},[1250,2666,2667,2669,2671,2673],{"class":1252,"line":340},[1250,2668,1858],{"class":1304},[1250,2670,1413],{"class":1259},[1250,2672,1416],{"class":1255},[1250,2674,1260],{"class":1259},[1250,2676,2677,2679,2681],{"class":1252,"line":329},[1250,2678,1869],{"class":1259},[1250,2680,1426],{"class":1304},[1250,2682,1480],{"class":1259},[1250,2684,2685,2687,2689,2692],{"class":1252,"line":318},[1250,2686,1471],{"class":1389},[1250,2688,1439],{"class":1259},[1250,2690,2691],{"class":1304},"id",[1250,2693,2495],{"class":1259},[1250,2695,2696,2698,2700,2703],{"class":1252,"line":134},[1250,2697,1471],{"class":1389},[1250,2699,1439],{"class":1259},[1250,2701,2702],{"class":1304},"keyword",[1250,2704,2154],{"class":1259},[1250,2706,2707],{"class":1252,"line":122},[1250,2708,2517],{"class":1259},[1250,2710,2711],{"class":1252,"line":31},[1250,2712,1925],{"class":1259},[1216,2714,2524,2715,1800,2717,2719,2720,1231],{},[1219,2716,2691],{},[1219,2718,2702],{}," 都會觸發 ",[1219,2721,1221],{},[1216,2723,2724,2725,2727],{},"改成 ",[1219,2726,2653],{}," 後，可以把要追蹤的 signal 直接列在第一個參數：",[1241,2729,2731],{"className":1243,"code":2730,"language":1245,"meta":1246,"style":1246},"explicitEffect(\n  [this.id],\n  ([id]) => {\n    console.log(id);\n    console.log(this.keyword());\n  }\n);\n",[1219,2732,2733,2740,2750,2764,2772,2788,2792],{"__ignoreMap":1246},[1250,2734,2735,2738],{"class":1252,"line":340},[1250,2736,2737],{"class":1304},"explicitEffect",[1250,2739,1480],{"class":1259},[1250,2741,2742,2745,2747],{"class":1252,"line":329},[1250,2743,2744],{"class":1259},"  [",[1250,2746,1128],{"class":1389},[1250,2748,2749],{"class":1259},".id],\n",[1250,2751,2752,2755,2757,2760,2762],{"class":1252,"line":318},[1250,2753,2754],{"class":1259},"  ([",[1250,2756,2691],{"class":1376},[1250,2758,2759],{"class":1259},"]) ",[1250,2761,1416],{"class":1255},[1250,2763,1260],{"class":1259},[1250,2765,2766,2768,2770],{"class":1252,"line":134},[1250,2767,1902],{"class":1259},[1250,2769,1426],{"class":1304},[1250,2771,2430],{"class":1259},[1250,2773,2774,2776,2778,2780,2782,2784,2786],{"class":1252,"line":122},[1250,2775,1902],{"class":1259},[1250,2777,1426],{"class":1304},[1250,2779,1386],{"class":1259},[1250,2781,1128],{"class":1389},[1250,2783,1439],{"class":1259},[1250,2785,2702],{"class":1304},[1250,2787,1445],{"class":1259},[1250,2789,2790],{"class":1252,"line":31},[1250,2791,1455],{"class":1259},[1250,2793,2794],{"class":1252,"line":17},[1250,2795,1393],{"class":1259},[1216,2797,2798,2799,2802,2803,2806],{},"這時只有 ",[1219,2800,2801],{},"this.id"," 會觸發 Callback，即使裡面讀取了 ",[1219,2804,2805],{},"this.keyword()","，它也不會被加入相依。",[1216,2808,2809,2810,2813],{},"如果裡面建立了 Subscription，也可以透過 ",[1219,2811,2812],{},"cleanup"," 清理：",[1241,2815,2817],{"className":1243,"code":2816,"language":1245,"meta":1246,"style":1246},"explicitEffect(\n  [this.id],\n  ([id], cleanup) => {\n    const sub =\n      this.userService\n        .getUser(id)\n        .subscribe();\n\n    cleanup(() => {\n      sub.unsubscribe();\n    });\n  }\n);\n",[1219,2818,2819,2825,2833,2850,2859,2865,2874,2882,2886,2897,2906,2910,2914],{"__ignoreMap":1246},[1250,2820,2821,2823],{"class":1252,"line":340},[1250,2822,2737],{"class":1304},[1250,2824,1480],{"class":1259},[1250,2826,2827,2829,2831],{"class":1252,"line":329},[1250,2828,2744],{"class":1259},[1250,2830,1128],{"class":1389},[1250,2832,2749],{"class":1259},[1250,2834,2835,2837,2839,2842,2844,2846,2848],{"class":1252,"line":318},[1250,2836,2754],{"class":1259},[1250,2838,2691],{"class":1376},[1250,2840,2841],{"class":1259},"], ",[1250,2843,2812],{"class":1376},[1250,2845,2224],{"class":1259},[1250,2847,1416],{"class":1255},[1250,2849,1260],{"class":1259},[1250,2851,2852,2855,2857],{"class":1252,"line":134},[1250,2853,2854],{"class":1255},"    const",[1250,2856,2256],{"class":1389},[1250,2858,2259],{"class":1255},[1250,2860,2861,2863],{"class":1252,"line":122},[1250,2862,1740],{"class":1389},[1250,2864,2008],{"class":1259},[1250,2866,2867,2870,2872],{"class":1252,"line":31},[1250,2868,2869],{"class":1259},"        .",[1250,2871,2016],{"class":1304},[1250,2873,2019],{"class":1259},[1250,2875,2876,2878,2880],{"class":1252,"line":17},[1250,2877,2869],{"class":1259},[1250,2879,2026],{"class":1304},[1250,2881,1996],{"class":1259},[1250,2883,2884],{"class":1252,"line":110},[1250,2885,1296],{"emptyLinePlaceholder":1295},[1250,2887,2888,2891,2893,2895],{"class":1252,"line":76},[1250,2889,2890],{"class":1304},"    cleanup",[1250,2892,1413],{"class":1259},[1250,2894,1416],{"class":1255},[1250,2896,1260],{"class":1259},[1250,2898,2899,2902,2904],{"class":1252,"line":247},[1250,2900,2901],{"class":1259},"      sub.",[1250,2903,2317],{"class":1304},[1250,2905,1996],{"class":1259},[1250,2907,2908],{"class":1252,"line":236},[1250,2909,1450],{"class":1259},[1250,2911,2912],{"class":1252,"line":225},[1250,2913,1455],{"class":1259},[1250,2915,2916],{"class":1252,"line":214},[1250,2917,1393],{"class":1259},[1216,2919,2920,2921,2924],{},"另外還可以透過 ",[1219,2922,2923],{},"defer: true"," 跳過第一次執行：",[1241,2926,2928],{"className":1243,"code":2927,"language":1245,"meta":1246,"style":1246},"explicitEffect(\n  [this.id],\n  ([id]) => {\n    console.log(\n      'id changed:',\n      id\n    );\n  },\n  {\n    defer: true,\n  }\n);\n",[1219,2929,2930,2936,2944,2956,2964,2971,2976,2980,2985,2990,3000,3004],{"__ignoreMap":1246},[1250,2931,2932,2934],{"class":1252,"line":340},[1250,2933,2737],{"class":1304},[1250,2935,1480],{"class":1259},[1250,2937,2938,2940,2942],{"class":1252,"line":329},[1250,2939,2744],{"class":1259},[1250,2941,1128],{"class":1389},[1250,2943,2749],{"class":1259},[1250,2945,2946,2948,2950,2952,2954],{"class":1252,"line":318},[1250,2947,2754],{"class":1259},[1250,2949,2691],{"class":1376},[1250,2951,2759],{"class":1259},[1250,2953,1416],{"class":1255},[1250,2955,1260],{"class":1259},[1250,2957,2958,2960,2962],{"class":1252,"line":134},[1250,2959,1902],{"class":1259},[1250,2961,1426],{"class":1304},[1250,2963,1480],{"class":1259},[1250,2965,2966,2969],{"class":1252,"line":122},[1250,2967,2968],{"class":1286},"      'id changed:'",[1250,2970,1319],{"class":1259},[1250,2972,2973],{"class":1252,"line":31},[1250,2974,2975],{"class":1259},"      id\n",[1250,2977,2978],{"class":1252,"line":17},[1250,2979,1502],{"class":1259},[1250,2981,2982],{"class":1252,"line":110},[1250,2983,2984],{"class":1259},"  },\n",[1250,2986,2987],{"class":1252,"line":76},[1250,2988,2989],{"class":1259},"  {\n",[1250,2991,2992,2995,2998],{"class":1252,"line":247},[1250,2993,2994],{"class":1259},"    defer: ",[1250,2996,2997],{"class":1389},"true",[1250,2999,1319],{"class":1259},[1250,3001,3002],{"class":1252,"line":236},[1250,3003,1455],{"class":1259},[1250,3005,3006],{"class":1252,"line":225},[1250,3007,1393],{"class":1259},[1216,3009,3010,3011,3013],{},"這樣只有後續 ",[1219,3012,2691],{}," 改變時才會執行 Callback。",[1216,3015,3016,3018,3019,1231],{},[1219,3017,2653],{}," 的重點是讓相依來源更明確，適合用在希望清楚控制觸發條件的副作用；它改變的是相依追蹤方式，不會改變這段邏輯本身適不適合使用 ",[1219,3020,1221],{},[1233,3022,3024],{"id":3023},"不要用-effect-搬動狀態","不要用 effect() 搬動狀態",[1216,3026,3027,3029],{},[1219,3028,1221],{}," 很常被拿來讀取一個 signal，再把結果寫進另一個 signal。",[1241,3031,3033],{"className":1243,"code":3032,"language":1245,"meta":1246,"style":1246},"readonly firstName =\n  signal('Antonio');\n\nreadonly lastName =\n  signal('Ling');\n\nreadonly fullName =\n  signal('');\n\neffect(() => {\n  this.fullName.set(\n    `${this.firstName()} ${this.lastName()}`\n  );\n});\n",[1219,3034,3035,3042,3054,3058,3065,3076,3080,3087,3098,3102,3112,3123,3153,3157],{"__ignoreMap":1246},[1250,3036,3037,3040],{"class":1252,"line":340},[1250,3038,3039],{"class":1259},"readonly firstName ",[1250,3041,2106],{"class":1255},[1250,3043,3044,3047,3049,3052],{"class":1252,"line":329},[1250,3045,3046],{"class":1304},"  signal",[1250,3048,1386],{"class":1259},[1250,3050,3051],{"class":1286},"'Antonio'",[1250,3053,1393],{"class":1259},[1250,3055,3056],{"class":1252,"line":318},[1250,3057,1296],{"emptyLinePlaceholder":1295},[1250,3059,3060,3063],{"class":1252,"line":134},[1250,3061,3062],{"class":1259},"readonly lastName ",[1250,3064,2106],{"class":1255},[1250,3066,3067,3069,3071,3074],{"class":1252,"line":122},[1250,3068,3046],{"class":1304},[1250,3070,1386],{"class":1259},[1250,3072,3073],{"class":1286},"'Ling'",[1250,3075,1393],{"class":1259},[1250,3077,3078],{"class":1252,"line":31},[1250,3079,1296],{"emptyLinePlaceholder":1295},[1250,3081,3082,3085],{"class":1252,"line":17},[1250,3083,3084],{"class":1259},"readonly fullName ",[1250,3086,2106],{"class":1255},[1250,3088,3089,3091,3093,3096],{"class":1252,"line":110},[1250,3090,3046],{"class":1304},[1250,3092,1386],{"class":1259},[1250,3094,3095],{"class":1286},"''",[1250,3097,1393],{"class":1259},[1250,3099,3100],{"class":1252,"line":76},[1250,3101,1296],{"emptyLinePlaceholder":1295},[1250,3103,3104,3106,3108,3110],{"class":1252,"line":247},[1250,3105,1858],{"class":1304},[1250,3107,1413],{"class":1259},[1250,3109,1416],{"class":1255},[1250,3111,1260],{"class":1259},[1250,3113,3114,3116,3119,3121],{"class":1252,"line":236},[1250,3115,2005],{"class":1389},[1250,3117,3118],{"class":1259},".fullName.",[1250,3120,1713],{"class":1304},[1250,3122,1480],{"class":1259},[1250,3124,3125,3128,3130,3132,3135,3138,3141,3143,3145,3148,3150],{"class":1252,"line":225},[1250,3126,3127],{"class":1286},"    `${",[1250,3129,1128],{"class":1389},[1250,3131,1439],{"class":1286},[1250,3133,3134],{"class":1304},"firstName",[1250,3136,3137],{"class":1286},"()",[1250,3139,3140],{"class":1286},"} ${",[1250,3142,1128],{"class":1389},[1250,3144,1439],{"class":1286},[1250,3146,3147],{"class":1304},"lastName",[1250,3149,3137],{"class":1286},[1250,3151,3152],{"class":1286},"}`\n",[1250,3154,3155],{"class":1252,"line":214},[1250,3156,2517],{"class":1259},[1250,3158,3159],{"class":1252,"line":203},[1250,3160,1925],{"class":1259},[1216,3162,3163,3164,3167,3168,1800,3170,3172],{},"這段程式可以正常運作，但 ",[1219,3165,3166],{},"fullName"," 完全由 ",[1219,3169,3134],{},[1219,3171,3147],{}," 決定，沒有必要另外維護一份 Writable Signal。",[1216,3174,3175,3176,2538],{},"這類可以直接從其他狀態推算出來的值，可以直接使用 ",[1219,3177,3178],{},"computed()",[1241,3180,3182],{"className":1243,"code":3181,"language":1245,"meta":1246,"style":1246},"readonly fullName =\n  computed(\n    () => `${this.firstName()} ${this.lastName()}`\n  );\n",[1219,3183,3184,3190,3197,3227],{"__ignoreMap":1246},[1250,3185,3186,3188],{"class":1252,"line":340},[1250,3187,3084],{"class":1259},[1250,3189,2106],{"class":1255},[1250,3191,3192,3195],{"class":1252,"line":329},[1250,3193,3194],{"class":1304},"  computed",[1250,3196,1480],{"class":1259},[1250,3198,3199,3202,3204,3207,3209,3211,3213,3215,3217,3219,3221,3223,3225],{"class":1252,"line":318},[1250,3200,3201],{"class":1259},"    () ",[1250,3203,1416],{"class":1255},[1250,3205,3206],{"class":1286}," `${",[1250,3208,1128],{"class":1389},[1250,3210,1439],{"class":1286},[1250,3212,3134],{"class":1304},[1250,3214,3137],{"class":1286},[1250,3216,3140],{"class":1286},[1250,3218,1128],{"class":1389},[1250,3220,1439],{"class":1286},[1250,3222,3147],{"class":1304},[1250,3224,3137],{"class":1286},[1250,3226,3152],{"class":1286},[1250,3228,3229],{"class":1252,"line":134},[1250,3230,2517],{"class":1259},[1216,3232,3233,3234,3236,3237,3239,3240,3242,3243,3245],{},"重點不是看到 ",[1219,3235,1221],{}," 就一定要改成 ",[1219,3238,3178],{},"，而是當 ",[1219,3241,1221],{}," 裡又去 ",[1219,3244,1552],{}," 另一個 signal 時，可以先確認這份狀態是不是真的需要另外保存。",[1233,3247,3249],{"id":3248},"打-api-也不一定需要-effect","打 API 也不一定需要 effect()",[1216,3251,3252,3253,3255],{},"另一個常見寫法，是用 signal 當查詢條件，在 ",[1219,3254,1221],{}," 裡發送 API 請求：",[1241,3257,3259],{"className":1243,"code":3258,"language":1245,"meta":1246,"style":1246},"readonly searchId =\n  signal('');\n\nreadonly person =\n  signal\u003CPerson | null>(null);\n\neffect(() => {\n  const id = this.searchId();\n\n  this.personService\n    .getPerson(id)\n    .subscribe(person => {\n      this.person.set(person);\n    });\n});\n",[1219,3260,3261,3268,3278,3282,3289,3311,3315,3325,3342,3346,3353,3362,3377,3389,3393],{"__ignoreMap":1246},[1250,3262,3263,3266],{"class":1252,"line":340},[1250,3264,3265],{"class":1259},"readonly searchId ",[1250,3267,2106],{"class":1255},[1250,3269,3270,3272,3274,3276],{"class":1252,"line":329},[1250,3271,3046],{"class":1304},[1250,3273,1386],{"class":1259},[1250,3275,3095],{"class":1286},[1250,3277,1393],{"class":1259},[1250,3279,3280],{"class":1252,"line":318},[1250,3281,1296],{"emptyLinePlaceholder":1295},[1250,3283,3284,3287],{"class":1252,"line":134},[1250,3285,3286],{"class":1259},"readonly person ",[1250,3288,2106],{"class":1255},[1250,3290,3291,3293,3295,3298,3301,3304,3306,3309],{"class":1252,"line":122},[1250,3292,3046],{"class":1304},[1250,3294,2117],{"class":1259},[1250,3296,3297],{"class":1365},"Person",[1250,3299,3300],{"class":1255}," |",[1250,3302,3303],{"class":1389}," null",[1250,3305,2123],{"class":1259},[1250,3307,3308],{"class":1389},"null",[1250,3310,1393],{"class":1259},[1250,3312,3313],{"class":1252,"line":31},[1250,3314,1296],{"emptyLinePlaceholder":1295},[1250,3316,3317,3319,3321,3323],{"class":1252,"line":17},[1250,3318,1858],{"class":1304},[1250,3320,1413],{"class":1259},[1250,3322,1416],{"class":1255},[1250,3324,1260],{"class":1259},[1250,3326,3327,3329,3331,3333,3335,3337,3340],{"class":1252,"line":110},[1250,3328,1980],{"class":1255},[1250,3330,1983],{"class":1389},[1250,3332,1380],{"class":1255},[1250,3334,1988],{"class":1389},[1250,3336,1439],{"class":1259},[1250,3338,3339],{"class":1304},"searchId",[1250,3341,1996],{"class":1259},[1250,3343,3344],{"class":1252,"line":76},[1250,3345,1296],{"emptyLinePlaceholder":1295},[1250,3347,3348,3350],{"class":1252,"line":247},[1250,3349,2005],{"class":1389},[1250,3351,3352],{"class":1259},".personService\n",[1250,3354,3355,3357,3360],{"class":1252,"line":236},[1250,3356,2013],{"class":1259},[1250,3358,3359],{"class":1304},"getPerson",[1250,3361,2019],{"class":1259},[1250,3363,3364,3366,3368,3370,3373,3375],{"class":1252,"line":225},[1250,3365,2013],{"class":1259},[1250,3367,2026],{"class":1304},[1250,3369,1386],{"class":1259},[1250,3371,3372],{"class":1376},"person",[1250,3374,1488],{"class":1255},[1250,3376,1260],{"class":1259},[1250,3378,3379,3381,3384,3386],{"class":1252,"line":214},[1250,3380,1740],{"class":1389},[1250,3382,3383],{"class":1259},".person.",[1250,3385,1713],{"class":1304},[1250,3387,3388],{"class":1259},"(person);\n",[1250,3390,3391],{"class":1252,"line":203},[1250,3392,1450],{"class":1259},[1250,3394,3395],{"class":1252,"line":192},[1250,3396,1925],{"class":1259},[1216,3398,3399],{},"一開始看起來不複雜，但搜尋功能通常還會加入 debounce、取消上一筆請求、避免重複查詢、錯誤處理，以及 Component Destroy 時取消訂閱等需求。",[1216,3401,3402],{},"如果需求包含這些非同步流程，RxJS 會更適合處理：",[1241,3404,3406],{"className":1243,"code":3405,"language":1245,"meta":1246,"style":1246},"readonly searchId =\n  signal('');\n\nprivate readonly searchId$ =\n  toObservable(this.searchId);\n\nreadonly person$ =\n  this.searchId$.pipe(\n    debounceTime(300),\n    distinctUntilChanged(),\n    switchMap(id =>\n      this.personService.getPerson(id)\n    ),\n    takeUntilDestroyed()\n  );\n",[1219,3407,3408,3414,3424,3428,3435,3447,3451,3458,3470,3483,3490,3502,3513,3518,3525],{"__ignoreMap":1246},[1250,3409,3410,3412],{"class":1252,"line":340},[1250,3411,3265],{"class":1259},[1250,3413,2106],{"class":1255},[1250,3415,3416,3418,3420,3422],{"class":1252,"line":329},[1250,3417,3046],{"class":1304},[1250,3419,1386],{"class":1259},[1250,3421,3095],{"class":1286},[1250,3423,1393],{"class":1259},[1250,3425,3426],{"class":1252,"line":318},[1250,3427,1296],{"emptyLinePlaceholder":1295},[1250,3429,3430,3433],{"class":1252,"line":134},[1250,3431,3432],{"class":1259},"private readonly searchId$ ",[1250,3434,2106],{"class":1255},[1250,3436,3437,3440,3442,3444],{"class":1252,"line":122},[1250,3438,3439],{"class":1304},"  toObservable",[1250,3441,1386],{"class":1259},[1250,3443,1128],{"class":1389},[1250,3445,3446],{"class":1259},".searchId);\n",[1250,3448,3449],{"class":1252,"line":31},[1250,3450,1296],{"emptyLinePlaceholder":1295},[1250,3452,3453,3456],{"class":1252,"line":17},[1250,3454,3455],{"class":1259},"readonly person$ ",[1250,3457,2106],{"class":1255},[1250,3459,3460,3462,3465,3468],{"class":1252,"line":110},[1250,3461,2005],{"class":1389},[1250,3463,3464],{"class":1259},".searchId$.",[1250,3466,3467],{"class":1304},"pipe",[1250,3469,1480],{"class":1259},[1250,3471,3472,3475,3477,3480],{"class":1252,"line":76},[1250,3473,3474],{"class":1304},"    debounceTime",[1250,3476,1386],{"class":1259},[1250,3478,3479],{"class":1389},"300",[1250,3481,3482],{"class":1259},"),\n",[1250,3484,3485,3488],{"class":1252,"line":247},[1250,3486,3487],{"class":1304},"    distinctUntilChanged",[1250,3489,2495],{"class":1259},[1250,3491,3492,3495,3497,3499],{"class":1252,"line":236},[1250,3493,3494],{"class":1304},"    switchMap",[1250,3496,1386],{"class":1259},[1250,3498,2691],{"class":1376},[1250,3500,3501],{"class":1255}," =>\n",[1250,3503,3504,3506,3509,3511],{"class":1252,"line":225},[1250,3505,1740],{"class":1389},[1250,3507,3508],{"class":1259},".personService.",[1250,3510,3359],{"class":1304},[1250,3512,2019],{"class":1259},[1250,3514,3515],{"class":1252,"line":214},[1250,3516,3517],{"class":1259},"    ),\n",[1250,3519,3520,3523],{"class":1252,"line":203},[1250,3521,3522],{"class":1304},"    takeUntilDestroyed",[1250,3524,2154],{"class":1259},[1250,3526,3527],{"class":1252,"line":192},[1250,3528,2517],{"class":1259},[1216,3530,3531,3532,3535],{},"如果畫面最後還是希望使用 signal，可以再透過 ",[1219,3533,3534],{},"toSignal()"," 轉回來：",[1241,3537,3539],{"className":1243,"code":3538,"language":1245,"meta":1246,"style":1246},"readonly person =\n  toSignal(this.person$, {\n    initialValue: null,\n  });\n",[1219,3540,3541,3547,3559,3568],{"__ignoreMap":1246},[1250,3542,3543,3545],{"class":1252,"line":340},[1250,3544,3286],{"class":1259},[1250,3546,2106],{"class":1255},[1250,3548,3549,3552,3554,3556],{"class":1252,"line":329},[1250,3550,3551],{"class":1304},"  toSignal",[1250,3553,1386],{"class":1259},[1250,3555,1128],{"class":1389},[1250,3557,3558],{"class":1259},".person$, {\n",[1250,3560,3561,3564,3566],{"class":1252,"line":318},[1250,3562,3563],{"class":1259},"    initialValue: ",[1250,3565,3308],{"class":1389},[1250,3567,1319],{"class":1259},[1250,3569,3570],{"class":1252,"line":134},[1250,3571,1920],{"class":1259},[1216,3573,3574,3575,3578,3579,3578,3582,3585],{},"如果只是「參數改變後重新取得資料」，前面介紹過的 ",[1219,3576,3577],{},"resource()","、",[1219,3580,3581],{},"httpResource()",[1219,3583,3584],{},"rxResource()"," 也可以直接處理這類情境，並提供 Loading、Error、Value 等狀態。",[1216,3587,3588,3589,3591],{},"重點不是 API 一定要改用哪一種，而是這類非同步資料取得，不一定需要靠 ",[1219,3590,1221],{}," 監聽參數、訂閱請求，再手動把結果寫回另一個 signal。",[1233,3593,3595],{"id":3594},"改畫面樣式也不一定需要-effect","改畫面樣式也不一定需要 effect()",[1216,3597,3598,3600],{},[1219,3599,1221],{}," 也常被拿來直接操作 DOM。",[1241,3602,3604],{"className":1243,"code":3603,"language":1245,"meta":1246,"style":1246},"readonly fontSize =\n  signal(16);\n\neffect(() => {\n  this.renderer.setStyle(\n    this.element.nativeElement,\n    '--main-font-size',\n    `${this.fontSize()}px`\n  );\n});\n",[1219,3605,3606,3613,3624,3628,3638,3650,3657,3664,3680,3684],{"__ignoreMap":1246},[1250,3607,3608,3611],{"class":1252,"line":340},[1250,3609,3610],{"class":1259},"readonly fontSize ",[1250,3612,2106],{"class":1255},[1250,3614,3615,3617,3619,3622],{"class":1252,"line":329},[1250,3616,3046],{"class":1304},[1250,3618,1386],{"class":1259},[1250,3620,3621],{"class":1389},"16",[1250,3623,1393],{"class":1259},[1250,3625,3626],{"class":1252,"line":318},[1250,3627,1296],{"emptyLinePlaceholder":1295},[1250,3629,3630,3632,3634,3636],{"class":1252,"line":134},[1250,3631,1858],{"class":1304},[1250,3633,1413],{"class":1259},[1250,3635,1416],{"class":1255},[1250,3637,1260],{"class":1259},[1250,3639,3640,3642,3645,3648],{"class":1252,"line":122},[1250,3641,2005],{"class":1389},[1250,3643,3644],{"class":1259},".renderer.",[1250,3646,3647],{"class":1304},"setStyle",[1250,3649,1480],{"class":1259},[1250,3651,3652,3654],{"class":1252,"line":31},[1250,3653,1471],{"class":1389},[1250,3655,3656],{"class":1259},".element.nativeElement,\n",[1250,3658,3659,3662],{"class":1252,"line":17},[1250,3660,3661],{"class":1286},"    '--main-font-size'",[1250,3663,1319],{"class":1259},[1250,3665,3666,3668,3670,3672,3675,3677],{"class":1252,"line":110},[1250,3667,3127],{"class":1286},[1250,3669,1128],{"class":1389},[1250,3671,1439],{"class":1286},[1250,3673,3674],{"class":1304},"fontSize",[1250,3676,3137],{"class":1286},[1250,3678,3679],{"class":1286},"}px`\n",[1250,3681,3682],{"class":1252,"line":76},[1250,3683,2517],{"class":1259},[1250,3685,3686],{"class":1252,"line":247},[1250,3687,1925],{"class":1259},[1216,3689,3690],{},"如果只是把 signal 對應到畫面樣式，可以直接透過 Angular 的 Binding：",[1241,3692,3694],{"className":1243,"code":3693,"language":1245,"meta":1246,"style":1246},"@Component({\n  selector: 'app-root',\n  host: {\n    '[style.--main-font-size]':\n      'fontSizePx()',\n  },\n})\nexport class AppComponent {\n  readonly fontSize =\n    signal(16);\n\n  readonly fontSizePx =\n    computed(\n      () => `${this.fontSize()}px`\n    );\n}\n",[1219,3695,3696,3704,3712,3717,3725,3732,3736,3740,3750,3759,3770,3774,3783,3790,3809,3813],{"__ignoreMap":1246},[1250,3697,3698,3700,3702],{"class":1252,"line":340},[1250,3699,1301],{"class":1259},[1250,3701,1305],{"class":1304},[1250,3703,1308],{"class":1259},[1250,3705,3706,3708,3710],{"class":1252,"line":329},[1250,3707,1313],{"class":1259},[1250,3709,1316],{"class":1286},[1250,3711,1319],{"class":1259},[1250,3713,3714],{"class":1252,"line":318},[1250,3715,3716],{"class":1259},"  host: {\n",[1250,3718,3719,3722],{"class":1252,"line":134},[1250,3720,3721],{"class":1286},"    '[style.--main-font-size]'",[1250,3723,3724],{"class":1259},":\n",[1250,3726,3727,3730],{"class":1252,"line":122},[1250,3728,3729],{"class":1286},"      'fontSizePx()'",[1250,3731,1319],{"class":1259},[1250,3733,3734],{"class":1252,"line":31},[1250,3735,2984],{"class":1259},[1250,3737,3738],{"class":1252,"line":17},[1250,3739,1354],{"class":1259},[1250,3741,3742,3744,3746,3748],{"class":1252,"line":110},[1250,3743,1359],{"class":1255},[1250,3745,1362],{"class":1255},[1250,3747,1366],{"class":1365},[1250,3749,1260],{"class":1259},[1250,3751,3752,3754,3757],{"class":1252,"line":76},[1250,3753,1373],{"class":1255},[1250,3755,3756],{"class":1376}," fontSize",[1250,3758,2259],{"class":1255},[1250,3760,3761,3764,3766,3768],{"class":1252,"line":247},[1250,3762,3763],{"class":1304},"    signal",[1250,3765,1386],{"class":1259},[1250,3767,3621],{"class":1389},[1250,3769,1393],{"class":1259},[1250,3771,3772],{"class":1252,"line":236},[1250,3773,1296],{"emptyLinePlaceholder":1295},[1250,3775,3776,3778,3781],{"class":1252,"line":225},[1250,3777,1373],{"class":1255},[1250,3779,3780],{"class":1376}," fontSizePx",[1250,3782,2259],{"class":1255},[1250,3784,3785,3788],{"class":1252,"line":214},[1250,3786,3787],{"class":1304},"    computed",[1250,3789,1480],{"class":1259},[1250,3791,3792,3795,3797,3799,3801,3803,3805,3807],{"class":1252,"line":203},[1250,3793,3794],{"class":1259},"      () ",[1250,3796,1416],{"class":1255},[1250,3798,3206],{"class":1286},[1250,3800,1128],{"class":1389},[1250,3802,1439],{"class":1286},[1250,3804,3674],{"class":1304},[1250,3806,3137],{"class":1286},[1250,3808,3679],{"class":1286},[1250,3810,3811],{"class":1252,"line":192},[1250,3812,1502],{"class":1259},[1250,3814,3815],{"class":1252,"line":181},[1250,3816,1511],{"class":1259},[1216,3818,3819,3820,3822],{},"這樣狀態和畫面的關係直接寫在 Host Binding 上，不需要再透過 ",[1219,3821,1221],{}," 手動修改 DOM。",[1216,3824,3825,3826,1231],{},"如果是第三方元件、Canvas，或 Angular Binding 不方便處理的外部 API，再考慮使用 ",[1219,3827,1221],{},[1233,3829,3831],{"id":3830},"什麼時候才需要-effect","什麼時候才需要 effect()？",[1216,3833,3834],{},"前面的例子可以整理出一個比較簡單的判斷方式：如果事情仍然屬於 Angular 內部的狀態、非同步資料或畫面綁定，通常可以先使用對應的 API。",[1216,3836,3837,3839],{},[1219,3838,1221],{}," 比較適合處理的是 signal 改變後，需要把結果交給 Angular 以外的 API。",[1216,3841,3842],{},"例如把 Theme 寫進 Local Storage：",[1241,3844,3846],{"className":1243,"code":3845,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  localStorage.setItem(\n    'theme',\n    this.theme()\n  );\n});\n",[1219,3847,3848,3858,3868,3875,3885,3889],{"__ignoreMap":1246},[1250,3849,3850,3852,3854,3856],{"class":1252,"line":340},[1250,3851,1858],{"class":1304},[1250,3853,1413],{"class":1259},[1250,3855,1416],{"class":1255},[1250,3857,1260],{"class":1259},[1250,3859,3860,3863,3866],{"class":1252,"line":329},[1250,3861,3862],{"class":1259},"  localStorage.",[1250,3864,3865],{"class":1304},"setItem",[1250,3867,1480],{"class":1259},[1250,3869,3870,3873],{"class":1252,"line":318},[1250,3871,3872],{"class":1286},"    'theme'",[1250,3874,1319],{"class":1259},[1250,3876,3877,3879,3881,3883],{"class":1252,"line":134},[1250,3878,1471],{"class":1389},[1250,3880,1439],{"class":1259},[1250,3882,1913],{"class":1304},[1250,3884,2154],{"class":1259},[1250,3886,3887],{"class":1252,"line":122},[1250,3888,2517],{"class":1259},[1250,3890,3891],{"class":1252,"line":31},[1250,3892,1925],{"class":1259},[1216,3894,3895],{},"記錄 Log：",[1241,3897,3899],{"className":1243,"code":3898,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  console.log(\n    'current user:',\n    this.user()\n  );\n});\n",[1219,3900,3901,3911,3919,3926,3936,3940],{"__ignoreMap":1246},[1250,3902,3903,3905,3907,3909],{"class":1252,"line":340},[1250,3904,1858],{"class":1304},[1250,3906,1413],{"class":1259},[1250,3908,1416],{"class":1255},[1250,3910,1260],{"class":1259},[1250,3912,3913,3915,3917],{"class":1252,"line":329},[1250,3914,1869],{"class":1259},[1250,3916,1426],{"class":1304},[1250,3918,1480],{"class":1259},[1250,3920,3921,3924],{"class":1252,"line":318},[1250,3922,3923],{"class":1286},"    'current user:'",[1250,3925,1319],{"class":1259},[1250,3927,3928,3930,3932,3934],{"class":1252,"line":134},[1250,3929,1471],{"class":1389},[1250,3931,1439],{"class":1259},[1250,3933,1880],{"class":1304},[1250,3935,2154],{"class":1259},[1250,3937,3938],{"class":1252,"line":122},[1250,3939,2517],{"class":1259},[1250,3941,3942],{"class":1252,"line":31},[1250,3943,1925],{"class":1259},[1216,3945,3946],{},"或更新第三方圖表：",[1241,3948,3950],{"className":1243,"code":3949,"language":1245,"meta":1246,"style":1246},"effect(() => {\n  this.chart.update(\n    this.chartData()\n  );\n});\n",[1219,3951,3952,3962,3973,3984,3988],{"__ignoreMap":1246},[1250,3953,3954,3956,3958,3960],{"class":1252,"line":340},[1250,3955,1858],{"class":1304},[1250,3957,1413],{"class":1259},[1250,3959,1416],{"class":1255},[1250,3961,1260],{"class":1259},[1250,3963,3964,3966,3969,3971],{"class":1252,"line":329},[1250,3965,2005],{"class":1389},[1250,3967,3968],{"class":1259},".chart.",[1250,3970,1477],{"class":1304},[1250,3972,1480],{"class":1259},[1250,3974,3975,3977,3979,3982],{"class":1252,"line":318},[1250,3976,1471],{"class":1389},[1250,3978,1439],{"class":1259},[1250,3980,3981],{"class":1304},"chartData",[1250,3983,2154],{"class":1259},[1250,3985,3986],{"class":1252,"line":134},[1250,3987,2517],{"class":1259},[1250,3989,3990],{"class":1252,"line":122},[1250,3991,1925],{"class":1259},[1216,3993,3994],{},"這些操作的目的都不是建立另一份 Angular 狀態，而是讓外部系統跟著目前的 signal 狀態更新。",[1216,3996,3997],{},[1536,3998],{"alt":3999,"src":4000},"依需求選擇 computed()、linkedSignal()、RxJS／Resource、Template Binding，最後才是 effect()","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851430\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fwhen-to-use-effect.png",[1233,4002,4003],{"id":4003},"本日結語",[1216,4005,4006,4008],{},[1219,4007,1221],{}," 本身沒有問題，真正要注意的是裡面放了什麼。",[1216,4010,4011],{},"如果是在維護另一份狀態、處理非同步資料或更新 Angular 可以直接綁定的畫面，可以先看看有沒有更合適的 API；如果是在 signal 改變後通知 Local Storage、Logging、Canvas 或第三方 Library，就比較符合 Side Effect 的用途。",[1216,4013,4014,4015,4017,4018,4020,4021,4023],{},"另外，",[1219,4016,1221],{}," 不是每次 ",[1219,4019,1552],{}," 都同步觸發的事件監聽器。它會由 Angular 排程執行，只追蹤同步執行期間讀取到的 signal，而裡面建立的 Subscription、Timer 或其他外部資源，也要記得透過 ",[1219,4022,2205],{}," 處理。",[4025,4026,4027],"h3",{"id":4027},"資料來源",[4029,4030,4031,4047,4058,4068,4078,4088],"ul",{},[4032,4033,4034,2538,4041,4043,4044,4046],"li",{},[4035,4036,4040],"a",{"href":4037,"rel":4038},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fsrc\u002Frender3\u002Freactivity\u002Feffect.ts",[4039],"nofollow","Angular 原始碼：effect.ts",[1219,4042,1221],{}," 由 Angular 排程執行，不在 ",[1219,4045,1552],{}," 當下觸發。",[4032,4048,4049,4054,4055,1231],{},[4035,4050,4053],{"href":4051,"rel":4052},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fprimitives\u002Fsignals\u002Fsrc\u002Feffect.ts",[4039],"Angular 原始碼：primitives\u002Fsignals\u002Fsrc\u002Feffect.ts","：每次執行前先跑 ",[1219,4056,4057],{},"cleanup()",[4032,4059,4060,2538,4065,4067],{},[4035,4061,4064],{"href":4062,"rel":4063},"https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fsrc\u002Frender3\u002Freactivity\u002Funtracked.ts",[4039],"Angular 原始碼：untracked.ts",[1219,4066,2537],{}," 讀取時不建立相依。",[4032,4069,4070,2538,4075,4077],{},[4035,4071,4074],{"href":4072,"rel":4073},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals\u002Feffect",[4039],"Angular Signals 官方文件：Effects",[1219,4076,1221],{}," 的適用情境與 cleanup。",[4032,4079,4080,2538,4085,4087],{},[4035,4081,4084],{"href":4082,"rel":4083},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#reactive-context-and-async-operations",[4039],"Angular Signals 官方文件：Reactive context and async operations",[1219,4086,1950],{}," 之後不會追蹤相依。",[4032,4089,4090,4095,4096,1231],{},[4035,4091,4094],{"href":4092,"rel":4093},"https:\u002F\u002Fngxtension.dev\u002Futilities\u002Fsignals\u002Fexplicit-effect\u002F",[4039],"ngxtension：explicitEffect","：明確指定相依與 ",[1219,4097,4098],{},"defer",[4100,4101,4102],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":1246,"searchDepth":134,"depth":134,"links":4104},[4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115],{"id":1235,"depth":329,"text":1236},{"id":1542,"depth":329,"text":1543},{"id":1842,"depth":329,"text":1843},{"id":2083,"depth":329,"text":2084},{"id":2447,"depth":329,"text":2448},{"id":2646,"depth":329,"text":2647},{"id":3023,"depth":329,"text":3024},{"id":3248,"depth":329,"text":3249},{"id":3594,"depth":329,"text":3595},{"id":3830,"depth":329,"text":3831},{"id":4003,"depth":329,"text":4003,"children":4116},[4117],{"id":4027,"depth":318,"text":4027},[11,13],"md",{},"---\ntitle: \"Day 17：effect() 不是拿來同步 Signal 的，Angular 的 Side Effect 該怎麼寫？\"\ndate: 2026-10-01 21:22:13\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"effect() 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10419779\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 17\n---\n\n`effect()` 是 Angular signal 裡很容易被誤用的一個 API。因為寫法直覺，只要讀取 signal，就能在值改變時執行一些事情，所以很常被拿來同步狀態、打 API，甚至再去修改其他 signal。\n\n這些寫法雖然能動，但用多了會讓狀態之間的關係變得難追。這篇會從 `effect()` 的執行方式開始，整理相依追蹤、cleanup，以及哪些情況真的需要使用 `effect()`。\n\n## effect() 的基本行為\n\n先看一個最簡單的例子：\n\n```ts\nimport {\n  Component,\n  effect,\n  signal,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cbutton (click)=\"increase()\">\n      {{ count() }}\n    \u003C\u002Fbutton>\n  `,\n})\nexport class AppComponent {\n  readonly count = signal(0);\n\n  constructor() {\n    effect(() => {\n      console.log('count:', this.count());\n    });\n  }\n\n  increase() {\n    this.count.update(\n      value => value + 1\n    );\n  }\n}\n```\n\n`effect()` 執行時，會記錄同步讀取過的 signal。上面的程式讀取了 `this.count()`，所以 `count` 會成為相依，之後只要它改變，Angular 就會再次安排 `effect()` 執行。\n\n和其他 signal API 一樣，`effect()` 會根據執行時讀取到的 signal 建立相依。不過它的用途不是產生新的狀態，而是在相依改變後執行一段程式。\n\n![computed() 推算新狀態，effect() 把狀態交給外部系統](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851412\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fcomputed-vs-effect.png)\n\n## effect() 不會在每次 set() 時立刻執行\n\n第一次使用 `effect()` 時，很容易把它理解成 signal 每次呼叫 `set()`，`effect()` 就會立刻執行，但實際上不是。\n\n```ts\nimport {\n  Component,\n  effect,\n  signal,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003Cp>num：{{ num() }}\u003C\u002Fp>\n  `,\n})\nexport class AppComponent {\n  readonly num = signal(2);\n\n  constructor() {\n    effect(() => {\n      console.log(this.num());\n    });\n\n    this.num.set(4);\n\n    setTimeout(() => {\n      this.num.set(8);\n    }, 1000);\n\n    this.num.set(12);\n  }\n}\n```\n\n執行後會先印出 `12`，一秒後再印出 `8`，中間的 `2` 和 `4` 不會依序出現。\n\n原因是 `effect()` 不會在每次 `set()` 當下同步執行，而是由 Angular 排程。`set(4)` 和 `set(12)` 都發生在 `effect()` 再次執行以前，所以等它真正執行時，讀到的已經是 `12`。\n\n![effect() 不是每次 set() 都立刻執行，讀到的是執行當下的最新值](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851417\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Feffect-scheduling.png)\n\n所以 `effect()` 看到的是執行當下的最新狀態，不是每一次發生過的變化。如果每個值都不能漏掉，例如操作事件、WebSocket 訊息，或需要保留完整變化順序的資料流，就不適合只靠 `effect()`，這類需求通常更適合使用 RxJS、Output 或其他事件機制。\n\n## effect() 只追蹤同步讀取的 signal\n\n`effect()` 的相依追蹤只會發生在同步執行期間。\n\n```ts\neffect(() => {\n  console.log(this.user());\n\n  setTimeout(() => {\n    console.log(this.theme());\n  });\n});\n```\n\n這個 `effect()` 會追蹤同步讀取的 `this.user()`，但不會追蹤 `setTimeout()` 裡的 `this.theme()`，因為 Callback 執行時，原本的 Reactive Context 已經結束。`subscribe()` 的 Callback 和 `await` 之後的程式也是一樣。\n\n![只有同步讀取的 Signal 會成為相依，setTimeout()、subscribe()、await 之後都不會](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851421\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fsync-dependency-tracking.png)\n\n例如下面這段程式：\n\n```ts\neffect(() => {\n  const id = this.userId();\n\n  this.userService\n    .getUser(id)\n    .subscribe(() => {\n      console.log(this.theme());\n    });\n});\n```\n\n`this.userId()` 是在 `effect()` 本體同步執行時讀取，所以會成為相依；`this.theme()` 則是在 `subscribe()` 的 Callback 裡讀取，因此 `theme` 改變時不會讓 `effect()` 重新執行。\n\n這和前面介紹過的 Reactive Context 是同一件事：Angular 只會記錄同步執行期間讀取到的 signal。\n\n## effect() 裡建立訂閱要記得 cleanup\n\n如果在 `effect()` 裡建立 Subscription，就要注意 `effect()` 可能會重新執行。\n\n```ts\nreadonly form =\n  viewChild.required\u003CNgForm>('form');\n\neffect(() => {\n  this.form()\n    .form\n    .valueChanges\n    .subscribe(value => {\n      console.log(value);\n    });\n});\n```\n\n每次 `effect()` 重跑，都會建立新的 Subscription，而前一次建立的 Subscription 不會自動取消。這時可以透過 `onCleanup()` 處理：\n\n```ts\neffect((onCleanup) => {\n  const form = this.form();\n\n  const sub =\n    form.form.valueChanges.subscribe(\n      value => {\n        console.log(value);\n      }\n    );\n\n  onCleanup(() => {\n    sub.unsubscribe();\n  });\n});\n```\n\n`onCleanup()` 會在下一次 `effect()` 執行前呼叫，`effect()` 被銷毀時也會執行，因此可以用來清掉前一次建立的資源。\n\n![相依改變時先執行 onCleanup() 取消舊資源，再重新執行 effect()](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851414\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Feffect-cleanup-lifecycle.png)\n\n除了 Observable，也可以用來清除 Timer：\n\n```ts\neffect((onCleanup) => {\n  const id = setInterval(() => {\n    console.log(this.count());\n  }, 1000);\n\n  onCleanup(() => {\n    clearInterval(id);\n  });\n});\n```\n\nAngular 會管理 `effect()` 本身的生命週期，但裡面建立的 Subscription、Timer 或其他外部資源，仍然要處理各自的清理。\n\n## 不想追蹤某個 signal，可以使用 untracked()\n\n有時候 `effect()` 裡需要讀取某個 signal，但不希望它成為觸發條件。\n\n```ts\neffect(() => {\n  console.log(\n    'user:',\n    this.user(),\n    'theme:',\n    this.theme()\n  );\n});\n```\n\n這裡的 `user` 和 `theme` 都會成為相依。假設需求只是 User 改變時記錄 Log，並順便把目前的 Theme 一起印出來，就不希望 Theme 改變時也重新執行 `effect()`。\n\n這時可以使用 `untracked()`：\n\n```ts\neffect(() => {\n  const user = this.user();\n  const theme = untracked(this.theme);\n\n  console.log(\n    'user:',\n    user,\n    'theme:',\n    theme\n  );\n});\n```\n\n這樣只有 `this.user()` 會成為相依，而 `theme` 雖然一樣可以讀取，但它的變化不會讓 `effect()` 再次執行。\n\n## explicitEffect 明確指定相依\n\n如果希望直接寫出哪些 signal 會觸發副作用，也可以使用 ngxtension 提供的 `explicitEffect()`。\n\n一般的 `effect()` 會依照 Callback 裡實際讀取的 signal 自動建立相依：\n\n```ts\neffect(() => {\n  console.log(\n    this.id(),\n    this.keyword()\n  );\n});\n```\n\n這裡的 `id` 和 `keyword` 都會觸發 `effect()`。\n\n改成 `explicitEffect()` 後，可以把要追蹤的 signal 直接列在第一個參數：\n\n```ts\nexplicitEffect(\n  [this.id],\n  ([id]) => {\n    console.log(id);\n    console.log(this.keyword());\n  }\n);\n```\n\n這時只有 `this.id` 會觸發 Callback，即使裡面讀取了 `this.keyword()`，它也不會被加入相依。\n\n如果裡面建立了 Subscription，也可以透過 `cleanup` 清理：\n\n```ts\nexplicitEffect(\n  [this.id],\n  ([id], cleanup) => {\n    const sub =\n      this.userService\n        .getUser(id)\n        .subscribe();\n\n    cleanup(() => {\n      sub.unsubscribe();\n    });\n  }\n);\n```\n\n另外還可以透過 `defer: true` 跳過第一次執行：\n\n```ts\nexplicitEffect(\n  [this.id],\n  ([id]) => {\n    console.log(\n      'id changed:',\n      id\n    );\n  },\n  {\n    defer: true,\n  }\n);\n```\n\n這樣只有後續 `id` 改變時才會執行 Callback。\n\n`explicitEffect()` 的重點是讓相依來源更明確，適合用在希望清楚控制觸發條件的副作用；它改變的是相依追蹤方式，不會改變這段邏輯本身適不適合使用 `effect()`。\n\n## 不要用 effect() 搬動狀態\n\n`effect()` 很常被拿來讀取一個 signal，再把結果寫進另一個 signal。\n\n```ts\nreadonly firstName =\n  signal('Antonio');\n\nreadonly lastName =\n  signal('Ling');\n\nreadonly fullName =\n  signal('');\n\neffect(() => {\n  this.fullName.set(\n    `${this.firstName()} ${this.lastName()}`\n  );\n});\n```\n\n這段程式可以正常運作，但 `fullName` 完全由 `firstName` 和 `lastName` 決定，沒有必要另外維護一份 Writable Signal。\n\n這類可以直接從其他狀態推算出來的值，可以直接使用 `computed()`：\n\n```ts\nreadonly fullName =\n  computed(\n    () => `${this.firstName()} ${this.lastName()}`\n  );\n```\n\n重點不是看到 `effect()` 就一定要改成 `computed()`，而是當 `effect()` 裡又去 `set()` 另一個 signal 時，可以先確認這份狀態是不是真的需要另外保存。\n\n## 打 API 也不一定需要 effect()\n\n另一個常見寫法，是用 signal 當查詢條件，在 `effect()` 裡發送 API 請求：\n\n```ts\nreadonly searchId =\n  signal('');\n\nreadonly person =\n  signal\u003CPerson | null>(null);\n\neffect(() => {\n  const id = this.searchId();\n\n  this.personService\n    .getPerson(id)\n    .subscribe(person => {\n      this.person.set(person);\n    });\n});\n```\n\n一開始看起來不複雜，但搜尋功能通常還會加入 debounce、取消上一筆請求、避免重複查詢、錯誤處理，以及 Component Destroy 時取消訂閱等需求。\n\n如果需求包含這些非同步流程，RxJS 會更適合處理：\n\n```ts\nreadonly searchId =\n  signal('');\n\nprivate readonly searchId$ =\n  toObservable(this.searchId);\n\nreadonly person$ =\n  this.searchId$.pipe(\n    debounceTime(300),\n    distinctUntilChanged(),\n    switchMap(id =>\n      this.personService.getPerson(id)\n    ),\n    takeUntilDestroyed()\n  );\n```\n\n如果畫面最後還是希望使用 signal，可以再透過 `toSignal()` 轉回來：\n\n```ts\nreadonly person =\n  toSignal(this.person$, {\n    initialValue: null,\n  });\n```\n\n如果只是「參數改變後重新取得資料」，前面介紹過的 `resource()`、`httpResource()`、`rxResource()` 也可以直接處理這類情境，並提供 Loading、Error、Value 等狀態。\n\n重點不是 API 一定要改用哪一種，而是這類非同步資料取得，不一定需要靠 `effect()` 監聽參數、訂閱請求，再手動把結果寫回另一個 signal。\n\n## 改畫面樣式也不一定需要 effect()\n\n`effect()` 也常被拿來直接操作 DOM。\n\n```ts\nreadonly fontSize =\n  signal(16);\n\neffect(() => {\n  this.renderer.setStyle(\n    this.element.nativeElement,\n    '--main-font-size',\n    `${this.fontSize()}px`\n  );\n});\n```\n\n如果只是把 signal 對應到畫面樣式，可以直接透過 Angular 的 Binding：\n\n```ts\n@Component({\n  selector: 'app-root',\n  host: {\n    '[style.--main-font-size]':\n      'fontSizePx()',\n  },\n})\nexport class AppComponent {\n  readonly fontSize =\n    signal(16);\n\n  readonly fontSizePx =\n    computed(\n      () => `${this.fontSize()}px`\n    );\n}\n```\n\n這樣狀態和畫面的關係直接寫在 Host Binding 上，不需要再透過 `effect()` 手動修改 DOM。\n\n如果是第三方元件、Canvas，或 Angular Binding 不方便處理的外部 API，再考慮使用 `effect()`。\n\n## 什麼時候才需要 effect()？\n\n前面的例子可以整理出一個比較簡單的判斷方式：如果事情仍然屬於 Angular 內部的狀態、非同步資料或畫面綁定，通常可以先使用對應的 API。\n\n`effect()` 比較適合處理的是 signal 改變後，需要把結果交給 Angular 以外的 API。\n\n例如把 Theme 寫進 Local Storage：\n\n```ts\neffect(() => {\n  localStorage.setItem(\n    'theme',\n    this.theme()\n  );\n});\n```\n\n記錄 Log：\n\n```ts\neffect(() => {\n  console.log(\n    'current user:',\n    this.user()\n  );\n});\n```\n\n或更新第三方圖表：\n\n```ts\neffect(() => {\n  this.chart.update(\n    this.chartData()\n  );\n});\n```\n\n這些操作的目的都不是建立另一份 Angular 狀態，而是讓外部系統跟著目前的 signal 狀態更新。\n\n![依需求選擇 computed()、linkedSignal()、RxJS／Resource、Template Binding，最後才是 effect()](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1790851430\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2017%EF%BC%9Aeffect%28%29%20%E4%B8%8D%E6%98%AF%E6%8B%BF%E4%BE%86%E5%90%8C%E6%AD%A5%20Signal%20%E7%9A%84%EF%BC%8CAngular%20%E7%9A%84%20Side%20Effect%20%E8%A9%B2%E6%80%8E%E9%BA%BC%E5%AF%AB%EF%BC%9F\u002Fwhen-to-use-effect.png)\n\n## 本日結語\n\n`effect()` 本身沒有問題，真正要注意的是裡面放了什麼。\n\n如果是在維護另一份狀態、處理非同步資料或更新 Angular 可以直接綁定的畫面，可以先看看有沒有更合適的 API；如果是在 signal 改變後通知 Local Storage、Logging、Canvas 或第三方 Library，就比較符合 Side Effect 的用途。\n\n另外，`effect()` 不是每次 `set()` 都同步觸發的事件監聽器。它會由 Angular 排程執行，只追蹤同步執行期間讀取到的 signal，而裡面建立的 Subscription、Timer 或其他外部資源，也要記得透過 `onCleanup()` 處理。\n\n### 資料來源\n\n- [Angular 原始碼：effect.ts](https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fsrc\u002Frender3\u002Freactivity\u002Feffect.ts)：`effect()` 由 Angular 排程執行，不在 `set()` 當下觸發。\n- [Angular 原始碼：primitives\u002Fsignals\u002Fsrc\u002Feffect.ts](https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fprimitives\u002Fsignals\u002Fsrc\u002Feffect.ts)：每次執行前先跑 `cleanup()`。\n- [Angular 原始碼：untracked.ts](https:\u002F\u002Fgithub.com\u002Fangular\u002Fangular\u002Fblob\u002Fmain\u002Fpackages\u002Fcore\u002Fsrc\u002Frender3\u002Freactivity\u002Funtracked.ts)：`untracked()` 讀取時不建立相依。\n- [Angular Signals 官方文件：Effects](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals\u002Feffect)：`effect()` 的適用情境與 cleanup。\n- [Angular Signals 官方文件：Reactive context and async operations](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals#reactive-context-and-async-operations)：`await` 之後不會追蹤相依。\n- [ngxtension：explicitEffect](https:\u002F\u002Fngxtension.dev\u002Futilities\u002Fsignals\u002Fexplicit-effect\u002F)：明確指定相依與 `defer`。\n",{"title":162,"description":163},{"name":20,"url":21,"day":170},{"loc":161,"images":4125,"lastmod":164},[4126,4127,4128,4129,4130],{"loc":1539},{"loc":1830},{"loc":1957},{"loc":2344},{"loc":4000},"ithome-2026-day-17","2026-10-01-ithome-2026-day-17",[11,12,13],"jVoUyXjle41avPjoeSC281EtyKVvBnFiEYTMCyA9Vx4",1792575173357]