[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fjs-event-loop":3},{"id":4,"title":5,"body":6,"category":66,"date":915,"description":916,"draft":917,"extension":918,"meta":919,"navigation":197,"path":920,"readingTime":317,"seo":921,"stem":922,"tags":923,"__hash__":926},"blog\u002Fblog\u002Fjs-event-loop.md","JavaScript 事件循環、非同步與作用域理解",{"type":7,"value":8,"toc":907},"minimark",[9,13,17,20,54,61,206,226,230,240,335,339,342,531,534,538,587,679,682,689,786,800,803,869,872,903],[10,11,12],"p",{},"JavaScript 在瀏覽器裡是單執行緒：同一時間只有一段程式碼在呼叫堆疊（call stack）上執行。非同步並不是「同時執行」，而是把工作排進佇列，等堆疊清空後再依規則一個一個取出來。看懂這套規則，就能解釋大部分「為什麼順序跟我想的不一樣」的問題。",[14,15,16],"h2",{"id":16},"事件循環的基本模型",[10,18,19],{},"一次循環大致是這樣：",[21,22,23,32,48,51],"ol",{},[24,25,26,27,31],"li",{},"從任務佇列（macrotask queue）取出一個任務執行，例如整段 script、",[28,29,30],"code",{},"setTimeout"," 的回呼、使用者事件。",[24,33,34,35,39,40,43,44,47],{},"這個任務結束後，",[36,37,38],"strong",{},"清空整個微任務佇列","（microtask queue），例如 ",[28,41,42],{},"Promise.then","、",[28,45,46],{},"queueMicrotask","。",[24,49,50],{},"瀏覽器視情況進行渲染。",[24,52,53],{},"回到第 1 步，取下一個任務。",[10,55,56,57,60],{},"重點是第 2 步：微任務會在「下一個任務之前」全部執行完，所以它永遠比 ",[28,58,59],{},"setTimeout(fn, 0)"," 更早。",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-js shiki shiki-themes github-dark","console.log('1')\nsetTimeout(() => console.log('2'), 0)\nPromise.resolve().then(() => console.log('3'))\nqueueMicrotask(() => console.log('4'))\nconsole.log('5')\n\n\u002F\u002F 輸出順序：1 5 3 4 2\n","js","",[28,69,70,93,124,158,178,192,199],{"__ignoreMap":67},[71,72,75,79,83,86,90],"span",{"class":73,"line":74},"line",1,[71,76,78],{"class":77},"s95oV","console.",[71,80,82],{"class":81},"svObZ","log",[71,84,85],{"class":77},"(",[71,87,89],{"class":88},"sU2Wk","'1'",[71,91,92],{"class":77},")\n",[71,94,96,98,101,105,108,110,112,115,118,122],{"class":73,"line":95},2,[71,97,30],{"class":81},[71,99,100],{"class":77},"(() ",[71,102,104],{"class":103},"snl16","=>",[71,106,107],{"class":77}," console.",[71,109,82],{"class":81},[71,111,85],{"class":77},[71,113,114],{"class":88},"'2'",[71,116,117],{"class":77},"), ",[71,119,121],{"class":120},"sDLfK","0",[71,123,92],{"class":77},[71,125,127,130,133,136,139,142,144,146,148,150,152,155],{"class":73,"line":126},3,[71,128,129],{"class":120},"Promise",[71,131,132],{"class":77},".",[71,134,135],{"class":81},"resolve",[71,137,138],{"class":77},"().",[71,140,141],{"class":81},"then",[71,143,100],{"class":77},[71,145,104],{"class":103},[71,147,107],{"class":77},[71,149,82],{"class":81},[71,151,85],{"class":77},[71,153,154],{"class":88},"'3'",[71,156,157],{"class":77},"))\n",[71,159,161,163,165,167,169,171,173,176],{"class":73,"line":160},4,[71,162,46],{"class":81},[71,164,100],{"class":77},[71,166,104],{"class":103},[71,168,107],{"class":77},[71,170,82],{"class":81},[71,172,85],{"class":77},[71,174,175],{"class":88},"'4'",[71,177,157],{"class":77},[71,179,181,183,185,187,190],{"class":73,"line":180},5,[71,182,78],{"class":77},[71,184,82],{"class":81},[71,186,85],{"class":77},[71,188,189],{"class":88},"'5'",[71,191,92],{"class":77},[71,193,195],{"class":73,"line":194},6,[71,196,198],{"emptyLinePlaceholder":197},true,"\n",[71,200,202],{"class":73,"line":201},7,[71,203,205],{"class":204},"sAwPA","\u002F\u002F 輸出順序：1 5 3 4 2\n",[10,207,208,209,43,212,215,216,43,219,222,223,47],{},"同步的 ",[28,210,211],{},"1",[28,213,214],{},"5"," 先跑完；接著清空微任務 ",[28,217,218],{},"3",[28,220,221],{},"4","；最後才輪到任務佇列裡的 ",[28,224,225],{},"2",[14,227,229],{"id":228},"async-await-只是-promise-的語法","async \u002F await 只是 Promise 的語法",[10,231,232,235,236,239],{},[28,233,234],{},"await"," 之後的程式碼，等於放進 ",[28,237,238],{},".then()"," 的回呼，也就是微任務。",[62,241,243],{"className":64,"code":242,"language":66,"meta":67,"style":67},"async function run() {\n  console.log('a')\n  await null\n  console.log('b') \u002F\u002F 微任務：等目前同步程式碼結束後才執行\n}\n\nrun()\nconsole.log('c')\n\u002F\u002F a c b\n",[28,244,245,259,273,281,298,303,307,315,329],{"__ignoreMap":67},[71,246,247,250,253,256],{"class":73,"line":74},[71,248,249],{"class":103},"async",[71,251,252],{"class":103}," function",[71,254,255],{"class":81}," run",[71,257,258],{"class":77},"() {\n",[71,260,261,264,266,268,271],{"class":73,"line":95},[71,262,263],{"class":77},"  console.",[71,265,82],{"class":81},[71,267,85],{"class":77},[71,269,270],{"class":88},"'a'",[71,272,92],{"class":77},[71,274,275,278],{"class":73,"line":126},[71,276,277],{"class":103},"  await",[71,279,280],{"class":120}," null\n",[71,282,283,285,287,289,292,295],{"class":73,"line":160},[71,284,263],{"class":77},[71,286,82],{"class":81},[71,288,85],{"class":77},[71,290,291],{"class":88},"'b'",[71,293,294],{"class":77},") ",[71,296,297],{"class":204},"\u002F\u002F 微任務：等目前同步程式碼結束後才執行\n",[71,299,300],{"class":73,"line":180},[71,301,302],{"class":77},"}\n",[71,304,305],{"class":73,"line":194},[71,306,198],{"emptyLinePlaceholder":197},[71,308,309,312],{"class":73,"line":201},[71,310,311],{"class":81},"run",[71,313,314],{"class":77},"()\n",[71,316,318,320,322,324,327],{"class":73,"line":317},8,[71,319,78],{"class":77},[71,321,82],{"class":81},[71,323,85],{"class":77},[71,325,326],{"class":88},"'c'",[71,328,92],{"class":77},[71,330,332],{"class":73,"line":331},9,[71,333,334],{"class":204},"\u002F\u002F a c b\n",[14,336,338],{"id":337},"常見問題一race-condition","常見問題一：Race Condition",[10,340,341],{},"使用者連續輸入搜尋字，較早送出的請求可能比較晚才回來，結果用舊資料蓋掉新資料。處理方式是在送出新請求前先取消舊的：",[62,343,345],{"className":64,"code":344,"language":66,"meta":67,"style":67},"let controller\n\nasync function search(query) {\n  controller?.abort()\n  controller = new AbortController()\n\n  try {\n    const res = await fetch(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`, {\n      signal: controller.signal,\n    })\n    render(await res.json())\n  }\n  catch (err) {\n    if (err.name !== 'AbortError') throw err\n  }\n}\n",[28,346,347,355,359,377,387,403,407,415,453,458,464,483,489,498,521,526],{"__ignoreMap":67},[71,348,349,352],{"class":73,"line":74},[71,350,351],{"class":103},"let",[71,353,354],{"class":77}," controller\n",[71,356,357],{"class":73,"line":95},[71,358,198],{"emptyLinePlaceholder":197},[71,360,361,363,365,368,370,374],{"class":73,"line":126},[71,362,249],{"class":103},[71,364,252],{"class":103},[71,366,367],{"class":81}," search",[71,369,85],{"class":77},[71,371,373],{"class":372},"s9osk","query",[71,375,376],{"class":77},") {\n",[71,378,379,382,385],{"class":73,"line":160},[71,380,381],{"class":77},"  controller?.",[71,383,384],{"class":81},"abort",[71,386,314],{"class":77},[71,388,389,392,395,398,401],{"class":73,"line":180},[71,390,391],{"class":77},"  controller ",[71,393,394],{"class":103},"=",[71,396,397],{"class":103}," new",[71,399,400],{"class":81}," AbortController",[71,402,314],{"class":77},[71,404,405],{"class":73,"line":194},[71,406,198],{"emptyLinePlaceholder":197},[71,408,409,412],{"class":73,"line":201},[71,410,411],{"class":103},"  try",[71,413,414],{"class":77}," {\n",[71,416,417,420,423,426,429,432,434,437,440,442,444,447,450],{"class":73,"line":317},[71,418,419],{"class":103},"    const",[71,421,422],{"class":120}," res",[71,424,425],{"class":103}," =",[71,427,428],{"class":103}," await",[71,430,431],{"class":81}," fetch",[71,433,85],{"class":77},[71,435,436],{"class":88},"`\u002Fapi\u002Fsearch?q=${",[71,438,439],{"class":81},"encodeURIComponent",[71,441,85],{"class":88},[71,443,373],{"class":77},[71,445,446],{"class":88},")",[71,448,449],{"class":88},"}`",[71,451,452],{"class":77},", {\n",[71,454,455],{"class":73,"line":331},[71,456,457],{"class":77},"      signal: controller.signal,\n",[71,459,461],{"class":73,"line":460},10,[71,462,463],{"class":77},"    })\n",[71,465,467,470,472,474,477,480],{"class":73,"line":466},11,[71,468,469],{"class":81},"    render",[71,471,85],{"class":77},[71,473,234],{"class":103},[71,475,476],{"class":77}," res.",[71,478,479],{"class":81},"json",[71,481,482],{"class":77},"())\n",[71,484,486],{"class":73,"line":485},12,[71,487,488],{"class":77},"  }\n",[71,490,492,495],{"class":73,"line":491},13,[71,493,494],{"class":103},"  catch",[71,496,497],{"class":77}," (err) {\n",[71,499,501,504,507,510,513,515,518],{"class":73,"line":500},14,[71,502,503],{"class":103},"    if",[71,505,506],{"class":77}," (err.name ",[71,508,509],{"class":103},"!==",[71,511,512],{"class":88}," 'AbortError'",[71,514,294],{"class":77},[71,516,517],{"class":103},"throw",[71,519,520],{"class":77}," err\n",[71,522,524],{"class":73,"line":523},15,[71,525,488],{"class":77},[71,527,529],{"class":73,"line":528},16,[71,530,302],{"class":77},[10,532,533],{},"不能取消的情境（例如第三方 SDK），可以改用「請求序號」：只接受最新一次請求的結果。",[14,535,537],{"id":536},"常見問題二promiseall-與-allsettled","常見問題二：Promise.all 與 allSettled",[539,540,541,557],"table",{},[542,543,544],"thead",{},[545,546,547,551,554],"tr",{},[548,549,550],"th",{},"方法",[548,552,553],{},"其中一個失敗時",[548,555,556],{},"適合情境",[558,559,560,574],"tbody",{},[545,561,562,568,571],{},[563,564,565],"td",{},[28,566,567],{},"Promise.all",[563,569,570],{},"立刻 reject，丟掉其他結果",[563,572,573],{},"全部成功才有意義",[545,575,576,581,584],{},[563,577,578],{},[28,579,580],{},"Promise.allSettled",[563,582,583],{},"等全部結束，逐一回報成功或失敗",[563,585,586],{},"部分失敗仍要顯示其他資料",[62,588,590],{"className":64,"code":589,"language":66,"meta":67,"style":67},"const results = await Promise.allSettled([loadProfile(), loadOrders(), loadCoupons()])\nconst ok = results.filter(r => r.status === 'fulfilled').map(r => r.value)\n",[28,591,592,632],{"__ignoreMap":67},[71,593,594,597,600,602,604,607,609,612,615,618,621,624,626,629],{"class":73,"line":74},[71,595,596],{"class":103},"const",[71,598,599],{"class":120}," results",[71,601,425],{"class":103},[71,603,428],{"class":103},[71,605,606],{"class":120}," Promise",[71,608,132],{"class":77},[71,610,611],{"class":81},"allSettled",[71,613,614],{"class":77},"([",[71,616,617],{"class":81},"loadProfile",[71,619,620],{"class":77},"(), ",[71,622,623],{"class":81},"loadOrders",[71,625,620],{"class":77},[71,627,628],{"class":81},"loadCoupons",[71,630,631],{"class":77},"()])\n",[71,633,634,636,639,641,644,647,649,652,655,658,661,664,667,670,672,674,676],{"class":73,"line":95},[71,635,596],{"class":103},[71,637,638],{"class":120}," ok",[71,640,425],{"class":103},[71,642,643],{"class":77}," results.",[71,645,646],{"class":81},"filter",[71,648,85],{"class":77},[71,650,651],{"class":372},"r",[71,653,654],{"class":103}," =>",[71,656,657],{"class":77}," r.status ",[71,659,660],{"class":103},"===",[71,662,663],{"class":88}," 'fulfilled'",[71,665,666],{"class":77},").",[71,668,669],{"class":81},"map",[71,671,85],{"class":77},[71,673,651],{"class":372},[71,675,654],{"class":103},[71,677,678],{"class":77}," r.value)\n",[14,680,681],{"id":681},"閉包與作用域",[10,683,684,685,688],{},"閉包是函式「記住」它被建立時所在的作用域。經典例子是迴圈裡的 ",[28,686,687],{},"var","：",[62,690,692],{"className":64,"code":691,"language":66,"meta":67,"style":67},"for (var i = 0; i \u003C 3; i++) setTimeout(() => console.log(i)) \u002F\u002F 3 3 3\nfor (let i = 0; i \u003C 3; i++) setTimeout(() => console.log(i)) \u002F\u002F 0 1 2\n",[28,693,694,745],{"__ignoreMap":67},[71,695,696,699,702,704,707,709,712,715,718,721,724,727,729,731,733,735,737,739,742],{"class":73,"line":74},[71,697,698],{"class":103},"for",[71,700,701],{"class":77}," (",[71,703,687],{"class":103},[71,705,706],{"class":77}," i ",[71,708,394],{"class":103},[71,710,711],{"class":120}," 0",[71,713,714],{"class":77},"; i ",[71,716,717],{"class":103},"\u003C",[71,719,720],{"class":120}," 3",[71,722,723],{"class":77},"; i",[71,725,726],{"class":103},"++",[71,728,294],{"class":77},[71,730,30],{"class":81},[71,732,100],{"class":77},[71,734,104],{"class":103},[71,736,107],{"class":77},[71,738,82],{"class":81},[71,740,741],{"class":77},"(i)) ",[71,743,744],{"class":204},"\u002F\u002F 3 3 3\n",[71,746,747,749,751,753,755,757,759,761,763,765,767,769,771,773,775,777,779,781,783],{"class":73,"line":95},[71,748,698],{"class":103},[71,750,701],{"class":77},[71,752,351],{"class":103},[71,754,706],{"class":77},[71,756,394],{"class":103},[71,758,711],{"class":120},[71,760,714],{"class":77},[71,762,717],{"class":103},[71,764,720],{"class":120},[71,766,723],{"class":77},[71,768,726],{"class":103},[71,770,294],{"class":77},[71,772,30],{"class":81},[71,774,100],{"class":77},[71,776,104],{"class":103},[71,778,107],{"class":77},[71,780,82],{"class":81},[71,782,741],{"class":77},[71,784,785],{"class":204},"\u002F\u002F 0 1 2\n",[10,787,788,790,791,794,795,797,798,47],{},[28,789,687],{}," 是函式作用域，三個回呼共用同一個 ",[28,792,793],{},"i","；",[28,796,351],{}," 是區塊作用域，每一圈都有自己的 ",[28,799,793],{},[10,801,802],{},"閉包也會讓被參考的物件一直無法被回收。常見的隱性洩漏來源有：忘記移除的事件監聽、未清除的計時器，以及長期存活的物件持有大型資料的參考。元件銷毀時記得清理它們：",[62,804,806],{"className":64,"code":805,"language":66,"meta":67,"style":67},"const onResize = () => { \u002F* ... *\u002F }\nwindow.addEventListener('resize', onResize)\n\n\u002F\u002F 元件卸載時\nwindow.removeEventListener('resize', onResize)\n",[28,807,808,831,847,851,856],{"__ignoreMap":67},[71,809,810,812,815,817,820,822,825,828],{"class":73,"line":74},[71,811,596],{"class":103},[71,813,814],{"class":81}," onResize",[71,816,425],{"class":103},[71,818,819],{"class":77}," () ",[71,821,104],{"class":103},[71,823,824],{"class":77}," { ",[71,826,827],{"class":204},"\u002F* ... *\u002F",[71,829,830],{"class":77}," }\n",[71,832,833,836,839,841,844],{"class":73,"line":95},[71,834,835],{"class":77},"window.",[71,837,838],{"class":81},"addEventListener",[71,840,85],{"class":77},[71,842,843],{"class":88},"'resize'",[71,845,846],{"class":77},", onResize)\n",[71,848,849],{"class":73,"line":126},[71,850,198],{"emptyLinePlaceholder":197},[71,852,853],{"class":73,"line":160},[71,854,855],{"class":204},"\u002F\u002F 元件卸載時\n",[71,857,858,860,863,865,867],{"class":73,"line":180},[71,859,835],{"class":77},[71,861,862],{"class":81},"removeEventListener",[71,864,85],{"class":77},[71,866,843],{"class":88},[71,868,846],{"class":77},[14,870,871],{"id":871},"小結",[873,874,875,883,888,891],"ul",{},[24,876,877,878,880,881,47],{},"微任務在下一個任務之前全部執行完，所以 ",[28,879,42],{}," 早於 ",[28,882,59],{},[24,884,885,887],{},[28,886,234],{}," 之後的程式碼是微任務。",[24,889,890],{},"處理非同步 UI 時，要同時想「失敗」與「過期的結果」：取消舊請求，或只接受最新結果。",[24,892,893,894,896,897,899,900,902],{},"用 ",[28,895,351],{}," \u002F ",[28,898,596],{}," 取代 ",[28,901,687],{},"，並在元件卸載時釋放監聽與計時器。",[904,905,906],"style",{},"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 .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}",{"title":67,"searchDepth":95,"depth":95,"links":908},[909,910,911,912,913,914],{"id":16,"depth":95,"text":16},{"id":228,"depth":95,"text":229},{"id":337,"depth":95,"text":338},{"id":536,"depth":95,"text":537},{"id":681,"depth":95,"text":681},{"id":871,"depth":95,"text":871},"2025-03-20","梳理 Event Loop、Promise 微任務隊列與閉包機制的實務踩坑心得，建立清晰的底層執行模型。",false,"md",{},"\u002Fblog\u002Fjs-event-loop",{"title":5,"description":916},"blog\u002Fjs-event-loop",[924,129,925],"Event Loop","Closure","IYirq9JQo5CDMjt9e2wK-hHKQZZdo7XNhWuzAVfsYtI"]