[{"data":1,"prerenderedAt":3865},["ShallowReactive",2],{"all-posts":3,"post:\u002Fithome-2026-day-22":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":104,"body":1212,"canonical":111,"categories":3852,"date":106,"description":105,"draft":15,"extension":3853,"meta":3854,"navigation":1403,"ogImage":9,"path":103,"rawbody":3855,"readingTime":110,"robots":9,"schemaOrg":9,"seo":3856,"series":3857,"sitemap":3858,"slug":3861,"stem":3862,"tags":3863,"updated":9,"wordCount":109,"__hash__":3864},"posts\u002F2026-10-06-ithome-2026-day-22.md",{"type":1213,"value":1214,"toc":3833},"minimark",[1215,1218,1274,1277,1280,1283,1326,1329,1334,1337,1365,1368,1421,1432,1435,1441,1489,1492,1530,1533,1541,1547,1550,1557,1650,1653,1673,1680,1683,1686,1754,1764,1767,1773,1777,1782,1785,2002,2005,2008,2020,2024,2029,2031,2164,2167,2170,2207,2218,2222,2225,2252,2259,2262,2268,2272,2275,2345,2351,2364,2370,2374,2377,2412,2419,2422,2452,2459,2466,2515,2524,2530,2535,2567,2573,2576,2582,2586,2594,2596,2678,2684,2692,2695,2707,2713,2718,2722,2725,2728,2800,2803,2846,2857,2860,2864,2869,2871,2998,3001,3004,3024,3029,3033,3043,3046,3072,3075,3079,3082,3084,3199,3202,3205,3212,3216,3222,3224,3414,3417,3425,3429,3432,3434,3540,3543,3576,3582,3586,3589,3601,3603,3667,3670,3673,3701,3707,3735,3742,3745,3752,3760,3769,3777,3780,3784,3829],[1216,1217,105],"p",{},[1219,1220,1225],"pre",{"className":1221,"code":1222,"language":1223,"meta":1224,"style":1224},"language-ts shiki shiki-themes github-light github-dark-dimmed","ngOnInit() \u002F\u002F Input 初始化後\nngAfterViewInit() \u002F\u002F View \u002F Child View 初始化後\nngAfterViewChecked() \u002F\u002F View \u002F Child View 每次檢查後\nngOnDestroy() \u002F\u002F Component 銷毀前\n","ts","",[1226,1227,1228,1244,1254,1264],"code",{"__ignoreMap":1224},[1229,1230,1232,1236,1240],"span",{"class":1231,"line":340},"line",[1229,1233,1235],{"class":1234},"sPO5f","ngOnInit",[1229,1237,1239],{"class":1238},"sYgZi","() ",[1229,1241,1243],{"class":1242},"sDN9O","\u002F\u002F Input 初始化後\n",[1229,1245,1246,1249,1251],{"class":1231,"line":329},[1229,1247,1248],{"class":1234},"ngAfterViewInit",[1229,1250,1239],{"class":1238},[1229,1252,1253],{"class":1242},"\u002F\u002F View \u002F Child View 初始化後\n",[1229,1255,1256,1259,1261],{"class":1231,"line":318},[1229,1257,1258],{"class":1234},"ngAfterViewChecked",[1229,1260,1239],{"class":1238},[1229,1262,1263],{"class":1242},"\u002F\u002F View \u002F Child View 每次檢查後\n",[1229,1265,1266,1269,1271],{"class":1231,"line":134},[1229,1267,1268],{"class":1234},"ngOnDestroy",[1229,1270,1239],{"class":1238},[1229,1272,1273],{"class":1242},"\u002F\u002F Component 銷毀前\n",[1216,1275,1276],{},"這些 Hook 本身沒有問題，只是有些需求關心的其實不是 Component 走到哪個階段，而是 DOM 什麼時候完成 Render、某個 Signal 更新後畫面是否已經同步，或建立的資源要怎麼跟著目前的 Context 一起清理。",[1216,1278,1279],{},"這類工作放在傳統 Lifecycle Hooks 中通常也能運作，但 Hook 描述的是 Component 的生命週期，和實際需求不一定完全一致。",[1216,1281,1282],{},"Angular 另外提供了：",[1219,1284,1286],{"className":1221,"code":1285,"language":1223,"meta":1224,"style":1224},"afterNextRender() \u002F\u002F 註冊後最近一次 Render 完成時執行一次\nafterEveryRender() \u002F\u002F 每次 Angular Render 完成後都會執行\nafterRenderEffect() \u002F\u002F 追蹤 Signal，等相關變化 Render 完成後再執行\nDestroyRef \u002F\u002F 註冊目前 Context 被銷毀時要執行的 Cleanup\n",[1226,1287,1288,1298,1308,1318],{"__ignoreMap":1224},[1229,1289,1290,1293,1295],{"class":1231,"line":340},[1229,1291,1292],{"class":1234},"afterNextRender",[1229,1294,1239],{"class":1238},[1229,1296,1297],{"class":1242},"\u002F\u002F 註冊後最近一次 Render 完成時執行一次\n",[1229,1299,1300,1303,1305],{"class":1231,"line":329},[1229,1301,1302],{"class":1234},"afterEveryRender",[1229,1304,1239],{"class":1238},[1229,1306,1307],{"class":1242},"\u002F\u002F 每次 Angular Render 完成後都會執行\n",[1229,1309,1310,1313,1315],{"class":1231,"line":318},[1229,1311,1312],{"class":1234},"afterRenderEffect",[1229,1314,1239],{"class":1238},[1229,1316,1317],{"class":1242},"\u002F\u002F 追蹤 Signal，等相關變化 Render 完成後再執行\n",[1229,1319,1320,1323],{"class":1231,"line":134},[1229,1321,1322],{"class":1238},"DestroyRef ",[1229,1324,1325],{"class":1242},"\u002F\u002F 註冊目前 Context 被銷毀時要執行的 Cleanup\n",[1216,1327,1328],{},"接下來就從幾個常見情境來看它們的差別。",[1330,1331,1333],"h2",{"id":1332},"ngafterviewinit-的限制","ngAfterViewInit() 的限制",[1216,1335,1336],{},"假設 Component 中有一個 DOM Element：",[1219,1338,1342],{"className":1339,"code":1340,"language":1341,"meta":1224,"style":1224},"language-html shiki shiki-themes github-light github-dark-dimmed","\u003Cdiv #chart>\u003C\u002Fdiv>\n","html",[1226,1343,1344],{"__ignoreMap":1224},[1229,1345,1346,1349,1353,1357,1360,1362],{"class":1231,"line":340},[1229,1347,1348],{"class":1238},"\u003C",[1229,1350,1352],{"class":1351},"sIAta","div",[1229,1354,1356],{"class":1355},"sMOND"," #chart",[1229,1358,1359],{"class":1238},">\u003C\u002F",[1229,1361,1352],{"class":1351},[1229,1363,1364],{"class":1238},">\n",[1216,1366,1367],{},"如果要初始化第三方 Chart Library，過去很常寫在：",[1219,1369,1371],{"className":1221,"code":1370,"language":1223,"meta":1224,"style":1224},"ngAfterViewInit() {\n  const element = this.chart.nativeElement;\n\n  new Chart(element);\n}\n",[1226,1372,1373,1380,1399,1405,1416],{"__ignoreMap":1224},[1229,1374,1375,1377],{"class":1231,"line":340},[1229,1376,1248],{"class":1234},[1229,1378,1379],{"class":1238},"() {\n",[1229,1381,1382,1386,1390,1393,1396],{"class":1231,"line":329},[1229,1383,1385],{"class":1384},"s7YZ4","  const",[1229,1387,1389],{"class":1388},"sviXB"," element",[1229,1391,1392],{"class":1384}," =",[1229,1394,1395],{"class":1388}," this",[1229,1397,1398],{"class":1238},".chart.nativeElement;\n",[1229,1400,1401],{"class":1231,"line":318},[1229,1402,1404],{"emptyLinePlaceholder":1403},true,"\n",[1229,1406,1407,1410,1413],{"class":1231,"line":134},[1229,1408,1409],{"class":1384},"  new",[1229,1411,1412],{"class":1234}," Chart",[1229,1414,1415],{"class":1238},"(element);\n",[1229,1417,1418],{"class":1231,"line":122},[1229,1419,1420],{"class":1238},"}\n",[1216,1422,1423,1424,1427,1428,1431],{},"這樣寫沒有問題，因為 ",[1226,1425,1426],{},"ngAfterViewInit()"," 執行時，Component 的 View 已經初始化完成，也可以取得 ",[1226,1429,1430],{},"ViewChild"," 的結果。",[1216,1433,1434],{},"但它仍然屬於 Change Detection Lifecycle，代表的是 View 已經初始化，不是專門提供給 Render 完成後的 DOM 操作。",[1216,1436,1437,1438,1440],{},"其中一個常見情況，是在 ",[1226,1439,1426],{}," 裡修改已經參與這一輪檢查的狀態：",[1219,1442,1444],{"className":1221,"code":1443,"language":1223,"meta":1224,"style":1224},"loading = true;\n\nngAfterViewInit() {\n  this.loading = false;\n}\n",[1226,1445,1446,1460,1464,1470,1485],{"__ignoreMap":1224},[1229,1447,1448,1451,1454,1457],{"class":1231,"line":340},[1229,1449,1450],{"class":1238},"loading ",[1229,1452,1453],{"class":1384},"=",[1229,1455,1456],{"class":1388}," true",[1229,1458,1459],{"class":1238},";\n",[1229,1461,1462],{"class":1231,"line":329},[1229,1463,1404],{"emptyLinePlaceholder":1403},[1229,1465,1466,1468],{"class":1231,"line":318},[1229,1467,1248],{"class":1234},[1229,1469,1379],{"class":1238},[1229,1471,1472,1475,1478,1480,1483],{"class":1231,"line":134},[1229,1473,1474],{"class":1388},"  this",[1229,1476,1477],{"class":1238},".loading ",[1229,1479,1453],{"class":1384},[1229,1481,1482],{"class":1388}," false",[1229,1484,1459],{"class":1238},[1229,1486,1487],{"class":1231,"line":122},[1229,1488,1420],{"class":1238},[1216,1490,1491],{},"Template 同時使用這個值：",[1219,1493,1495],{"className":1221,"code":1494,"language":1223,"meta":1224,"style":1224},"@if (loading) {\n  \u003Cp>Loading...\u003C\u002Fp>\n}\n",[1226,1496,1497,1508,1526],{"__ignoreMap":1224},[1229,1498,1499,1502,1505],{"class":1231,"line":340},[1229,1500,1501],{"class":1238},"@",[1229,1503,1504],{"class":1234},"if",[1229,1506,1507],{"class":1238}," (loading) {\n",[1229,1509,1510,1513,1516,1519,1522,1524],{"class":1231,"line":329},[1229,1511,1512],{"class":1238},"  \u003C",[1229,1514,1216],{"class":1515},"sOLd2",[1229,1517,1518],{"class":1238},">Loading",[1229,1520,1521],{"class":1384},"...\u003C\u002F",[1229,1523,1216],{"class":1238},[1229,1525,1364],{"class":1384},[1229,1527,1528],{"class":1231,"line":318},[1229,1529,1420],{"class":1238},[1216,1531,1532],{},"Angular 已經檢查過這個 View，這時又修改會影響畫面的狀態，在開發模式下就可能出現：",[1219,1534,1539],{"className":1535,"code":1537,"language":1538,"meta":1224},[1536],"language-text","ExpressionChangedAfterItHasBeenCheckedError\n","text",[1226,1540,1537],{"__ignoreMap":1224},[1216,1542,1543,1544,1546],{},"問題不是 ",[1226,1545,1426],{}," 不能修改資料，而是這一輪 Change Detection 已經檢查過畫面，值卻又在同一輪裡發生改變。",[1216,1548,1549],{},"如果使用 Angular 22 預設的 Zoneless Change Detection，有些狀態變化不一定會立刻進入下一次檢查，因此這類問題不一定每次都會直接看到錯誤。",[1216,1551,1552,1553,1556],{},"測試時可以在 ",[1226,1554,1555],{},"main.ts"," 加上：",[1219,1558,1560],{"className":1221,"code":1559,"language":1223,"meta":1224,"style":1224},"import {\n  provideCheckNoChangesConfig,\n} from '@angular\u002Fcore';\n\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideCheckNoChangesConfig({\n      exhaustive: true,\n      interval: 1000,\n    }),\n  ],\n});\n",[1226,1561,1562,1570,1575,1589,1593,1601,1606,1614,1625,1635,1640,1645],{"__ignoreMap":1224},[1229,1563,1564,1567],{"class":1231,"line":340},[1229,1565,1566],{"class":1384},"import",[1229,1568,1569],{"class":1238}," {\n",[1229,1571,1572],{"class":1231,"line":329},[1229,1573,1574],{"class":1238},"  provideCheckNoChangesConfig,\n",[1229,1576,1577,1580,1583,1587],{"class":1231,"line":318},[1229,1578,1579],{"class":1238},"} ",[1229,1581,1582],{"class":1384},"from",[1229,1584,1586],{"class":1585},"s-HuK"," '@angular\u002Fcore'",[1229,1588,1459],{"class":1238},[1229,1590,1591],{"class":1231,"line":134},[1229,1592,1404],{"emptyLinePlaceholder":1403},[1229,1594,1595,1598],{"class":1231,"line":122},[1229,1596,1597],{"class":1234},"bootstrapApplication",[1229,1599,1600],{"class":1238},"(AppComponent, {\n",[1229,1602,1603],{"class":1231,"line":31},[1229,1604,1605],{"class":1238},"  providers: [\n",[1229,1607,1608,1611],{"class":1231,"line":17},[1229,1609,1610],{"class":1234},"    provideCheckNoChangesConfig",[1229,1612,1613],{"class":1238},"({\n",[1229,1615,1616,1619,1622],{"class":1231,"line":110},[1229,1617,1618],{"class":1238},"      exhaustive: ",[1229,1620,1621],{"class":1388},"true",[1229,1623,1624],{"class":1238},",\n",[1229,1626,1627,1630,1633],{"class":1231,"line":76},[1229,1628,1629],{"class":1238},"      interval: ",[1229,1631,1632],{"class":1388},"1000",[1229,1634,1624],{"class":1238},[1229,1636,1637],{"class":1231,"line":247},[1229,1638,1639],{"class":1238},"    }),\n",[1229,1641,1642],{"class":1231,"line":236},[1229,1643,1644],{"class":1238},"  ],\n",[1229,1646,1647],{"class":1231,"line":225},[1229,1648,1649],{"class":1238},"});\n",[1216,1651,1652],{},"provideCheckNoChangesConfig 參數說明：",[1654,1655,1656,1663],"ul",{},[1657,1658,1659,1662],"li",{},[1226,1660,1661],{},"exhaustive: true","：檢查所有掛在 Application 上的 View，而不只目前被標記需要更新的 View。",[1657,1664,1665,1668,1669,1672],{},[1226,1666,1667],{},"interval: 1000","：每 1 秒額外執行一次 ",[1226,1670,1671],{},"checkNoChanges"," 檢查。",[1216,1674,1675,1676,1679],{},"這樣即使某個 View 沒有再次被正常排進 Change Detection，也比較容易看到「畫面上次檢查到的值」和「目前實際值」已經不同，方便觀察 ",[1226,1677,1678],{},"ExpressionChangedAfterItHasBeenCheckedError","。",[1216,1681,1682],{},"這個設定比較適合拿來除錯或示範，不需要為了避免這類問題長期開啟高頻率檢查。",[1216,1684,1685],{},"另一種情況是直接操作 DOM：",[1219,1687,1689],{"className":1221,"code":1688,"language":1223,"meta":1224,"style":1224},"ngAfterViewInit() {\n  const element = this.chart.nativeElement;\n\n  element.style.width = '500px';\n\n  const width =\n    element.getBoundingClientRect().width;\n}\n",[1226,1690,1691,1697,1709,1713,1725,1729,1739,1750],{"__ignoreMap":1224},[1229,1692,1693,1695],{"class":1231,"line":340},[1229,1694,1248],{"class":1234},[1229,1696,1379],{"class":1238},[1229,1698,1699,1701,1703,1705,1707],{"class":1231,"line":329},[1229,1700,1385],{"class":1384},[1229,1702,1389],{"class":1388},[1229,1704,1392],{"class":1384},[1229,1706,1395],{"class":1388},[1229,1708,1398],{"class":1238},[1229,1710,1711],{"class":1231,"line":318},[1229,1712,1404],{"emptyLinePlaceholder":1403},[1229,1714,1715,1718,1720,1723],{"class":1231,"line":134},[1229,1716,1717],{"class":1238},"  element.style.width ",[1229,1719,1453],{"class":1384},[1229,1721,1722],{"class":1585}," '500px'",[1229,1724,1459],{"class":1238},[1229,1726,1727],{"class":1231,"line":122},[1229,1728,1404],{"emptyLinePlaceholder":1403},[1229,1730,1731,1733,1736],{"class":1231,"line":31},[1229,1732,1385],{"class":1384},[1229,1734,1735],{"class":1388}," width",[1229,1737,1738],{"class":1384}," =\n",[1229,1740,1741,1744,1747],{"class":1231,"line":17},[1229,1742,1743],{"class":1238},"    element.",[1229,1745,1746],{"class":1234},"getBoundingClientRect",[1229,1748,1749],{"class":1238},"().width;\n",[1229,1751,1752],{"class":1231,"line":110},[1229,1753,1420],{"class":1238},[1216,1755,1756,1759,1760,1763],{},[1226,1757,1758],{},"element.style.width"," 是修改 DOM，",[1226,1761,1762],{},"getBoundingClientRect()"," 則需要讀取 Element 實際排版後的尺寸。",[1216,1765,1766],{},"如果程式一直在修改 DOM 後立刻讀取 Layout，瀏覽器可能需要反覆重新計算版面。這種 DOM Write 和 Read 不斷交錯的情況，就是常說的 Layout Thrashing。",[1216,1768,1769,1770,1679],{},"如果需求本來就是等這一輪 Render 完成後再處理 DOM，就可以使用 ",[1226,1771,1772],{},"afterNextRender()",[1330,1774,1776],{"id":1775},"afternextrenderrender-完成後執行一次","afterNextRender()：Render 完成後執行一次",[1216,1778,1779,1781],{},[1226,1780,1772],{}," 會在註冊後最近一次 Angular Render 完成時執行一次。",[1216,1783,1784],{},"例如：",[1219,1786,1788],{"className":1221,"code":1787,"language":1223,"meta":1224,"style":1224},"import {\n  afterNextRender,\n  Component,\n  ElementRef,\n  viewChild,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-chart',\n  template: `\n    \u003Cdiv #chart>\u003C\u002Fdiv>\n  `,\n})\nexport class ChartComponent {\n  chart =\n    viewChild.required\u003CElementRef\u003CHTMLDivElement>>(\n      'chart',\n    );\n\n  constructor() {\n    afterNextRender(() => {\n      const element =\n        this.chart().nativeElement;\n\n      new Chart(element);\n    });\n  }\n}\n",[1226,1789,1790,1796,1801,1806,1811,1816,1826,1830,1839,1849,1857,1862,1869,1874,1887,1895,1916,1923,1928,1932,1939,1952,1961,1975,1979,1988,1993,1998],{"__ignoreMap":1224},[1229,1791,1792,1794],{"class":1231,"line":340},[1229,1793,1566],{"class":1384},[1229,1795,1569],{"class":1238},[1229,1797,1798],{"class":1231,"line":329},[1229,1799,1800],{"class":1238},"  afterNextRender,\n",[1229,1802,1803],{"class":1231,"line":318},[1229,1804,1805],{"class":1238},"  Component,\n",[1229,1807,1808],{"class":1231,"line":134},[1229,1809,1810],{"class":1238},"  ElementRef,\n",[1229,1812,1813],{"class":1231,"line":122},[1229,1814,1815],{"class":1238},"  viewChild,\n",[1229,1817,1818,1820,1822,1824],{"class":1231,"line":31},[1229,1819,1579],{"class":1238},[1229,1821,1582],{"class":1384},[1229,1823,1586],{"class":1585},[1229,1825,1459],{"class":1238},[1229,1827,1828],{"class":1231,"line":17},[1229,1829,1404],{"emptyLinePlaceholder":1403},[1229,1831,1832,1834,1837],{"class":1231,"line":110},[1229,1833,1501],{"class":1238},[1229,1835,1836],{"class":1234},"Component",[1229,1838,1613],{"class":1238},[1229,1840,1841,1844,1847],{"class":1231,"line":76},[1229,1842,1843],{"class":1238},"  selector: ",[1229,1845,1846],{"class":1585},"'app-chart'",[1229,1848,1624],{"class":1238},[1229,1850,1851,1854],{"class":1231,"line":247},[1229,1852,1853],{"class":1238},"  template: ",[1229,1855,1856],{"class":1585},"`\n",[1229,1858,1859],{"class":1231,"line":236},[1229,1860,1861],{"class":1585},"    \u003Cdiv #chart>\u003C\u002Fdiv>\n",[1229,1863,1864,1867],{"class":1231,"line":225},[1229,1865,1866],{"class":1585},"  `",[1229,1868,1624],{"class":1238},[1229,1870,1871],{"class":1231,"line":214},[1229,1872,1873],{"class":1238},"})\n",[1229,1875,1876,1879,1882,1885],{"class":1231,"line":203},[1229,1877,1878],{"class":1384},"export",[1229,1880,1881],{"class":1384}," class",[1229,1883,1884],{"class":1515}," ChartComponent",[1229,1886,1569],{"class":1238},[1229,1888,1889,1893],{"class":1231,"line":192},[1229,1890,1892],{"class":1891},"stnAF","  chart",[1229,1894,1738],{"class":1384},[1229,1896,1897,1900,1903,1905,1908,1910,1913],{"class":1231,"line":181},[1229,1898,1899],{"class":1238},"    viewChild.",[1229,1901,1902],{"class":1234},"required",[1229,1904,1348],{"class":1238},[1229,1906,1907],{"class":1515},"ElementRef",[1229,1909,1348],{"class":1238},[1229,1911,1912],{"class":1515},"HTMLDivElement",[1229,1914,1915],{"class":1238},">>(\n",[1229,1917,1918,1921],{"class":1231,"line":170},[1229,1919,1920],{"class":1585},"      'chart'",[1229,1922,1624],{"class":1238},[1229,1924,1925],{"class":1231,"line":159},[1229,1926,1927],{"class":1238},"    );\n",[1229,1929,1930],{"class":1231,"line":148},[1229,1931,1404],{"emptyLinePlaceholder":1403},[1229,1933,1934,1937],{"class":1231,"line":137},[1229,1935,1936],{"class":1384},"  constructor",[1229,1938,1379],{"class":1238},[1229,1940,1941,1944,1947,1950],{"class":1231,"line":125},[1229,1942,1943],{"class":1234},"    afterNextRender",[1229,1945,1946],{"class":1238},"(() ",[1229,1948,1949],{"class":1384},"=>",[1229,1951,1569],{"class":1238},[1229,1953,1954,1957,1959],{"class":1231,"line":113},[1229,1955,1956],{"class":1384},"      const",[1229,1958,1389],{"class":1388},[1229,1960,1738],{"class":1384},[1229,1962,1963,1966,1969,1972],{"class":1231,"line":101},[1229,1964,1965],{"class":1388},"        this",[1229,1967,1968],{"class":1238},".",[1229,1970,1971],{"class":1234},"chart",[1229,1973,1974],{"class":1238},"().nativeElement;\n",[1229,1976,1977],{"class":1231,"line":90},[1229,1978,1404],{"emptyLinePlaceholder":1403},[1229,1980,1981,1984,1986],{"class":1231,"line":79},[1229,1982,1983],{"class":1384},"      new",[1229,1985,1412],{"class":1234},[1229,1987,1415],{"class":1238},[1229,1989,1990],{"class":1231,"line":67},[1229,1991,1992],{"class":1238},"    });\n",[1229,1994,1995],{"class":1231,"line":56},[1229,1996,1997],{"class":1238},"  }\n",[1229,1999,2000],{"class":1231,"line":45},[1229,2001,1420],{"class":1238},[1216,2003,2004],{},"這類寫法適合初始化第三方 UI Library、Canvas，或第一次需要讀取實際 DOM 狀態的工作。",[1216,2006,2007],{},"兩者差異：",[1654,2009,2010,2015],{},[1657,2011,2012,2014],{},[1226,2013,1426],{}," 關心的是 Component View 是否初始化完成。",[1657,2016,2017,2019],{},[1226,2018,1772],{}," 關心的是這一輪 Angular Render 是否已經完成。",[1330,2021,2023],{"id":2022},"拆分-dom-read-與-write","拆分 DOM Read 與 Write",[1216,2025,2026,2028],{},[1226,2027,1772],{}," 除了直接傳入 Callback，也可以把 DOM 操作拆成不同 Phase。",[1216,2030,1784],{},[1219,2032,2034],{"className":1221,"code":2033,"language":1223,"meta":1224,"style":1224},"afterNextRender({\n  write: () => {\n    this.chart()\n      .nativeElement\n      .style.width = '500px';\n  },\n\n  read: () => {\n    const width =\n      this.chart()\n        .nativeElement\n        .getBoundingClientRect()\n        .width;\n\n    console.log(width);\n  },\n});\n",[1226,2035,2036,2042,2054,2066,2071,2082,2087,2091,2102,2111,2122,2127,2136,2141,2145,2156,2160],{"__ignoreMap":1224},[1229,2037,2038,2040],{"class":1231,"line":340},[1229,2039,1292],{"class":1234},[1229,2041,1613],{"class":1238},[1229,2043,2044,2047,2050,2052],{"class":1231,"line":329},[1229,2045,2046],{"class":1234},"  write",[1229,2048,2049],{"class":1238},": () ",[1229,2051,1949],{"class":1384},[1229,2053,1569],{"class":1238},[1229,2055,2056,2059,2061,2063],{"class":1231,"line":318},[1229,2057,2058],{"class":1388},"    this",[1229,2060,1968],{"class":1238},[1229,2062,1971],{"class":1234},[1229,2064,2065],{"class":1238},"()\n",[1229,2067,2068],{"class":1231,"line":134},[1229,2069,2070],{"class":1238},"      .nativeElement\n",[1229,2072,2073,2076,2078,2080],{"class":1231,"line":122},[1229,2074,2075],{"class":1238},"      .style.width ",[1229,2077,1453],{"class":1384},[1229,2079,1722],{"class":1585},[1229,2081,1459],{"class":1238},[1229,2083,2084],{"class":1231,"line":31},[1229,2085,2086],{"class":1238},"  },\n",[1229,2088,2089],{"class":1231,"line":17},[1229,2090,1404],{"emptyLinePlaceholder":1403},[1229,2092,2093,2096,2098,2100],{"class":1231,"line":110},[1229,2094,2095],{"class":1234},"  read",[1229,2097,2049],{"class":1238},[1229,2099,1949],{"class":1384},[1229,2101,1569],{"class":1238},[1229,2103,2104,2107,2109],{"class":1231,"line":76},[1229,2105,2106],{"class":1384},"    const",[1229,2108,1735],{"class":1388},[1229,2110,1738],{"class":1384},[1229,2112,2113,2116,2118,2120],{"class":1231,"line":247},[1229,2114,2115],{"class":1388},"      this",[1229,2117,1968],{"class":1238},[1229,2119,1971],{"class":1234},[1229,2121,2065],{"class":1238},[1229,2123,2124],{"class":1231,"line":236},[1229,2125,2126],{"class":1238},"        .nativeElement\n",[1229,2128,2129,2132,2134],{"class":1231,"line":225},[1229,2130,2131],{"class":1238},"        .",[1229,2133,1746],{"class":1234},[1229,2135,2065],{"class":1238},[1229,2137,2138],{"class":1231,"line":214},[1229,2139,2140],{"class":1238},"        .width;\n",[1229,2142,2143],{"class":1231,"line":203},[1229,2144,1404],{"emptyLinePlaceholder":1403},[1229,2146,2147,2150,2153],{"class":1231,"line":192},[1229,2148,2149],{"class":1238},"    console.",[1229,2151,2152],{"class":1234},"log",[1229,2154,2155],{"class":1238},"(width);\n",[1229,2157,2158],{"class":1231,"line":181},[1229,2159,2086],{"class":1238},[1229,2161,2162],{"class":1231,"line":170},[1229,2163,1649],{"class":1238},[1216,2165,2166],{},"這裡會先處理 DOM 修改，再讀取尺寸或位置，避免修改與讀取一直交錯。",[1216,2168,2169],{},"Render Phase 一共有：",[1219,2171,2173],{"className":1221,"code":2172,"language":1223,"meta":1224,"style":1224},"earlyRead \u002F\u002F 優先讀取 DOM 狀態，只有真的需要在 Write 前讀取時使用\nwrite \u002F\u002F 修改 DOM，例如 style、class 或其他會影響版面的操作\nmixedReadWrite \u002F\u002F 同時包含 DOM 讀取與修改，能避免就盡量避免\nread \u002F\u002F 在所有 Write 完成後，再讀取尺寸、位置等 Layout 資訊\n",[1226,2174,2175,2183,2191,2199],{"__ignoreMap":1224},[1229,2176,2177,2180],{"class":1231,"line":340},[1229,2178,2179],{"class":1238},"earlyRead ",[1229,2181,2182],{"class":1242},"\u002F\u002F 優先讀取 DOM 狀態，只有真的需要在 Write 前讀取時使用\n",[1229,2184,2185,2188],{"class":1231,"line":329},[1229,2186,2187],{"class":1238},"write ",[1229,2189,2190],{"class":1242},"\u002F\u002F 修改 DOM，例如 style、class 或其他會影響版面的操作\n",[1229,2192,2193,2196],{"class":1231,"line":318},[1229,2194,2195],{"class":1238},"mixedReadWrite ",[1229,2197,2198],{"class":1242},"\u002F\u002F 同時包含 DOM 讀取與修改，能避免就盡量避免\n",[1229,2200,2201,2204],{"class":1231,"line":134},[1229,2202,2203],{"class":1238},"read ",[1229,2205,2206],{"class":1242},"\u002F\u002F 在所有 Write 完成後，再讀取尺寸、位置等 Layout 資訊\n",[1216,2208,2209,2210,2213,2214,2217],{},"一般情況下，如果可以明確區分修改和讀取，優先使用 ",[1226,2211,2212],{},"write"," 與 ",[1226,2215,2216],{},"read"," 就好。",[1330,2219,2221],{"id":2220},"ngafterviewchecked-的限制","ngAfterViewChecked() 的限制",[1216,2223,2224],{},"如果需求不是只執行一次，而是每次畫面更新後都要重新處理 DOM，過去可能會寫：",[1219,2226,2228],{"className":1221,"code":2227,"language":1223,"meta":1224,"style":1224},"ngAfterViewChecked() {\n  this.measureElement();\n}\n",[1226,2229,2230,2236,2248],{"__ignoreMap":1224},[1229,2231,2232,2234],{"class":1231,"line":340},[1229,2233,1258],{"class":1234},[1229,2235,1379],{"class":1238},[1229,2237,2238,2240,2242,2245],{"class":1231,"line":329},[1229,2239,1474],{"class":1388},[1229,2241,1968],{"class":1238},[1229,2243,2244],{"class":1234},"measureElement",[1229,2246,2247],{"class":1238},"();\n",[1229,2249,2250],{"class":1231,"line":318},[1229,2251,1420],{"class":1238},[1216,2253,2254,2255,2258],{},"但 ",[1226,2256,2257],{},"ngAfterViewChecked()"," 代表的是 Angular 已經完成這個 Component View 的 Change Detection 檢查。",[1216,2260,2261],{},"如果需求本來就是每次 Render 完成後都要讀取最新 DOM 狀態，這個 Hook 的語意就沒有那麼直接。",[1216,2263,2264,2265,2267],{},"另外 ",[1226,2266,2257],{}," 本身執行頻率可能很高，如果裡面持續讀取 Layout 或做較重的 DOM 操作，也容易增加不必要的成本。",[1330,2269,2271],{"id":2270},"aftereveryrender每次-render-完成後執行","afterEveryRender()：每次 Render 完成後執行",[1216,2273,2274],{},"如果需要在每次 Application Render 完成後執行，可以使用：",[1219,2276,2278],{"className":1221,"code":2277,"language":1223,"meta":1224,"style":1224},"afterEveryRender({\n  read: () => {\n    const height =\n      this.chart()\n        .nativeElement\n        .scrollHeight;\n\n    console.log(height);\n  },\n});\n",[1226,2279,2280,2286,2296,2305,2315,2319,2324,2328,2337,2341],{"__ignoreMap":1224},[1229,2281,2282,2284],{"class":1231,"line":340},[1229,2283,1302],{"class":1234},[1229,2285,1613],{"class":1238},[1229,2287,2288,2290,2292,2294],{"class":1231,"line":329},[1229,2289,2095],{"class":1234},[1229,2291,2049],{"class":1238},[1229,2293,1949],{"class":1384},[1229,2295,1569],{"class":1238},[1229,2297,2298,2300,2303],{"class":1231,"line":318},[1229,2299,2106],{"class":1384},[1229,2301,2302],{"class":1388}," height",[1229,2304,1738],{"class":1384},[1229,2306,2307,2309,2311,2313],{"class":1231,"line":134},[1229,2308,2115],{"class":1388},[1229,2310,1968],{"class":1238},[1229,2312,1971],{"class":1234},[1229,2314,2065],{"class":1238},[1229,2316,2317],{"class":1231,"line":122},[1229,2318,2126],{"class":1238},[1229,2320,2321],{"class":1231,"line":31},[1229,2322,2323],{"class":1238},"        .scrollHeight;\n",[1229,2325,2326],{"class":1231,"line":17},[1229,2327,1404],{"emptyLinePlaceholder":1403},[1229,2329,2330,2332,2334],{"class":1231,"line":110},[1229,2331,2149],{"class":1238},[1229,2333,2152],{"class":1234},[1229,2335,2336],{"class":1238},"(height);\n",[1229,2338,2339],{"class":1231,"line":76},[1229,2340,2086],{"class":1238},[1229,2342,2343],{"class":1231,"line":247},[1229,2344,1649],{"class":1238},[1216,2346,2347,2348,2350],{},"和 ",[1226,2349,1772],{}," 相比，兩者最大的差別在執行次數：",[1654,2352,2353,2358],{},[1657,2354,2355,2357],{},[1226,2356,1772],{},"：註冊後，在最近一次 Render 完成時執行一次，之後不會再觸發。",[1657,2359,2360,2363],{},[1226,2361,2362],{},"afterEveryRender()","：註冊後，每次 Application Render 完成都會執行。",[1216,2365,2366,2367,2369],{},"因此 ",[1226,2368,2362],{}," 裡的工作要盡量保持單純，否則和目前需求無關的 Render，也可能跟著執行同一段程式。",[1330,2371,2373],{"id":2372},"aftereveryrender-不知道是哪個資料改變","afterEveryRender() 不知道是哪個資料改變",[1216,2375,2376],{},"假設現在有一個 Signal：",[1219,2378,2380],{"className":1221,"code":2379,"language":1223,"meta":1224,"style":1224},"chartData = signal([10, 20, 30]);\n",[1226,2381,2382],{"__ignoreMap":1224},[1229,2383,2384,2387,2389,2392,2395,2398,2401,2404,2406,2409],{"class":1231,"line":340},[1229,2385,2386],{"class":1238},"chartData ",[1229,2388,1453],{"class":1384},[1229,2390,2391],{"class":1234}," signal",[1229,2393,2394],{"class":1238},"([",[1229,2396,2397],{"class":1388},"10",[1229,2399,2400],{"class":1238},", ",[1229,2402,2403],{"class":1388},"20",[1229,2405,2400],{"class":1238},[1229,2407,2408],{"class":1388},"30",[1229,2410,2411],{"class":1238},"]);\n",[1216,2413,2414,2415,2418],{},"只有 ",[1226,2416,2417],{},"chartData"," 改變時，才需要更新 Chart。",[1216,2420,2421],{},"如果直接寫：",[1219,2423,2425],{"className":1221,"code":2424,"language":1223,"meta":1224,"style":1224},"afterEveryRender(() => {\n  this.updateChart();\n});\n",[1226,2426,2427,2437,2448],{"__ignoreMap":1224},[1229,2428,2429,2431,2433,2435],{"class":1231,"line":340},[1229,2430,1302],{"class":1234},[1229,2432,1946],{"class":1238},[1229,2434,1949],{"class":1384},[1229,2436,1569],{"class":1238},[1229,2438,2439,2441,2443,2446],{"class":1231,"line":329},[1229,2440,1474],{"class":1388},[1229,2442,1968],{"class":1238},[1229,2444,2445],{"class":1234},"updateChart",[1229,2447,2247],{"class":1238},[1229,2449,2450],{"class":1231,"line":318},[1229,2451,1649],{"class":1238},[1216,2453,2454,2455,2458],{},"只要 Application 發生 Render，",[1226,2456,2457],{},"updateChart()"," 就會執行，即使這次變化和 Chart 沒有關係。",[1216,2460,2461,2462,2465],{},"這時可能會想到前面介紹過的 ",[1226,2463,2464],{},"effect()","：",[1219,2467,2469],{"className":1221,"code":2468,"language":1223,"meta":1224,"style":1224},"effect(() => {\n  this.chart.updateData(\n    this.chartData(),\n  );\n});\n",[1226,2470,2471,2482,2495,2506,2511],{"__ignoreMap":1224},[1229,2472,2473,2476,2478,2480],{"class":1231,"line":340},[1229,2474,2475],{"class":1234},"effect",[1229,2477,1946],{"class":1238},[1229,2479,1949],{"class":1384},[1229,2481,1569],{"class":1238},[1229,2483,2484,2486,2489,2492],{"class":1231,"line":329},[1229,2485,1474],{"class":1388},[1229,2487,2488],{"class":1238},".chart.",[1229,2490,2491],{"class":1234},"updateData",[1229,2493,2494],{"class":1238},"(\n",[1229,2496,2497,2499,2501,2503],{"class":1231,"line":318},[1229,2498,2058],{"class":1388},[1229,2500,1968],{"class":1238},[1229,2502,2417],{"class":1234},[1229,2504,2505],{"class":1238},"(),\n",[1229,2507,2508],{"class":1231,"line":134},[1229,2509,2510],{"class":1238},"  );\n",[1229,2512,2513],{"class":1231,"line":122},[1229,2514,1649],{"class":1238},[1216,2516,2517,2519,2520,2523],{},[1226,2518,2464],{}," 可以追蹤 ",[1226,2521,2522],{},"chartData()","，所以只有相關 Signal 改變時才會重新執行。",[1216,2525,2526,2527,2529],{},"但如果後面的操作還會依賴最新 DOM 狀態，只知道 Signal 已經改變還不夠，因為 ",[1226,2528,2464],{}," 本身不代表這次變化造成的 Render 已經完成。",[1216,2531,2532,2533,2465],{},"例如 Template 也使用了 ",[1226,2534,2522],{},[1219,2536,2538],{"className":1339,"code":2537,"language":1341,"meta":1224,"style":1224},"\u003Cp>資料筆數：{{ chartData().length }}\u003C\u002Fp>\n\u003Cdiv #chart>\u003C\u002Fdiv>\n",[1226,2539,2540,2553],{"__ignoreMap":1224},[1229,2541,2542,2544,2546,2549,2551],{"class":1231,"line":340},[1229,2543,1348],{"class":1238},[1229,2545,1216],{"class":1351},[1229,2547,2548],{"class":1238},">資料筆數：{{ chartData().length }}\u003C\u002F",[1229,2550,1216],{"class":1351},[1229,2552,1364],{"class":1238},[1229,2554,2555,2557,2559,2561,2563,2565],{"class":1231,"line":329},[1229,2556,1348],{"class":1238},[1229,2558,1352],{"class":1351},[1229,2560,1356],{"class":1355},[1229,2562,1359],{"class":1238},[1229,2564,1352],{"class":1351},[1229,2566,1364],{"class":1238},[1216,2568,2569,2570,2572],{},"當 ",[1226,2571,2417],{}," 改變時，Angular 還需要把這次變化反映到畫面上。",[1216,2574,2575],{},"如果第三方 Library 接下來要讀取最新的尺寸、位置或其他 DOM 狀態，就要等這次畫面更新完成後再處理。",[1216,2577,2578,2579,1679],{},"這時就可以使用 ",[1226,2580,2581],{},"afterRenderEffect()",[1330,2583,2585],{"id":2584},"afterrendereffectsignal-改變後等-render-完成","afterRenderEffect()：Signal 改變後等 Render 完成",[1216,2587,2588,2590,2591,2593],{},[1226,2589,2581],{}," 和 ",[1226,2592,2464],{}," 一樣，會追蹤執行過程中讀取的 Signal。",[1216,2595,1784],{},[1219,2597,2599],{"className":1221,"code":2598,"language":1223,"meta":1224,"style":1224},"chartData = signal([10, 20, 30]);\n\nconstructor() {\n  afterRenderEffect(() => {\n    this.chart.updateData(\n      this.chartData(),\n    );\n  });\n}\n",[1226,2600,2601,2623,2627,2634,2645,2655,2665,2669,2674],{"__ignoreMap":1224},[1229,2602,2603,2605,2607,2609,2611,2613,2615,2617,2619,2621],{"class":1231,"line":340},[1229,2604,2386],{"class":1238},[1229,2606,1453],{"class":1384},[1229,2608,2391],{"class":1234},[1229,2610,2394],{"class":1238},[1229,2612,2397],{"class":1388},[1229,2614,2400],{"class":1238},[1229,2616,2403],{"class":1388},[1229,2618,2400],{"class":1238},[1229,2620,2408],{"class":1388},[1229,2622,2411],{"class":1238},[1229,2624,2625],{"class":1231,"line":329},[1229,2626,1404],{"emptyLinePlaceholder":1403},[1229,2628,2629,2632],{"class":1231,"line":318},[1229,2630,2631],{"class":1234},"constructor",[1229,2633,1379],{"class":1238},[1229,2635,2636,2639,2641,2643],{"class":1231,"line":134},[1229,2637,2638],{"class":1234},"  afterRenderEffect",[1229,2640,1946],{"class":1238},[1229,2642,1949],{"class":1384},[1229,2644,1569],{"class":1238},[1229,2646,2647,2649,2651,2653],{"class":1231,"line":122},[1229,2648,2058],{"class":1388},[1229,2650,2488],{"class":1238},[1229,2652,2491],{"class":1234},[1229,2654,2494],{"class":1238},[1229,2656,2657,2659,2661,2663],{"class":1231,"line":31},[1229,2658,2115],{"class":1388},[1229,2660,1968],{"class":1238},[1229,2662,2417],{"class":1234},[1229,2664,2505],{"class":1238},[1229,2666,2667],{"class":1231,"line":17},[1229,2668,1927],{"class":1238},[1229,2670,2671],{"class":1231,"line":110},[1229,2672,2673],{"class":1238},"  });\n",[1229,2675,2676],{"class":1231,"line":76},[1229,2677,1420],{"class":1238},[1216,2679,2680,2681,2683],{},"這裡讀取了 ",[1226,2682,2522],{},"，所以它會成為 Dependency。",[1216,2685,2569,2686,2688,2689,2691],{},[1226,2687,2417],{}," 改變時，",[1226,2690,2581],{}," 會等這次 Render 完成後再執行。",[1216,2693,2694],{},"兩者可以先這樣區分：",[1654,2696,2697,2702],{},[1657,2698,2699,2701],{},[1226,2700,2464],{},"：Signal 改變後執行 Side Effect。",[1657,2703,2704,2706],{},[1226,2705,2581],{},"：Signal 改變後，等 Render 完成再處理 DOM 相關工作。",[1216,2708,2709,2710,2712],{},"如果只是寫 Log、同步 Storage，或呼叫不依賴 DOM 的程式，使用一般 ",[1226,2711,2464],{}," 就可以。",[1216,2714,2715,2716,1679],{},"如果接下來要更新第三方 Chart、量測 Layout，或操作最新的 DOM，再考慮 ",[1226,2717,2581],{},[1330,2719,2721],{"id":2720},"afternextrender-與-afterrendereffect-搭配使用","afterNextRender() 與 afterRenderEffect() 搭配使用",[1216,2723,2724],{},"第三方 UI Library 很適合看出這兩個 API 的分工。",[1216,2726,2727],{},"第一次建立 Chart：",[1219,2729,2731],{"className":1221,"code":2730,"language":1223,"meta":1224,"style":1224},"constructor() {\n  afterNextRender(() => {\n    this.chart = new Chart(\n      this.canvas().nativeElement,\n      this.chartData(),\n    );\n  });\n}\n",[1226,2732,2733,2739,2750,2766,2778,2788,2792,2796],{"__ignoreMap":1224},[1229,2734,2735,2737],{"class":1231,"line":340},[1229,2736,2631],{"class":1234},[1229,2738,1379],{"class":1238},[1229,2740,2741,2744,2746,2748],{"class":1231,"line":329},[1229,2742,2743],{"class":1234},"  afterNextRender",[1229,2745,1946],{"class":1238},[1229,2747,1949],{"class":1384},[1229,2749,1569],{"class":1238},[1229,2751,2752,2754,2757,2759,2762,2764],{"class":1231,"line":318},[1229,2753,2058],{"class":1388},[1229,2755,2756],{"class":1238},".chart ",[1229,2758,1453],{"class":1384},[1229,2760,2761],{"class":1384}," new",[1229,2763,1412],{"class":1234},[1229,2765,2494],{"class":1238},[1229,2767,2768,2770,2772,2775],{"class":1231,"line":134},[1229,2769,2115],{"class":1388},[1229,2771,1968],{"class":1238},[1229,2773,2774],{"class":1234},"canvas",[1229,2776,2777],{"class":1238},"().nativeElement,\n",[1229,2779,2780,2782,2784,2786],{"class":1231,"line":122},[1229,2781,2115],{"class":1388},[1229,2783,1968],{"class":1238},[1229,2785,2417],{"class":1234},[1229,2787,2505],{"class":1238},[1229,2789,2790],{"class":1231,"line":31},[1229,2791,1927],{"class":1238},[1229,2793,2794],{"class":1231,"line":17},[1229,2795,2673],{"class":1238},[1229,2797,2798],{"class":1231,"line":110},[1229,2799,1420],{"class":1238},[1216,2801,2802],{},"後續資料改變：",[1219,2804,2806],{"className":1221,"code":2805,"language":1223,"meta":1224,"style":1224},"afterRenderEffect(() => {\n  this.chart.updateData(\n    this.chartData(),\n  );\n});\n",[1226,2807,2808,2818,2828,2838,2842],{"__ignoreMap":1224},[1229,2809,2810,2812,2814,2816],{"class":1231,"line":340},[1229,2811,1312],{"class":1234},[1229,2813,1946],{"class":1238},[1229,2815,1949],{"class":1384},[1229,2817,1569],{"class":1238},[1229,2819,2820,2822,2824,2826],{"class":1231,"line":329},[1229,2821,1474],{"class":1388},[1229,2823,2488],{"class":1238},[1229,2825,2491],{"class":1234},[1229,2827,2494],{"class":1238},[1229,2829,2830,2832,2834,2836],{"class":1231,"line":318},[1229,2831,2058],{"class":1388},[1229,2833,1968],{"class":1238},[1229,2835,2417],{"class":1234},[1229,2837,2505],{"class":1238},[1229,2839,2840],{"class":1231,"line":134},[1229,2841,2510],{"class":1238},[1229,2843,2844],{"class":1231,"line":122},[1229,2845,1649],{"class":1238},[1216,2847,2848,2849,2851,2852,2854,2855,1679],{},"第一次初始化只需要做一次，所以交給 ",[1226,2850,1772],{},"；後續則根據 ",[1226,2853,2417],{}," 的變化更新 Chart，交給 ",[1226,2856,2581],{},[1216,2858,2859],{},"這樣不用每次 Render 都重新初始化 Chart，也不會因為其他和 Chart 無關的 Render 而執行更新。",[1330,2861,2863],{"id":2862},"afterrendereffect-的-cleanup","afterRenderEffect() 的 Cleanup",[1216,2865,2866,2868],{},[1226,2867,2581],{}," 也可以註冊 Cleanup。",[1216,2870,1784],{},[1219,2872,2874],{"className":1221,"code":2873,"language":1223,"meta":1224,"style":1224},"afterRenderEffect((onCleanup) => {\n  const observer =\n    new ResizeObserver(() => {\n      console.log('resize');\n    });\n\n  observer.observe(\n    this.element().nativeElement,\n  );\n\n  onCleanup(() => {\n    observer.disconnect();\n  });\n});\n",[1226,2875,2876,2893,2902,2916,2932,2936,2940,2950,2961,2965,2969,2980,2990,2994],{"__ignoreMap":1224},[1229,2877,2878,2880,2883,2886,2889,2891],{"class":1231,"line":340},[1229,2879,1312],{"class":1234},[1229,2881,2882],{"class":1238},"((",[1229,2884,2885],{"class":1891},"onCleanup",[1229,2887,2888],{"class":1238},") ",[1229,2890,1949],{"class":1384},[1229,2892,1569],{"class":1238},[1229,2894,2895,2897,2900],{"class":1231,"line":329},[1229,2896,1385],{"class":1384},[1229,2898,2899],{"class":1388}," observer",[1229,2901,1738],{"class":1384},[1229,2903,2904,2907,2910,2912,2914],{"class":1231,"line":318},[1229,2905,2906],{"class":1384},"    new",[1229,2908,2909],{"class":1234}," ResizeObserver",[1229,2911,1946],{"class":1238},[1229,2913,1949],{"class":1384},[1229,2915,1569],{"class":1238},[1229,2917,2918,2921,2923,2926,2929],{"class":1231,"line":134},[1229,2919,2920],{"class":1238},"      console.",[1229,2922,2152],{"class":1234},[1229,2924,2925],{"class":1238},"(",[1229,2927,2928],{"class":1585},"'resize'",[1229,2930,2931],{"class":1238},");\n",[1229,2933,2934],{"class":1231,"line":122},[1229,2935,1992],{"class":1238},[1229,2937,2938],{"class":1231,"line":31},[1229,2939,1404],{"emptyLinePlaceholder":1403},[1229,2941,2942,2945,2948],{"class":1231,"line":17},[1229,2943,2944],{"class":1238},"  observer.",[1229,2946,2947],{"class":1234},"observe",[1229,2949,2494],{"class":1238},[1229,2951,2952,2954,2956,2959],{"class":1231,"line":110},[1229,2953,2058],{"class":1388},[1229,2955,1968],{"class":1238},[1229,2957,2958],{"class":1234},"element",[1229,2960,2777],{"class":1238},[1229,2962,2963],{"class":1231,"line":76},[1229,2964,2510],{"class":1238},[1229,2966,2967],{"class":1231,"line":247},[1229,2968,1404],{"emptyLinePlaceholder":1403},[1229,2970,2971,2974,2976,2978],{"class":1231,"line":236},[1229,2972,2973],{"class":1234},"  onCleanup",[1229,2975,1946],{"class":1238},[1229,2977,1949],{"class":1384},[1229,2979,1569],{"class":1238},[1229,2981,2982,2985,2988],{"class":1231,"line":225},[1229,2983,2984],{"class":1238},"    observer.",[1229,2986,2987],{"class":1234},"disconnect",[1229,2989,2247],{"class":1238},[1229,2991,2992],{"class":1231,"line":214},[1229,2993,2673],{"class":1238},[1229,2995,2996],{"class":1231,"line":203},[1229,2997,1649],{"class":1238},[1216,2999,3000],{},"當 Effect 下一次重新執行，或對應的 Injection Context 被銷毀時，上一輪建立的 Observer 就會先被清掉。",[1216,3002,3003],{},"如果需求本身就是監聽 DOM 狀態，瀏覽器其實已經提供對應的 API：",[1654,3005,3006,3012,3018],{},[1657,3007,3008,3011],{},[1226,3009,3010],{},"ResizeObserver","：監聽 Element 尺寸變化。",[1657,3013,3014,3017],{},[1226,3015,3016],{},"MutationObserver","：監聽 DOM 結構或 Attribute 變化。",[1657,3019,3020,3023],{},[1226,3021,3022],{},"IntersectionObserver","：監聽 Element 是否進入可視範圍。",[1216,3025,3026,3027,1679],{},"所以不一定所有 DOM 監聽都需要交給 ",[1226,3028,2581],{},[1330,3030,3032],{"id":3031},"render-api-與-ssr","Render API 與 SSR",[1216,3034,3035,3037,3038,2590,3040,3042],{},[1226,3036,1772],{},"、",[1226,3039,2362],{},[1226,3041,2581],{}," 都不會在 SSR 或 Prerender 階段執行。",[1216,3044,3045],{},"因此像這些只存在瀏覽器中的 API：",[1219,3047,3049],{"className":1221,"code":3048,"language":1223,"meta":1224,"style":1224},"window\ndocument\ngetBoundingClientRect()\nCanvas\n",[1226,3050,3051,3056,3061,3067],{"__ignoreMap":1224},[1229,3052,3053],{"class":1231,"line":340},[1229,3054,3055],{"class":1238},"window\n",[1229,3057,3058],{"class":1231,"line":329},[1229,3059,3060],{"class":1238},"document\n",[1229,3062,3063,3065],{"class":1231,"line":318},[1229,3064,1746],{"class":1234},[1229,3066,2065],{"class":1238},[1229,3068,3069],{"class":1231,"line":134},[1229,3070,3071],{"class":1238},"Canvas\n",[1216,3073,3074],{},"如果本來就只需要在 Render 後使用，可以直接放進 Render Callback，不需要另外把平台判斷和 Render 時機混在一起。",[1330,3076,3078],{"id":3077},"ngondestroy-的限制","ngOnDestroy() 的限制",[1216,3080,3081],{},"除了 Render，Lifecycle 還有另一個常見問題，就是 Cleanup。",[1216,3083,1784],{},[1219,3085,3087],{"className":1221,"code":3086,"language":1223,"meta":1224,"style":1224},"export class DemoComponent\n  implements OnDestroy {\n\n  private timer = window.setInterval(\n    () => {\n      console.log('tick');\n    },\n    1000,\n  );\n\n  ngOnDestroy() {\n    clearInterval(this.timer);\n  }\n}\n",[1226,3088,3089,3098,3108,3112,3130,3139,3152,3157,3164,3168,3172,3179,3191,3195],{"__ignoreMap":1224},[1229,3090,3091,3093,3095],{"class":1231,"line":340},[1229,3092,1878],{"class":1384},[1229,3094,1881],{"class":1384},[1229,3096,3097],{"class":1515}," DemoComponent\n",[1229,3099,3100,3103,3106],{"class":1231,"line":329},[1229,3101,3102],{"class":1384},"  implements",[1229,3104,3105],{"class":1355}," OnDestroy",[1229,3107,1569],{"class":1238},[1229,3109,3110],{"class":1231,"line":318},[1229,3111,1404],{"emptyLinePlaceholder":1403},[1229,3113,3114,3117,3120,3122,3125,3128],{"class":1231,"line":134},[1229,3115,3116],{"class":1384},"  private",[1229,3118,3119],{"class":1891}," timer",[1229,3121,1392],{"class":1384},[1229,3123,3124],{"class":1238}," window.",[1229,3126,3127],{"class":1234},"setInterval",[1229,3129,2494],{"class":1238},[1229,3131,3132,3135,3137],{"class":1231,"line":122},[1229,3133,3134],{"class":1238},"    () ",[1229,3136,1949],{"class":1384},[1229,3138,1569],{"class":1238},[1229,3140,3141,3143,3145,3147,3150],{"class":1231,"line":31},[1229,3142,2920],{"class":1238},[1229,3144,2152],{"class":1234},[1229,3146,2925],{"class":1238},[1229,3148,3149],{"class":1585},"'tick'",[1229,3151,2931],{"class":1238},[1229,3153,3154],{"class":1231,"line":17},[1229,3155,3156],{"class":1238},"    },\n",[1229,3158,3159,3162],{"class":1231,"line":110},[1229,3160,3161],{"class":1388},"    1000",[1229,3163,1624],{"class":1238},[1229,3165,3166],{"class":1231,"line":76},[1229,3167,2510],{"class":1238},[1229,3169,3170],{"class":1231,"line":247},[1229,3171,1404],{"emptyLinePlaceholder":1403},[1229,3173,3174,3177],{"class":1231,"line":236},[1229,3175,3176],{"class":1234},"  ngOnDestroy",[1229,3178,1379],{"class":1238},[1229,3180,3181,3184,3186,3188],{"class":1231,"line":225},[1229,3182,3183],{"class":1234},"    clearInterval",[1229,3185,2925],{"class":1238},[1229,3187,1128],{"class":1388},[1229,3189,3190],{"class":1238},".timer);\n",[1229,3192,3193],{"class":1231,"line":214},[1229,3194,1997],{"class":1238},[1229,3196,3197],{"class":1231,"line":203},[1229,3198,1420],{"class":1238},[1216,3200,3201],{},"這樣寫沒有問題。",[1216,3203,3204],{},"比較麻煩的是，建立資源和清理資源的位置可能離得很遠。",[1216,3206,3207,3208,3211],{},"Component 中如果同時建立 Timer、Event Listener、Observer 或第三方 Instance，最後全部集中在 ",[1226,3209,3210],{},"ngOnDestroy()"," 清理，程式越大，就越容易新增了 Setup，卻忘記補上對應的 Cleanup。",[1330,3213,3215],{"id":3214},"destroyref讓-setup-與-cleanup-放在一起","DestroyRef：讓 Setup 與 Cleanup 放在一起",[1216,3217,3218,3221],{},[1226,3219,3220],{},"DestroyRef"," 可以直接註冊目前 Context 被銷毀時要執行的 Callback。",[1216,3223,1784],{},[1219,3225,3227],{"className":1221,"code":3226,"language":1223,"meta":1224,"style":1224},"import {\n  Component,\n  DestroyRef,\n  inject,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-demo',\n  template: ``,\n})\nexport class DemoComponent {\n  private destroyRef =\n    inject(DestroyRef);\n\n  constructor() {\n    const timer =\n      window.setInterval(() => {\n        console.log('tick');\n      }, 1000);\n\n    this.destroyRef.onDestroy(() => {\n      clearInterval(timer);\n    });\n  }\n}\n",[1226,3228,3229,3235,3239,3244,3249,3259,3263,3271,3280,3289,3293,3304,3313,3321,3325,3331,3339,3352,3365,3374,3378,3394,3402,3406,3410],{"__ignoreMap":1224},[1229,3230,3231,3233],{"class":1231,"line":340},[1229,3232,1566],{"class":1384},[1229,3234,1569],{"class":1238},[1229,3236,3237],{"class":1231,"line":329},[1229,3238,1805],{"class":1238},[1229,3240,3241],{"class":1231,"line":318},[1229,3242,3243],{"class":1238},"  DestroyRef,\n",[1229,3245,3246],{"class":1231,"line":134},[1229,3247,3248],{"class":1238},"  inject,\n",[1229,3250,3251,3253,3255,3257],{"class":1231,"line":122},[1229,3252,1579],{"class":1238},[1229,3254,1582],{"class":1384},[1229,3256,1586],{"class":1585},[1229,3258,1459],{"class":1238},[1229,3260,3261],{"class":1231,"line":31},[1229,3262,1404],{"emptyLinePlaceholder":1403},[1229,3264,3265,3267,3269],{"class":1231,"line":17},[1229,3266,1501],{"class":1238},[1229,3268,1836],{"class":1234},[1229,3270,1613],{"class":1238},[1229,3272,3273,3275,3278],{"class":1231,"line":110},[1229,3274,1843],{"class":1238},[1229,3276,3277],{"class":1585},"'app-demo'",[1229,3279,1624],{"class":1238},[1229,3281,3282,3284,3287],{"class":1231,"line":76},[1229,3283,1853],{"class":1238},[1229,3285,3286],{"class":1585},"``",[1229,3288,1624],{"class":1238},[1229,3290,3291],{"class":1231,"line":247},[1229,3292,1873],{"class":1238},[1229,3294,3295,3297,3299,3302],{"class":1231,"line":236},[1229,3296,1878],{"class":1384},[1229,3298,1881],{"class":1384},[1229,3300,3301],{"class":1515}," DemoComponent",[1229,3303,1569],{"class":1238},[1229,3305,3306,3308,3311],{"class":1231,"line":225},[1229,3307,3116],{"class":1384},[1229,3309,3310],{"class":1891}," destroyRef",[1229,3312,1738],{"class":1384},[1229,3314,3315,3318],{"class":1231,"line":214},[1229,3316,3317],{"class":1234},"    inject",[1229,3319,3320],{"class":1238},"(DestroyRef);\n",[1229,3322,3323],{"class":1231,"line":203},[1229,3324,1404],{"emptyLinePlaceholder":1403},[1229,3326,3327,3329],{"class":1231,"line":192},[1229,3328,1936],{"class":1384},[1229,3330,1379],{"class":1238},[1229,3332,3333,3335,3337],{"class":1231,"line":181},[1229,3334,2106],{"class":1384},[1229,3336,3119],{"class":1388},[1229,3338,1738],{"class":1384},[1229,3340,3341,3344,3346,3348,3350],{"class":1231,"line":170},[1229,3342,3343],{"class":1238},"      window.",[1229,3345,3127],{"class":1234},[1229,3347,1946],{"class":1238},[1229,3349,1949],{"class":1384},[1229,3351,1569],{"class":1238},[1229,3353,3354,3357,3359,3361,3363],{"class":1231,"line":159},[1229,3355,3356],{"class":1238},"        console.",[1229,3358,2152],{"class":1234},[1229,3360,2925],{"class":1238},[1229,3362,3149],{"class":1585},[1229,3364,2931],{"class":1238},[1229,3366,3367,3370,3372],{"class":1231,"line":148},[1229,3368,3369],{"class":1238},"      }, ",[1229,3371,1632],{"class":1388},[1229,3373,2931],{"class":1238},[1229,3375,3376],{"class":1231,"line":137},[1229,3377,1404],{"emptyLinePlaceholder":1403},[1229,3379,3380,3382,3385,3388,3390,3392],{"class":1231,"line":125},[1229,3381,2058],{"class":1388},[1229,3383,3384],{"class":1238},".destroyRef.",[1229,3386,3387],{"class":1234},"onDestroy",[1229,3389,1946],{"class":1238},[1229,3391,1949],{"class":1384},[1229,3393,1569],{"class":1238},[1229,3395,3396,3399],{"class":1231,"line":113},[1229,3397,3398],{"class":1234},"      clearInterval",[1229,3400,3401],{"class":1238},"(timer);\n",[1229,3403,3404],{"class":1231,"line":101},[1229,3405,1992],{"class":1238},[1229,3407,3408],{"class":1231,"line":90},[1229,3409,1997],{"class":1238},[1229,3411,3412],{"class":1231,"line":79},[1229,3413,1420],{"class":1238},[1216,3415,3416],{},"Timer 在哪裡建立，就可以在附近直接寫對應的 Cleanup。",[1216,3418,3419,3421,3422,3424],{},[1226,3420,3220],{}," 不是用來取代 ",[1226,3423,3210],{},"，而是讓建立與清理的程式不用拆得那麼遠。",[1330,3426,3428],{"id":3427},"destroyref-也可以交給其他-function","DestroyRef 也可以交給其他 Function",[1216,3430,3431],{},"這個特性在抽共用邏輯時會更明顯。",[1216,3433,1784],{},[1219,3435,3437],{"className":1221,"code":3436,"language":1223,"meta":1224,"style":1224},"function setupTimer(\n  destroyRef: DestroyRef,\n) {\n  const timer =\n    window.setInterval(() => {\n      console.log('tick');\n    }, 1000);\n\n  destroyRef.onDestroy(() => {\n    clearInterval(timer);\n  });\n}\n",[1226,3438,3439,3449,3462,3467,3475,3488,3500,3509,3513,3526,3532,3536],{"__ignoreMap":1224},[1229,3440,3441,3444,3447],{"class":1231,"line":340},[1229,3442,3443],{"class":1384},"function",[1229,3445,3446],{"class":1234}," setupTimer",[1229,3448,2494],{"class":1238},[1229,3450,3451,3454,3457,3460],{"class":1231,"line":329},[1229,3452,3453],{"class":1891},"  destroyRef",[1229,3455,3456],{"class":1384},":",[1229,3458,3459],{"class":1515}," DestroyRef",[1229,3461,1624],{"class":1238},[1229,3463,3464],{"class":1231,"line":318},[1229,3465,3466],{"class":1238},") {\n",[1229,3468,3469,3471,3473],{"class":1231,"line":134},[1229,3470,1385],{"class":1384},[1229,3472,3119],{"class":1388},[1229,3474,1738],{"class":1384},[1229,3476,3477,3480,3482,3484,3486],{"class":1231,"line":122},[1229,3478,3479],{"class":1238},"    window.",[1229,3481,3127],{"class":1234},[1229,3483,1946],{"class":1238},[1229,3485,1949],{"class":1384},[1229,3487,1569],{"class":1238},[1229,3489,3490,3492,3494,3496,3498],{"class":1231,"line":31},[1229,3491,2920],{"class":1238},[1229,3493,2152],{"class":1234},[1229,3495,2925],{"class":1238},[1229,3497,3149],{"class":1585},[1229,3499,2931],{"class":1238},[1229,3501,3502,3505,3507],{"class":1231,"line":17},[1229,3503,3504],{"class":1238},"    }, ",[1229,3506,1632],{"class":1388},[1229,3508,2931],{"class":1238},[1229,3510,3511],{"class":1231,"line":110},[1229,3512,1404],{"emptyLinePlaceholder":1403},[1229,3514,3515,3518,3520,3522,3524],{"class":1231,"line":76},[1229,3516,3517],{"class":1238},"  destroyRef.",[1229,3519,3387],{"class":1234},[1229,3521,1946],{"class":1238},[1229,3523,1949],{"class":1384},[1229,3525,1569],{"class":1238},[1229,3527,3528,3530],{"class":1231,"line":247},[1229,3529,3183],{"class":1234},[1229,3531,3401],{"class":1238},[1229,3533,3534],{"class":1231,"line":236},[1229,3535,2673],{"class":1238},[1229,3537,3538],{"class":1231,"line":225},[1229,3539,1420],{"class":1238},[1216,3541,3542],{},"Component 只需要：",[1219,3544,3546],{"className":1221,"code":3545,"language":1223,"meta":1224,"style":1224},"constructor() {\n  setupTimer(\n    inject(DestroyRef),\n  );\n}\n",[1226,3547,3548,3554,3561,3568,3572],{"__ignoreMap":1224},[1229,3549,3550,3552],{"class":1231,"line":340},[1229,3551,2631],{"class":1234},[1229,3553,1379],{"class":1238},[1229,3555,3556,3559],{"class":1231,"line":329},[1229,3557,3558],{"class":1234},"  setupTimer",[1229,3560,2494],{"class":1238},[1229,3562,3563,3565],{"class":1231,"line":318},[1229,3564,3317],{"class":1234},[1229,3566,3567],{"class":1238},"(DestroyRef),\n",[1229,3569,3570],{"class":1231,"line":134},[1229,3571,2510],{"class":1238},[1229,3573,3574],{"class":1231,"line":122},[1229,3575,1420],{"class":1238},[1216,3577,3578,3579,3581],{},"這段 Helper 自己就知道資源什麼時候建立，也知道什麼時候該清掉，不需要再回到 Component 的 ",[1226,3580,3210],{}," 裡補 Cleanup。",[1330,3583,3585],{"id":3584},"rxjs-可以直接使用-takeuntildestroyed","RxJS 可以直接使用 takeUntilDestroyed()",[1216,3587,3588],{},"如果 Cleanup 對象是 Observable Subscription，可以直接使用：",[1219,3590,3592],{"className":1221,"code":3591,"language":1223,"meta":1224,"style":1224},"takeUntilDestroyed()\n",[1226,3593,3594],{"__ignoreMap":1224},[1229,3595,3596,3599],{"class":1231,"line":340},[1229,3597,3598],{"class":1234},"takeUntilDestroyed",[1229,3600,2065],{"class":1238},[1216,3602,1784],{},[1219,3604,3606],{"className":1221,"code":3605,"language":1223,"meta":1224,"style":1224},"this.userService.users$\n  .pipe(\n    takeUntilDestroyed(),\n  )\n  .subscribe(users => {\n    console.log(users);\n  });\n",[1226,3607,3608,3615,3625,3632,3637,3654,3663],{"__ignoreMap":1224},[1229,3609,3610,3612],{"class":1231,"line":340},[1229,3611,1128],{"class":1388},[1229,3613,3614],{"class":1238},".userService.users$\n",[1229,3616,3617,3620,3623],{"class":1231,"line":329},[1229,3618,3619],{"class":1238},"  .",[1229,3621,3622],{"class":1234},"pipe",[1229,3624,2494],{"class":1238},[1229,3626,3627,3630],{"class":1231,"line":318},[1229,3628,3629],{"class":1234},"    takeUntilDestroyed",[1229,3631,2505],{"class":1238},[1229,3633,3634],{"class":1231,"line":134},[1229,3635,3636],{"class":1238},"  )\n",[1229,3638,3639,3641,3644,3646,3649,3652],{"class":1231,"line":122},[1229,3640,3619],{"class":1238},[1229,3642,3643],{"class":1234},"subscribe",[1229,3645,2925],{"class":1238},[1229,3647,3648],{"class":1891},"users",[1229,3650,3651],{"class":1384}," =>",[1229,3653,1569],{"class":1238},[1229,3655,3656,3658,3660],{"class":1231,"line":31},[1229,3657,2149],{"class":1238},[1229,3659,2152],{"class":1234},[1229,3661,3662],{"class":1238},"(users);\n",[1229,3664,3665],{"class":1231,"line":17},[1229,3666,2673],{"class":1238},[1216,3668,3669],{},"當目前的 Injection Context 被 Destroy 時，Subscription 就會自動結束。",[1216,3671,3672],{},"以前常見的寫法是自己準備：",[1219,3674,3676],{"className":1221,"code":3675,"language":1223,"meta":1224,"style":1224},"private destroy$ =\n  new Subject\u003Cvoid>();\n",[1226,3677,3678,3686],{"__ignoreMap":1224},[1229,3679,3680,3683],{"class":1231,"line":340},[1229,3681,3682],{"class":1238},"private destroy$ ",[1229,3684,3685],{"class":1384},"=\n",[1229,3687,3688,3690,3693,3695,3698],{"class":1231,"line":329},[1229,3689,1409],{"class":1384},[1229,3691,3692],{"class":1234}," Subject",[1229,3694,1348],{"class":1238},[1229,3696,3697],{"class":1388},"void",[1229,3699,3700],{"class":1238},">();\n",[1216,3702,3703,3704,3706],{},"再在 ",[1226,3705,3210],{}," 裡：",[1219,3708,3710],{"className":1221,"code":3709,"language":1223,"meta":1224,"style":1224},"this.destroy$.next();\nthis.destroy$.complete();\n",[1226,3711,3712,3724],{"__ignoreMap":1224},[1229,3713,3714,3716,3719,3722],{"class":1231,"line":340},[1229,3715,1128],{"class":1388},[1229,3717,3718],{"class":1238},".destroy$.",[1229,3720,3721],{"class":1234},"next",[1229,3723,2247],{"class":1238},[1229,3725,3726,3728,3730,3733],{"class":1231,"line":329},[1229,3727,1128],{"class":1388},[1229,3729,3718],{"class":1238},[1229,3731,3732],{"class":1234},"complete",[1229,3734,2247],{"class":1238},[1216,3736,3737,3738,3741],{},"如果目的只是跟著 Angular 的生命週期取消 Subscription，",[1226,3739,3740],{},"takeUntilDestroyed()"," 可以少掉這一層手動管理。",[1330,3743,3744],{"id":3744},"本日小結",[1216,3746,3747],{},[3748,3749],"img",{"alt":3750,"src":3751},"Angular Render Hooks 怎麼選？看執行次數、Signal 依賴與 Cleanup","https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791353425\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2022%EF%BC%9AAngular%20Render%20Lifecycle%EF%BC%8C%E5%BE%9E%20afterNextRender%20%E5%88%B0%20DestroyRef\u002Fangular-render-hooks-comparison-v2.png",[1216,3753,3754,3755,2590,3757,3759],{},"傳統 Lifecycle Hooks 處理的是 Component 本身的生命週期，但有些需求其實更在意 Render 完成後的 DOM 狀態。這時 ",[1226,3756,1772],{},[1226,3758,2362],{}," 可以直接對應 Render 完成後的工作。",[1216,3761,3762,3763,3765,3766,3768],{},"如果還需要根據 Signal 的變化決定是否執行，",[1226,3764,2581],{}," 就能補上一般 ",[1226,3767,2464],{}," 缺少的 Render 時機，避免資料已經改了，但 DOM 還沒更新完成就開始操作。",[1216,3770,3771,3772,3774,3775,1679],{},"Cleanup 的部分則可以透過 ",[1226,3773,3220],{},"，讓資源建立和清理放得更靠近；RxJS Subscription 也可以直接搭配 ",[1226,3776,3740],{},[1216,3778,3779],{},"這些 API 沒有取代傳統 Lifecycle Hooks，而是把 Component Lifecycle、Render 時機和 Cleanup 分得更清楚，寫法也會更接近實際要處理的問題。",[3781,3782,3783],"h3",{"id":3783},"資料來源",[1654,3785,3786,3796,3808,3821],{},[1657,3787,3788,3795],{},[3789,3790,3794],"a",{"href":3791,"rel":3792},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fcomponents\u002Flifecycle#aftereveryrender-and-afternextrender",[3793],"nofollow","Angular 官方文件：afterEveryRender and afterNextRender","：Render Callback 在所有元件 Render 完成後執行，且不在 SSR 與 Prerender 執行。",[1657,3797,3798,3803,3804,3807],{},[3789,3799,3802],{"href":3800,"rel":3801},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fcomponents\u002Flifecycle#destroyref",[3793],"Angular 官方文件：DestroyRef","：用 ",[1226,3805,3806],{},"onDestroy()"," 在建立資源的地方註冊 Cleanup。",[1657,3809,3810,2465,3815,3817,3818,3820],{},[3789,3811,3814],{"href":3812,"rel":3813},"https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals\u002Feffect#side-effects-on-dom-elements",[3793],"Angular Signals 官方文件：Side effects on DOM elements",[1226,3816,2464],{}," 與 Render 時機的差異，以及 ",[1226,3819,2581],{}," 的使用情境。",[1657,3822,3823,3828],{},[3789,3824,3827],{"href":3825,"rel":3826},"https:\u002F\u002Fangular.dev\u002Fecosystem\u002Frxjs-interop\u002Ftake-until-destroyed",[3793],"Angular 官方文件：Unsubscribing with takeUntilDestroyed","：Context 銷毀時自動結束 Subscription。",[3830,3831,3832],"style",{},"html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}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);}html pre.shiki code .sIAta, html code.shiki .sIAta{--shiki-default:#22863A;--shiki-dark:#8DDB8C}html pre.shiki code .sMOND, html code.shiki .sMOND{--shiki-default:#6F42C1;--shiki-dark:#6CB6FF}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}",{"title":1224,"searchDepth":134,"depth":134,"links":3834},[3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849],{"id":1332,"depth":329,"text":1333},{"id":1775,"depth":329,"text":1776},{"id":2022,"depth":329,"text":2023},{"id":2220,"depth":329,"text":2221},{"id":2270,"depth":329,"text":2271},{"id":2372,"depth":329,"text":2373},{"id":2584,"depth":329,"text":2585},{"id":2720,"depth":329,"text":2721},{"id":2862,"depth":329,"text":2863},{"id":3031,"depth":329,"text":3032},{"id":3077,"depth":329,"text":3078},{"id":3214,"depth":329,"text":3215},{"id":3427,"depth":329,"text":3428},{"id":3584,"depth":329,"text":3585},{"id":3744,"depth":329,"text":3744,"children":3850},[3851],{"id":3783,"depth":318,"text":3783},[11,13],"md",{},"---\ntitle: \"Day 22：Angular Render Lifecycle，從 afterNextRender 到 DestroyRef\"\ndate: 2026-10-06 23:36:23\ntags:\n  - \"iThome 鐵人賽\"\n  - \"2026 鐵人賽\"\n  - \"Angular\"\ncategories:\n  - \"iThome 鐵人賽\"\n  - \"Angular\"\ndescription: \"過去在 Angular 中，只要遇到初始化、View 建立或資源清理，通常會先想到 Lifecycle Hooks：\"\ncanonical: https:\u002F\u002Fithelp.ithome.com.tw\u002Farticles\u002F10421810\nseries:\n  name: \"2026 iThome 鐵人賽：Angular 22 Signal 進化論\"\n  url: https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20116554\u002Fironman\u002F9939\n  day: 22\n---\n\n過去在 Angular 中，只要遇到初始化、View 建立或資源清理，通常會先想到 Lifecycle Hooks：\n\n```ts\nngOnInit() \u002F\u002F Input 初始化後\nngAfterViewInit() \u002F\u002F View \u002F Child View 初始化後\nngAfterViewChecked() \u002F\u002F View \u002F Child View 每次檢查後\nngOnDestroy() \u002F\u002F Component 銷毀前\n```\n\n這些 Hook 本身沒有問題，只是有些需求關心的其實不是 Component 走到哪個階段，而是 DOM 什麼時候完成 Render、某個 Signal 更新後畫面是否已經同步，或建立的資源要怎麼跟著目前的 Context 一起清理。\n\n這類工作放在傳統 Lifecycle Hooks 中通常也能運作，但 Hook 描述的是 Component 的生命週期，和實際需求不一定完全一致。\n\nAngular 另外提供了：\n\n```ts\nafterNextRender() \u002F\u002F 註冊後最近一次 Render 完成時執行一次\nafterEveryRender() \u002F\u002F 每次 Angular Render 完成後都會執行\nafterRenderEffect() \u002F\u002F 追蹤 Signal，等相關變化 Render 完成後再執行\nDestroyRef \u002F\u002F 註冊目前 Context 被銷毀時要執行的 Cleanup\n```\n\n接下來就從幾個常見情境來看它們的差別。\n\n## ngAfterViewInit() 的限制\n\n假設 Component 中有一個 DOM Element：\n\n```html\n\u003Cdiv #chart>\u003C\u002Fdiv>\n```\n\n如果要初始化第三方 Chart Library，過去很常寫在：\n\n```ts\nngAfterViewInit() {\n  const element = this.chart.nativeElement;\n\n  new Chart(element);\n}\n```\n\n這樣寫沒有問題，因為 `ngAfterViewInit()` 執行時，Component 的 View 已經初始化完成，也可以取得 `ViewChild` 的結果。\n\n但它仍然屬於 Change Detection Lifecycle，代表的是 View 已經初始化，不是專門提供給 Render 完成後的 DOM 操作。\n\n其中一個常見情況，是在 `ngAfterViewInit()` 裡修改已經參與這一輪檢查的狀態：\n\n```ts\nloading = true;\n\nngAfterViewInit() {\n  this.loading = false;\n}\n```\n\nTemplate 同時使用這個值：\n\n```ts\n@if (loading) {\n  \u003Cp>Loading...\u003C\u002Fp>\n}\n```\n\nAngular 已經檢查過這個 View，這時又修改會影響畫面的狀態，在開發模式下就可能出現：\n\n```text\nExpressionChangedAfterItHasBeenCheckedError\n```\n\n問題不是 `ngAfterViewInit()` 不能修改資料，而是這一輪 Change Detection 已經檢查過畫面，值卻又在同一輪裡發生改變。\n\n如果使用 Angular 22 預設的 Zoneless Change Detection，有些狀態變化不一定會立刻進入下一次檢查，因此這類問題不一定每次都會直接看到錯誤。\n\n測試時可以在 `main.ts` 加上：\n\n```ts\nimport {\n  provideCheckNoChangesConfig,\n} from '@angular\u002Fcore';\n\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideCheckNoChangesConfig({\n      exhaustive: true,\n      interval: 1000,\n    }),\n  ],\n});\n```\n\nprovideCheckNoChangesConfig 參數說明：\n\n- `exhaustive: true`：檢查所有掛在 Application 上的 View，而不只目前被標記需要更新的 View。\n- `interval: 1000`：每 1 秒額外執行一次 `checkNoChanges` 檢查。\n\n這樣即使某個 View 沒有再次被正常排進 Change Detection，也比較容易看到「畫面上次檢查到的值」和「目前實際值」已經不同，方便觀察 `ExpressionChangedAfterItHasBeenCheckedError`。\n\n這個設定比較適合拿來除錯或示範，不需要為了避免這類問題長期開啟高頻率檢查。\n\n另一種情況是直接操作 DOM：\n\n```ts\nngAfterViewInit() {\n  const element = this.chart.nativeElement;\n\n  element.style.width = '500px';\n\n  const width =\n    element.getBoundingClientRect().width;\n}\n```\n\n`element.style.width` 是修改 DOM，`getBoundingClientRect()` 則需要讀取 Element 實際排版後的尺寸。\n\n如果程式一直在修改 DOM 後立刻讀取 Layout，瀏覽器可能需要反覆重新計算版面。這種 DOM Write 和 Read 不斷交錯的情況，就是常說的 Layout Thrashing。\n\n如果需求本來就是等這一輪 Render 完成後再處理 DOM，就可以使用 `afterNextRender()`。\n\n## afterNextRender()：Render 完成後執行一次\n\n`afterNextRender()` 會在註冊後最近一次 Angular Render 完成時執行一次。\n\n例如：\n\n```ts\nimport {\n  afterNextRender,\n  Component,\n  ElementRef,\n  viewChild,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-chart',\n  template: `\n    \u003Cdiv #chart>\u003C\u002Fdiv>\n  `,\n})\nexport class ChartComponent {\n  chart =\n    viewChild.required\u003CElementRef\u003CHTMLDivElement>>(\n      'chart',\n    );\n\n  constructor() {\n    afterNextRender(() => {\n      const element =\n        this.chart().nativeElement;\n\n      new Chart(element);\n    });\n  }\n}\n```\n\n這類寫法適合初始化第三方 UI Library、Canvas，或第一次需要讀取實際 DOM 狀態的工作。\n\n兩者差異：\n\n- `ngAfterViewInit()` 關心的是 Component View 是否初始化完成。\n- `afterNextRender()` 關心的是這一輪 Angular Render 是否已經完成。\n\n## 拆分 DOM Read 與 Write\n\n`afterNextRender()` 除了直接傳入 Callback，也可以把 DOM 操作拆成不同 Phase。\n\n例如：\n\n```ts\nafterNextRender({\n  write: () => {\n    this.chart()\n      .nativeElement\n      .style.width = '500px';\n  },\n\n  read: () => {\n    const width =\n      this.chart()\n        .nativeElement\n        .getBoundingClientRect()\n        .width;\n\n    console.log(width);\n  },\n});\n```\n\n這裡會先處理 DOM 修改，再讀取尺寸或位置，避免修改與讀取一直交錯。\n\nRender Phase 一共有：\n\n```ts\nearlyRead \u002F\u002F 優先讀取 DOM 狀態，只有真的需要在 Write 前讀取時使用\nwrite \u002F\u002F 修改 DOM，例如 style、class 或其他會影響版面的操作\nmixedReadWrite \u002F\u002F 同時包含 DOM 讀取與修改，能避免就盡量避免\nread \u002F\u002F 在所有 Write 完成後，再讀取尺寸、位置等 Layout 資訊\n```\n\n一般情況下，如果可以明確區分修改和讀取，優先使用 `write` 與 `read` 就好。\n\n## ngAfterViewChecked() 的限制\n\n如果需求不是只執行一次，而是每次畫面更新後都要重新處理 DOM，過去可能會寫：\n\n```ts\nngAfterViewChecked() {\n  this.measureElement();\n}\n```\n\n但 `ngAfterViewChecked()` 代表的是 Angular 已經完成這個 Component View 的 Change Detection 檢查。\n\n如果需求本來就是每次 Render 完成後都要讀取最新 DOM 狀態，這個 Hook 的語意就沒有那麼直接。\n\n另外 `ngAfterViewChecked()` 本身執行頻率可能很高，如果裡面持續讀取 Layout 或做較重的 DOM 操作，也容易增加不必要的成本。\n\n## afterEveryRender()：每次 Render 完成後執行\n\n如果需要在每次 Application Render 完成後執行，可以使用：\n\n```ts\nafterEveryRender({\n  read: () => {\n    const height =\n      this.chart()\n        .nativeElement\n        .scrollHeight;\n\n    console.log(height);\n  },\n});\n```\n\n和 `afterNextRender()` 相比，兩者最大的差別在執行次數：\n\n- `afterNextRender()`：註冊後，在最近一次 Render 完成時執行一次，之後不會再觸發。\n- `afterEveryRender()`：註冊後，每次 Application Render 完成都會執行。\n\n因此 `afterEveryRender()` 裡的工作要盡量保持單純，否則和目前需求無關的 Render，也可能跟著執行同一段程式。\n\n## afterEveryRender() 不知道是哪個資料改變\n\n假設現在有一個 Signal：\n\n```ts\nchartData = signal([10, 20, 30]);\n```\n\n只有 `chartData` 改變時，才需要更新 Chart。\n\n如果直接寫：\n\n```ts\nafterEveryRender(() => {\n  this.updateChart();\n});\n```\n\n只要 Application 發生 Render，`updateChart()` 就會執行，即使這次變化和 Chart 沒有關係。\n\n這時可能會想到前面介紹過的 `effect()`：\n\n```ts\neffect(() => {\n  this.chart.updateData(\n    this.chartData(),\n  );\n});\n```\n\n`effect()` 可以追蹤 `chartData()`，所以只有相關 Signal 改變時才會重新執行。\n\n但如果後面的操作還會依賴最新 DOM 狀態，只知道 Signal 已經改變還不夠，因為 `effect()` 本身不代表這次變化造成的 Render 已經完成。\n\n例如 Template 也使用了 `chartData()`：\n\n```html\n\u003Cp>資料筆數：{{ chartData().length }}\u003C\u002Fp>\n\u003Cdiv #chart>\u003C\u002Fdiv>\n```\n\n當 `chartData` 改變時，Angular 還需要把這次變化反映到畫面上。\n\n如果第三方 Library 接下來要讀取最新的尺寸、位置或其他 DOM 狀態，就要等這次畫面更新完成後再處理。\n\n這時就可以使用 `afterRenderEffect()`。\n\n## afterRenderEffect()：Signal 改變後等 Render 完成\n\n`afterRenderEffect()` 和 `effect()` 一樣，會追蹤執行過程中讀取的 Signal。\n\n例如：\n\n```ts\nchartData = signal([10, 20, 30]);\n\nconstructor() {\n  afterRenderEffect(() => {\n    this.chart.updateData(\n      this.chartData(),\n    );\n  });\n}\n```\n\n這裡讀取了 `chartData()`，所以它會成為 Dependency。\n\n當 `chartData` 改變時，`afterRenderEffect()` 會等這次 Render 完成後再執行。\n\n兩者可以先這樣區分：\n\n- `effect()`：Signal 改變後執行 Side Effect。\n- `afterRenderEffect()`：Signal 改變後，等 Render 完成再處理 DOM 相關工作。\n\n如果只是寫 Log、同步 Storage，或呼叫不依賴 DOM 的程式，使用一般 `effect()` 就可以。\n\n如果接下來要更新第三方 Chart、量測 Layout，或操作最新的 DOM，再考慮 `afterRenderEffect()`。\n\n## afterNextRender() 與 afterRenderEffect() 搭配使用\n\n第三方 UI Library 很適合看出這兩個 API 的分工。\n\n第一次建立 Chart：\n\n```ts\nconstructor() {\n  afterNextRender(() => {\n    this.chart = new Chart(\n      this.canvas().nativeElement,\n      this.chartData(),\n    );\n  });\n}\n```\n\n後續資料改變：\n\n```ts\nafterRenderEffect(() => {\n  this.chart.updateData(\n    this.chartData(),\n  );\n});\n```\n\n第一次初始化只需要做一次，所以交給 `afterNextRender()`；後續則根據 `chartData` 的變化更新 Chart，交給 `afterRenderEffect()`。\n\n這樣不用每次 Render 都重新初始化 Chart，也不會因為其他和 Chart 無關的 Render 而執行更新。\n\n## afterRenderEffect() 的 Cleanup\n\n`afterRenderEffect()` 也可以註冊 Cleanup。\n\n例如：\n\n```ts\nafterRenderEffect((onCleanup) => {\n  const observer =\n    new ResizeObserver(() => {\n      console.log('resize');\n    });\n\n  observer.observe(\n    this.element().nativeElement,\n  );\n\n  onCleanup(() => {\n    observer.disconnect();\n  });\n});\n```\n\n當 Effect 下一次重新執行，或對應的 Injection Context 被銷毀時，上一輪建立的 Observer 就會先被清掉。\n\n如果需求本身就是監聽 DOM 狀態，瀏覽器其實已經提供對應的 API：\n\n- `ResizeObserver`：監聽 Element 尺寸變化。\n- `MutationObserver`：監聽 DOM 結構或 Attribute 變化。\n- `IntersectionObserver`：監聽 Element 是否進入可視範圍。\n\n所以不一定所有 DOM 監聽都需要交給 `afterRenderEffect()`。\n\n## Render API 與 SSR\n\n`afterNextRender()`、`afterEveryRender()` 和 `afterRenderEffect()` 都不會在 SSR 或 Prerender 階段執行。\n\n因此像這些只存在瀏覽器中的 API：\n\n```ts\nwindow\ndocument\ngetBoundingClientRect()\nCanvas\n```\n\n如果本來就只需要在 Render 後使用，可以直接放進 Render Callback，不需要另外把平台判斷和 Render 時機混在一起。\n\n## ngOnDestroy() 的限制\n\n除了 Render，Lifecycle 還有另一個常見問題，就是 Cleanup。\n\n例如：\n\n```ts\nexport class DemoComponent\n  implements OnDestroy {\n\n  private timer = window.setInterval(\n    () => {\n      console.log('tick');\n    },\n    1000,\n  );\n\n  ngOnDestroy() {\n    clearInterval(this.timer);\n  }\n}\n```\n\n這樣寫沒有問題。\n\n比較麻煩的是，建立資源和清理資源的位置可能離得很遠。\n\nComponent 中如果同時建立 Timer、Event Listener、Observer 或第三方 Instance，最後全部集中在 `ngOnDestroy()` 清理，程式越大，就越容易新增了 Setup，卻忘記補上對應的 Cleanup。\n\n## DestroyRef：讓 Setup 與 Cleanup 放在一起\n\n`DestroyRef` 可以直接註冊目前 Context 被銷毀時要執行的 Callback。\n\n例如：\n\n```ts\nimport {\n  Component,\n  DestroyRef,\n  inject,\n} from '@angular\u002Fcore';\n\n@Component({\n  selector: 'app-demo',\n  template: ``,\n})\nexport class DemoComponent {\n  private destroyRef =\n    inject(DestroyRef);\n\n  constructor() {\n    const timer =\n      window.setInterval(() => {\n        console.log('tick');\n      }, 1000);\n\n    this.destroyRef.onDestroy(() => {\n      clearInterval(timer);\n    });\n  }\n}\n```\n\nTimer 在哪裡建立，就可以在附近直接寫對應的 Cleanup。\n\n`DestroyRef` 不是用來取代 `ngOnDestroy()`，而是讓建立與清理的程式不用拆得那麼遠。\n\n## DestroyRef 也可以交給其他 Function\n\n這個特性在抽共用邏輯時會更明顯。\n\n例如：\n\n```ts\nfunction setupTimer(\n  destroyRef: DestroyRef,\n) {\n  const timer =\n    window.setInterval(() => {\n      console.log('tick');\n    }, 1000);\n\n  destroyRef.onDestroy(() => {\n    clearInterval(timer);\n  });\n}\n```\n\nComponent 只需要：\n\n```ts\nconstructor() {\n  setupTimer(\n    inject(DestroyRef),\n  );\n}\n```\n\n這段 Helper 自己就知道資源什麼時候建立，也知道什麼時候該清掉，不需要再回到 Component 的 `ngOnDestroy()` 裡補 Cleanup。\n\n## RxJS 可以直接使用 takeUntilDestroyed()\n\n如果 Cleanup 對象是 Observable Subscription，可以直接使用：\n\n```ts\ntakeUntilDestroyed()\n```\n\n例如：\n\n```ts\nthis.userService.users$\n  .pipe(\n    takeUntilDestroyed(),\n  )\n  .subscribe(users => {\n    console.log(users);\n  });\n```\n\n當目前的 Injection Context 被 Destroy 時，Subscription 就會自動結束。\n\n以前常見的寫法是自己準備：\n\n```ts\nprivate destroy$ =\n  new Subject\u003Cvoid>();\n```\n\n再在 `ngOnDestroy()` 裡：\n\n```ts\nthis.destroy$.next();\nthis.destroy$.complete();\n```\n\n如果目的只是跟著 Angular 的生命週期取消 Subscription，`takeUntilDestroyed()` 可以少掉這一層手動管理。\n\n## 本日小結\n\n![Angular Render Hooks 怎麼選？看執行次數、Signal 依賴與 Cleanup](https:\u002F\u002Fres.cloudinary.com\u002Fdseg0uwc9\u002Fimage\u002Fupload\u002Fv1791353425\u002F2026%20%E9%90%B5%E4%BA%BA%E8%B3%BD\u002FDay%2022%EF%BC%9AAngular%20Render%20Lifecycle%EF%BC%8C%E5%BE%9E%20afterNextRender%20%E5%88%B0%20DestroyRef\u002Fangular-render-hooks-comparison-v2.png)\n\n傳統 Lifecycle Hooks 處理的是 Component 本身的生命週期，但有些需求其實更在意 Render 完成後的 DOM 狀態。這時 `afterNextRender()` 和 `afterEveryRender()` 可以直接對應 Render 完成後的工作。\n\n如果還需要根據 Signal 的變化決定是否執行，`afterRenderEffect()` 就能補上一般 `effect()` 缺少的 Render 時機，避免資料已經改了，但 DOM 還沒更新完成就開始操作。\n\nCleanup 的部分則可以透過 `DestroyRef`，讓資源建立和清理放得更靠近；RxJS Subscription 也可以直接搭配 `takeUntilDestroyed()`。\n\n這些 API 沒有取代傳統 Lifecycle Hooks，而是把 Component Lifecycle、Render 時機和 Cleanup 分得更清楚，寫法也會更接近實際要處理的問題。\n\n### 資料來源\n\n- [Angular 官方文件：afterEveryRender and afterNextRender](https:\u002F\u002Fangular.dev\u002Fguide\u002Fcomponents\u002Flifecycle#aftereveryrender-and-afternextrender)：Render Callback 在所有元件 Render 完成後執行，且不在 SSR 與 Prerender 執行。\n- [Angular 官方文件：DestroyRef](https:\u002F\u002Fangular.dev\u002Fguide\u002Fcomponents\u002Flifecycle#destroyref)：用 `onDestroy()` 在建立資源的地方註冊 Cleanup。\n- [Angular Signals 官方文件：Side effects on DOM elements](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals\u002Feffect#side-effects-on-dom-elements)：`effect()` 與 Render 時機的差異，以及 `afterRenderEffect()` 的使用情境。\n- [Angular 官方文件：Unsubscribing with takeUntilDestroyed](https:\u002F\u002Fangular.dev\u002Fecosystem\u002Frxjs-interop\u002Ftake-until-destroyed)：Context 銷毀時自動結束 Subscription。\n",{"title":104,"description":105},{"name":20,"url":21,"day":113},{"loc":103,"images":3859,"lastmod":106},[3860],{"loc":3751},"ithome-2026-day-22","2026-10-06-ithome-2026-day-22",[11,12,13],"AYbYPQvysAnGcKsZ6gY73892Exhjhfm3ZKV2BoYLZSM",1792575173109]