[{"data":1,"prerenderedAt":5199},["ShallowReactive",2],{"home-notes":3,"home-essays":4736},[4,2065,4200],{"id":5,"title":6,"body":7,"date":2053,"description":2054,"draft":2055,"extension":2056,"meta":2057,"navigation":234,"path":2058,"seo":2059,"stem":2060,"tags":2061,"__hash__":2064},"notes\u002Fnotes\u002Fvue3-computed-watch.md","Vue 3 的 computed 和 watch：算出一個值，還是做一件事",{"type":8,"value":9,"toc":2025},"minimark",[10,14,42,46,53,135,139,144,151,304,331,350,353,356,360,371,561,568,667,671,682,751,754,758,765,800,807,834,838,841,864,867,872,888,893,901,906,909,919,923,929,931,994,1001,1100,1102,1152,1159,1317,1327,1330,1380,1384,1439,1442,1446,1449,1574,1600,1651,1675,1681,1685,1739,1743,1746,1769,1773,1927,1930,1934,1986,1991,2021],[11,12,13],"h2",{"id":13},"目錄",[15,16,17,21,24,27,30,33,36,39],"ol",{},[18,19,20],"li",{},"一句話",[18,22,23],{},"computed：算出一個值",[18,25,26],{},"watch：值變了之後去做一件事",[18,28,29],{},"怎麼判斷要用哪一個",[18,31,32],{},"寫法",[18,34,35],{},"例子：購物車的免運門檻",[18,37,38],{},"我原本的誤解",[18,40,41],{},"練習",[11,43,45],{"id":44},"_1-一句話","1. 一句話",[47,48,49],"p",{},[50,51,52],"strong",{},"要「得到一個值」用 computed，要「做一件事」用 watch。",[54,55,56,72],"table",{},[57,58,59],"thead",{},[60,61,62,66,69],"tr",{},[63,64,65],"th",{},"差異",[63,67,68],{},"computed",[63,70,71],{},"watch",[73,74,75,87,98,109,124],"tbody",{},[60,76,77,81,84],{},[78,79,80],"td",{},"目的",[78,82,83],{},"從現有資料算出一個值",[78,85,86],{},"值變了之後去做一件事（副作用）",[60,88,89,92,95],{},[78,90,91],{},"有無回傳值",[78,93,94],{},"有，拿來在 template 或其他地方用",[78,96,97],{},"沒有，執行 callback",[60,99,100,103,106],{},[78,101,102],{},"依賴",[78,104,105],{},"自動追蹤",[78,107,108],{},"明確指定要看哪一個",[60,110,111,114,117],{},[78,112,113],{},"舊值",[78,115,116],{},"通常不用（3.4 起 getter 可以拿到上一次的結果）",[78,118,119,123],{},[120,121,122],"code",{},"(newVal, oldVal)"," 都拿得到",[60,125,126,129,132],{},[78,127,128],{},"適合",[78,130,131],{},"篩選清單、組字串、加總、判斷布林",[78,133,134],{},"打 API、寫 localStorage、改網址、送分析事件",[11,136,138],{"id":137},"_2-computed算出一個值","2. computed：算出一個值",[140,141,143],"h3",{"id":142},"追蹤的是實際讀到的資料","追蹤的是「實際讀到的」資料",[47,145,146,147,150],{},"Vue 不是去看函式裡寫了哪些變數，而是在",[50,148,149],{},"執行時","記下這次讀到了哪些響應式資料。有條件分支時，沒走到的分支裡的資料不會被追蹤：",[152,153,158],"pre",{"className":154,"code":155,"language":156,"meta":157,"style":157},"language-js shiki shiki-themes github-dark","const showDetail = ref(false)\nconst name = ref('Jason')\nconst detail = ref('...')\n\nconst text = computed(() => {\n  if (!showDetail.value) return name.value\n  return name.value + detail.value   \u002F\u002F showDetail 是 false 時，detail 沒被讀到\n})\n","js","",[120,159,160,190,210,229,236,258,279,298],{"__ignoreMap":157},[161,162,165,169,173,176,180,184,187],"span",{"class":163,"line":164},"line",1,[161,166,168],{"class":167},"snl16","const",[161,170,172],{"class":171},"sDLfK"," showDetail",[161,174,175],{"class":167}," =",[161,177,179],{"class":178},"svObZ"," ref",[161,181,183],{"class":182},"s95oV","(",[161,185,186],{"class":171},"false",[161,188,189],{"class":182},")\n",[161,191,193,195,198,200,202,204,208],{"class":163,"line":192},2,[161,194,168],{"class":167},[161,196,197],{"class":171}," name",[161,199,175],{"class":167},[161,201,179],{"class":178},[161,203,183],{"class":182},[161,205,207],{"class":206},"sU2Wk","'Jason'",[161,209,189],{"class":182},[161,211,213,215,218,220,222,224,227],{"class":163,"line":212},3,[161,214,168],{"class":167},[161,216,217],{"class":171}," detail",[161,219,175],{"class":167},[161,221,179],{"class":178},[161,223,183],{"class":182},[161,225,226],{"class":206},"'...'",[161,228,189],{"class":182},[161,230,232],{"class":163,"line":231},4,[161,233,235],{"emptyLinePlaceholder":234},true,"\n",[161,237,239,241,244,246,249,252,255],{"class":163,"line":238},5,[161,240,168],{"class":167},[161,242,243],{"class":171}," text",[161,245,175],{"class":167},[161,247,248],{"class":178}," computed",[161,250,251],{"class":182},"(() ",[161,253,254],{"class":167},"=>",[161,256,257],{"class":182}," {\n",[161,259,261,264,267,270,273,276],{"class":163,"line":260},6,[161,262,263],{"class":167},"  if",[161,265,266],{"class":182}," (",[161,268,269],{"class":167},"!",[161,271,272],{"class":182},"showDetail.value) ",[161,274,275],{"class":167},"return",[161,277,278],{"class":182}," name.value\n",[161,280,282,285,288,291,294],{"class":163,"line":281},7,[161,283,284],{"class":167},"  return",[161,286,287],{"class":182}," name.value ",[161,289,290],{"class":167},"+",[161,292,293],{"class":182}," detail.value   ",[161,295,297],{"class":296},"sAwPA","\u002F\u002F showDetail 是 false 時，detail 沒被讀到\n",[161,299,301],{"class":163,"line":300},8,[161,302,303],{"class":182},"})\n",[47,305,306,309,310,312,313,316,317,320,321,323,324,327,328,330],{},[120,307,308],{},"showDetail"," 是 ",[120,311,186],{}," 時，改 ",[120,314,315],{},"detail"," 不會讓 ",[120,318,319],{},"text"," 重新計算。等 ",[120,322,308],{}," 變成 ",[120,325,326],{},"true","、重算一次之後，",[120,329,315],{}," 才會被加進追蹤。",[47,332,333,334,337,338,341,342,345,346,349],{},"資料一定要是",[50,335,336],{},"響應式的","（",[120,339,340],{},"ref","、",[120,343,344],{},"reactive","、props、其他 computed）。一般的 ",[120,347,348],{},"let x = 1"," 改了，Vue 不會知道。",[140,351,352],{"id":352},"懶計算和快取",[47,354,355],{},"依賴改了，computed 不會馬上重算，只會被標記成「過期」，等下次有人讀它才真的重算。依賴沒改，讀幾次都是拿快取。這是它和 method 的差別：method 每次呼叫都會重跑。",[140,357,359],{"id":358},"值跟型別無關","「值」跟型別無關",[47,361,362,363,366,367,370],{},"數字、布林、字串、陣列、物件都可以是 computed。重點在兩件事：",[50,364,365],{},"有沒有回傳結果","，以及",[50,368,369],{},"有沒有改到別的東西","。",[152,372,374],{"className":154,"code":373,"language":156,"meta":157,"style":157},"const total = computed(() => cart.value.reduce((s, i) => s + i.price * i.qty, 0))  \u002F\u002F 數字\nconst freeShipping = computed(() => total.value > 1000)                             \u002F\u002F 布林\nconst summary = computed(() => `共 ${cart.value.length} 件，NT$ ${total.value}`)    \u002F\u002F 字串\nconst inStock = computed(() => products.value.filter(p => p.stock > 0))             \u002F\u002F 陣列\n",[120,375,376,438,468,519],{"__ignoreMap":157},[161,377,378,380,383,385,387,389,391,394,397,400,404,407,410,413,415,418,420,423,426,429,432,435],{"class":163,"line":164},[161,379,168],{"class":167},[161,381,382],{"class":171}," total",[161,384,175],{"class":167},[161,386,248],{"class":178},[161,388,251],{"class":182},[161,390,254],{"class":167},[161,392,393],{"class":182}," cart.value.",[161,395,396],{"class":178},"reduce",[161,398,399],{"class":182},"((",[161,401,403],{"class":402},"s9osk","s",[161,405,406],{"class":182},", ",[161,408,409],{"class":402},"i",[161,411,412],{"class":182},") ",[161,414,254],{"class":167},[161,416,417],{"class":182}," s ",[161,419,290],{"class":167},[161,421,422],{"class":182}," i.price ",[161,424,425],{"class":167},"*",[161,427,428],{"class":182}," i.qty, ",[161,430,431],{"class":171},"0",[161,433,434],{"class":182},"))  ",[161,436,437],{"class":296},"\u002F\u002F 數字\n",[161,439,440,442,445,447,449,451,453,456,459,462,465],{"class":163,"line":192},[161,441,168],{"class":167},[161,443,444],{"class":171}," freeShipping",[161,446,175],{"class":167},[161,448,248],{"class":178},[161,450,251],{"class":182},[161,452,254],{"class":167},[161,454,455],{"class":182}," total.value ",[161,457,458],{"class":167},">",[161,460,461],{"class":171}," 1000",[161,463,464],{"class":182},")                             ",[161,466,467],{"class":296},"\u002F\u002F 布林\n",[161,469,470,472,475,477,479,481,483,486,489,492,495,497,500,503,506,508,510,513,516],{"class":163,"line":212},[161,471,168],{"class":167},[161,473,474],{"class":171}," summary",[161,476,175],{"class":167},[161,478,248],{"class":178},[161,480,251],{"class":182},[161,482,254],{"class":167},[161,484,485],{"class":206}," `共 ${",[161,487,488],{"class":182},"cart",[161,490,491],{"class":206},".",[161,493,494],{"class":182},"value",[161,496,491],{"class":206},[161,498,499],{"class":171},"length",[161,501,502],{"class":206},"} 件，NT$ ${",[161,504,505],{"class":182},"total",[161,507,491],{"class":206},[161,509,494],{"class":182},[161,511,512],{"class":206},"}`",[161,514,515],{"class":182},")    ",[161,517,518],{"class":296},"\u002F\u002F 字串\n",[161,520,521,523,526,528,530,532,534,537,540,542,544,547,550,552,555,558],{"class":163,"line":231},[161,522,168],{"class":167},[161,524,525],{"class":171}," inStock",[161,527,175],{"class":167},[161,529,248],{"class":178},[161,531,251],{"class":182},[161,533,254],{"class":167},[161,535,536],{"class":182}," products.value.",[161,538,539],{"class":178},"filter",[161,541,183],{"class":182},[161,543,47],{"class":402},[161,545,546],{"class":167}," =>",[161,548,549],{"class":182}," p.stock ",[161,551,458],{"class":167},[161,553,554],{"class":171}," 0",[161,556,557],{"class":182},"))             ",[161,559,560],{"class":296},"\u002F\u002F 陣列\n",[47,562,563,564,567],{},"常見的陷阱是 ",[120,565,566],{},"sort()","，它會直接改原本的陣列，回傳的型別看起來沒問題，但已經是在 computed 裡「做事」了：",[152,569,571],{"className":154,"code":570,"language":156,"meta":157,"style":157},"\u002F\u002F ❌ 改到了 products\nconst sorted = computed(() => products.value.sort((a, b) => a.price - b.price))\n\n\u002F\u002F ✅ 回傳新陣列\nconst sorted = computed(() => products.value.toSorted((a, b) => a.price - b.price))\n",[120,572,573,578,621,625,630],{"__ignoreMap":157},[161,574,575],{"class":163,"line":164},[161,576,577],{"class":296},"\u002F\u002F ❌ 改到了 products\n",[161,579,580,582,585,587,589,591,593,595,598,600,603,605,608,610,612,615,618],{"class":163,"line":192},[161,581,168],{"class":167},[161,583,584],{"class":171}," sorted",[161,586,175],{"class":167},[161,588,248],{"class":178},[161,590,251],{"class":182},[161,592,254],{"class":167},[161,594,536],{"class":182},[161,596,597],{"class":178},"sort",[161,599,399],{"class":182},[161,601,602],{"class":402},"a",[161,604,406],{"class":182},[161,606,607],{"class":402},"b",[161,609,412],{"class":182},[161,611,254],{"class":167},[161,613,614],{"class":182}," a.price ",[161,616,617],{"class":167},"-",[161,619,620],{"class":182}," b.price))\n",[161,622,623],{"class":163,"line":212},[161,624,235],{"emptyLinePlaceholder":234},[161,626,627],{"class":163,"line":231},[161,628,629],{"class":296},"\u002F\u002F ✅ 回傳新陣列\n",[161,631,632,634,636,638,640,642,644,646,649,651,653,655,657,659,661,663,665],{"class":163,"line":238},[161,633,168],{"class":167},[161,635,584],{"class":171},[161,637,175],{"class":167},[161,639,248],{"class":178},[161,641,251],{"class":182},[161,643,254],{"class":167},[161,645,536],{"class":182},[161,647,648],{"class":178},"toSorted",[161,650,399],{"class":182},[161,652,602],{"class":402},[161,654,406],{"class":182},[161,656,607],{"class":402},[161,658,412],{"class":182},[161,660,254],{"class":167},[161,662,614],{"class":182},[161,664,617],{"class":167},[161,666,620],{"class":182},[11,668,670],{"id":669},"_3-watch值變了之後去做一件事","3. watch：值變了之後去做一件事",[47,672,673,674,677,678,681],{},"watch 處理的通常是 ",[50,675,676],{},"Vue 管不到的地方","：API、localStorage、網址、計時器、分析事件、",[120,679,680],{},"focus()"," 和捲動、第三方套件（地圖、圖表）。",[152,683,685],{"className":154,"code":684,"language":156,"meta":157,"style":157},"const keyword = ref('')\n\nwatch(keyword, async (val) => {\n  result.value = await fetchSearch(val)\n})\n",[120,686,687,705,709,730,747],{"__ignoreMap":157},[161,688,689,691,694,696,698,700,703],{"class":163,"line":164},[161,690,168],{"class":167},[161,692,693],{"class":171}," keyword",[161,695,175],{"class":167},[161,697,179],{"class":178},[161,699,183],{"class":182},[161,701,702],{"class":206},"''",[161,704,189],{"class":182},[161,706,707],{"class":163,"line":192},[161,708,235],{"emptyLinePlaceholder":234},[161,710,711,713,716,719,721,724,726,728],{"class":163,"line":212},[161,712,71],{"class":178},[161,714,715],{"class":182},"(keyword, ",[161,717,718],{"class":167},"async",[161,720,266],{"class":182},[161,722,723],{"class":402},"val",[161,725,412],{"class":182},[161,727,254],{"class":167},[161,729,257],{"class":182},[161,731,732,735,738,741,744],{"class":163,"line":231},[161,733,734],{"class":182},"  result.value ",[161,736,737],{"class":167},"=",[161,739,740],{"class":167}," await",[161,742,743],{"class":178}," fetchSearch",[161,745,746],{"class":182},"(val)\n",[161,748,749],{"class":163,"line":238},[161,750,303],{"class":182},[47,752,753],{},"這裡沒有比較前後值，還是要用 watch，因為打 API 是副作用。",[140,755,757],{"id":756},"很多時候連-watch-都不用","很多時候連 watch 都不用",[47,759,760,761,764],{},"如果值是",[50,762,763],{},"使用者操作改的","，在那個事件裡直接呼叫方法就好：",[152,766,770],{"className":767,"code":768,"language":769,"meta":157,"style":157},"language-html shiki shiki-themes github-dark","\u003Cselect v-model=\"productId\" @change=\"loadProduct\">\n","html",[120,771,772],{"__ignoreMap":157},[161,773,774,777,781,784,786,789,792,794,797],{"class":163,"line":164},[161,775,776],{"class":182},"\u003C",[161,778,780],{"class":779},"s4JwU","select",[161,782,783],{"class":178}," v-model",[161,785,737],{"class":182},[161,787,788],{"class":206},"\"productId\"",[161,790,791],{"class":178}," @change",[161,793,737],{"class":182},[161,795,796],{"class":206},"\"loadProduct\"",[161,798,799],{"class":182},">\n",[47,801,802,803,806],{},"watch 適合用在",[50,804,805],{},"不知道是誰改的、或有很多地方會改","的情況：",[808,809,810,825,828,831],"ul",{},[18,811,812,813,816,817,820,821,824],{},"路由參數變了（",[120,814,815],{},"\u002Fproduct\u002F1"," 連到 ",[120,818,819],{},"\u002Fproduct\u002F2","，Vue Router 會重用同一個元件，",[120,822,823],{},"onMounted"," 不會再跑）",[18,826,827],{},"props 從父元件傳進來",[18,829,830],{},"Pinia store 的狀態被其他元件改",[18,832,833],{},"同一個值有好幾個地方會改，不想每個地方都記得呼叫",[11,835,837],{"id":836},"_4-怎麼判斷要用哪一個","4. 怎麼判斷要用哪一個",[140,839,840],{"id":840},"判斷順序",[15,842,843,848,854,859],{},[18,844,845,846],{},"是算出一個值嗎？→ ",[50,847,68],{},[18,849,850,851],{},"是做一件事，而且觸發點很清楚（點擊、輸入、送出）？→ ",[50,852,853],{},"寫在事件處理函式",[18,855,856,857],{},"是做一件事，但值可能從各種地方被改？→ ",[50,858,71],{},[18,860,861,862],{},"要做的事需要知道「從什麼變成什麼」？→ 用 watch 的 ",[120,863,122],{},[140,865,866],{"id":866},"三個幫助判斷的問法",[47,868,869],{},[50,870,871],{},"換成「是」或「去」的句子",[808,873,874,881],{},[18,875,876,877,880],{},"「免運提示",[50,878,879],{},"是","：總金額大於 1000 時顯示」→ 描述狀態，computed",[18,882,883,884,887],{},"「總金額變了，",[50,885,886],{},"去","送分析事件」→ 動作，watch",[47,889,890],{},[50,891,892],{},"拿掉畫面，這件事還要發生嗎？",[808,894,895,898],{},[18,896,897],{},"免運提示：沒有畫面就沒意義，computed",[18,899,900],{},"分析事件：沒有畫面，伺服器還是要收到，watch",[47,902,903],{},[50,904,905],{},"這件事碰到外部世界了嗎？",[47,907,908],{},"和畫面有關的幾乎都是 computed，碰到外部世界的才是 watch。",[47,910,911,912,915,916,370],{},"在 jQuery 或原生 JS 裡，「顯示」真的是一件事，要自己找元素、改 ",[120,913,914],{},"display","。Vue 把這件事接手了，我們只描述狀態，畫面由 Vue 更新。所以在 Vue 裡，",[50,917,918],{},"畫面上的東西幾乎都不算「做事」",[11,920,922],{"id":921},"_5-寫法","5. 寫法",[47,924,925,926,370],{},"以下是 Vue 3 的 ",[120,927,928],{},"\u003Cscript setup>",[140,930,68],{"id":68},[152,932,934],{"className":154,"code":933,"language":156,"meta":157,"style":157},"import { computed } from 'vue'\n\nconst total = computed(() => price.value * qty.value)\n\nconsole.log(total.value)  \u002F\u002F script 裡要加 .value，template 裡不用\n",[120,935,936,950,954,976,980],{"__ignoreMap":157},[161,937,938,941,944,947],{"class":163,"line":164},[161,939,940],{"class":167},"import",[161,942,943],{"class":182}," { computed } ",[161,945,946],{"class":167},"from",[161,948,949],{"class":206}," 'vue'\n",[161,951,952],{"class":163,"line":192},[161,953,235],{"emptyLinePlaceholder":234},[161,955,956,958,960,962,964,966,968,971,973],{"class":163,"line":212},[161,957,168],{"class":167},[161,959,382],{"class":171},[161,961,175],{"class":167},[161,963,248],{"class":178},[161,965,251],{"class":182},[161,967,254],{"class":167},[161,969,970],{"class":182}," price.value ",[161,972,425],{"class":167},[161,974,975],{"class":182}," qty.value)\n",[161,977,978],{"class":163,"line":231},[161,979,235],{"emptyLinePlaceholder":234},[161,981,982,985,988,991],{"class":163,"line":238},[161,983,984],{"class":182},"console.",[161,986,987],{"class":178},"log",[161,989,990],{"class":182},"(total.value)  ",[161,992,993],{"class":296},"\u002F\u002F script 裡要加 .value，template 裡不用\n",[47,995,996,997,1000],{},"可寫的 computed（常用在 ",[120,998,999],{},"v-model"," 綁定 props）：",[152,1002,1004],{"className":154,"code":1003,"language":156,"meta":157,"style":157},"const fullName = computed({\n  get: () => `${firstName.value} ${lastName.value}`,\n  set: (val) => {\n    [firstName.value, lastName.value] = val.split(' ')\n  },\n})\n",[120,1005,1006,1020,1055,1071,1091,1096],{"__ignoreMap":157},[161,1007,1008,1010,1013,1015,1017],{"class":163,"line":164},[161,1009,168],{"class":167},[161,1011,1012],{"class":171}," fullName",[161,1014,175],{"class":167},[161,1016,248],{"class":178},[161,1018,1019],{"class":182},"({\n",[161,1021,1022,1025,1028,1030,1033,1036,1038,1040,1043,1046,1048,1050,1052],{"class":163,"line":192},[161,1023,1024],{"class":178},"  get",[161,1026,1027],{"class":182},": () ",[161,1029,254],{"class":167},[161,1031,1032],{"class":206}," `${",[161,1034,1035],{"class":182},"firstName",[161,1037,491],{"class":206},[161,1039,494],{"class":182},[161,1041,1042],{"class":206},"} ${",[161,1044,1045],{"class":182},"lastName",[161,1047,491],{"class":206},[161,1049,494],{"class":182},[161,1051,512],{"class":206},[161,1053,1054],{"class":182},",\n",[161,1056,1057,1060,1063,1065,1067,1069],{"class":163,"line":212},[161,1058,1059],{"class":178},"  set",[161,1061,1062],{"class":182},": (",[161,1064,723],{"class":402},[161,1066,412],{"class":182},[161,1068,254],{"class":167},[161,1070,257],{"class":182},[161,1072,1073,1076,1078,1081,1084,1086,1089],{"class":163,"line":231},[161,1074,1075],{"class":182},"    [firstName.value, lastName.value] ",[161,1077,737],{"class":167},[161,1079,1080],{"class":182}," val.",[161,1082,1083],{"class":178},"split",[161,1085,183],{"class":182},[161,1087,1088],{"class":206},"' '",[161,1090,189],{"class":182},[161,1092,1093],{"class":163,"line":238},[161,1094,1095],{"class":182},"  },\n",[161,1097,1098],{"class":163,"line":260},[161,1099,303],{"class":182},[140,1101,71],{"id":71},[152,1103,1105],{"className":154,"code":1104,"language":156,"meta":157,"style":157},"import { watch } from 'vue'\n\nwatch(要看的東西, (新值, 舊值) => {\n  要做的事\n}, { 選項 })\n",[120,1106,1107,1118,1122,1142,1147],{"__ignoreMap":157},[161,1108,1109,1111,1114,1116],{"class":163,"line":164},[161,1110,940],{"class":167},[161,1112,1113],{"class":182}," { watch } ",[161,1115,946],{"class":167},[161,1117,949],{"class":206},[161,1119,1120],{"class":163,"line":192},[161,1121,235],{"emptyLinePlaceholder":234},[161,1123,1124,1126,1129,1132,1134,1136,1138,1140],{"class":163,"line":212},[161,1125,71],{"class":178},[161,1127,1128],{"class":182},"(要看的東西, (",[161,1130,1131],{"class":402},"新值",[161,1133,406],{"class":182},[161,1135,113],{"class":402},[161,1137,412],{"class":182},[161,1139,254],{"class":167},[161,1141,257],{"class":182},[161,1143,1144],{"class":163,"line":231},[161,1145,1146],{"class":182},"  要做的事\n",[161,1148,1149],{"class":163,"line":238},[161,1150,1151],{"class":182},"}, { 選項 })\n",[47,1153,1154,1155,1158],{},"第一個參數最容易寫錯。",[50,1156,1157],{},"傳「會變的那個東西」，不要傳「它現在的值」","：",[152,1160,1162],{"className":154,"code":1161,"language":156,"meta":157,"style":157},"const count = ref(0)\nconst user = reactive({ name: 'Jason', age: 30 })\n\nwatch(count, ...)                    \u002F\u002F ✅ ref 直接傳，不加 .value\nwatch(user, ...)                     \u002F\u002F ✅ reactive 物件直接傳，自動 deep\nwatch(() => user.age, ...)           \u002F\u002F ✅ 物件裡的屬性要包成函式\nwatch([count, () => user.age], ...)  \u002F\u002F ✅ 看好幾個用陣列\n\nwatch(count.value, ...)              \u002F\u002F ❌ 傳進去的是數字 0\nwatch(user.age, ...)                 \u002F\u002F ❌ 傳進去的是數字 30\n",[120,1163,1164,1181,1207,1211,1227,1242,1261,1281,1285,1301],{"__ignoreMap":157},[161,1165,1166,1168,1171,1173,1175,1177,1179],{"class":163,"line":164},[161,1167,168],{"class":167},[161,1169,1170],{"class":171}," count",[161,1172,175],{"class":167},[161,1174,179],{"class":178},[161,1176,183],{"class":182},[161,1178,431],{"class":171},[161,1180,189],{"class":182},[161,1182,1183,1185,1188,1190,1193,1196,1198,1201,1204],{"class":163,"line":192},[161,1184,168],{"class":167},[161,1186,1187],{"class":171}," user",[161,1189,175],{"class":167},[161,1191,1192],{"class":178}," reactive",[161,1194,1195],{"class":182},"({ name: ",[161,1197,207],{"class":206},[161,1199,1200],{"class":182},", age: ",[161,1202,1203],{"class":171},"30",[161,1205,1206],{"class":182}," })\n",[161,1208,1209],{"class":163,"line":212},[161,1210,235],{"emptyLinePlaceholder":234},[161,1212,1213,1215,1218,1221,1224],{"class":163,"line":231},[161,1214,71],{"class":178},[161,1216,1217],{"class":182},"(count, ",[161,1219,1220],{"class":167},"...",[161,1222,1223],{"class":182},")                    ",[161,1225,1226],{"class":296},"\u002F\u002F ✅ ref 直接傳，不加 .value\n",[161,1228,1229,1231,1234,1236,1239],{"class":163,"line":238},[161,1230,71],{"class":178},[161,1232,1233],{"class":182},"(user, ",[161,1235,1220],{"class":167},[161,1237,1238],{"class":182},")                     ",[161,1240,1241],{"class":296},"\u002F\u002F ✅ reactive 物件直接傳，自動 deep\n",[161,1243,1244,1246,1248,1250,1253,1255,1258],{"class":163,"line":260},[161,1245,71],{"class":178},[161,1247,251],{"class":182},[161,1249,254],{"class":167},[161,1251,1252],{"class":182}," user.age, ",[161,1254,1220],{"class":167},[161,1256,1257],{"class":182},")           ",[161,1259,1260],{"class":296},"\u002F\u002F ✅ 物件裡的屬性要包成函式\n",[161,1262,1263,1265,1268,1270,1273,1275,1278],{"class":163,"line":281},[161,1264,71],{"class":178},[161,1266,1267],{"class":182},"([count, () ",[161,1269,254],{"class":167},[161,1271,1272],{"class":182}," user.age], ",[161,1274,1220],{"class":167},[161,1276,1277],{"class":182},")  ",[161,1279,1280],{"class":296},"\u002F\u002F ✅ 看好幾個用陣列\n",[161,1282,1283],{"class":163,"line":300},[161,1284,235],{"emptyLinePlaceholder":234},[161,1286,1288,1290,1293,1295,1298],{"class":163,"line":1287},9,[161,1289,71],{"class":178},[161,1291,1292],{"class":182},"(count.value, ",[161,1294,1220],{"class":167},[161,1296,1297],{"class":182},")              ",[161,1299,1300],{"class":296},"\u002F\u002F ❌ 傳進去的是數字 0\n",[161,1302,1304,1306,1309,1311,1314],{"class":163,"line":1303},10,[161,1305,71],{"class":178},[161,1307,1308],{"class":182},"(user.age, ",[161,1310,1220],{"class":167},[161,1312,1313],{"class":182},")                 ",[161,1315,1316],{"class":296},"\u002F\u002F ❌ 傳進去的是數字 30\n",[47,1318,1319,1322,1323,1326],{},[120,1320,1321],{},"user.age"," 一讀出來就是固定的數字，包成 ",[120,1324,1325],{},"() => user.age"," 才能讓 Vue 每次重讀。",[47,1328,1329],{},"選項：",[54,1331,1332,1342],{},[57,1333,1334],{},[60,1335,1336,1339],{},[63,1337,1338],{},"選項",[63,1340,1341],{},"作用",[73,1343,1344,1357,1370],{},[60,1345,1346,1351],{},[78,1347,1348],{},[120,1349,1350],{},"immediate: true",[78,1352,1353,1354],{},"建立時先跑一次，這時舊值是 ",[120,1355,1356],{},"undefined",[60,1358,1359,1364],{},[78,1360,1361],{},[120,1362,1363],{},"deep: true",[78,1365,1366,1367,1369],{},"看物件內部的變化。直接 watch ",[120,1368,344],{}," 物件預設就是 deep，watch 一個回傳物件的 getter，只有整個物件被換掉才會觸發",[60,1371,1372,1377],{},[78,1373,1374],{},[120,1375,1376],{},"once: true",[78,1378,1379],{},"只觸發一次（Vue 3.4 起）",[140,1381,1383],{"id":1382},"watcheffect","watchEffect",[152,1385,1387],{"className":154,"code":1386,"language":156,"meta":157,"style":157},"import { watchEffect } from 'vue'\n\nwatchEffect(() => {\n  localStorage.setItem('dark', String(dark.value))\n})\n",[120,1388,1389,1400,1404,1414,1435],{"__ignoreMap":157},[161,1390,1391,1393,1396,1398],{"class":163,"line":164},[161,1392,940],{"class":167},[161,1394,1395],{"class":182}," { watchEffect } ",[161,1397,946],{"class":167},[161,1399,949],{"class":206},[161,1401,1402],{"class":163,"line":192},[161,1403,235],{"emptyLinePlaceholder":234},[161,1405,1406,1408,1410,1412],{"class":163,"line":212},[161,1407,1383],{"class":178},[161,1409,251],{"class":182},[161,1411,254],{"class":167},[161,1413,257],{"class":182},[161,1415,1416,1419,1422,1424,1427,1429,1432],{"class":163,"line":231},[161,1417,1418],{"class":182},"  localStorage.",[161,1420,1421],{"class":178},"setItem",[161,1423,183],{"class":182},[161,1425,1426],{"class":206},"'dark'",[161,1428,406],{"class":182},[161,1430,1431],{"class":178},"String",[161,1433,1434],{"class":182},"(dark.value))\n",[161,1436,1437],{"class":163,"line":238},[161,1438,303],{"class":182},[47,1440,1441],{},"像 computed 一樣自動追蹤讀到的資料，但用途像 watch，是做副作用的。建立時會先跑一次，拿不到舊值。",[11,1443,1445],{"id":1444},"_6-例子購物車的免運門檻","6. 例子：購物車的免運門檻",[47,1447,1448],{},"需求：顯示總金額，超過 1000 時顯示「已達免運門檻」，並送一次分析事件。",[152,1450,1452],{"className":154,"code":1451,"language":156,"meta":157,"style":157},"const total = computed(() =>\n  cart.value.reduce((sum, item) => sum + item.price * item.qty, 0)\n)\nconst freeShipping = computed(() => total.value > 1000)\n\nwatch(freeShipping, (reached) => {\n  if (reached) sendEvent('free_shipping_reached')\n})\n",[120,1453,1454,1469,1507,1511,1533,1537,1553,1570],{"__ignoreMap":157},[161,1455,1456,1458,1460,1462,1464,1466],{"class":163,"line":164},[161,1457,168],{"class":167},[161,1459,382],{"class":171},[161,1461,175],{"class":167},[161,1463,248],{"class":178},[161,1465,251],{"class":182},[161,1467,1468],{"class":167},"=>\n",[161,1470,1471,1474,1476,1478,1481,1483,1486,1488,1490,1493,1495,1498,1500,1503,1505],{"class":163,"line":192},[161,1472,1473],{"class":182},"  cart.value.",[161,1475,396],{"class":178},[161,1477,399],{"class":182},[161,1479,1480],{"class":402},"sum",[161,1482,406],{"class":182},[161,1484,1485],{"class":402},"item",[161,1487,412],{"class":182},[161,1489,254],{"class":167},[161,1491,1492],{"class":182}," sum ",[161,1494,290],{"class":167},[161,1496,1497],{"class":182}," item.price ",[161,1499,425],{"class":167},[161,1501,1502],{"class":182}," item.qty, ",[161,1504,431],{"class":171},[161,1506,189],{"class":182},[161,1508,1509],{"class":163,"line":212},[161,1510,189],{"class":182},[161,1512,1513,1515,1517,1519,1521,1523,1525,1527,1529,1531],{"class":163,"line":231},[161,1514,168],{"class":167},[161,1516,444],{"class":171},[161,1518,175],{"class":167},[161,1520,248],{"class":178},[161,1522,251],{"class":182},[161,1524,254],{"class":167},[161,1526,455],{"class":182},[161,1528,458],{"class":167},[161,1530,461],{"class":171},[161,1532,189],{"class":182},[161,1534,1535],{"class":163,"line":238},[161,1536,235],{"emptyLinePlaceholder":234},[161,1538,1539,1541,1544,1547,1549,1551],{"class":163,"line":260},[161,1540,71],{"class":178},[161,1542,1543],{"class":182},"(freeShipping, (",[161,1545,1546],{"class":402},"reached",[161,1548,412],{"class":182},[161,1550,254],{"class":167},[161,1552,257],{"class":182},[161,1554,1555,1557,1560,1563,1565,1568],{"class":163,"line":281},[161,1556,263],{"class":167},[161,1558,1559],{"class":182}," (reached) ",[161,1561,1562],{"class":178},"sendEvent",[161,1564,183],{"class":182},[161,1566,1567],{"class":206},"'free_shipping_reached'",[161,1569,189],{"class":182},[161,1571,1572],{"class":163,"line":300},[161,1573,303],{"class":182},[152,1575,1577],{"className":767,"code":1576,"language":769,"meta":157,"style":157},"\u003Cp v-if=\"freeShipping\">已達免運門檻\u003C\u002Fp>\n",[120,1578,1579],{"__ignoreMap":157},[161,1580,1581,1583,1585,1588,1590,1593,1596,1598],{"class":163,"line":164},[161,1582,776],{"class":182},[161,1584,47],{"class":779},[161,1586,1587],{"class":178}," v-if",[161,1589,737],{"class":182},[161,1591,1592],{"class":206},"\"freeShipping\"",[161,1594,1595],{"class":182},">已達免運門檻\u003C\u002F",[161,1597,47],{"class":779},[161,1599,799],{"class":182},[54,1601,1602,1615],{},[57,1603,1604],{},[60,1605,1606,1609,1612],{},[63,1607,1608],{},"需求",[63,1610,1611],{},"用什麼",[63,1613,1614],{},"原因",[73,1616,1617,1627,1641],{},[60,1618,1619,1622,1624],{},[78,1620,1621],{},"總金額",[78,1623,68],{},[78,1625,1626],{},"從購物車算出的值",[60,1628,1629,1632,1638],{},[78,1630,1631],{},"顯示免運提示",[78,1633,1634,1635],{},"computed 加 ",[120,1636,1637],{},"v-if",[78,1639,1640],{},"只是一個布林值，金額降回 1000 以下時會自動消失",[60,1642,1643,1646,1648],{},[78,1644,1645],{},"送分析事件",[78,1647,71],{},[78,1649,1650],{},"副作用",[47,1652,1653,1654,1657,1658,1660,1661,1664,1665,1668,1669,1671,1672,1674],{},"送事件要看的是",[50,1655,1656],{},"跨過門檻的那一刻","。如果直接 watch ",[120,1659,505],{}," 再判斷 ",[120,1662,1663],{},"val > 1000","，超過之後每改一次數量就會再送一次。watch ",[120,1666,1667],{},"freeShipping"," 只有在 ",[120,1670,326],{}," 和 ",[120,1673,186],{}," 之間切換時才觸發，就沒有這個問題。",[47,1676,1677,1678,370],{},"這也是 computed 和 watch 搭配的方式：",[50,1679,1680],{},"computed 把狀態整理好，watch 只負責在狀態改變時做事",[11,1682,1684],{"id":1683},"_7-我原本的誤解","7. 我原本的誤解",[54,1686,1687,1697],{},[57,1688,1689],{},[60,1690,1691,1694],{},[63,1692,1693],{},"我原本以為",[63,1695,1696],{},"實際上",[73,1698,1699,1707,1715,1723,1731],{},[60,1700,1701,1704],{},[78,1702,1703],{},"computed 會在「列在函數裡的變數」改了之後重算",[78,1705,1706],{},"追蹤的是執行時實際讀到的響應式資料，沒走到的分支不算",[60,1708,1709,1712],{},[78,1710,1711],{},"顯示「已達免運門檻」是一件事，要用 watch",[78,1713,1714],{},"它只是根據總金額算出的布林值，用 computed",[60,1716,1717,1720],{},[78,1718,1719],{},"有沒有要比較前後值，是選 watch 的依據",[78,1721,1722],{},"依據是有沒有副作用。不比較前後值也可能要用 watch（打 API），3.4 起 computed 也拿得到上一次的值",[60,1724,1725,1728],{},[78,1726,1727],{},"變數改了要呼叫方法，就只能用 watch",[78,1729,1730],{},"方法只是在算東西，放在 computed 裡沒問題。觸發點清楚的話，直接寫在事件裡",[60,1732,1733,1736],{},[78,1734,1735],{},"「算出一個值」是指數字、布林這類基本型別",[78,1737,1738],{},"跟型別無關，陣列、物件都可以。重點是有沒有改到別的東西",[11,1740,1742],{"id":1741},"_8-練習","8. 練習",[140,1744,1745],{"id":1745},"分類",[15,1747,1748,1751,1754,1757,1760,1763],{},[18,1749,1750],{},"輸入框為空時，送出按鈕變灰",[18,1752,1753],{},"使用者切換深色模式後，記住設定，下次打開還是深色",[18,1755,1756],{},"顯示「共 N 筆，已選 M 筆」",[18,1758,1759],{},"切換到另一個商品 ID 時，載入那個商品的資料",[18,1761,1762],{},"密碼長度不到 8 碼時，顯示紅色的提示",[18,1764,1765,1766],{},"搜尋關鍵字改變時，網址跟著變成 ",[120,1767,1768],{},"?q=關鍵字",[140,1770,1772],{"id":1771},"找錯下面有-3-個錯誤","找錯：下面有 3 個錯誤",[152,1774,1776],{"className":154,"code":1775,"language":156,"meta":157,"style":157},"const form = reactive({ email: '', password: '' })\nconst keyword = ref('')\n\nconst isValid = computed(() => {\n  form.password.length >= 8\n})\n\nwatch(form.email, (val) => {\n  console.log('email 改了', val)\n})\n\nwatch(keyword.value, (val) => {\n  fetchSearch(val)\n})\n",[120,1777,1778,1801,1817,1821,1838,1851,1855,1859,1874,1889,1893,1898,1914,1922],{"__ignoreMap":157},[161,1779,1780,1782,1785,1787,1789,1792,1794,1797,1799],{"class":163,"line":164},[161,1781,168],{"class":167},[161,1783,1784],{"class":171}," form",[161,1786,175],{"class":167},[161,1788,1192],{"class":178},[161,1790,1791],{"class":182},"({ email: ",[161,1793,702],{"class":206},[161,1795,1796],{"class":182},", password: ",[161,1798,702],{"class":206},[161,1800,1206],{"class":182},[161,1802,1803,1805,1807,1809,1811,1813,1815],{"class":163,"line":192},[161,1804,168],{"class":167},[161,1806,693],{"class":171},[161,1808,175],{"class":167},[161,1810,179],{"class":178},[161,1812,183],{"class":182},[161,1814,702],{"class":206},[161,1816,189],{"class":182},[161,1818,1819],{"class":163,"line":212},[161,1820,235],{"emptyLinePlaceholder":234},[161,1822,1823,1825,1828,1830,1832,1834,1836],{"class":163,"line":231},[161,1824,168],{"class":167},[161,1826,1827],{"class":171}," isValid",[161,1829,175],{"class":167},[161,1831,248],{"class":178},[161,1833,251],{"class":182},[161,1835,254],{"class":167},[161,1837,257],{"class":182},[161,1839,1840,1843,1845,1848],{"class":163,"line":238},[161,1841,1842],{"class":182},"  form.password.",[161,1844,499],{"class":171},[161,1846,1847],{"class":167}," >=",[161,1849,1850],{"class":171}," 8\n",[161,1852,1853],{"class":163,"line":260},[161,1854,303],{"class":182},[161,1856,1857],{"class":163,"line":281},[161,1858,235],{"emptyLinePlaceholder":234},[161,1860,1861,1863,1866,1868,1870,1872],{"class":163,"line":300},[161,1862,71],{"class":178},[161,1864,1865],{"class":182},"(form.email, (",[161,1867,723],{"class":402},[161,1869,412],{"class":182},[161,1871,254],{"class":167},[161,1873,257],{"class":182},[161,1875,1876,1879,1881,1883,1886],{"class":163,"line":1287},[161,1877,1878],{"class":182},"  console.",[161,1880,987],{"class":178},[161,1882,183],{"class":182},[161,1884,1885],{"class":206},"'email 改了'",[161,1887,1888],{"class":182},", val)\n",[161,1890,1891],{"class":163,"line":1303},[161,1892,303],{"class":182},[161,1894,1896],{"class":163,"line":1895},11,[161,1897,235],{"emptyLinePlaceholder":234},[161,1899,1901,1903,1906,1908,1910,1912],{"class":163,"line":1900},12,[161,1902,71],{"class":178},[161,1904,1905],{"class":182},"(keyword.value, (",[161,1907,723],{"class":402},[161,1909,412],{"class":182},[161,1911,254],{"class":167},[161,1913,257],{"class":182},[161,1915,1917,1920],{"class":163,"line":1916},13,[161,1918,1919],{"class":178},"  fetchSearch",[161,1921,746],{"class":182},[161,1923,1925],{"class":163,"line":1924},14,[161,1926,303],{"class":182},[140,1928,1929],{"id":1929},"答案",[47,1931,1932],{},[50,1933,1745],{},[15,1935,1936,1946,1956,1959,1973,1979],{},[18,1937,1938,1939,1942,1943],{},"computed。記得先 ",[120,1940,1941],{},"trim()","，只輸入空白也算空：",[120,1944,1945],{},"computed(() => text.value.trim().length > 0)",[18,1947,1948,1949,1952,1953],{},"watch（寫 localStorage）。也可以寫在切換鈕的 ",[120,1950,1951],{},"@click","，但設定可能從好幾個地方被改，用 watch 比較不會漏。另外，啟動時要把設定讀回來：",[120,1954,1955],{},"ref(localStorage.getItem('dark') === 'true')",[18,1957,1958],{},"computed（組字串）",[18,1960,1961,1962,1965,1966,1969,1970],{},"看是誰改的。下拉選單切換，用 ",[120,1963,1964],{},"@change","。網址 ",[120,1967,1968],{},"\u002Fproduct\u002F:id"," 切換，用 ",[120,1971,1972],{},"watch(() => route.params.id, loadProduct, { immediate: true })",[18,1974,1975,1976,1978],{},"computed（布林值，加上 ",[120,1977,1637],{}," 或 class）",[18,1980,1981,1982,1985],{},"watch（改網址）。實務上會加 debounce，並用 ",[120,1983,1984],{},"router.replace"," 避免塞滿瀏覽紀錄",[47,1987,1988],{},[50,1989,1990],{},"找錯",[15,1992,1993,2004,2013],{},[18,1994,1995,1998,1999,2001,2002],{},[120,1996,1997],{},"isValid"," 的函式用了大括號卻沒有 ",[120,2000,275],{},"，結果永遠是 ",[120,2003,1356],{},[18,2005,2006,2009,2010],{},[120,2007,2008],{},"watch(form.email, ...)"," 傳進去的是字串，要改成 ",[120,2011,2012],{},"() => form.email",[18,2014,2015,2009,2018],{},[120,2016,2017],{},"watch(keyword.value, ...)",[120,2019,2020],{},"keyword",[2022,2023,2024],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":157,"searchDepth":192,"depth":192,"links":2026},[2027,2028,2029,2034,2037,2041,2046,2047,2048],{"id":13,"depth":192,"text":13},{"id":44,"depth":192,"text":45},{"id":137,"depth":192,"text":138,"children":2030},[2031,2032,2033],{"id":142,"depth":212,"text":143},{"id":352,"depth":212,"text":352},{"id":358,"depth":212,"text":359},{"id":669,"depth":192,"text":670,"children":2035},[2036],{"id":756,"depth":212,"text":757},{"id":836,"depth":192,"text":837,"children":2038},[2039,2040],{"id":840,"depth":212,"text":840},{"id":866,"depth":212,"text":866},{"id":921,"depth":192,"text":922,"children":2042},[2043,2044,2045],{"id":68,"depth":212,"text":68},{"id":71,"depth":212,"text":71},{"id":1382,"depth":212,"text":1383},{"id":1444,"depth":192,"text":1445},{"id":1683,"depth":192,"text":1684},{"id":1741,"depth":192,"text":1742,"children":2049},[2050,2051,2052],{"id":1745,"depth":212,"text":1745},{"id":1771,"depth":212,"text":1772},{"id":1929,"depth":212,"text":1929},"2026-10-27","computed 從現有資料推導出新的值，watch 在值改變後去做有副作用的事。分不清楚時，先問自己是在「描述狀態」還是在「執行動作」。",false,"md",{},"\u002Fnotes\u002Fvue3-computed-watch",{"title":6,"description":2054},"notes\u002Fvue3-computed-watch",[2062,2063],"Vue","面試準備","iAs6xshsf8MRmhPYTCZ-zAtPzuIKmUq3eyiwF4qA9lI",{"id":2066,"title":2067,"body":2068,"date":4192,"description":4193,"draft":2055,"extension":2056,"meta":4194,"navigation":234,"path":4195,"seo":4196,"stem":4197,"tags":4198,"__hash__":4199},"notes\u002Fnotes\u002Fvue3-reactivity.md","Vue 3 的響應式原理：Proxy、track 和 trigger",{"type":8,"value":2069,"toc":4168},[2070,2072,2093,2095,2098,2102,2105,2163,2174,2248,2252,2256,2263,2303,2307,2310,2313,2316,2355,2358,2387,2394,2398,2447,2464,2468,2472,2475,2496,2504,2508,2520,2527,2594,2600,2604,2611,2715,2730,2734,2737,2786,2789,2871,2879,2883,2887,2894,2901,2958,2987,2991,2994,3004,3065,3071,3075,3164,3168,3175,4165],[11,2071,13],{"id":13},[15,2073,2074,2076,2079,2082,2085,2088,2090],{},[18,2075,20],{},[18,2077,2078],{},"核心模型：三個角色",[18,2080,2081],{},"怎麼運作：get 登記、set 觸發、完整的循環",[18,2083,2084],{},"怎麼用：reactive、ref、shallowRef",[18,2086,2087],{},"和 Vue 2 的差別",[18,2089,38],{},[18,2091,2092],{},"附錄：可以執行的最小實作",[11,2094,45],{"id":44},[47,2096,2097],{},"Vue 3 用 Proxy 攔截資料的讀寫：讀取（get）時登記「誰在用這筆資料」，寫入（set）時通知這些人重新執行，畫面就更新了。",[11,2099,2101],{"id":2100},"_2-核心模型三個角色","2. 核心模型：三個角色",[47,2103,2104],{},"整個響應式系統只有三個角色，後面所有內容都是在講它們怎麼互動。",[54,2106,2107,2120],{},[57,2108,2109],{},[60,2110,2111,2114,2117],{},[63,2112,2113],{},"角色",[63,2115,2116],{},"是什麼",[63,2118,2119],{},"例子",[73,2121,2122,2138,2149],{},[60,2123,2124,2127,2130],{},[78,2125,2126],{},"資料",[78,2128,2129],{},"被 Proxy 包住的物件，讀寫都會被攔截",[78,2131,2132,341,2135],{},[120,2133,2134],{},"reactive({ count: 0 })",[120,2136,2137],{},"ref(0)",[60,2139,2140,2143,2146],{},[78,2141,2142],{},"effect",[78,2144,2145],{},"會讀資料的函式，資料變了要重新執行",[78,2147,2148],{},"元件的渲染函式、computed、watch",[60,2150,2151,2154,2157],{},[78,2152,2153],{},"名單",[78,2155,2156],{},"每個屬性一份，記錄哪些 effect 讀過它",[78,2158,2159,2162],{},[120,2160,2161],{},"count"," 的名單上有「元件 A 的渲染函式」",[47,2164,2165,2166,2169,2170,2173],{},"我用 axios 的攔截器來理解 Proxy：請求和回應都先經過攔截器處理，使用的人照常呼叫 ",[120,2167,2168],{},"axios.get()","，感覺不到它存在。Proxy 也一樣，照常寫 ",[120,2171,2172],{},"state.count","，讀寫都會先被攔下來。",[152,2175,2177],{"className":154,"code":2176,"language":156,"meta":157,"style":157},"const raw = { count: 0 }\nconst state = reactive(raw)\n\nstate.count       \u002F\u002F 經過 Proxy 的 get\nstate.count = 1   \u002F\u002F 經過 Proxy 的 set\nraw.count = 2     \u002F\u002F 繞過 Proxy，Vue 不知道，畫面不會更新\n",[120,2178,2179,2196,2210,2214,2222,2235],{"__ignoreMap":157},[161,2180,2181,2183,2186,2188,2191,2193],{"class":163,"line":164},[161,2182,168],{"class":167},[161,2184,2185],{"class":171}," raw",[161,2187,175],{"class":167},[161,2189,2190],{"class":182}," { count: ",[161,2192,431],{"class":171},[161,2194,2195],{"class":182}," }\n",[161,2197,2198,2200,2203,2205,2207],{"class":163,"line":192},[161,2199,168],{"class":167},[161,2201,2202],{"class":171}," state",[161,2204,175],{"class":167},[161,2206,1192],{"class":178},[161,2208,2209],{"class":182},"(raw)\n",[161,2211,2212],{"class":163,"line":212},[161,2213,235],{"emptyLinePlaceholder":234},[161,2215,2216,2219],{"class":163,"line":231},[161,2217,2218],{"class":182},"state.count       ",[161,2220,2221],{"class":296},"\u002F\u002F 經過 Proxy 的 get\n",[161,2223,2224,2227,2229,2232],{"class":163,"line":238},[161,2225,2226],{"class":182},"state.count ",[161,2228,737],{"class":167},[161,2230,2231],{"class":171}," 1",[161,2233,2234],{"class":296},"   \u002F\u002F 經過 Proxy 的 set\n",[161,2236,2237,2240,2242,2245],{"class":163,"line":260},[161,2238,2239],{"class":182},"raw.count ",[161,2241,737],{"class":167},[161,2243,2244],{"class":171}," 2",[161,2246,2247],{"class":296},"     \u002F\u002F 繞過 Proxy，Vue 不知道，畫面不會更新\n",[11,2249,2251],{"id":2250},"_3-怎麼運作","3. 怎麼運作",[140,2253,2255],{"id":2254},"get回傳值順便登記track","get：回傳值，順便登記（track）",[47,2257,2258,2259,2262],{},"get 本來的工作是把值交出去，同時把",[50,2260,2261],{},"正在執行的 effect"," 加進這個屬性的名單。",[808,2264,2265,2277,2287,2293],{},[18,2266,2267,2270,2271,2273,2274,2276],{},[50,2268,2269],{},"每個屬性各有一份名單","：改 ",[120,2272,2161],{}," 只會通知讀過 ",[120,2275,2161],{}," 的 effect",[18,2278,2279,2282,2283,2286],{},[50,2280,2281],{},"不會重複登記","：名單用 ",[120,2284,2285],{},"Set"," 存",[18,2288,2289,2292],{},[50,2290,2291],{},"只有在 effect 裡讀才會登記","：在一般函式或事件處理裡讀，沒有正在執行的 effect，就不登記",[18,2294,2295,2298,2299,2302],{},[50,2296,2297],{},"每次執行都重新登記","：有 ",[120,2300,2301],{},"if"," 判斷時，只有實際讀到的屬性會被登記",[140,2304,2306],{"id":2305},"set寫入然後觸發trigger","set：寫入，然後觸發（trigger）",[47,2308,2309],{},"觸發是 set 最主要的工作。沒有觸發，名單上的 effect 就不知道資料變了，會一直用舊的值。",[47,2311,2312],{},"set 寫入新的值之後，如果值真的有變，就把這個屬性名單上的 effect 一個一個拿出來重新執行。值沒變就不通知。",[47,2314,2315],{},"被通知的 effect 不一定馬上執行：",[54,2317,2318,2327],{},[57,2319,2320],{},[60,2321,2322,2324],{},[63,2323,2142],{},[63,2325,2326],{},"收到通知後",[73,2328,2329,2341,2348],{},[60,2330,2331,2334],{},[78,2332,2333],{},"元件渲染",[78,2335,2336,2337,2340],{},"排進佇列，同一輪的多次修改合併成一次渲染，",[120,2338,2339],{},"nextTick"," 就是在等這個佇列",[60,2342,2343,2345],{},[78,2344,68],{},[78,2346,2347],{},"只標記舊值作廢，下次被讀到才重算",[60,2349,2350,2352],{},[78,2351,71],{},[78,2353,2354],{},"依設定的時機執行回呼",[140,2356,2357],{"id":2357},"完整的循環",[15,2359,2360,2369,2375,2381,2384],{},[18,2361,2362,2363,2365,2366,2368],{},"渲染函式執行，讀到 ",[120,2364,2161],{},"：get 回傳值，把渲染函式登記到 ",[120,2367,2161],{}," 的名單",[18,2370,2371,2372,2374],{},"修改 ",[120,2373,2161],{},"：set 寫入，把名單上的渲染函式拿出來重新執行",[18,2376,2377,2378,2380],{},"渲染函式重新執行，又讀到 ",[120,2379,2161],{},"：get 拿到新的值，重新登記",[18,2382,2383],{},"渲染函式產生新的虛擬 DOM，和舊的比對（diff），只更新有變化的真實 DOM",[18,2385,2386],{},"等下一次 set，回到第 2 步",[47,2388,2389,2390,2393],{},"第 3 步又呼叫了 get，但不會無限循環，因為 get 只登記、不觸發。反過來說，如果在渲染函式或 computed 裡",[50,2391,2392],{},"修改","資料，就可能變成「set、重新渲染、又 set」的循環，所以 computed 裡不應該改資料。",[140,2395,2397],{"id":2396},"例子v-model","例子：v-model",[152,2399,2401],{"className":767,"code":2400,"language":769,"meta":157,"style":157},"\u003Cinput v-model=\"name\">\n\u003C!-- 編譯後大致是 -->\n\u003Cinput :value=\"name\" @input=\"name = $event.target.value\">\n",[120,2402,2403,2419,2424],{"__ignoreMap":157},[161,2404,2405,2407,2410,2412,2414,2417],{"class":163,"line":164},[161,2406,776],{"class":182},[161,2408,2409],{"class":779},"input",[161,2411,783],{"class":178},[161,2413,737],{"class":182},[161,2415,2416],{"class":206},"\"name\"",[161,2418,799],{"class":182},[161,2420,2421],{"class":163,"line":192},[161,2422,2423],{"class":296},"\u003C!-- 編譯後大致是 -->\n",[161,2425,2426,2428,2430,2433,2435,2437,2440,2442,2445],{"class":163,"line":212},[161,2427,776],{"class":182},[161,2429,2409],{"class":779},[161,2431,2432],{"class":178}," :value",[161,2434,737],{"class":182},[161,2436,2416],{"class":206},[161,2438,2439],{"class":178}," @input",[161,2441,737],{"class":182},[161,2443,2444],{"class":206},"\"name = $event.target.value\"",[161,2446,799],{"class":182},[808,2448,2449,2458],{},[18,2450,2451,2454,2455,2368],{},[120,2452,2453],{},":value=\"name\""," 在渲染時讀取：元件的渲染函式被登記到 ",[120,2456,2457],{},"name",[18,2459,2460,2463],{},[120,2461,2462],{},"@input"," 在打字時寫入：觸發 set，元件重新渲染",[11,2465,2467],{"id":2466},"_4-怎麼用reactiverefshallowref","4. 怎麼用：reactive、ref、shallowRef",[140,2469,2471],{"id":2470},"proxy-只能包物件","Proxy 只能包物件",[47,2473,2474],{},"數字、字串、布林值不是物件，Proxy 包不了。",[808,2476,2477,2483],{},[18,2478,2479,2482],{},[120,2480,2481],{},"reactive()"," 直接用 Proxy，所以只能放物件（物件、陣列、Map、Set）",[18,2484,2485,2488,2489,2492,2493,2495],{},[120,2486,2487],{},"ref()"," 為了能放任何值，做一個物件把值放在 ",[120,2490,2491],{},".value"," 裡，再用 getter、setter 攔截 ",[120,2494,2491],{}," 的讀寫",[47,2497,2498,2500,2501,2503],{},[120,2499,340],{}," 要寫 ",[120,2502,2491],{}," 的原因就在這裡：要有一個屬性可以攔截。在 template 裡會自動解開，直接寫名稱就好。",[140,2505,2507],{"id":2506},"ref-放物件其實是兩層","ref 放物件：其實是兩層",[47,2509,2510,2512,2513,2515,2516,2519],{},[120,2511,340],{}," 放的值如果是物件，會在內部用 ",[120,2514,344],{}," 轉成 Proxy，所以 ",[120,2517,2518],{},"ref([])"," 有兩層，各有自己的名單：",[152,2521,2525],{"className":2522,"code":2524,"language":319},[2523],"language-text","list（ref 物件）\n └─ .value         第一層：getter、setter，名單掛在 list 的 value\n     └─ Proxy(陣列)  第二層：reactive，名單掛在陣列的索引、length\n",[120,2526,2524],{"__ignoreMap":157},[152,2528,2530],{"className":154,"code":2529,"language":156,"meta":157,"style":157},"const list = ref([])\nlist.value.push(4)       \u002F\u002F 觸發第二層：陣列的 length 變了，.value 還是同一個陣列\nlist.value = [7, 8, 9]   \u002F\u002F 觸發第一層：.value 換成新的陣列，新陣列也會先轉成 Proxy\n",[120,2531,2532,2546,2565],{"__ignoreMap":157},[161,2533,2534,2536,2539,2541,2543],{"class":163,"line":164},[161,2535,168],{"class":167},[161,2537,2538],{"class":171}," list",[161,2540,175],{"class":167},[161,2542,179],{"class":178},[161,2544,2545],{"class":182},"([])\n",[161,2547,2548,2551,2554,2556,2559,2562],{"class":163,"line":192},[161,2549,2550],{"class":182},"list.value.",[161,2552,2553],{"class":178},"push",[161,2555,183],{"class":182},[161,2557,2558],{"class":171},"4",[161,2560,2561],{"class":182},")       ",[161,2563,2564],{"class":296},"\u002F\u002F 觸發第二層：陣列的 length 變了，.value 還是同一個陣列\n",[161,2566,2567,2570,2572,2575,2578,2580,2583,2585,2588,2591],{"class":163,"line":212},[161,2568,2569],{"class":182},"list.value ",[161,2571,737],{"class":167},[161,2573,2574],{"class":182}," [",[161,2576,2577],{"class":171},"7",[161,2579,406],{"class":182},[161,2581,2582],{"class":171},"8",[161,2584,406],{"class":182},[161,2586,2587],{"class":171},"9",[161,2589,2590],{"class":182},"]   ",[161,2592,2593],{"class":296},"\u002F\u002F 觸發第一層：.value 換成新的陣列，新陣列也會先轉成 Proxy\n",[47,2595,2596,2597,2599],{},"第二行是陣列用 ",[120,2598,340],{}," 最實用的地方：打 API 拿到新的列表，可以直接整個換掉。",[140,2601,2603],{"id":2602},"reactive-的坑","reactive 的坑",[47,2605,2606,2608,2609,1158],{},[120,2607,344],{}," 只有第二層，沒有外面那層 ",[120,2610,2491],{},[152,2612,2614],{"className":154,"code":2613,"language":156,"meta":157,"style":157},"let state = reactive({ list: [] })\nstate = reactive({ list: [1, 2] })\n\u002F\u002F 只是把普通變數指向新的 Proxy，沒有經過任何 set\n\u002F\u002F 元件登記在舊的 Proxy 上，不會收到通知\n\nconst s = reactive({ count: 0 })\nlet { count } = s\ncount++\n\u002F\u002F 解構等於把值複製出來，count 只是普通的數字，已經不經過 Proxy\n\u002F\u002F 要保留響應的話改用 toRefs(s)\n",[120,2615,2616,2631,2654,2659,2664,2668,2686,2698,2705,2710],{"__ignoreMap":157},[161,2617,2618,2621,2624,2626,2628],{"class":163,"line":164},[161,2619,2620],{"class":167},"let",[161,2622,2623],{"class":182}," state ",[161,2625,737],{"class":167},[161,2627,1192],{"class":178},[161,2629,2630],{"class":182},"({ list: [] })\n",[161,2632,2633,2636,2638,2640,2643,2646,2648,2651],{"class":163,"line":192},[161,2634,2635],{"class":182},"state ",[161,2637,737],{"class":167},[161,2639,1192],{"class":178},[161,2641,2642],{"class":182},"({ list: [",[161,2644,2645],{"class":171},"1",[161,2647,406],{"class":182},[161,2649,2650],{"class":171},"2",[161,2652,2653],{"class":182},"] })\n",[161,2655,2656],{"class":163,"line":212},[161,2657,2658],{"class":296},"\u002F\u002F 只是把普通變數指向新的 Proxy，沒有經過任何 set\n",[161,2660,2661],{"class":163,"line":231},[161,2662,2663],{"class":296},"\u002F\u002F 元件登記在舊的 Proxy 上，不會收到通知\n",[161,2665,2666],{"class":163,"line":238},[161,2667,235],{"emptyLinePlaceholder":234},[161,2669,2670,2672,2675,2677,2679,2682,2684],{"class":163,"line":260},[161,2671,168],{"class":167},[161,2673,2674],{"class":171}," s",[161,2676,175],{"class":167},[161,2678,1192],{"class":178},[161,2680,2681],{"class":182},"({ count: ",[161,2683,431],{"class":171},[161,2685,1206],{"class":182},[161,2687,2688,2690,2693,2695],{"class":163,"line":281},[161,2689,2620],{"class":167},[161,2691,2692],{"class":182}," { count } ",[161,2694,737],{"class":167},[161,2696,2697],{"class":182}," s\n",[161,2699,2700,2702],{"class":163,"line":300},[161,2701,2161],{"class":182},[161,2703,2704],{"class":167},"++\n",[161,2706,2707],{"class":163,"line":1287},[161,2708,2709],{"class":296},"\u002F\u002F 解構等於把值複製出來，count 只是普通的數字，已經不經過 Proxy\n",[161,2711,2712],{"class":163,"line":1303},[161,2713,2714],{"class":296},"\u002F\u002F 要保留響應的話改用 toRefs(s)\n",[47,2716,2717,2718,2720,2721,2723,2724,2726,2727,2729],{},"所以 ",[120,2719,340],{}," 多包一層 ",[120,2722,2491],{}," 不只是寫法上的麻煩，它就是 ",[120,2725,340],{}," 比較泛用的原因：換值變成「寫 ",[120,2728,2491],{},"」，會經過 setter，就能觸發。",[140,2731,2733],{"id":2732},"shallowref","shallowRef",[47,2735,2736],{},"只有第一層，不會把內容轉成 Proxy。適合很大、只會整個換掉的資料：",[152,2738,2740],{"className":154,"code":2739,"language":156,"meta":157,"style":157},"const big = shallowRef([])\nbig.value = res.data   \u002F\u002F 觸發\nbig.value.push(1)      \u002F\u002F 不觸發，沒有第二層\n",[120,2741,2742,2756,2769],{"__ignoreMap":157},[161,2743,2744,2746,2749,2751,2754],{"class":163,"line":164},[161,2745,168],{"class":167},[161,2747,2748],{"class":171}," big",[161,2750,175],{"class":167},[161,2752,2753],{"class":178}," shallowRef",[161,2755,2545],{"class":182},[161,2757,2758,2761,2763,2766],{"class":163,"line":192},[161,2759,2760],{"class":182},"big.value ",[161,2762,737],{"class":167},[161,2764,2765],{"class":182}," res.data   ",[161,2767,2768],{"class":296},"\u002F\u002F 觸發\n",[161,2770,2771,2774,2776,2778,2780,2783],{"class":163,"line":212},[161,2772,2773],{"class":182},"big.value.",[161,2775,2553],{"class":178},[161,2777,183],{"class":182},[161,2779,2645],{"class":171},[161,2781,2782],{"class":182},")      ",[161,2784,2785],{"class":296},"\u002F\u002F 不觸發，沒有第二層\n",[140,2787,2788],{"id":2788},"怎麼選",[54,2790,2791,2815],{},[57,2792,2793],{},[60,2794,2795,2797,2803,2806,2809,2812],{},[63,2796],{},[63,2798,2799,2800,2802],{},"第一層（",[120,2801,2491],{},"）",[63,2804,2805],{},"第二層（Proxy）",[63,2807,2808],{},"整個換掉",[63,2810,2811],{},"改內容",[63,2813,2814],{},"能放基本型別",[73,2816,2817,2838,2855],{},[60,2818,2819,2823,2826,2829,2832,2835],{},[78,2820,2821],{},[120,2822,344],{},[78,2824,2825],{},"沒有",[78,2827,2828],{},"有",[78,2830,2831],{},"不會觸發",[78,2833,2834],{},"會觸發",[78,2836,2837],{},"不行",[60,2839,2840,2844,2846,2848,2850,2852],{},[78,2841,2842],{},[120,2843,340],{},[78,2845,2828],{},[78,2847,2828],{},[78,2849,2834],{},[78,2851,2834],{},[78,2853,2854],{},"可以",[60,2856,2857,2861,2863,2865,2867,2869],{},[78,2858,2859],{},[120,2860,2733],{},[78,2862,2828],{},[78,2864,2825],{},[78,2866,2834],{},[78,2868,2831],{},[78,2870,2854],{},[47,2872,2873,2874,2876,2877,370],{},"Vue 官方文件（Reactivity Fundamentals）列出 ",[120,2875,344],{}," 的三個限制：只能放物件、不能整個換掉、解構會斷掉響應，因此建議宣告響應式狀態時優先用 ",[120,2878,2487],{},[11,2880,2882],{"id":2881},"_5-和-vue-2-的差別","5. 和 Vue 2 的差別",[140,2884,2886],{"id":2885},"vue-2-用-defineproperty","Vue 2 用 defineProperty",[47,2888,2889,2890,2893],{},"Vue 2 在初始化時遞迴走過整個資料，對每一層的每個屬性用 ",[120,2891,2892],{},"Object.defineProperty"," 設定 getter、setter。所以 Vue 2 也有深層響應，改巢狀物件的屬性一樣會更新畫面。",[47,2895,2896,2897,2900],{},"問題在於 ",[120,2898,2899],{},"defineProperty"," 只能攔截設定當下已經存在的屬性，而且 Vue 2 沒有對陣列的每個索引設定 getter、setter（陣列可能很長，成本太高）。所以這些偵測不到：",[152,2902,2904],{"className":154,"code":2903,"language":156,"meta":157,"style":157},"this.items[0] = 'x'     \u002F\u002F 用索引改\nthis.items.length = 0   \u002F\u002F 改長度\nthis.user.age = 30      \u002F\u002F 新增一開始不存在的屬性\n",[120,2905,2906,2927,2943],{"__ignoreMap":157},[161,2907,2908,2911,2914,2916,2919,2921,2924],{"class":163,"line":164},[161,2909,2910],{"class":171},"this",[161,2912,2913],{"class":182},".items[",[161,2915,431],{"class":171},[161,2917,2918],{"class":182},"] ",[161,2920,737],{"class":167},[161,2922,2923],{"class":206}," 'x'",[161,2925,2926],{"class":296},"     \u002F\u002F 用索引改\n",[161,2928,2929,2931,2934,2936,2938,2940],{"class":163,"line":192},[161,2930,2910],{"class":171},[161,2932,2933],{"class":182},".items.",[161,2935,499],{"class":171},[161,2937,175],{"class":167},[161,2939,554],{"class":171},[161,2941,2942],{"class":296},"   \u002F\u002F 改長度\n",[161,2944,2945,2947,2950,2952,2955],{"class":163,"line":212},[161,2946,2910],{"class":171},[161,2948,2949],{"class":182},".user.age ",[161,2951,737],{"class":167},[161,2953,2954],{"class":171}," 30",[161,2956,2957],{"class":296},"      \u002F\u002F 新增一開始不存在的屬性\n",[47,2959,2960,2961,341,2963,341,2966,341,2969,341,2972,341,2975,341,2977,2980,2981,2984,2985,370],{},"Vue 2 的補救是改寫陣列的 7 個方法（",[120,2962,2553],{},[120,2964,2965],{},"pop",[120,2967,2968],{},"shift",[120,2970,2971],{},"unshift",[120,2973,2974],{},"splice",[120,2976,597],{},[120,2978,2979],{},"reverse","），其他情況要用 ",[120,2982,2983],{},"Vue.set"," 或 ",[120,2986,2974],{},[140,2988,2990],{"id":2989},"vue-3-用-proxy","Vue 3 用 Proxy",[47,2992,2993],{},"Proxy 攔截整個物件的所有操作，不管屬性一開始存不存在，上面三種寫法都會經過 set。",[47,2995,2996,2997,3000,3001,3003],{},"深層響應也改成",[50,2998,2999],{},"讀到才轉換","：get 讀到的值如果是物件，當下才用 ",[120,3002,344],{}," 包起來回傳，沒讀到的巢狀物件就一直不會被處理。",[54,3005,3006,3022],{},[57,3007,3008],{},[60,3009,3010,3013,3016,3019],{},[63,3011,3012],{},"問題",[63,3014,3015],{},"Vue 2",[63,3017,3018],{},"Vue 3",[63,3020,3021],{},"解決的關鍵",[73,3023,3024,3038,3051],{},[60,3025,3026,3029,3032,3035],{},[78,3027,3028],{},"巢狀物件改了要響應",[78,3030,3031],{},"可以（初始化時遞迴）",[78,3033,3034],{},"可以（讀到才轉換）",[78,3036,3037],{},"兩個版本都有，Vue 3 改成延遲",[60,3039,3040,3043,3046,3048],{},[78,3041,3042],{},"用索引改陣列、改長度",[78,3044,3045],{},"偵測不到",[78,3047,2854],{},[78,3049,3050],{},"Proxy",[60,3052,3053,3056,3061,3063],{},[78,3054,3055],{},"物件新增屬性",[78,3057,3058,3059],{},"偵測不到，要用 ",[120,3060,2983],{},[78,3062,2854],{},[78,3064,3050],{},[47,3066,3067,3068,3070],{},"所以 Vue 3 改用 Proxy 的好處有兩個：能攔截新增的屬性、陣列索引和長度，不用再寫 ",[120,3069,2983],{},"。深層響應改成讀到才轉換，大資料初始化比較快。",[11,3072,3074],{"id":3073},"_6-我原本的誤解","6. 我原本的誤解",[54,3076,3077,3085],{},[57,3078,3079],{},[60,3080,3081,3083],{},[63,3082,1693],{},[63,3084,1696],{},[73,3086,3087,3098,3109,3117,3125,3137,3156],{},[60,3088,3089,3092],{},[78,3090,3091],{},"Proxy 監聽的是元件",[78,3093,3094,3095,3097],{},"Proxy 包的是",[50,3096,2126],{},"，元件是讀資料的 effect，被登記在名單上",[60,3099,3100,3103],{},[78,3101,3102],{},"Proxy 是一種封裝",[78,3104,879,3105,3108],{},[50,3106,3107],{},"攔截","。封裝是把東西藏起來，Proxy 沒有藏任何東西，介面也沒變",[60,3110,3111,3114],{},[78,3112,3113],{},"用「監聽」形容 Proxy",[78,3115,3116],{},"口語可以，但監聽是事後收到通知，攔截是事前攔下來，還能決定回傳什麼",[60,3118,3119,3122],{},[78,3120,3121],{},"set 通知是「副作用」",[78,3123,3124],{},"在 Vue 的術語裡，effect（副作用）指的是被通知的那些函式，通知這件事叫觸發",[60,3126,3127,3130],{},[78,3128,3129],{},"set 觸發後，名單上的 DOM 再呼叫一次 get",[78,3131,3132,3133,3136],{},"名單上是",[50,3134,3135],{},"函式","不是 DOM。函式重新執行時順便呼叫了 get，改 DOM 的是 diff 之後的 patch",[60,3138,3139,3144],{},[78,3140,3141,3142],{},"畫面會更新是因為 ",[120,3143,999],{},[78,3145,3146,3148,3149,3152,3153,3155],{},[120,3147,999],{}," 只是 ",[120,3150,3151],{},":value"," 讀加 ",[120,3154,2462],{}," 寫，任何響應式資料改了畫面都會更新",[60,3157,3158,3161],{},[78,3159,3160],{},"深層攔截是為了解決 Vue 2 陣列的問題",[78,3162,3163],{},"兩件事。Vue 2 本來就有深層響應，陣列問題是靠 Proxy 解決的",[11,3165,3167],{"id":3166},"_7-附錄可以執行的最小實作","7. 附錄：可以執行的最小實作",[47,3169,3170,3171,3174],{},"存成 ",[120,3172,3173],{},".js"," 用 Node 執行。這個版本沒有排程佇列，所以每次修改都會馬上重新執行，Vue 實際上會合併成一次渲染。",[152,3176,3178],{"className":154,"code":3177,"language":156,"meta":157,"style":157},"const targetMap = new WeakMap()   \u002F\u002F 資料 → 屬性 → 名單\nlet activeEffect = null           \u002F\u002F 現在正在執行的 effect\n\nfunction track(target, key) {\n  if (!activeEffect) return\n  let depsMap = targetMap.get(target)\n  if (!depsMap) targetMap.set(target, (depsMap = new Map()))\n  let dep = depsMap.get(key)\n  if (!dep) depsMap.set(key, (dep = new Set()))\n  dep.add(activeEffect)\n}\n\nfunction trigger(target, key) {\n  targetMap.get(target)?.get(key)?.forEach(effect => effect())\n}\n\nfunction reactive(obj) {\n  return new Proxy(obj, {\n    get(target, key, receiver) {\n      track(target, key)\n      const res = Reflect.get(target, key, receiver)\n      \u002F\u002F 讀到物件才轉成 Proxy（延遲轉換）\n      return typeof res === 'object' && res !== null ? reactive(res) : res\n    },\n    set(target, key, value, receiver) {\n      const isNew = !Object.prototype.hasOwnProperty.call(target, key)\n      const changed = !Object.is(target[key], value)\n      const ok = Reflect.set(target, key, value, receiver)\n      if (changed) trigger(target, key)\n      \u002F\u002F 陣列新增索引時（例如 push），length 已經跟著變了，要另外通知讀過 length 的人\n      if (isNew && Array.isArray(target)) trigger(target, 'length')\n      return ok\n    }\n  })\n}\n\nfunction ref(value) {\n  const wrapper = {\n    _value: toReactive(value),\n    get value() {\n      track(wrapper, 'value')\n      return this._value\n    },\n    set value(newVal) {\n      if (Object.is(newVal, this._value)) return\n      this._value = toReactive(newVal)\n      trigger(wrapper, 'value')\n    }\n  }\n  return wrapper\n}\n\nfunction toReactive(v) {\n  return typeof v === 'object' && v !== null ? reactive(v) : v\n}\n\nfunction effect(fn) {\n  activeEffect = fn\n  fn()\n  activeEffect = null\n}\n\n\u002F\u002F reactive：只有讀過的屬性會觸發\nconst state = reactive({ count: 0, other: 0 })\neffect(() => console.log('render, count =', state.count))\nstate.count++   \u002F\u002F 印出 count = 1\nstate.other++   \u002F\u002F 不會印，effect 沒有讀 other\n\n\u002F\u002F ref 放陣列：兩層都會觸發\nconst list = ref([])\neffect(() => console.log('render, length =', list.value.length))\nlist.value.push(4)       \u002F\u002F 第二層，印出 length = 1\nlist.value = [7, 8, 9]   \u002F\u002F 第一層，印出 length = 3\n",[120,3179,3180,3201,3216,3220,3241,3255,3274,3301,3318,3343,3354,3359,3363,3380,3410,3415,3420,3434,3447,3468,3477,3496,3502,3544,3550,3574,3603,3624,3641,3655,3661,3691,3699,3705,3711,3716,3721,3734,3747,3759,3770,3783,3794,3799,3813,3833,3850,3862,3867,3873,3881,3886,3891,3905,3939,3944,3949,3963,3974,3983,3993,3998,4003,4009,4031,4053,4064,4075,4080,4086,4099,4125,4141],{"__ignoreMap":157},[161,3181,3182,3184,3187,3189,3192,3195,3198],{"class":163,"line":164},[161,3183,168],{"class":167},[161,3185,3186],{"class":171}," targetMap",[161,3188,175],{"class":167},[161,3190,3191],{"class":167}," new",[161,3193,3194],{"class":178}," WeakMap",[161,3196,3197],{"class":182},"()   ",[161,3199,3200],{"class":296},"\u002F\u002F 資料 → 屬性 → 名單\n",[161,3202,3203,3205,3208,3210,3213],{"class":163,"line":192},[161,3204,2620],{"class":167},[161,3206,3207],{"class":182}," activeEffect ",[161,3209,737],{"class":167},[161,3211,3212],{"class":171}," null",[161,3214,3215],{"class":296},"           \u002F\u002F 現在正在執行的 effect\n",[161,3217,3218],{"class":163,"line":212},[161,3219,235],{"emptyLinePlaceholder":234},[161,3221,3222,3225,3228,3230,3233,3235,3238],{"class":163,"line":231},[161,3223,3224],{"class":167},"function",[161,3226,3227],{"class":178}," track",[161,3229,183],{"class":182},[161,3231,3232],{"class":402},"target",[161,3234,406],{"class":182},[161,3236,3237],{"class":402},"key",[161,3239,3240],{"class":182},") {\n",[161,3242,3243,3245,3247,3249,3252],{"class":163,"line":238},[161,3244,263],{"class":167},[161,3246,266],{"class":182},[161,3248,269],{"class":167},[161,3250,3251],{"class":182},"activeEffect) ",[161,3253,3254],{"class":167},"return\n",[161,3256,3257,3260,3263,3265,3268,3271],{"class":163,"line":260},[161,3258,3259],{"class":167},"  let",[161,3261,3262],{"class":182}," depsMap ",[161,3264,737],{"class":167},[161,3266,3267],{"class":182}," targetMap.",[161,3269,3270],{"class":178},"get",[161,3272,3273],{"class":182},"(target)\n",[161,3275,3276,3278,3280,3282,3285,3288,3291,3293,3295,3298],{"class":163,"line":281},[161,3277,263],{"class":167},[161,3279,266],{"class":182},[161,3281,269],{"class":167},[161,3283,3284],{"class":182},"depsMap) targetMap.",[161,3286,3287],{"class":178},"set",[161,3289,3290],{"class":182},"(target, (depsMap ",[161,3292,737],{"class":167},[161,3294,3191],{"class":167},[161,3296,3297],{"class":178}," Map",[161,3299,3300],{"class":182},"()))\n",[161,3302,3303,3305,3308,3310,3313,3315],{"class":163,"line":300},[161,3304,3259],{"class":167},[161,3306,3307],{"class":182}," dep ",[161,3309,737],{"class":167},[161,3311,3312],{"class":182}," depsMap.",[161,3314,3270],{"class":178},[161,3316,3317],{"class":182},"(key)\n",[161,3319,3320,3322,3324,3326,3329,3331,3334,3336,3338,3341],{"class":163,"line":1287},[161,3321,263],{"class":167},[161,3323,266],{"class":182},[161,3325,269],{"class":167},[161,3327,3328],{"class":182},"dep) depsMap.",[161,3330,3287],{"class":178},[161,3332,3333],{"class":182},"(key, (dep ",[161,3335,737],{"class":167},[161,3337,3191],{"class":167},[161,3339,3340],{"class":178}," Set",[161,3342,3300],{"class":182},[161,3344,3345,3348,3351],{"class":163,"line":1303},[161,3346,3347],{"class":182},"  dep.",[161,3349,3350],{"class":178},"add",[161,3352,3353],{"class":182},"(activeEffect)\n",[161,3355,3356],{"class":163,"line":1895},[161,3357,3358],{"class":182},"}\n",[161,3360,3361],{"class":163,"line":1900},[161,3362,235],{"emptyLinePlaceholder":234},[161,3364,3365,3367,3370,3372,3374,3376,3378],{"class":163,"line":1916},[161,3366,3224],{"class":167},[161,3368,3369],{"class":178}," trigger",[161,3371,183],{"class":182},[161,3373,3232],{"class":402},[161,3375,406],{"class":182},[161,3377,3237],{"class":402},[161,3379,3240],{"class":182},[161,3381,3382,3385,3387,3390,3392,3395,3398,3400,3402,3404,3407],{"class":163,"line":1924},[161,3383,3384],{"class":182},"  targetMap.",[161,3386,3270],{"class":178},[161,3388,3389],{"class":182},"(target)?.",[161,3391,3270],{"class":178},[161,3393,3394],{"class":182},"(key)?.",[161,3396,3397],{"class":178},"forEach",[161,3399,183],{"class":182},[161,3401,2142],{"class":402},[161,3403,546],{"class":167},[161,3405,3406],{"class":178}," effect",[161,3408,3409],{"class":182},"())\n",[161,3411,3413],{"class":163,"line":3412},15,[161,3414,3358],{"class":182},[161,3416,3418],{"class":163,"line":3417},16,[161,3419,235],{"emptyLinePlaceholder":234},[161,3421,3423,3425,3427,3429,3432],{"class":163,"line":3422},17,[161,3424,3224],{"class":167},[161,3426,1192],{"class":178},[161,3428,183],{"class":182},[161,3430,3431],{"class":402},"obj",[161,3433,3240],{"class":182},[161,3435,3437,3439,3441,3444],{"class":163,"line":3436},18,[161,3438,284],{"class":167},[161,3440,3191],{"class":167},[161,3442,3443],{"class":178}," Proxy",[161,3445,3446],{"class":182},"(obj, {\n",[161,3448,3450,3453,3455,3457,3459,3461,3463,3466],{"class":163,"line":3449},19,[161,3451,3452],{"class":178},"    get",[161,3454,183],{"class":182},[161,3456,3232],{"class":402},[161,3458,406],{"class":182},[161,3460,3237],{"class":402},[161,3462,406],{"class":182},[161,3464,3465],{"class":402},"receiver",[161,3467,3240],{"class":182},[161,3469,3471,3474],{"class":163,"line":3470},20,[161,3472,3473],{"class":178},"      track",[161,3475,3476],{"class":182},"(target, key)\n",[161,3478,3480,3483,3486,3488,3491,3493],{"class":163,"line":3479},21,[161,3481,3482],{"class":167},"      const",[161,3484,3485],{"class":171}," res",[161,3487,175],{"class":167},[161,3489,3490],{"class":182}," Reflect.",[161,3492,3270],{"class":178},[161,3494,3495],{"class":182},"(target, key, receiver)\n",[161,3497,3499],{"class":163,"line":3498},22,[161,3500,3501],{"class":296},"      \u002F\u002F 讀到物件才轉成 Proxy（延遲轉換）\n",[161,3503,3505,3508,3511,3514,3517,3520,3523,3525,3528,3530,3533,3535,3538,3541],{"class":163,"line":3504},23,[161,3506,3507],{"class":167},"      return",[161,3509,3510],{"class":167}," typeof",[161,3512,3513],{"class":182}," res ",[161,3515,3516],{"class":167},"===",[161,3518,3519],{"class":206}," 'object'",[161,3521,3522],{"class":167}," &&",[161,3524,3513],{"class":182},[161,3526,3527],{"class":167},"!==",[161,3529,3212],{"class":171},[161,3531,3532],{"class":167}," ?",[161,3534,1192],{"class":178},[161,3536,3537],{"class":182},"(res) ",[161,3539,3540],{"class":167},":",[161,3542,3543],{"class":182}," res\n",[161,3545,3547],{"class":163,"line":3546},24,[161,3548,3549],{"class":182},"    },\n",[161,3551,3553,3556,3558,3560,3562,3564,3566,3568,3570,3572],{"class":163,"line":3552},25,[161,3554,3555],{"class":178},"    set",[161,3557,183],{"class":182},[161,3559,3232],{"class":402},[161,3561,406],{"class":182},[161,3563,3237],{"class":402},[161,3565,406],{"class":182},[161,3567,494],{"class":402},[161,3569,406],{"class":182},[161,3571,3465],{"class":402},[161,3573,3240],{"class":182},[161,3575,3577,3579,3582,3584,3587,3590,3592,3595,3598,3601],{"class":163,"line":3576},26,[161,3578,3482],{"class":167},[161,3580,3581],{"class":171}," isNew",[161,3583,175],{"class":167},[161,3585,3586],{"class":167}," !",[161,3588,3589],{"class":171},"Object",[161,3591,491],{"class":182},[161,3593,3594],{"class":171},"prototype",[161,3596,3597],{"class":182},".hasOwnProperty.",[161,3599,3600],{"class":178},"call",[161,3602,3476],{"class":182},[161,3604,3606,3608,3611,3613,3615,3618,3621],{"class":163,"line":3605},27,[161,3607,3482],{"class":167},[161,3609,3610],{"class":171}," changed",[161,3612,175],{"class":167},[161,3614,3586],{"class":167},[161,3616,3617],{"class":182},"Object.",[161,3619,3620],{"class":178},"is",[161,3622,3623],{"class":182},"(target[key], value)\n",[161,3625,3627,3629,3632,3634,3636,3638],{"class":163,"line":3626},28,[161,3628,3482],{"class":167},[161,3630,3631],{"class":171}," ok",[161,3633,175],{"class":167},[161,3635,3490],{"class":182},[161,3637,3287],{"class":178},[161,3639,3640],{"class":182},"(target, key, value, receiver)\n",[161,3642,3644,3647,3650,3653],{"class":163,"line":3643},29,[161,3645,3646],{"class":167},"      if",[161,3648,3649],{"class":182}," (changed) ",[161,3651,3652],{"class":178},"trigger",[161,3654,3476],{"class":182},[161,3656,3658],{"class":163,"line":3657},30,[161,3659,3660],{"class":296},"      \u002F\u002F 陣列新增索引時（例如 push），length 已經跟著變了，要另外通知讀過 length 的人\n",[161,3662,3664,3666,3669,3672,3675,3678,3681,3683,3686,3689],{"class":163,"line":3663},31,[161,3665,3646],{"class":167},[161,3667,3668],{"class":182}," (isNew ",[161,3670,3671],{"class":167},"&&",[161,3673,3674],{"class":182}," Array.",[161,3676,3677],{"class":178},"isArray",[161,3679,3680],{"class":182},"(target)) ",[161,3682,3652],{"class":178},[161,3684,3685],{"class":182},"(target, ",[161,3687,3688],{"class":206},"'length'",[161,3690,189],{"class":182},[161,3692,3694,3696],{"class":163,"line":3693},32,[161,3695,3507],{"class":167},[161,3697,3698],{"class":182}," ok\n",[161,3700,3702],{"class":163,"line":3701},33,[161,3703,3704],{"class":182},"    }\n",[161,3706,3708],{"class":163,"line":3707},34,[161,3709,3710],{"class":182},"  })\n",[161,3712,3714],{"class":163,"line":3713},35,[161,3715,3358],{"class":182},[161,3717,3719],{"class":163,"line":3718},36,[161,3720,235],{"emptyLinePlaceholder":234},[161,3722,3724,3726,3728,3730,3732],{"class":163,"line":3723},37,[161,3725,3224],{"class":167},[161,3727,179],{"class":178},[161,3729,183],{"class":182},[161,3731,494],{"class":402},[161,3733,3240],{"class":182},[161,3735,3737,3740,3743,3745],{"class":163,"line":3736},38,[161,3738,3739],{"class":167},"  const",[161,3741,3742],{"class":171}," wrapper",[161,3744,175],{"class":167},[161,3746,257],{"class":182},[161,3748,3750,3753,3756],{"class":163,"line":3749},39,[161,3751,3752],{"class":182},"    _value: ",[161,3754,3755],{"class":178},"toReactive",[161,3757,3758],{"class":182},"(value),\n",[161,3760,3762,3764,3767],{"class":163,"line":3761},40,[161,3763,3452],{"class":167},[161,3765,3766],{"class":178}," value",[161,3768,3769],{"class":182},"() {\n",[161,3771,3773,3775,3778,3781],{"class":163,"line":3772},41,[161,3774,3473],{"class":178},[161,3776,3777],{"class":182},"(wrapper, ",[161,3779,3780],{"class":206},"'value'",[161,3782,189],{"class":182},[161,3784,3786,3788,3791],{"class":163,"line":3785},42,[161,3787,3507],{"class":167},[161,3789,3790],{"class":171}," this",[161,3792,3793],{"class":182},"._value\n",[161,3795,3797],{"class":163,"line":3796},43,[161,3798,3549],{"class":182},[161,3800,3802,3804,3806,3808,3811],{"class":163,"line":3801},44,[161,3803,3555],{"class":167},[161,3805,3766],{"class":178},[161,3807,183],{"class":182},[161,3809,3810],{"class":402},"newVal",[161,3812,3240],{"class":182},[161,3814,3816,3818,3821,3823,3826,3828,3831],{"class":163,"line":3815},45,[161,3817,3646],{"class":167},[161,3819,3820],{"class":182}," (Object.",[161,3822,3620],{"class":178},[161,3824,3825],{"class":182},"(newVal, ",[161,3827,2910],{"class":171},[161,3829,3830],{"class":182},"._value)) ",[161,3832,3254],{"class":167},[161,3834,3836,3839,3842,3844,3847],{"class":163,"line":3835},46,[161,3837,3838],{"class":171},"      this",[161,3840,3841],{"class":182},"._value ",[161,3843,737],{"class":167},[161,3845,3846],{"class":178}," toReactive",[161,3848,3849],{"class":182},"(newVal)\n",[161,3851,3853,3856,3858,3860],{"class":163,"line":3852},47,[161,3854,3855],{"class":178},"      trigger",[161,3857,3777],{"class":182},[161,3859,3780],{"class":206},[161,3861,189],{"class":182},[161,3863,3865],{"class":163,"line":3864},48,[161,3866,3704],{"class":182},[161,3868,3870],{"class":163,"line":3869},49,[161,3871,3872],{"class":182},"  }\n",[161,3874,3876,3878],{"class":163,"line":3875},50,[161,3877,284],{"class":167},[161,3879,3880],{"class":182}," wrapper\n",[161,3882,3884],{"class":163,"line":3883},51,[161,3885,3358],{"class":182},[161,3887,3889],{"class":163,"line":3888},52,[161,3890,235],{"emptyLinePlaceholder":234},[161,3892,3894,3896,3898,3900,3903],{"class":163,"line":3893},53,[161,3895,3224],{"class":167},[161,3897,3846],{"class":178},[161,3899,183],{"class":182},[161,3901,3902],{"class":402},"v",[161,3904,3240],{"class":182},[161,3906,3908,3910,3912,3915,3917,3919,3921,3923,3925,3927,3929,3931,3934,3936],{"class":163,"line":3907},54,[161,3909,284],{"class":167},[161,3911,3510],{"class":167},[161,3913,3914],{"class":182}," v ",[161,3916,3516],{"class":167},[161,3918,3519],{"class":206},[161,3920,3522],{"class":167},[161,3922,3914],{"class":182},[161,3924,3527],{"class":167},[161,3926,3212],{"class":171},[161,3928,3532],{"class":167},[161,3930,1192],{"class":178},[161,3932,3933],{"class":182},"(v) ",[161,3935,3540],{"class":167},[161,3937,3938],{"class":182}," v\n",[161,3940,3942],{"class":163,"line":3941},55,[161,3943,3358],{"class":182},[161,3945,3947],{"class":163,"line":3946},56,[161,3948,235],{"emptyLinePlaceholder":234},[161,3950,3952,3954,3956,3958,3961],{"class":163,"line":3951},57,[161,3953,3224],{"class":167},[161,3955,3406],{"class":178},[161,3957,183],{"class":182},[161,3959,3960],{"class":402},"fn",[161,3962,3240],{"class":182},[161,3964,3966,3969,3971],{"class":163,"line":3965},58,[161,3967,3968],{"class":182},"  activeEffect ",[161,3970,737],{"class":167},[161,3972,3973],{"class":182}," fn\n",[161,3975,3977,3980],{"class":163,"line":3976},59,[161,3978,3979],{"class":178},"  fn",[161,3981,3982],{"class":182},"()\n",[161,3984,3986,3988,3990],{"class":163,"line":3985},60,[161,3987,3968],{"class":182},[161,3989,737],{"class":167},[161,3991,3992],{"class":171}," null\n",[161,3994,3996],{"class":163,"line":3995},61,[161,3997,3358],{"class":182},[161,3999,4001],{"class":163,"line":4000},62,[161,4002,235],{"emptyLinePlaceholder":234},[161,4004,4006],{"class":163,"line":4005},63,[161,4007,4008],{"class":296},"\u002F\u002F reactive：只有讀過的屬性會觸發\n",[161,4010,4012,4014,4016,4018,4020,4022,4024,4027,4029],{"class":163,"line":4011},64,[161,4013,168],{"class":167},[161,4015,2202],{"class":171},[161,4017,175],{"class":167},[161,4019,1192],{"class":178},[161,4021,2681],{"class":182},[161,4023,431],{"class":171},[161,4025,4026],{"class":182},", other: ",[161,4028,431],{"class":171},[161,4030,1206],{"class":182},[161,4032,4034,4036,4038,4040,4043,4045,4047,4050],{"class":163,"line":4033},65,[161,4035,2142],{"class":178},[161,4037,251],{"class":182},[161,4039,254],{"class":167},[161,4041,4042],{"class":182}," console.",[161,4044,987],{"class":178},[161,4046,183],{"class":182},[161,4048,4049],{"class":206},"'render, count ='",[161,4051,4052],{"class":182},", state.count))\n",[161,4054,4056,4058,4061],{"class":163,"line":4055},66,[161,4057,2172],{"class":182},[161,4059,4060],{"class":167},"++",[161,4062,4063],{"class":296},"   \u002F\u002F 印出 count = 1\n",[161,4065,4067,4070,4072],{"class":163,"line":4066},67,[161,4068,4069],{"class":182},"state.other",[161,4071,4060],{"class":167},[161,4073,4074],{"class":296},"   \u002F\u002F 不會印，effect 沒有讀 other\n",[161,4076,4078],{"class":163,"line":4077},68,[161,4079,235],{"emptyLinePlaceholder":234},[161,4081,4083],{"class":163,"line":4082},69,[161,4084,4085],{"class":296},"\u002F\u002F ref 放陣列：兩層都會觸發\n",[161,4087,4089,4091,4093,4095,4097],{"class":163,"line":4088},70,[161,4090,168],{"class":167},[161,4092,2538],{"class":171},[161,4094,175],{"class":167},[161,4096,179],{"class":178},[161,4098,2545],{"class":182},[161,4100,4102,4104,4106,4108,4110,4112,4114,4117,4120,4122],{"class":163,"line":4101},71,[161,4103,2142],{"class":178},[161,4105,251],{"class":182},[161,4107,254],{"class":167},[161,4109,4042],{"class":182},[161,4111,987],{"class":178},[161,4113,183],{"class":182},[161,4115,4116],{"class":206},"'render, length ='",[161,4118,4119],{"class":182},", list.value.",[161,4121,499],{"class":171},[161,4123,4124],{"class":182},"))\n",[161,4126,4128,4130,4132,4134,4136,4138],{"class":163,"line":4127},72,[161,4129,2550],{"class":182},[161,4131,2553],{"class":178},[161,4133,183],{"class":182},[161,4135,2558],{"class":171},[161,4137,2561],{"class":182},[161,4139,4140],{"class":296},"\u002F\u002F 第二層，印出 length = 1\n",[161,4142,4144,4146,4148,4150,4152,4154,4156,4158,4160,4162],{"class":163,"line":4143},73,[161,4145,2569],{"class":182},[161,4147,737],{"class":167},[161,4149,2574],{"class":182},[161,4151,2577],{"class":171},[161,4153,406],{"class":182},[161,4155,2582],{"class":171},[161,4157,406],{"class":182},[161,4159,2587],{"class":171},[161,4161,2590],{"class":182},[161,4163,4164],{"class":296},"\u002F\u002F 第一層，印出 length = 3\n",[2022,4166,4167],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":157,"searchDepth":192,"depth":192,"links":4169},[4170,4171,4172,4173,4179,4186,4190,4191],{"id":13,"depth":192,"text":13},{"id":44,"depth":192,"text":45},{"id":2100,"depth":192,"text":2101},{"id":2250,"depth":192,"text":2251,"children":4174},[4175,4176,4177,4178],{"id":2254,"depth":212,"text":2255},{"id":2305,"depth":212,"text":2306},{"id":2357,"depth":212,"text":2357},{"id":2396,"depth":212,"text":2397},{"id":2466,"depth":192,"text":2467,"children":4180},[4181,4182,4183,4184,4185],{"id":2470,"depth":212,"text":2471},{"id":2506,"depth":212,"text":2507},{"id":2602,"depth":212,"text":2603},{"id":2732,"depth":212,"text":2733},{"id":2788,"depth":212,"text":2788},{"id":2881,"depth":192,"text":2882,"children":4187},[4188,4189],{"id":2885,"depth":212,"text":2886},{"id":2989,"depth":212,"text":2990},{"id":3073,"depth":192,"text":3074},{"id":3166,"depth":192,"text":3167},"2026-10-22","畫面會自動更新，是因為 Proxy 攔截了資料的讀寫：讀取時登記誰在用，寫入時通知它們重新執行。",{},"\u002Fnotes\u002Fvue3-reactivity",{"title":2067,"description":4193},"notes\u002Fvue3-reactivity",[2062,2063],"e1rgKC2tkAG6L1zuGIg_Nsgk4xPg2fqCdJogYUi_ZYE",{"id":4201,"title":4202,"body":4203,"date":4725,"description":4726,"draft":2055,"extension":2056,"meta":4727,"navigation":234,"path":4728,"seo":4729,"stem":4730,"tags":4731,"__hash__":4735},"notes\u002Fnotes\u002Fwireguard-ping-stale-mac.md","VPN 連得進來，卻 ping 不到家裡那一台：換主機板後殘留的舊 MAC",{"type":8,"value":4204,"toc":4715},[4205,4207,4229,4233,4244,4247,4251,4320,4330,4334,4561,4564,4568,4578,4595,4599,4604,4610,4615,4618,4623,4626,4631,4636,4640,4676,4680],[11,4206,13],{"id":13},[15,4208,4209,4211,4214,4217,4220,4223,4226],{},[18,4210,3012],{},[18,4212,4213],{},"環境",[18,4215,4216],{},"排查時間序",[18,4218,4219],{},"根本原因",[18,4221,4222],{},"幾個一開始想不通的地方",[18,4224,4225],{},"待辦",[18,4227,4228],{},"學到的事",[11,4230,4232],{"id":4231},"_1-問題","1. 問題",[47,4234,4235,4236,4239,4240,4243],{},"從外面透過 WireGuard 連回家（Mac，VPN IP ",[120,4237,4238],{},"10.66.0.2","），ping 不到 HomeStation（",[120,4241,4242],{},"192.168.66.180","）。但同一個 LAN 裡的另一台主機 WKPHP 可以 ping 得到。",[47,4245,4246],{},"兩天前也發生過一次，當時重開 HomeStation 就好了，所以這次決定查到底。",[11,4248,4250],{"id":4249},"_2-環境","2. 環境",[54,4252,4253,4265],{},[57,4254,4255],{},[60,4256,4257,4260,4262],{},[63,4258,4259],{},"設備",[63,4261,2113],{},[63,4263,4264],{},"IP",[73,4266,4267,4280,4295,4308],{},[60,4268,4269,4272,4275],{},[78,4270,4271],{},"ER605",[78,4273,4274],{},"路由器，WireGuard server，DHCP",[78,4276,4277],{},[120,4278,4279],{},"192.168.66.1",[60,4281,4282,4285,4291],{},[78,4283,4284],{},"HomeStation",[78,4286,4287,4288],{},"要連的目標，網卡 ",[120,4289,4290],{},"eno1",[78,4292,4293],{},[120,4294,4242],{},[60,4296,4297,4300,4303],{},[78,4298,4299],{},"WKPHP",[78,4301,4302],{},"同一個 LAN 的另一台主機",[78,4304,4305],{},[120,4306,4307],{},"192.168.66.190",[60,4309,4310,4313,4316],{},[78,4311,4312],{},"Mac",[78,4314,4315],{},"從外面連 VPN 的客戶端",[78,4317,4318],{},[120,4319,4238],{},[47,4321,4322,4323,4326,4327,370],{},"MAC 位址用代號替代：HomeStation 新主機板的網卡是 ",[120,4324,4325],{},"AA:BB:CC:DD:EE:FF","，換掉的舊網卡是 ",[120,4328,4329],{},"11:22:33:44:55:66",[11,4331,4333],{"id":4332},"_3-排查時間序2026-10-23","3. 排查時間序（2026-10-23）",[54,4335,4336,4349],{},[57,4337,4338],{},[60,4339,4340,4343,4346],{},[63,4341,4342],{},"時間",[63,4344,4345],{},"動作",[63,4347,4348],{},"結果與判斷",[73,4350,4351,4365,4382,4395,4407,4429,4443,4457,4471,4485,4496,4513,4534,4550],{},[60,4352,4353,4356,4362],{},[78,4354,4355],{},"21:24",[78,4357,4358,4359],{},"Mac 透過 VPN ping ",[120,4360,4361],{},".180",[78,4363,4364],{},"不通",[60,4366,4367,4370,4376],{},[78,4368,4369],{},"21:25",[78,4371,4358,4372,4375],{},[120,4373,4374],{},".190","（WKPHP）",[78,4377,4378,4379,4381],{},"通，TTL 63。VPN 和 ER605 的轉送都正常，問題出在 ",[120,4380,4361],{}," 這台",[60,4383,4384,4387,4392],{},[78,4385,4386],{},"21:27",[78,4388,4389,4390],{},"從 WKPHP ping ",[120,4391,4361],{},[78,4393,4394],{},"通。HomeStation 本身在線，LAN 內也正常",[60,4396,4397,4400,4403],{},[78,4398,4399],{},"21:28",[78,4401,4402],{},"查 Mac 的 VPN IP",[78,4404,4405],{},[120,4406,4238],{},[60,4408,4409,4412,4419],{},[78,4410,4411],{},"21:29 到 21:30",[78,4413,4414,4415,4418],{},"Mac 無法直接 SSH 到 HomeStation，改從 WKPHP 跳板（",[120,4416,4417],{},"ssh -J","）進去，檢查路由和防火牆",[78,4420,4421,4422,4424,4425,4428],{},"回程路由正常走 ",[120,4423,4279],{},"，default route 正確。iptables 裡有殘留的 ufw 規則，但 ",[120,4426,4427],{},"ufw"," 指令已經不存在",[60,4430,4431,4434,4440],{},[78,4432,4433],{},"21:31 到 21:32",[78,4435,4436,4437],{},"HomeStation 上跑 ",[120,4438,4439],{},"tcpdump",[78,4441,4442],{},"看得到 request，但沒有 reply",[60,4444,4445,4448,4454],{},[78,4446,4447],{},"21:34",[78,4449,4450,4451],{},"加 ",[120,4452,4453],{},"iptables -I INPUT 1 -s 10.66.0.0\u002F24 -j ACCEPT",[78,4455,4456],{},"還是不通，排除 INPUT 白名單的問題",[60,4458,4459,4462,4468],{},[78,4460,4461],{},"21:36",[78,4463,4464,4465,4467],{},"看 INPUT 的計數，",[120,4466,4439],{}," 改抓所有介面",[78,4469,4470],{},"計數是 0，封包根本沒進到 INPUT",[60,4472,4473,4475,4478],{},[78,4474,4461],{},[78,4476,4477],{},"檢查 nft、raw、mangle",[78,4479,4480,4481,4484],{},"只有 Docker 自動產生的規則，計數都是 0。Docker 的網段也避開了 ",[120,4482,4483],{},"192.168.64.0\u002F20","，排除網段衝突",[60,4486,4487,4490,4493],{},[78,4488,4489],{},"21:42 到 21:43",[78,4491,4492],{},"ER605 的 log 出現 PPPoE PADI timeout，VPN 整個斷線",[78,4494,4495],{},"HiNet 主線短暫斷線。這是另一件事，之後自己恢復",[60,4497,4498,4501,4507],{},[78,4499,4500],{},"21:46",[78,4502,4503,4504,4506],{},"查 ER605 的 DHCP 保留清單，和 HomeStation ",[120,4505,4290],{}," 的 MAC",[78,4508,4509,4510,4512],{},"兩邊都是 ",[120,4511,4325],{},"，一致",[60,4514,4515,4518,4524],{},[78,4516,4517],{},"21:47",[78,4519,4520,4523],{},[120,4521,4522],{},"tcpdump -e"," 看封包的 MAC",[78,4525,4526,4527,4529,4530,4533],{},"ER605 把封包送到 ",[120,4528,4329],{},"，",[50,4531,4532],{},"不是"," HomeStation 的 MAC",[60,4535,4536,4539,4542],{},[78,4537,4538],{},"21:50",[78,4540,4541],{},"查 ER605 的 ARP 表",[78,4543,4544,4546,4547],{},[120,4545,4361],{}," 確實對應到 ",[120,4548,4549],{},"11-22-33-44-55-66",[60,4551,4552,4555,4558],{},[78,4553,4554],{},"21:54",[78,4556,4557],{},"確認這個 MAC 是誰",[78,4559,4560],{},"HomeStation 換主機板之前的舊網卡",[47,4562,4563],{},"轉折點在 21:36 和 21:47：INPUT 計數是 0，代表封包在進防火牆之前就被丟了。往防火牆規則裡找是找不到的，要往更底層的第二層（MAC）看。",[11,4565,4567],{"id":4566},"_4-根本原因","4. 根本原因",[47,4569,4570,4571,4573,4574,4577],{},"ER605 的設定裡還留著「",[120,4572,4361],{}," 對應舊主機板 MAC」的紀錄，留在 ",[50,4575,4576],{},"IP-MAC Binding"," 裡（2026-10-23 在路由器上找到並改掉）。",[15,4579,4580,4583,4586,4592],{},[18,4581,4582],{},"換主機板後，新網卡上線時廣播了正確的 MAC，ER605 學到之後暫時用這筆新的動態紀錄，所以看起來一切正常",[18,4584,4585],{},"過一陣子，舊的紀錄又蓋回來",[18,4587,4588,4589,4591],{},"ER605 把要送給 ",[120,4590,4361],{}," 的封包，全部用舊網卡的 MAC 送出去",[18,4593,4594],{},"那張網卡已經不存在，HomeStation 收到「目的 MAC 不是自己」的封包，kernel 在進入防火牆之前就丟掉了",[11,4596,4598],{"id":4597},"_5-幾個一開始想不通的地方","5. 幾個一開始想不通的地方",[47,4600,4601],{},[50,4602,4603],{},"為什麼 WKPHP ping 得到？",[47,4605,4606,4607,4609],{},"WKPHP 和 HomeStation 在同一個 LAN，WKPHP 自己發 ARP 問「誰是 ",[120,4608,4361],{},"」，HomeStation 用新的 MAC 回答，整個過程不經過 ER605 的 ARP 表。從 VPN 進來的封包要經過 ER605 轉送，才會用到那筆錯的紀錄。",[47,4611,4612],{},[50,4613,4614],{},"為什麼重開 HomeStation 會暫時好？",[47,4616,4617],{},"開機時網卡上線會重新廣播自己的 MAC，ER605 又學到正確的那筆，直到舊紀錄再次蓋回來。所以兩天前的「重開就好」只是暫時的。",[47,4619,4620],{},[50,4621,4622],{},"目的 MAC 是一張不存在的網卡，HomeStation 為什麼還收得到？",[47,4624,4625],{},"交換器的 MAC 表裡沒有這個舊 MAC，遇到不認識的目的地就會把封包送到所有埠（unknown unicast flooding），所以 HomeStation 的網卡還是收得到。",[47,4627,4628],{},[50,4629,4630],{},"收得到，為什麼 tcpdump 看得到、INPUT 計數卻是 0？",[47,4632,4633,4635],{},[120,4634,4439],{}," 會把網卡切成混雜模式（promiscuous mode），連目的 MAC 不是自己的封包也會抓下來。但 kernel 處理時，目的 MAC 不符的封包會被標成「給別台主機的」，在進入 IP 層和 netfilter 之前就丟掉了。所以出現「tcpdump 看得到 request、防火牆計數是 0、沒有 reply」的組合。",[11,4637,4639],{"id":4638},"_6-待辦","6. 待辦",[808,4641,4644,4655,4664,4670],{"className":4642},[4643],"contains-task-list",[18,4645,4648,4651,4652,4654],{"className":4646},[4647],"task-list-item",[2409,4649],{"checked":234,"disabled":234,"type":4650},"checkbox"," ER605：把 IP-MAC Binding 裡 ",[120,4653,4361],{}," 的 MAC 改成新網卡的 MAC，並存檔（2026-10-23）",[18,4656,4658,4660,4661,4663],{"className":4657},[4647],[2409,4659],{"checked":234,"disabled":234,"type":4650}," HomeStation：移除測試時加的 ",[120,4662,4453],{},"（重開機也會自動消失）",[18,4665,4667,4669],{"className":4666},[4647],[2409,4668],{"checked":234,"disabled":234,"type":4650}," 觀察：改完兩天後（2026-10-25）從 VPN ping 得到。上次「重開就好」大約兩天後就復發，這次撐過了同樣的時間",[18,4671,4673,4675],{"className":4672},[4647],[2409,4674],{"disabled":234,"type":4650}," 順手整理：殘留的 ufw 規則是從哪裡載入的，另外處理",[11,4677,4679],{"id":4678},"_7-學到的事","7. 學到的事",[808,4681,4682,4688,4700,4709],{},[18,4683,4684,4687],{},[50,4685,4686],{},"換硬體要連路由器的設定一起換","：DHCP 保留和 IP-MAC Binding 都綁 MAC，換主機板、換網卡時兩邊都要更新，只改一邊會出現這種「時好時壞」的問題",[18,4689,4690,4693,4694,4696,4697,4699],{},[50,4691,4692],{},"先用對照組縮小範圍","：ping 同網段的另一台（",[120,4695,4374],{},"）通、ping 目標（",[120,4698,4361],{},"）不通，一步就排除了 VPN 和路由器轉送",[18,4701,4702,4705,4706,4708],{},[50,4703,4704],{},"防火牆計數是 0，就往下一層找","：封包沒進 INPUT，規則寫得再對也沒用。",[120,4707,4522],{}," 印出 MAC，第二層的問題才看得到",[18,4710,4711,4714],{},[50,4712,4713],{},"「重開就好」不是修好","：會自己恢復的問題，背後通常有個會被重新蓋回來的狀態",{"title":157,"searchDepth":192,"depth":192,"links":4716},[4717,4718,4719,4720,4721,4722,4723,4724],{"id":13,"depth":192,"text":13},{"id":4231,"depth":192,"text":4232},{"id":4249,"depth":192,"text":4250},{"id":4332,"depth":192,"text":4333},{"id":4566,"depth":192,"text":4567},{"id":4597,"depth":192,"text":4598},{"id":4638,"depth":192,"text":4639},{"id":4678,"depth":192,"text":4679},"2026-10-23","同一個 LAN 內 ping 得到，從 WireGuard 進來就不通。最後發現是路由器還記著換主機板之前的舊網卡 MAC。",{},"\u002Fnotes\u002Fwireguard-ping-stale-mac",{"title":4202,"description":4726},"notes\u002Fwireguard-ping-stale-mac",[4732,4733,4734],"網路","WireGuard","排查紀錄","o-NFGtShQbPRSYAxvAJ6acAOkSbrYF9kngwOagKLd6s",[4737,4979],{"id":4738,"title":4739,"body":4740,"date":4192,"description":4971,"draft":2055,"extension":2056,"meta":4972,"navigation":234,"path":4973,"seo":4974,"stem":4975,"tags":4976,"__hash__":4978},"essays\u002Fessays\u002Flazy-and-opportunistic.md","我懶，所以我投機取巧",{"type":8,"value":4741,"toc":4965},[4742,4745,4757,4760,4763,4766,4769,4773,4776,4781,4791,4797,4803,4807,4810,4813,4816,4819,4822,4825,4828,4831,4834,4837,4851,4854,4865,4868,4871,4877,4883,4889,4892,4895,4898,4902,4905,4908,4911,4914,4917,4920,4923,4926,4929,4932,4935,4938,4941,4944,4947,4950,4953,4956,4959,4962],[47,4743,4744],{},"某次在YT上刷到的脫口秀裡聽到一個段子（請恕我真想不起出處是哪了）：「三思而後行」，在暗諷職場的甩鍋文化。",[4746,4747,4748,4751,4754],"blockquote",{},[47,4749,4750],{},"一思：能不能不做？",[47,4752,4753],{},"再思：能不能晚點做？",[47,4755,4756],{},"三思：能不能讓別人做？",[47,4758,4759],{},"台下笑成一片。",[47,4761,4762],{},"我也笑了，而且笑得有點心虛，因為畢竟誰都想當薪水小偷。",[47,4764,4765],{},"後來經歷的一些事，沉澱反思後，我忽然意識到這是一個最高級的行動準則！",[47,4767,4768],{},"我懶，我投機取巧，我大方承認且理直氣壯，因為我很清楚一件事：時間是最貴的成本，花掉了就拿不回來。",[11,4770,4772],{"id":4771},"一笑聲背後的機會成本","一、笑聲背後的機會成本",[47,4774,4775],{},"脫口秀好笑，是因為它戳中了大家心照不宣的偷懶念頭。但現實是，組織裡那個有求必應、什麼都接的「好人」，才可能是整個團隊效率不彰的老鼠屎。\n而且他還很冤的要扛著做到流汗，嫌到流瀾的罵名，甚至主管可能完全沒有意識到這是最可怕的經費黑洞（加班費）。",[4746,4777,4778],{},[47,4779,4780],{},"一起把段子裡的調侃剝開，剩下的是資源分配和邊界管理哲學。",[47,4782,4783,4786,4787,4790],{},[50,4784,4785],{},"一思：能不能不做：先審需求。","\n很多臨時起意的事，或是客戶看到新功能的想法發散，有時只要冷靜問一句： ",[50,4788,4789],{},"「不做會怎樣？它真的解決了什麼問題？」"," 多半就自己消失了。不懂得拒絕的人，不是比較負責，而是沒把自己有限的時間當成成本。在他的帳本上，時間是免費的，所以什麼都可以接。可是時間偏偏是最貴的那一項。\n工程師的時間，就是寶貴的產能。",[47,4792,4793,4796],{},[50,4794,4795],{},"再思：能不能晚點做：讓時間幫你過濾。","\n不少被包裝成十萬火急的需求，其實只是需求方當下的焦慮，或是還沒想清楚的半成品。第一時間衝上去做，很可能陪著對方改了好幾版，最後全部推翻。晚一點回應，讓子彈飛一會兒，是成本最低的過濾方式。\n工程師們開發功能很像急診室，講求的是檢傷分類，不是先來後到，或是誰比較大聲（當然在工程師這邊可以用錢插隊....急診不行）",[47,4798,4799,4802],{},[50,4800,4801],{},"三思：能不能讓別人做：找到最適合的人或工具。","\n什麼都自己來，產出的上限就是一天的工時。把工作交給更適合的工具、流程或夥伴才能提高團隊效率，或是你要叫它甩鍋也行，我不爭辯。重點是，鍋甩給更適合的人，整體的成本就降下來了。\n用對的人與工具，才會是最有效率的辦法。",[11,4804,4806],{"id":4805},"二實際經歷懶出來的架構","二、實際經歷：懶出來的架構",[47,4808,4809],{},"從心虛到理直氣壯，我自己就經歷過一次。",[47,4811,4812],{},"我負責的是遊戲後台的報告畫面。需求很簡單：根據後端傳來的資料，用捲動的方式產生列表。每個遊戲是一個產品，各有自己的後端。",[47,4814,4815],{},"一開始走的是最直覺的一對一：一個產品一個前端站，前端直接把後端的位置寫死在程式裡。我意識到這樣做會碰到幾個問題：",[47,4817,4818],{},"第一是跨域，瀏覽器擋下前端打到另一個網域的請求，得處理 CORS。（前端一定都撞過這個從前端是無解的問題）",[47,4820,4821],{},"第二是後端的位置直接寫在前端，誰打開瀏覽器的開發者工具都看得到。（等著被 DDoS）",[47,4823,4824],{},"第三是後端一動，前端就得跟著動（甚至有的時候我根本不知道後端改了，還需要定期去「輪詢」確認後端位置）。維運有時候會改網域，我就得改程式、重新部署。而且一個產品不只一套環境，DEV、SIT、UAT、PROD 各有各的後端位置，改一次就是改好幾份。(工讀生等級的維護...沒啥技術含量，但就是需要花時間做手工藝，還容易出錯)",[47,4826,4827],{},"我是真的懶得處理跨域，因為這是瀏覽器限制，有些後端根本不知道這是啥玩意，所以我根據我不務正業時學到的Nginx知識，請維運幫我做反向代理（關公面前耍大刀...當下很抖）。前端只打同一個網域的路徑，由 NGINX 轉給後端。跨域的問題沒了，後端的位置藏起來了，維運要換網域，也是他改他的設定，前端完全不用動。",[47,4829,4830],{},"本來只是為了偷懶，後來我卻忽然意識到：既然轉發是由反向代理路由決定的，那一個前端，不就可以根據路由接 N 個後端嗎？",[47,4832,4833],{},"於是我換了做法。報告畫面根據網址判斷現在是哪個產品，動態組出對應的 API 路由，反向代理再依路由轉發到該產品的後端。",[47,4835,4836],{},"從此要新增一個產品時：",[808,4838,4839,4845],{},[18,4840,4841,4844],{},[50,4842,4843],{},"維運","：在反向代理加一行，指到新的後端。連那個後端的位置，都是維運自己配的",[18,4846,4847,4850],{},[50,4848,4849],{},"前端","：不用架新站，也不用重新部署，只要把新產品要用的圖片放上去",[47,4852,4853],{},"在 NGINX 裡，大概就是多這麼一行（示意）：",[152,4855,4859],{"className":4856,"code":4857,"language":4858,"meta":157,"style":157},"language-nginx shiki shiki-themes github-dark","location \u002Fapi\u002Fproduct-c\u002F { proxy_pass http:\u002F\u002F\u003C維運配的後端位置>\u002F; }\n","nginx",[120,4860,4861],{"__ignoreMap":157},[161,4862,4863],{"class":163,"line":164},[161,4864,4857],{},[47,4866,4867],{},"一開始請維運加設定時，我心裡其實有點過意不去：「明明是我這邊要支援新產品，怎麼變成維運要動手？我是不是把工作推給別人？」",[47,4869,4870],{},"後來我才發現，我根本沒有多推什麼給他，說不定還幫他省了事。",[47,4872,4873,4876],{},[50,4874,4875],{},"第一，那一行本來就省不掉。","\n就算維持一對一，為了閃過跨域、藏起後端，每個站本來就需要反向代理，每個前端專案各有一份設定檔，各自寫著自己的後端在哪裡。每多一個產品，維運就要多顧一個站：申請網域、處理憑證、安排部署，一樣都少不了。解耦之後，這些設定集中到同一個專案裡，一個產品一行。網域、憑證、部署全部省掉，要查、要改也只看一個地方。維運的工作不但沒有變多，反而少了一大截，而且省下的都是實實在在的真金白銀與時間。",[47,4878,4879,4882],{},[50,4880,4881],{},"第二，讓最適合的節點做最適合的事。","\n「哪個路由轉到哪個後端」本來就是反向代理的強項。放在 NGINX，改一行設定就生效。放在前端，就是改程式、打包、部署，每次都走完整的上線流程。",[47,4884,4885,4888],{},[50,4886,4887],{},"第三，產品越多，省得越多。","\n我曾經認真想像過，如果一直維持一對一會變成什麼樣子：假如產品成長到十個（遊戲換個皮就算一個新產品，十個其實很快），每個產品要分 DEV、SIT、UAT、PROD 四套環境，就是四十份要維護的程式碼。維運改一次網域，我就要翻遍四十份，找出該改的地方，再一份一份重新部署。",[47,4890,4891],{},"只要是人做的，就一定會改錯、會漏改。差別只在於，是在測試環境被發現，還是上線之後才被使用者發現。",[47,4893,4894],{},"那是我能預想到的地獄。（我不要）",[47,4896,4897],{},"解耦之後，新增產品的成本被壓到兩件小事：維運在各環境的設定加一行，前端放幾張圖。產品再多，前端始終只有一份程式碼要顧，維運始終只有一份設定要管。",[11,4899,4901],{"id":4900},"三do-smart-not-hard","三、Do smart, not hard",[47,4903,4904],{},"英文常說「Work smart, not hard」。但我更喜歡把 work 換成 do：「Do smart, not hard」。work 談的是工作的手段，do 談的是行動的選擇，先決定做什麼、不做什麼，再談怎麼做。",[47,4906,4907],{},"用力過猛的人，做事像一個單體架構：什麼都往自己身上堆，靠意志力硬扛所有流量。遇到問題，第一個念頭是「我要怎麼加班把它寫出來？」。累了，就告訴自己：「我很累，所以我很負責。」花掉的是一去不回的工時和精力。",[47,4909,4910],{},"做得聰明的人，做事像解耦之後的系統：先畫清楚邊界，再把負載分給最適合的節點。遇到問題，第一個念頭是「這件事一定要存在嗎？誰來做成本最低？」。做完了，他想的是：「這套流程跑順了，我隨時可以走開。」他花的力氣，最後變成可以重複使用的工具、架構和自動化。",[47,4912,4913],{},"這種懶，其實很早就有人替它說話。",[47,4915,4916],{},"網路上某個經理人流傳一句話：「我會選一個懶人去做困難的工作，因為他會找到最輕鬆的方法。」出處是誰不重要，這句話能流傳這麼廣，代表很多人都有同感。",[47,4918,4919],{},"軍事史上也有一個常被提起的分類：軍官可以分成聰明、愚蠢、勤奮、懶惰四種特質。又笨又勤快的最危險，因為他會不停製造錯誤。又聰明又勤快的，適合當幕僚。而最適合當最高統帥的，是又聰明又懶惰的人：他討厭窮忙，所以懂得放手，懂得抓重點。（不怕壞人千方百計，就怕蠢人靈機一動）",[47,4921,4922],{},"到了軟體工程，Perl 語言的作者賴瑞‧沃爾乾脆把「懶惰」列為程式設計師的三大美德之一：因為懶得重複做同一件事，所以會寫腳本、做工具、設計架構，讓系統自己跑。",[47,4924,4925],{},"把視野再拉遠一點，諾基亞有句廣告詞：「科技始終來自於人性。」我更願意說：科技始終來自於惰性。",[47,4927,4928],{},"人類的發明史，幾乎就是第三思的歷史：能不能讓別人做？只是這個「別人」，後來變成了機器。懶得走路，有了輪子、汽車、飛機。懶得手算，有了算盤，最後有了電腦。懶得洗衣服、掃地，有了洗衣機和掃地機器人。工程師懶得每次手動部署，於是有了 CI\u002FCD、Docker，還有我前面說的反向代理。",[47,4930,4931],{},"勤奮推動的是眼前的進度，懶推動的是做事方法的改變。如果每個人都甘願靠蠻力硬撐，我們大概到現在還在手抄帳本，反正「加班就抄得完」。",[47,4933,4934],{},"剛入行時，我是用力過猛的那種人（濫好人），總在想：「怎麼用我自己掌控的工具，把需求全部兜出來？」現在我比較常想的是：「這件事放在系統的哪個環節處理，整體的維護成本最低？」",[47,4936,4937],{},"Do smart 就是投機取巧。投機，是看準時機：什麼事現在不必做、什麼事可以等以及什麼事現在不做就會死。取巧，是挑最省力的做法：交給誰、用什麼工具或辦法。這四個字被用成貶義太久了，但拆開來看，講的正是做事最需要的判斷。（事分輕重緩急）",[47,4939,4940],{},"真正該怕的，是「用戰術上的勤奮，掩蓋戰略上的懶惰」：手上很忙，腦袋很懶，懶得想這件事該不該做、該誰來做。真正的推卸責任，是自己沒想清楚，就把混亂丟給下游。成熟的分工，是看清系統的全貌，設計出合理的介面和規範，讓每個環節在最擅長的位置運轉。",[11,4942,4943],{"id":4943},"再三思",[47,4945,4946],{},"所以，下次再聽到那個段子，不妨認真把三思再想一遍。",[47,4948,4949],{},"一思，能不能不做？砍掉的假需求，就是省下的專注力。",[47,4951,4952],{},"再思，能不能晚點做？讓時間先替你過濾一輪雜訊，手上有沒有更重要的事需要先解決。",[47,4954,4955],{},"三思，能不能讓別人做？讓維運加那一行，讓 NGINX 轉那個請求，讓機器跑那些重複的事。",[47,4957,4958],{},"懶人的三思，一次都不能少：每一思，省下的都是往後無數次的重複勞動（還有時間，時間就是金錢）。",[47,4960,4961],{},"懶就是懶，投機取巧就是投機取巧，不必換個好聽的名字。我只是比較早想通：時間最貴，所以每一分鐘都要花在只有我能做、而且真正值得做的事情上。其他的，能不做就不做，能晚點就晚點，能讓別人做就讓別人做！",[2022,4963,4964],{},"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);}",{"title":157,"searchDepth":192,"depth":192,"links":4966},[4967,4968,4969,4970],{"id":4771,"depth":192,"text":4772},{"id":4805,"depth":192,"text":4806},{"id":4900,"depth":192,"text":4901},{"id":4943,"depth":192,"text":4943},"聽懂脫口秀的「三思」，才讀懂系統架構",{},"\u002Fessays\u002Flazy-and-opportunistic",{"title":4739,"description":4971},"essays\u002Flazy-and-opportunistic",[4977],"隨筆","pCqejfeno1hOSp4OAhCl-etvuIu6GBVpb4VKUu_0tC8",{"id":4980,"title":4981,"body":4982,"date":5191,"description":5192,"draft":2055,"extension":2056,"meta":5193,"navigation":234,"path":5194,"seo":5195,"stem":5196,"tags":5197,"__hash__":5198},"essays\u002Fessays\u002Fwhy-this-site.md","為什麼做這個網站",{"type":8,"value":4983,"toc":5189},[4984,4989,4992,4997,5000,5003,5008,5011,5016,5019,5024,5032,5035,5040,5043,5048,5051,5056,5059,5062,5067,5077,5082,5085,5090,5093,5102,5105,5150,5155,5158,5169,5172,5180,5183,5186],[4746,4985,4986],{},[47,4987,4988],{},"因為在準備自己的履歷，發現很多想講但放不下的廢話。\n索性就來浪費才華，用自己學到的技能來弄個可以讓我寫廢話的地方。",[47,4990,4991],{},"很多想寫的東西，但是我覺得寫在關於又很冗長，所以就寫在這裡吧。",[4746,4993,4994],{},[47,4995,4996],{},"忽然覺得自己一直都在不務正業...",[47,4998,4999],{},"大學唸的是生物科技，某天心血來潮去圖書館借了一本Java的書來啃，開篇教你用TXT手刻後編譯，超硬",[47,5001,5002],{},"心想反正學費都付了，就去旁修一些想學的課（賺！）。先是 C#，上半學期很好玩，因為C#不是冷冰冰的CLI介面，對於新手而言很有成就感，但老師下半學期都只講考證照的內容我就棄坑了，\n後來又去學Flash (R.I.P 淚)也是跟上面一樣的狀況，上半學期超好玩，要處理物體的碰撞邊界什麼的，但下學期就只講證照考試內容。\n開始工作後也花錢去學了一點Android...但是老實說外面學店教的，真的只是皮毛",[4746,5004,5005],{},[47,5006,5007],{},"某程度上，我對程式是有想知道在幹嘛的動力",[47,5009,5010],{},"上台北從事硬體工程師是我的第二份工作，我還記得我面試的時候，\n面試我的是(前)老闆，他問了我一句：公司實驗室內的設備，你認得多少？\n涉世未深的我直接回：「我一台都不認識，我大學實驗專題做的是電泳跟轉印。」\n再然後就是錄取通知...(笑)\n工作的內容就是修理化學的分析設備\n高效液相層析儀HPLC 液相層析串聯質譜LC-MS等等\n工作的時候發現，我大學時期為了打遊戲\n學會了沙盒，",[4746,5012,5013],{},[47,5014,5015],{},"從頭學起，碼農人生",[47,5017,5018],{},"之後為了家中的新成員，想著需要一份可以在任何地方(家裡)都能做事的工作\n我又想起我大學時期學的內容，這次我想要系統性的從根本好好的學習",[4746,5020,5021],{},[47,5022,5023],{},"後端變前端",[47,5025,5026,5027,5031],{},"我選擇JAVA(因為後端錢多)，就這樣從變數如何宣告，迴圈，到封裝，繼承，抽象方法...\n然後我就去當前端了(笑)\n故事背景是，我一個朋友的團隊內，網頁前端已經斷層\n甚至願意找一個什麼都不會的來",[5028,5029,5030],"del",{},"背鍋"," ....",[47,5033,5034],{},"所以其實面試當天就是現場錄取，(虧我還西裝筆挺)當天就開始教我網頁怎麼開F12，看console錯誤跟下指令...\n接著是那位帶我的前輩在一週後離職(哭啊)，而且後來才知道那位前輩還是個PHP後端兼前端。\n我還記得負責QA的同事跑來問我，你扛得住嗎？",[4746,5036,5037],{},[47,5038,5039],{},"我那時告訴我自己，我絕不說我不知道當藉口，但我一定要搞清楚",[47,5041,5042],{},"默默地記下同事給出來的資訊後自己查\n慢慢懂什麼是環境，前端後端的分工差異\nAPI是什麼以及怎麼接，Git是啥鬼，webSocket是什麼，還有CDN是什麼",[4746,5044,5045],{},[47,5046,5047],{},"而讓我開始摸索架站的起點，是起因於一個很基本的低級錯誤",[47,5049,5050],{},"在本地開發的時候作業系統用的是windows，當時犯一個很低級的錯誤。\n我在本地看圖片結果是正常的，但是放到線上主機就變成叉燒包。\n我查了很久，最後是維運同仁直接進到伺服器內查看檔案，才發現是檔案名稱大小寫的問題。",[4746,5052,5053],{},[47,5054,5055],{},"我想知道，但我不知道怎麼開口問",[47,5057,5058],{},"其實一直很想知道，我前端寫好的網頁，是如何放到網路上變成別人可以看的網站\n我一直有的藍圖就是以前(印象是國中)學Dreamweaver 時老師教作業傳到FTP主機內且改成index.html就能被看到\n後來去學java時又接觸了Apache Tomcat,但其實對於網頁要如何變成網站都很模糊",[47,5060,5061],{},"在心中有完整的原理架構，已經是我從事硬體工程師的習慣\n當有問題我可以跟著邏輯脈絡一路往下追，這習慣一直留著。\n而前端又是一個問題的分類帽(笑)，客服，PM看到畫面有問題，最直觀就是看到畫面出錯，下意識先找前端問，\n而我的習慣又是接到客戶電話反饋問題，我在心中會開始推算大概可能有問題的地方在哪\n但是現在的前端工作有個盲區：\n如果問題是前端，我可以從程式碼看出來\n如果問題是後端，可以從API看出來\n如果是維運層面，就抓瞎，因為我根本不懂我寫的網頁到底是怎麼被放到網路上被人看見，\n基於上述的原因，我開始嘗試自建個人的實驗伺服器，至少，我可以先打包丟上去確認不會再有名稱大小寫的低能問題",[4746,5063,5064],{},[47,5065,5066],{},"舊電腦重生，Linux Yes",[47,5068,5069,5070,5076],{},"我想起我有一台老筆電，我決定讓他穢土轉生\n我幫那台電腦灌了centOS\n也很剛好有個前端的同事有接觸過centOS，他跟我說去看",[602,5071,5075],{"href":5072,"rel":5073},"https:\u002F\u002Flinux.vbird.org\u002F",[5074],"nofollow","鳥哥","，跟著一步一步做\n我照著我的印象，安裝了Tomcat後將我寫好的東西丟一份上去，為了可以好傳檔案，還裝了FTP\n這時維運同仁聽到我們在討論，他覺得前端會想弄伺服器來玩是真的很罕見(他不好意思說我不務正業)\n他當時隨口丟下一句：「幹嘛不直接用 Docker 加 Nginx？」\n我以為他說的是鴨子...",[4746,5078,5079],{},[47,5080,5081],{},"GCP?",[47,5083,5084],{},"再然後我發現GCP有新手禮包300美金可以玩\n我也去試著玩了一下，只能說，線上的效能跟我筆電比起來爛多了\n而且很多內網外網設定等等...\n我嫌麻煩，也就只是玩過就關掉了",[4746,5086,5087],{},[47,5088,5089],{},"資訊爆炸，學無止境",[47,5091,5092],{},"而我因為是外行，很多時候我也是會到處看教學來跟上團隊溝通\n身為一個網路重度依賴患者，常常流連YT是很正常的事。\n某天無意間我刷到有人在教git:",[808,5094,5095],{},[18,5096,5097],{},[602,5098,5101],{"href":5099,"rel":5100},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=OG0g2tEF2XU",[5074],"直接使用git pull拉代码，被同事狠狠diss了！",[47,5103,5104],{},"淺顯易懂還有操作步驟跟說明，我後來陸陸續續在這個YTer頻道跟著做了很多東西:",[808,5106,5107,5114,5121,5129,5136,5143],{},[18,5108,5109],{},[602,5110,5113],{"href":5111,"rel":5112},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=Evdky7VEzj4",[5074],"Github的王炸功能，但很少人知道怎么用？",[18,5115,5116],{},[602,5117,5120],{"href":5118,"rel":5119},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=lFMFgUv0gr0",[5074],"四个简单的小脚本，完美解决Windows使用痛点，Win10右键菜单，永久暂停Windows更新，卸载小组件，家庭版开启hyper-v",[18,5122,5123,5128],{},[602,5124,5127],{"href":5125,"rel":5126},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=tXPvKIQGS8I",[5074],"1分钟学会Win\u002FLinux\u002FMac上安装Docker","(原來是docker 不是 duck)",[18,5130,5131],{},[602,5132,5135],{"href":5133,"rel":5134},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=L1eDBgdCmuU",[5074],"Cloudflare浑身都是宝，普通用户能白嫖多少服务？",[18,5137,5138],{},[602,5139,5142],{"href":5140,"rel":5141},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=_-IPi1a774E",[5074],"40分钟的Docker实战攻略，一期视频精通Docker",[18,5144,5145],{},[602,5146,5149],{"href":5147,"rel":5148},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=bMiOYsDtpGM",[5074],"为什么React的漏洞能攻破服务器",[4746,5151,5152],{},[47,5153,5154],{},"遇事不決問AI，你的隨身高級萬能家教",[47,5156,5157],{},"後來，AI開始發展，我個人對於AI的態度，就是他是一個我可以不用管他會不會生氣，也不用管他有沒有空的萬能資深前輩",[808,5159,5160,5163,5166],{},[18,5161,5162],{},"nginx是什麼",[18,5164,5165],{},"什麼是反向代理，怎麼做",[18,5167,5168],{},"jenkins是什麼",[47,5170,5171],{},"等等...我不知道的我就直接問AI\n而且AI有個好處，就是你不用做功課，可以逮著亂問，反正他沒脾氣\n我再根據我拿到的資訊，繼續用我理解的語言反問是否正確",[808,5173,5174,5177],{},[18,5175,5176],{},"這個指令要怎麼下",[18,5178,5179],{},"我想做什麼，怎麼辦",[47,5181,5182],{},"從此又開始不務正業的自學了 Nginx、Docker 和 CI\u002FCD腳本",[47,5184,5185],{},"但也因此，我終於有全端的視野，意外的發現其實很多問題的根本解方不是從前端，而是從產品的架構上下手\n也省下很多無意義的工作時間消耗。",[47,5187,5188],{},"到此回過頭再看，那時不務正業學到的知識，好像讓我能更輕鬆的完成本職工作",{"title":157,"searchDepth":192,"depth":192,"links":5190},[],"2026-10-15","想亂寫一些在簡歷中放不下的東西",{},"\u002Fessays\u002Fwhy-this-site",{"title":4981,"description":5192},"essays\u002Fwhy-this-site",[4977],"k3G4l6a7Kb33wNu8zyEPXl1LoRuYeKnYY7737_pAbQ4",1793181520266]