[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fvue2-vs-vue3-components":3},{"id":4,"title":5,"body":6,"category":402,"date":732,"description":733,"draft":734,"extension":735,"meta":736,"navigation":111,"path":737,"readingTime":557,"seo":738,"stem":739,"tags":740,"__hash__":743},"blog\u002Fblog\u002Fvue2-vs-vue3-components.md","Vue 2 與 Vue 3 元件生命週期與雙向綁定差異解析",{"type":7,"value":8,"toc":724},"minimark",[9,13,18,34,130,138,237,240,358,369,373,398,455,580,596,600,656,659,711,714,720],[10,11,12],"p",{},"從 Vue 2 遷移到 Vue 3，最容易出問題的不是語法本身，而是「預設行為改變」：同樣的程式碼在新版仍能跑，但行為不一樣。這篇整理元件層級最常碰到的幾個差異，以及遷移時的檢查重點。",[14,15,17],"h2",{"id":16},"響應式系統defineproperty-到-proxy","響應式系統：defineProperty 到 Proxy",[10,19,20,21,25,26,29,30,33],{},"Vue 2 用 ",[22,23,24],"code",{},"Object.defineProperty"," 攔截屬性，所以有幾個限制：新增屬性、用索引改陣列元素，都偵測不到，必須使用 ",[22,27,28],{},"Vue.set","。Vue 3 改用 ",[22,31,32],{},"Proxy","，這些限制消失了。",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-js shiki shiki-themes github-dark","\u002F\u002F Vue 2：新增屬性不會觸發更新\nthis.user.age = 30        \u002F\u002F 無響應\nthis.$set(this.user, 'age', 30)\n\n\u002F\u002F Vue 3：直接指定即可\nstate.user.age = 30\n","js","",[22,42,43,52,73,106,113,119],{"__ignoreMap":40},[44,45,48],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sAwPA","\u002F\u002F Vue 2：新增屬性不會觸發更新\n",[44,53,55,59,63,67,70],{"class":46,"line":54},2,[44,56,58],{"class":57},"sDLfK","this",[44,60,62],{"class":61},"s95oV",".user.age ",[44,64,66],{"class":65},"snl16","=",[44,68,69],{"class":57}," 30",[44,71,72],{"class":50},"        \u002F\u002F 無響應\n",[44,74,76,78,81,85,88,90,93,97,100,103],{"class":46,"line":75},3,[44,77,58],{"class":57},[44,79,80],{"class":61},".",[44,82,84],{"class":83},"svObZ","$set",[44,86,87],{"class":61},"(",[44,89,58],{"class":57},[44,91,92],{"class":61},".user, ",[44,94,96],{"class":95},"sU2Wk","'age'",[44,98,99],{"class":61},", ",[44,101,102],{"class":57},"30",[44,104,105],{"class":61},")\n",[44,107,109],{"class":46,"line":108},4,[44,110,112],{"emptyLinePlaceholder":111},true,"\n",[44,114,116],{"class":46,"line":115},5,[44,117,118],{"class":50},"\u002F\u002F Vue 3：直接指定即可\n",[44,120,122,125,127],{"class":46,"line":121},6,[44,123,124],{"class":61},"state.user.age ",[44,126,66],{"class":65},[44,128,129],{"class":57}," 30\n",[10,131,132,133,137],{},"但 Proxy 帶來另一個陷阱：",[134,135,136],"strong",{},"解構會丟失響應性","。",[35,139,141],{"className":37,"code":140,"language":39,"meta":40,"style":40},"import { reactive, toRefs } from 'vue'\n\nconst state = reactive({ count: 0 })\n\nconst { count } = state          \u002F\u002F count 只是一個普通數字，不再響應\nconst { count: c } = toRefs(state) \u002F\u002F c 是 ref，保持響應\n",[22,142,143,157,161,184,188,209],{"__ignoreMap":40},[44,144,145,148,151,154],{"class":46,"line":47},[44,146,147],{"class":65},"import",[44,149,150],{"class":61}," { reactive, toRefs } ",[44,152,153],{"class":65},"from",[44,155,156],{"class":95}," 'vue'\n",[44,158,159],{"class":46,"line":54},[44,160,112],{"emptyLinePlaceholder":111},[44,162,163,166,169,172,175,178,181],{"class":46,"line":75},[44,164,165],{"class":65},"const",[44,167,168],{"class":57}," state",[44,170,171],{"class":65}," =",[44,173,174],{"class":83}," reactive",[44,176,177],{"class":61},"({ count: ",[44,179,180],{"class":57},"0",[44,182,183],{"class":61}," })\n",[44,185,186],{"class":46,"line":108},[44,187,112],{"emptyLinePlaceholder":111},[44,189,190,192,195,198,201,203,206],{"class":46,"line":115},[44,191,165],{"class":65},[44,193,194],{"class":61}," { ",[44,196,197],{"class":57},"count",[44,199,200],{"class":61}," } ",[44,202,66],{"class":65},[44,204,205],{"class":61}," state          ",[44,207,208],{"class":50},"\u002F\u002F count 只是一個普通數字，不再響應\n",[44,210,211,213,215,218,221,224,226,228,231,234],{"class":46,"line":121},[44,212,165],{"class":65},[44,214,194],{"class":61},[44,216,197],{"class":217},"s9osk",[44,219,220],{"class":61},": ",[44,222,223],{"class":57},"c",[44,225,200],{"class":61},[44,227,66],{"class":65},[44,229,230],{"class":83}," toRefs",[44,232,233],{"class":61},"(state) ",[44,235,236],{"class":50},"\u002F\u002F c 是 ref，保持響應\n",[14,238,239],{"id":239},"生命週期對照",[241,242,243,259],"table",{},[244,245,246],"thead",{},[247,248,249,253,256],"tr",{},[250,251,252],"th",{},"Vue 2",[250,254,255],{},"Vue 3（Options API）",[250,257,258],{},"Composition API",[260,261,262,284,304,324,341],"tbody",{},[247,263,264,274,277],{},[265,266,267,270,271],"td",{},[22,268,269],{},"beforeCreate"," \u002F ",[22,272,273],{},"created",[265,275,276],{},"同左",[265,278,279,280,283],{},"直接寫在 ",[22,281,282],{},"setup()"," 內",[247,285,286,294,296],{},[265,287,288,270,291],{},[22,289,290],{},"beforeMount",[22,292,293],{},"mounted",[265,295,276],{},[265,297,298,270,301],{},[22,299,300],{},"onBeforeMount",[22,302,303],{},"onMounted",[247,305,306,314,316],{},[265,307,308,270,311],{},[22,309,310],{},"beforeUpdate",[22,312,313],{},"updated",[265,315,276],{},[265,317,318,270,321],{},[22,319,320],{},"onBeforeUpdate",[22,322,323],{},"onUpdated",[247,325,326,331,336],{},[265,327,328],{},[22,329,330],{},"beforeDestroy",[265,332,333],{},[22,334,335],{},"beforeUnmount",[265,337,338],{},[22,339,340],{},"onBeforeUnmount",[247,342,343,348,353],{},[265,344,345],{},[22,346,347],{},"destroyed",[265,349,350],{},[22,351,352],{},"unmounted",[265,354,355],{},[22,356,357],{},"onUnmounted",[10,359,360,361,364,365,368],{},"重點是 ",[22,362,363],{},"destroy"," 改名為 ",[22,366,367],{},"unmount","。遷移時如果漏改，舊名稱的鉤子在 Vue 3 不會執行，原本放在裡面的清理工作（移除監聽、清除計時器）就默默失效了。",[14,370,372],{"id":371},"v-model-的改變","v-model 的改變",[10,374,375,376,379,380,383,384,387,388,391,392,395,396,137],{},"Vue 2 的 ",[22,377,378],{},"v-model"," 預設綁 ",[22,381,382],{},"value"," prop 與 ",[22,385,386],{},"input"," 事件；Vue 3 改成 ",[22,389,390],{},"modelValue"," 與 ",[22,393,394],{},"update:modelValue","，並且支援多個 ",[22,397,378],{},[35,399,403],{"className":400,"code":401,"language":402,"meta":40,"style":40},"language-vue shiki shiki-themes github-dark","\u003C!-- 父層 -->\n\u003CUserForm v-model:name=\"name\" v-model:email=\"email\" \u002F>\n","vue",[22,404,405,410],{"__ignoreMap":40},[44,406,407],{"class":46,"line":47},[44,408,409],{"class":50},"\u003C!-- 父層 -->\n",[44,411,412,415,419,422,425,428,430,433,435,437,439,441,444,446,448,450,452],{"class":46,"line":54},[44,413,414],{"class":61},"\u003C",[44,416,418],{"class":417},"s4JwU","UserForm",[44,420,421],{"class":83}," v-model",[44,423,424],{"class":61},":",[44,426,427],{"class":83},"name",[44,429,66],{"class":61},[44,431,432],{"class":95},"\"",[44,434,427],{"class":61},[44,436,432],{"class":95},[44,438,421],{"class":83},[44,440,424],{"class":61},[44,442,443],{"class":83},"email",[44,445,66],{"class":61},[44,447,432],{"class":95},[44,449,443],{"class":61},[44,451,432],{"class":95},[44,453,454],{"class":61}," \u002F>\n",[35,456,458],{"className":400,"code":457,"language":402,"meta":40,"style":40},"\u003C!-- 子層（Vue 3.4+）：defineModel 取代手動宣告 props 與 emit -->\n\u003Cscript setup>\nconst name = defineModel('name')\nconst email = defineModel('email')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"name\">\n  \u003Cinput v-model=\"email\">\n\u003C\u002Ftemplate>\n",[22,459,460,465,478,497,515,524,528,538,555,571],{"__ignoreMap":40},[44,461,462],{"class":46,"line":47},[44,463,464],{"class":50},"\u003C!-- 子層（Vue 3.4+）：defineModel 取代手動宣告 props 與 emit -->\n",[44,466,467,469,472,475],{"class":46,"line":54},[44,468,414],{"class":61},[44,470,471],{"class":417},"script",[44,473,474],{"class":83}," setup",[44,476,477],{"class":61},">\n",[44,479,480,482,485,487,490,492,495],{"class":46,"line":75},[44,481,165],{"class":65},[44,483,484],{"class":57}," name",[44,486,171],{"class":65},[44,488,489],{"class":83}," defineModel",[44,491,87],{"class":61},[44,493,494],{"class":95},"'name'",[44,496,105],{"class":61},[44,498,499,501,504,506,508,510,513],{"class":46,"line":108},[44,500,165],{"class":65},[44,502,503],{"class":57}," email",[44,505,171],{"class":65},[44,507,489],{"class":83},[44,509,87],{"class":61},[44,511,512],{"class":95},"'email'",[44,514,105],{"class":61},[44,516,517,520,522],{"class":46,"line":115},[44,518,519],{"class":61},"\u003C\u002F",[44,521,471],{"class":417},[44,523,477],{"class":61},[44,525,526],{"class":46,"line":121},[44,527,112],{"emptyLinePlaceholder":111},[44,529,531,533,536],{"class":46,"line":530},7,[44,532,414],{"class":61},[44,534,535],{"class":417},"template",[44,537,477],{"class":61},[44,539,541,544,546,548,550,553],{"class":46,"line":540},8,[44,542,543],{"class":61},"  \u003C",[44,545,386],{"class":417},[44,547,421],{"class":83},[44,549,66],{"class":61},[44,551,552],{"class":95},"\"name\"",[44,554,477],{"class":61},[44,556,558,560,562,564,566,569],{"class":46,"line":557},9,[44,559,543],{"class":61},[44,561,386],{"class":417},[44,563,421],{"class":83},[44,565,66],{"class":61},[44,567,568],{"class":95},"\"email\"",[44,570,477],{"class":61},[44,572,574,576,578],{"class":46,"line":573},10,[44,575,519],{"class":61},[44,577,535],{"class":417},[44,579,477],{"class":61},[10,581,582,583,270,585,587,588,270,590,592,593,595],{},"自訂元件如果在 Vue 2 依賴 ",[22,584,382],{},[22,586,386],{},"，遷移時要改成 ",[22,589,390],{},[22,591,394],{},"，否則父層的 ",[22,594,378],{}," 會失效。",[14,597,599],{"id":598},"插槽與事件-api","插槽與事件 API",[601,602,603,622,645],"ul",{},[604,605,606,607,610,611,614,615,618,619,137],"li",{},"Vue 3 把 ",[22,608,609],{},"$scopedSlots"," 併入 ",[22,612,613],{},"$slots","，所有插槽都是函式；範本語法統一用 ",[22,616,617],{},"v-slot"," 或 ",[22,620,621],{},"#",[604,623,624,627,628,627,631,634,635,638,639,270,642,137],{},[22,625,626],{},"$on","、",[22,629,630],{},"$off",[22,632,633],{},"$once"," 在 Vue 3 被移除。以往拿元件實例當 Event Bus 的寫法需要改用 ",[22,636,637],{},"mitt"," 之類的函式庫，或改成 props \u002F emits、",[22,640,641],{},"provide",[22,643,644],{},"inject",[604,646,647,648,651,652,655],{},"元件可以有多個根節點（Fragments）。這時 ",[22,649,650],{},"class"," 等 fallthrough 屬性不再自動套用到單一根節點，需要用 ",[22,653,654],{},"v-bind=\"$attrs\""," 指定位置。",[14,657,658],{"id":658},"遷移檢查清單",[660,661,662,675,690,701,708],"ol",{},[604,663,664,665,270,667,669,670,672,673,137],{},"找出所有 ",[22,666,382],{},[22,668,386],{}," 風格的自訂 ",[22,671,378],{},"，改成 ",[22,674,390],{},[604,676,677,678,627,680,627,682,627,684,627,686,627,688,137],{},"搜尋 ",[22,679,330],{},[22,681,347],{},[22,683,626],{},[22,685,630],{},[22,687,609],{},[22,689,84],{},[604,691,692,693,696,697,700],{},"檢查所有解構 ",[22,694,695],{},"reactive"," 物件的地方，改用 ",[22,698,699],{},"toRefs"," 或直接存取屬性。",[604,702,703,704,707],{},"多根節點元件：確認 ",[22,705,706],{},"$attrs"," 套用在預期的元素上。",[604,709,710],{},"使用 UI 元件庫時，同步查閱該版本的遷移指南，因為元件 API 與預設樣式常有差異。",[14,712,713],{"id":713},"小結",[10,715,716,717,719],{},"遷移的核心是「理解差異再改」，而不是把舊版寫法原封不動搬到新版。生命週期改名、",[22,718,378],{}," 協議、解構失去響應性這三項，最容易在沒有錯誤訊息的情況下造成行為異常，值得優先逐項檢查。",[721,722,723],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":40,"searchDepth":54,"depth":54,"links":725},[726,727,728,729,730,731],{"id":16,"depth":54,"text":17},{"id":239,"depth":54,"text":239},{"id":371,"depth":54,"text":372},{"id":598,"depth":54,"text":599},{"id":658,"depth":54,"text":658},{"id":713,"depth":54,"text":713},"2025-07-15","從 v-model 演進、defineModel 到 reactive\u002Fref 踩坑實錄，整理元件實作的關鍵差異。",false,"md",{},"\u002Fblog\u002Fvue2-vs-vue3-components",{"title":5,"description":733},"blog\u002Fvue2-vs-vue3-components",[741,742,378],"Vue 3","Migration","ukERlUAiwHrNqTd_dl5URU4BpzyV3PFLTb3llo-ivNY"]