[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"knowledge-\u002Fapi\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Frxjs":3,"knowledge-related-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Frxjs":1577,"knowledge-series-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Frxjs":1597},{"source":4,"connector":5,"page":6},"sqlite","better-sqlite3",{"path":7,"title":8,"description":9,"category":10,"tags":11,"date":15,"pinned":16,"draft":16,"body":17,"seo":1573,"stem":1574,"id":1575,"extension":1576},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Frxjs","RxJS 互操作","Angular 内置 RxJS 处理异步流（HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等）。Signal 适合同步\u002F短链路状态；长生命周期流仍常用 Observable。涵盖 async 管道、toSignal\u002FtoObservable、常用操作符、最佳实践与常见坑。","技能小册",[12,13,14],"angular","signals","rxjs","2026-04-06",false,{"type":18,"value":19,"toc":1561},"minimark",[20,24,54,58,61,89,96,100,364,382,385,636,648,652,657,771,828,835,839,1185,1188,1192,1289,1298,1301,1354,1357,1463,1466,1492,1495,1498,1557],[21,22,8],"h1",{"id":23},"rxjs-互操作",[25,26,27,28,35,36,40,41,44,45,48,49,53],"p",{},"Angular 内置 ",[29,30,34],"a",{"href":31,"rel":32},"https:\u002F\u002Frxjs.dev\u002F",[33],"nofollow","RxJS"," 处理异步流（HTTP、",[37,38,39],"code",{},"HttpClient","、路由事件、",[37,42,43],{},"FormControl"," 的 ",[37,46,47],{},"valueChanges"," 等）。Signal 适合同步\u002F短链路状态；",[50,51,52],"strong",{},"长生命周期流","仍常用 Observable。",[55,56,57],"h2",{"id":57},"核心概念",[25,59,60],{},"Signal 与 Observable 解决的是不同层面的问题：",[62,63,64,75],"ul",{},[65,66,67,70,71,74],"li",{},[50,68,69],{},"Signal","：表示「当前值」，天生同步、有当前状态、可被 ",[37,72,73],{},"computed"," 派生，适合 UI 状态。",[65,76,77,80,81,84,85,88],{},[50,78,79],{},"Observable","：表示「一段时间内的事件序列」，可以取消订阅、组合（",[37,82,83],{},"switchMap","、",[37,86,87],{},"combineLatest","）、表达「尚未有值」「多次发出」等语义，适合处理 HTTP 请求、WebSocket、用户输入流等。",[25,90,91,92,95],{},"二者不是替代关系，而是配合：HTTP 请求、路由参数变化等",[50,93,94],{},"天然是流","，用 RxJS 处理；处理完成后落地为 UI 状态时转换成 Signal。",[55,97,99],{"id":98},"模板中的-async-管道","模板中的 async 管道",[101,102,107],"pre",{"className":103,"code":104,"language":105,"meta":106,"style":106},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","import { AsyncPipe } from \"@angular\u002Fcommon\";\nimport { HttpClient } from \"@angular\u002Fcommon\u002Fhttp\";\nimport { inject } from \"@angular\u002Fcore\";\n\n@Component({\n  standalone: true,\n  imports: [AsyncPipe],\n  template: `@if (user$ | async; as user) { \u003Cp>{{ user.name }}\u003C\u002Fp> }`,\n})\nexport class ProfileComponent {\n  private http = inject(HttpClient);\n  user$ = this.http.get\u003CUser>(\"\u002Fapi\u002Fme\");\n}\n","ts","",[37,108,109,146,169,192,199,215,232,245,264,273,290,312,358],{"__ignoreMap":106},[110,111,114,118,122,126,129,132,136,140,143],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"sVHd0","import",[110,119,121],{"class":120},"sP7_E"," {",[110,123,125],{"class":124},"su5hD"," AsyncPipe",[110,127,128],{"class":120}," }",[110,130,131],{"class":116}," from",[110,133,135],{"class":134},"sjJ54"," \"",[110,137,139],{"class":138},"s_sjI","@angular\u002Fcommon",[110,141,142],{"class":134},"\"",[110,144,145],{"class":120},";\n",[110,147,149,151,153,156,158,160,162,165,167],{"class":112,"line":148},2,[110,150,117],{"class":116},[110,152,121],{"class":120},[110,154,155],{"class":124}," HttpClient",[110,157,128],{"class":120},[110,159,131],{"class":116},[110,161,135],{"class":134},[110,163,164],{"class":138},"@angular\u002Fcommon\u002Fhttp",[110,166,142],{"class":134},[110,168,145],{"class":120},[110,170,172,174,176,179,181,183,185,188,190],{"class":112,"line":171},3,[110,173,117],{"class":116},[110,175,121],{"class":120},[110,177,178],{"class":124}," inject",[110,180,128],{"class":120},[110,182,131],{"class":116},[110,184,135],{"class":134},[110,186,187],{"class":138},"@angular\u002Fcore",[110,189,142],{"class":134},[110,191,145],{"class":120},[110,193,195],{"class":112,"line":194},4,[110,196,198],{"emptyLinePlaceholder":197},true,"\n",[110,200,202,205,209,212],{"class":112,"line":201},5,[110,203,204],{"class":120},"@",[110,206,208],{"class":207},"sGLFI","Component",[110,210,211],{"class":124},"(",[110,213,214],{"class":120},"{\n",[110,216,218,222,225,229],{"class":112,"line":217},6,[110,219,221],{"class":220},"skxfh","  standalone",[110,223,224],{"class":120},":",[110,226,228],{"class":227},"syTEX"," true",[110,230,231],{"class":120},",\n",[110,233,235,238,240,243],{"class":112,"line":234},7,[110,236,237],{"class":220},"  imports",[110,239,224],{"class":120},[110,241,242],{"class":124}," [AsyncPipe]",[110,244,231],{"class":120},[110,246,248,251,253,256,259,262],{"class":112,"line":247},8,[110,249,250],{"class":220},"  template",[110,252,224],{"class":120},[110,254,255],{"class":134}," `",[110,257,258],{"class":138},"@if (user$ | async; as user) { \u003Cp>{{ user.name }}\u003C\u002Fp> }",[110,260,261],{"class":134},"`",[110,263,231],{"class":120},[110,265,267,270],{"class":112,"line":266},9,[110,268,269],{"class":120},"}",[110,271,272],{"class":124},")\n",[110,274,276,279,283,287],{"class":112,"line":275},10,[110,277,278],{"class":116},"export",[110,280,282],{"class":281},"sbsja"," class",[110,284,286],{"class":285},"sbgvK"," ProfileComponent",[110,288,289],{"class":120}," {\n",[110,291,293,296,300,304,307,310],{"class":112,"line":292},11,[110,294,295],{"class":281},"  private",[110,297,299],{"class":298},"sucvu"," http",[110,301,303],{"class":302},"smGrS"," =",[110,305,178],{"class":306},"sVXei",[110,308,309],{"class":124},"(HttpClient)",[110,311,145],{"class":120},[110,313,315,318,320,324,327,330,332,335,338,341,344,346,348,351,353,356],{"class":112,"line":314},12,[110,316,317],{"class":298},"  user$",[110,319,303],{"class":302},[110,321,323],{"class":322},"s39Yj"," this",[110,325,326],{"class":120},".",[110,328,329],{"class":124},"http",[110,331,326],{"class":120},[110,333,334],{"class":306},"get",[110,336,337],{"class":120},"\u003C",[110,339,340],{"class":285},"User",[110,342,343],{"class":120},">",[110,345,211],{"class":124},[110,347,142],{"class":134},[110,349,350],{"class":138},"\u002Fapi\u002Fme",[110,352,142],{"class":134},[110,354,355],{"class":124},")",[110,357,145],{"class":120},[110,359,361],{"class":112,"line":360},13,[110,362,363],{"class":120},"}\n",[25,365,366,369,370,373,374,377,378,381],{},[37,367,368],{},"async"," 管道负责订阅与取消订阅，避免在组件中手动 ",[37,371,372],{},"subscribe"," 导致泄漏（仍须在类中手动订阅时，在 ",[37,375,376],{},"ngOnDestroy"," 或 ",[37,379,380],{},"takeUntilDestroyed"," 中清理）。",[55,383,384],{"id":384},"手动订阅时的清理",[101,386,388],{"className":103,"code":387,"language":105,"meta":106,"style":106},"import { DestroyRef, inject } from \"@angular\u002Fcore\";\nimport { takeUntilDestroyed } from \"@angular\u002Fcore\u002Frxjs-interop\";\n\n@Component({ standalone: true, template: `` })\nexport class LiveTickerComponent {\n  private destroyRef = inject(DestroyRef);\n  price = signal(0);\n\n  constructor(private ws: PriceSocketService) {\n    this.ws.price$\n      .pipe(takeUntilDestroyed(this.destroyRef))\n      .subscribe((p) => this.price.set(p));\n  }\n}\n",[37,389,390,416,438,442,474,485,501,521,525,548,563,588,626,631],{"__ignoreMap":106},[110,391,392,394,396,399,402,404,406,408,410,412,414],{"class":112,"line":113},[110,393,117],{"class":116},[110,395,121],{"class":120},[110,397,398],{"class":124}," DestroyRef",[110,400,401],{"class":120},",",[110,403,178],{"class":124},[110,405,128],{"class":120},[110,407,131],{"class":116},[110,409,135],{"class":134},[110,411,187],{"class":138},[110,413,142],{"class":134},[110,415,145],{"class":120},[110,417,418,420,422,425,427,429,431,434,436],{"class":112,"line":148},[110,419,117],{"class":116},[110,421,121],{"class":120},[110,423,424],{"class":124}," takeUntilDestroyed",[110,426,128],{"class":120},[110,428,131],{"class":116},[110,430,135],{"class":134},[110,432,433],{"class":138},"@angular\u002Fcore\u002Frxjs-interop",[110,435,142],{"class":134},[110,437,145],{"class":120},[110,439,440],{"class":112,"line":171},[110,441,198],{"emptyLinePlaceholder":197},[110,443,444,446,448,450,453,456,458,460,462,465,467,470,472],{"class":112,"line":194},[110,445,204],{"class":120},[110,447,208],{"class":207},[110,449,211],{"class":124},[110,451,452],{"class":120},"{",[110,454,455],{"class":220}," standalone",[110,457,224],{"class":120},[110,459,228],{"class":227},[110,461,401],{"class":120},[110,463,464],{"class":220}," template",[110,466,224],{"class":120},[110,468,469],{"class":134}," ``",[110,471,128],{"class":120},[110,473,272],{"class":124},[110,475,476,478,480,483],{"class":112,"line":201},[110,477,278],{"class":116},[110,479,282],{"class":281},[110,481,482],{"class":285}," LiveTickerComponent",[110,484,289],{"class":120},[110,486,487,489,492,494,496,499],{"class":112,"line":217},[110,488,295],{"class":281},[110,490,491],{"class":298}," destroyRef",[110,493,303],{"class":302},[110,495,178],{"class":306},[110,497,498],{"class":124},"(DestroyRef)",[110,500,145],{"class":120},[110,502,503,506,508,511,513,517,519],{"class":112,"line":234},[110,504,505],{"class":298},"  price",[110,507,303],{"class":302},[110,509,510],{"class":306}," signal",[110,512,211],{"class":124},[110,514,516],{"class":515},"srdBf","0",[110,518,355],{"class":124},[110,520,145],{"class":120},[110,522,523],{"class":112,"line":247},[110,524,198],{"emptyLinePlaceholder":197},[110,526,527,530,532,535,539,541,544,546],{"class":112,"line":266},[110,528,529],{"class":281},"  constructor",[110,531,211],{"class":120},[110,533,534],{"class":281},"private",[110,536,538],{"class":537},"s99_P"," ws",[110,540,224],{"class":302},[110,542,543],{"class":285}," PriceSocketService",[110,545,355],{"class":120},[110,547,289],{"class":120},[110,549,550,553,555,558,560],{"class":112,"line":275},[110,551,552],{"class":322},"    this",[110,554,326],{"class":120},[110,556,557],{"class":124},"ws",[110,559,326],{"class":120},[110,561,562],{"class":124},"price$\n",[110,564,565,568,571,573,575,577,580,582,585],{"class":112,"line":292},[110,566,567],{"class":120},"      .",[110,569,570],{"class":207},"pipe",[110,572,211],{"class":220},[110,574,380],{"class":207},[110,576,211],{"class":220},[110,578,579],{"class":322},"this",[110,581,326],{"class":120},[110,583,584],{"class":124},"destroyRef",[110,586,587],{"class":220},"))\n",[110,589,590,592,594,596,598,600,602,605,607,609,612,614,617,619,621,624],{"class":112,"line":314},[110,591,567],{"class":120},[110,593,372],{"class":207},[110,595,211],{"class":220},[110,597,211],{"class":120},[110,599,25],{"class":537},[110,601,355],{"class":120},[110,603,604],{"class":281}," =>",[110,606,323],{"class":322},[110,608,326],{"class":120},[110,610,611],{"class":124},"price",[110,613,326],{"class":120},[110,615,616],{"class":207},"set",[110,618,211],{"class":220},[110,620,25],{"class":124},[110,622,623],{"class":220},"))",[110,625,145],{"class":120},[110,627,628],{"class":112,"line":360},[110,629,630],{"class":120},"  }\n",[110,632,634],{"class":112,"line":633},14,[110,635,363],{"class":120},[25,637,638,640,641,644,645,647],{},[37,639,380],{}," 会在组件\u002F服务销毁时自动完成流，替代手写 ",[37,642,643],{},"Subject"," + ",[37,646,376],{}," 的样板代码。",[55,649,651],{"id":650},"tosignal-toobservable","toSignal \u002F toObservable",[25,653,654,656],{},[37,655,433],{}," 提供互操作：",[101,658,660],{"className":103,"code":659,"language":105,"meta":106,"style":106},"import { toSignal } from \"@angular\u002Fcore\u002Frxjs-interop\";\n\n\u002F\u002F Observable → Signal（可指定 initialValue）\nconst data = toSignal(this.http.get\u003CItem[]>(\"\u002Fapi\u002Fitems\"), {\n  initialValue: [] as Item[],\n});\n",[37,661,662,683,687,693,743,763],{"__ignoreMap":106},[110,663,664,666,668,671,673,675,677,679,681],{"class":112,"line":113},[110,665,117],{"class":116},[110,667,121],{"class":120},[110,669,670],{"class":124}," toSignal",[110,672,128],{"class":120},[110,674,131],{"class":116},[110,676,135],{"class":134},[110,678,433],{"class":138},[110,680,142],{"class":134},[110,682,145],{"class":120},[110,684,685],{"class":112,"line":148},[110,686,198],{"emptyLinePlaceholder":197},[110,688,689],{"class":112,"line":171},[110,690,692],{"class":691},"sutJx","\u002F\u002F Observable → Signal（可指定 initialValue）\n",[110,694,695,698,702,704,706,708,710,712,714,716,718,720,723,726,728,730,732,735,737,739,741],{"class":112,"line":194},[110,696,697],{"class":281},"const",[110,699,701],{"class":700},"s_hVV"," data",[110,703,303],{"class":302},[110,705,670],{"class":207},[110,707,211],{"class":124},[110,709,579],{"class":322},[110,711,326],{"class":120},[110,713,329],{"class":124},[110,715,326],{"class":120},[110,717,334],{"class":207},[110,719,337],{"class":120},[110,721,722],{"class":285},"Item",[110,724,725],{"class":124},"[]",[110,727,343],{"class":120},[110,729,211],{"class":124},[110,731,142],{"class":134},[110,733,734],{"class":138},"\u002Fapi\u002Fitems",[110,736,142],{"class":134},[110,738,355],{"class":124},[110,740,401],{"class":120},[110,742,289],{"class":120},[110,744,745,748,750,753,756,759,761],{"class":112,"line":201},[110,746,747],{"class":220},"  initialValue",[110,749,224],{"class":120},[110,751,752],{"class":124}," [] ",[110,754,755],{"class":116},"as",[110,757,758],{"class":285}," Item",[110,760,725],{"class":124},[110,762,231],{"class":120},[110,764,765,767,769],{"class":112,"line":217},[110,766,269],{"class":120},[110,768,355],{"class":124},[110,770,145],{"class":120},[101,772,774],{"className":103,"code":773,"language":105,"meta":106,"style":106},"import { toObservable } from \"@angular\u002Fcore\u002Frxjs-interop\";\n\n\u002F\u002F Signal → Observable\nconst count$ = toObservable(this.count);\n",[37,775,776,797,801,806],{"__ignoreMap":106},[110,777,778,780,782,785,787,789,791,793,795],{"class":112,"line":113},[110,779,117],{"class":116},[110,781,121],{"class":120},[110,783,784],{"class":124}," toObservable",[110,786,128],{"class":120},[110,788,131],{"class":116},[110,790,135],{"class":134},[110,792,433],{"class":138},[110,794,142],{"class":134},[110,796,145],{"class":120},[110,798,799],{"class":112,"line":148},[110,800,198],{"emptyLinePlaceholder":197},[110,802,803],{"class":112,"line":171},[110,804,805],{"class":691},"\u002F\u002F Signal → Observable\n",[110,807,808,810,813,815,817,819,821,823,826],{"class":112,"line":194},[110,809,697],{"class":281},[110,811,812],{"class":700}," count$",[110,814,303],{"class":302},[110,816,784],{"class":207},[110,818,211],{"class":124},[110,820,579],{"class":322},[110,822,326],{"class":120},[110,824,825],{"class":124},"count)",[110,827,145],{"class":120},[25,829,830,831,834],{},"用于在 ",[50,832,833],{},"Signal 组件"," 中消费 HTTP 结果，或把 Signal 传给仍基于 RxJS 的 API。",[55,836,838],{"id":837},"综合示例搜索防抖","综合示例：搜索防抖",[101,840,842],{"className":103,"code":841,"language":105,"meta":106,"style":106},"@Component({\n  standalone: true,\n  imports: [AsyncPipe],\n  template: `\n    \u003Cinput [value]=\"keyword()\" (input)=\"keyword.set($event.target.value)\" \u002F>\n    @for (item of results(); track item.id) {\n      \u003Cdiv>{{ item.name }}\u003C\u002Fdiv>\n    }\n  `,\n})\nexport class SearchComponent {\n  private http = inject(HttpClient);\n  keyword = signal(\"\");\n\n  private keyword$ = toObservable(this.keyword);\n\n  results = toSignal(\n    this.keyword$.pipe(\n      debounceTime(300),\n      filter((k) => k.length > 0),\n      switchMap((k) => this.http.get\u003CItem[]>(`\u002Fapi\u002Fsearch?q=${k}`)),\n      catchError(() => of([]))\n    ),\n    { initialValue: [] as Item[] }\n  );\n}\n",[37,843,844,854,864,874,883,888,893,898,903,910,916,927,941,959,963,986,991,1004,1020,1035,1070,1123,1142,1150,1172,1180],{"__ignoreMap":106},[110,845,846,848,850,852],{"class":112,"line":113},[110,847,204],{"class":120},[110,849,208],{"class":207},[110,851,211],{"class":124},[110,853,214],{"class":120},[110,855,856,858,860,862],{"class":112,"line":148},[110,857,221],{"class":220},[110,859,224],{"class":120},[110,861,228],{"class":227},[110,863,231],{"class":120},[110,865,866,868,870,872],{"class":112,"line":171},[110,867,237],{"class":220},[110,869,224],{"class":120},[110,871,242],{"class":124},[110,873,231],{"class":120},[110,875,876,878,880],{"class":112,"line":194},[110,877,250],{"class":220},[110,879,224],{"class":120},[110,881,882],{"class":134}," `\n",[110,884,885],{"class":112,"line":201},[110,886,887],{"class":138},"    \u003Cinput [value]=\"keyword()\" (input)=\"keyword.set($event.target.value)\" \u002F>\n",[110,889,890],{"class":112,"line":217},[110,891,892],{"class":138},"    @for (item of results(); track item.id) {\n",[110,894,895],{"class":112,"line":234},[110,896,897],{"class":138},"      \u003Cdiv>{{ item.name }}\u003C\u002Fdiv>\n",[110,899,900],{"class":112,"line":247},[110,901,902],{"class":138},"    }\n",[110,904,905,908],{"class":112,"line":266},[110,906,907],{"class":134},"  `",[110,909,231],{"class":120},[110,911,912,914],{"class":112,"line":275},[110,913,269],{"class":120},[110,915,272],{"class":124},[110,917,918,920,922,925],{"class":112,"line":292},[110,919,278],{"class":116},[110,921,282],{"class":281},[110,923,924],{"class":285}," SearchComponent",[110,926,289],{"class":120},[110,928,929,931,933,935,937,939],{"class":112,"line":314},[110,930,295],{"class":281},[110,932,299],{"class":298},[110,934,303],{"class":302},[110,936,178],{"class":306},[110,938,309],{"class":124},[110,940,145],{"class":120},[110,942,943,946,948,950,952,955,957],{"class":112,"line":360},[110,944,945],{"class":298},"  keyword",[110,947,303],{"class":302},[110,949,510],{"class":306},[110,951,211],{"class":124},[110,953,954],{"class":134},"\"\"",[110,956,355],{"class":124},[110,958,145],{"class":120},[110,960,961],{"class":112,"line":633},[110,962,198],{"emptyLinePlaceholder":197},[110,964,966,968,971,973,975,977,979,981,984],{"class":112,"line":965},15,[110,967,295],{"class":281},[110,969,970],{"class":298}," keyword$",[110,972,303],{"class":302},[110,974,784],{"class":306},[110,976,211],{"class":124},[110,978,579],{"class":322},[110,980,326],{"class":120},[110,982,983],{"class":124},"keyword)",[110,985,145],{"class":120},[110,987,989],{"class":112,"line":988},16,[110,990,198],{"emptyLinePlaceholder":197},[110,992,994,997,999,1001],{"class":112,"line":993},17,[110,995,996],{"class":298},"  results",[110,998,303],{"class":302},[110,1000,670],{"class":306},[110,1002,1003],{"class":124},"(\n",[110,1005,1007,1009,1011,1014,1016,1018],{"class":112,"line":1006},18,[110,1008,552],{"class":322},[110,1010,326],{"class":120},[110,1012,1013],{"class":124},"keyword$",[110,1015,326],{"class":120},[110,1017,570],{"class":306},[110,1019,1003],{"class":124},[110,1021,1023,1026,1028,1031,1033],{"class":112,"line":1022},19,[110,1024,1025],{"class":306},"      debounceTime",[110,1027,211],{"class":124},[110,1029,1030],{"class":515},"300",[110,1032,355],{"class":124},[110,1034,231],{"class":120},[110,1036,1038,1041,1043,1045,1048,1050,1052,1055,1057,1060,1063,1066,1068],{"class":112,"line":1037},20,[110,1039,1040],{"class":306},"      filter",[110,1042,211],{"class":124},[110,1044,211],{"class":120},[110,1046,1047],{"class":537},"k",[110,1049,355],{"class":120},[110,1051,604],{"class":281},[110,1053,1054],{"class":124}," k",[110,1056,326],{"class":120},[110,1058,1059],{"class":700},"length",[110,1061,1062],{"class":302}," >",[110,1064,1065],{"class":515}," 0",[110,1067,355],{"class":124},[110,1069,231],{"class":120},[110,1071,1073,1076,1078,1080,1082,1084,1086,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106,1108,1111,1114,1116,1119,1121],{"class":112,"line":1072},21,[110,1074,1075],{"class":306},"      switchMap",[110,1077,211],{"class":124},[110,1079,211],{"class":120},[110,1081,1047],{"class":537},[110,1083,355],{"class":120},[110,1085,604],{"class":281},[110,1087,323],{"class":322},[110,1089,326],{"class":120},[110,1091,329],{"class":124},[110,1093,326],{"class":120},[110,1095,334],{"class":306},[110,1097,337],{"class":120},[110,1099,722],{"class":285},[110,1101,725],{"class":124},[110,1103,343],{"class":120},[110,1105,211],{"class":124},[110,1107,261],{"class":134},[110,1109,1110],{"class":138},"\u002Fapi\u002Fsearch?q=",[110,1112,1113],{"class":134},"${",[110,1115,1047],{"class":124},[110,1117,1118],{"class":134},"}`",[110,1120,623],{"class":124},[110,1122,231],{"class":120},[110,1124,1126,1129,1131,1134,1136,1139],{"class":112,"line":1125},22,[110,1127,1128],{"class":306},"      catchError",[110,1130,211],{"class":124},[110,1132,1133],{"class":120},"()",[110,1135,604],{"class":281},[110,1137,1138],{"class":306}," of",[110,1140,1141],{"class":124},"([]))\n",[110,1143,1145,1148],{"class":112,"line":1144},23,[110,1146,1147],{"class":124},"    )",[110,1149,231],{"class":120},[110,1151,1153,1156,1159,1161,1163,1165,1167,1170],{"class":112,"line":1152},24,[110,1154,1155],{"class":120},"    {",[110,1157,1158],{"class":220}," initialValue",[110,1160,224],{"class":120},[110,1162,752],{"class":124},[110,1164,755],{"class":116},[110,1166,758],{"class":285},[110,1168,1169],{"class":124},"[] ",[110,1171,363],{"class":120},[110,1173,1175,1178],{"class":112,"line":1174},25,[110,1176,1177],{"class":124},"  )",[110,1179,145],{"class":120},[110,1181,1183],{"class":112,"line":1182},26,[110,1184,363],{"class":120},[25,1186,1187],{},"这个模式把「输入是 Signal」「防抖\u002F切换请求是 RxJS 强项」「渲染结果是 Signal」三者结合起来，是 Signal + RxJS 互操作的典型用法。",[55,1189,1191],{"id":1190},"常用操作符概念","常用操作符（概念）",[1193,1194,1195,1208],"table",{},[1196,1197,1198],"thead",{},[1199,1200,1201,1205],"tr",{},[1202,1203,1204],"th",{},"操作符",[1202,1206,1207],{},"用途",[1209,1210,1211,1222,1231,1241,1251,1261,1271,1280],"tbody",{},[1199,1212,1213,1219],{},[1214,1215,1216],"td",{},[37,1217,1218],{},"map",[1214,1220,1221],{},"映射值",[1199,1223,1224,1228],{},[1214,1225,1226],{},[37,1227,83],{},[1214,1229,1230],{},"切换内部 Observable（如搜索防抖，新请求取消旧请求）",[1199,1232,1233,1238],{},[1214,1234,1235],{},[37,1236,1237],{},"mergeMap",[1214,1239,1240],{},"并行处理多个内部 Observable，不取消旧的",[1199,1242,1243,1248],{},[1214,1244,1245],{},[37,1246,1247],{},"concatMap",[1214,1249,1250],{},"按顺序串行处理，保证前一个完成才处理下一个",[1199,1252,1253,1258],{},[1214,1254,1255],{},[37,1256,1257],{},"catchError",[1214,1259,1260],{},"错误恢复",[1199,1262,1263,1268],{},[1214,1264,1265],{},[37,1266,1267],{},"debounceTime",[1214,1269,1270],{},"防抖输入",[1199,1272,1273,1277],{},[1214,1274,1275],{},[37,1276,87],{},[1214,1278,1279],{},"组合多个流的最新值",[1199,1281,1282,1286],{},[1214,1283,1284],{},[37,1285,380],{},[1214,1287,1288],{},"组件\u002F服务销毁时自动取消订阅",[25,1290,1291,1292,1297],{},"细节见 ",[29,1293,1296],{"href":1294,"rel":1295},"https:\u002F\u002Frxjs.dev\u002Fguide\u002Foperators",[33],"RxJS 文档","。",[55,1299,1300],{"id":1300},"最佳实践",[62,1302,1303,1314,1323,1326,1340],{},[65,1304,1305,1306,1308,1309,1311,1312,1297],{},"HTTP 请求场景默认用 ",[37,1307,83],{},"（新请求应取消旧请求，如搜索框输入），批量提交等场景才用 ",[37,1310,1237],{},"\u002F",[37,1313,1247],{},[65,1315,1316,1317,1319,1320,1322],{},"模板消费 Observable 优先用 ",[37,1318,368],{}," 管道；类中必须手动订阅时用 ",[37,1321,380],{}," 避免遗漏清理。",[65,1324,1325],{},"短生命周期、同步读取的 UI 状态迁移到 Signal；长期存在的事件流（WebSocket、路由参数变化）保留 Observable。",[65,1327,1328,1331,1332,1335,1336,1339],{},[37,1329,1330],{},"toSignal"," 务必提供 ",[37,1333,1334],{},"initialValue","，否则在首次数据到达前读取会得到 ",[37,1337,1338],{},"undefined","，需额外处理。",[65,1341,1342,1343,1346,1347,1349,1350,1353],{},"避免在 ",[37,1344,1345],{},"effect"," 中手动 ",[37,1348,372],{},"，优先用 ",[37,1351,1352],{},"toObservable"," 转换后交给 RxJS 操作符处理。",[55,1355,1356],{"id":1356},"常见坑",[1193,1358,1359,1372],{},[1196,1360,1361],{},[1199,1362,1363,1366,1369],{},[1202,1364,1365],{},"现象",[1202,1367,1368],{},"常见原因",[1202,1370,1371],{},"处理",[1209,1373,1374,1393,1411,1426,1446],{},[1199,1375,1376,1379,1385],{},[1214,1377,1378],{},"组件销毁后仍收到旧订阅回调（甚至报错）",[1214,1380,1381,1382,1384],{},"手动 ",[37,1383,372],{}," 未清理",[1214,1386,1387,1388,377,1390,1392],{},"用 ",[37,1389,380],{},[37,1391,368],{}," 管道",[1199,1394,1395,1398,1406],{},[1214,1396,1397],{},"搜索结果闪现「上一次」结果",[1214,1399,1400,1401,1403,1404],{},"用了 ",[37,1402,1237],{}," 而非 ",[37,1405,83],{},[1214,1407,1408,1409],{},"需要取消旧请求的场景改用 ",[37,1410,83],{},[1199,1412,1413,1418,1421],{},[1214,1414,1415,1417],{},[37,1416,1330],{}," 读取报类型错误 `T",[1214,1419,1420],{},"undefined`",[1214,1422,1423,1424],{},"未传 ",[37,1425,1334],{},[1199,1427,1428,1431,1437],{},[1214,1429,1430],{},"HTTP 请求重复发出多次",[1214,1432,1433,1434,1436],{},"Observable 被多处 ",[37,1435,372],{},"（HttpClient 默认冷 Observable，每次订阅都发请求）",[1214,1438,1387,1439,1442,1443,1445],{},[37,1440,1441],{},"shareReplay"," 缓存，或改用 ",[37,1444,1330],{}," 只订阅一次",[1199,1447,1448,1453,1456],{},[1214,1449,1450,1452],{},[37,1451,87],{}," 一直不发出值",[1214,1454,1455],{},"其中一个源 Observable 还未 emit 过初始值",[1214,1457,1458,1459,1462],{},"确保所有源都有初始值（如用 ",[37,1460,1461],{},"startWith","）",[55,1464,1465],{"id":1465},"延伸阅读",[62,1467,1468,1475,1482],{},[65,1469,1470,1474],{},[29,1471,1473],{"href":1472},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fsignals\u002Fbasics","Signal 基础","：signal\u002Fcomputed\u002Feffect",[65,1476,1477,1481],{},[29,1478,1480],{"href":1479},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fstate","状态管理","：Service + Signal + RxJS 组合",[65,1483,1484,1488,1489,1491],{},[29,1485,1487],{"href":1486},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fforms","表单","：",[37,1490,47],{}," 的 RxJS 用法",[55,1493,1494],{"id":1494},"参考文献",[25,1496,1497],{},"以下链接在编写时均可正常访问：",[1193,1499,1500,1510],{},[1196,1501,1502],{},[1199,1503,1504,1507],{},[1202,1505,1506],{},"资料",[1202,1508,1509],{},"说明",[1209,1511,1512,1523,1534,1546],{},[1199,1513,1514,1520],{},[1214,1515,1516],{},[29,1517,1519],{"href":31,"rel":1518},[33],"RxJS 官网",[1214,1521,1522],{},"操作符与概念",[1199,1524,1525,1532],{},[1214,1526,1527],{},[29,1528,1531],{"href":1529,"rel":1530},"https:\u002F\u002Fangular.dev\u002Fecosystem\u002Frxjs-interop",[33],"Angular：RxJS 互操作",[1214,1533,651],{},[1199,1535,1536,1543],{},[1214,1537,1538],{},[29,1539,1542],{"href":1540,"rel":1541},"https:\u002F\u002Fangular.dev\u002Fapi\u002Fcommon\u002FAsyncPipe",[33],"AsyncPipe",[1214,1544,1545],{},"模板订阅",[1199,1547,1548,1554],{},[1214,1549,1550],{},[29,1551,380],{"href":1552,"rel":1553},"https:\u002F\u002Fangular.dev\u002Fapi\u002Fcore\u002Frxjs-interop\u002FtakeUntilDestroyed",[33],[1214,1555,1556],{},"自动取消订阅",[1558,1559,1560],"style",{},"html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sucvu, html code.shiki .sucvu{--shiki-light:#E53935;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":106,"searchDepth":148,"depth":148,"links":1562},[1563,1564,1565,1566,1567,1568,1569,1570,1571,1572],{"id":57,"depth":148,"text":57},{"id":98,"depth":148,"text":99},{"id":384,"depth":148,"text":384},{"id":650,"depth":148,"text":651},{"id":837,"depth":148,"text":838},{"id":1190,"depth":148,"text":1191},{"id":1300,"depth":148,"text":1300},{"id":1356,"depth":148,"text":1356},{"id":1465,"depth":148,"text":1465},{"id":1494,"depth":148,"text":1494},{"title":8,"description":9},"knowledge\u002Fbooklet\u002Fframe\u002FAngular\u002Fsignals\u002Frxjs","knowledge\u002Fknowledge\u002Fbooklet\u002Fframe\u002FAngular\u002Fsignals\u002Frxjs.md","md",{"items":1578},[1579,1581,1585,1587,1589,1593],{"path":1472,"title":1473,"description":1580,"category":10,"score":266},"Signals 是 Angular 16 引入的响应式原语：可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新，涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。",{"path":1582,"title":1583,"description":1584,"category":10,"score":194},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fecology","生态集成","Angular 官方维护路由、HTTP、表单、动画等；UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方\u002F半官方库、常用第三方库、与 Vue\u002FReact 生态对照、选型建议与常见坑。",{"path":1486,"title":1487,"description":1586,"category":10,"score":194},"Angular 提供 响应式表单（Reactive Forms） 与 模板驱动表单（Template-driven）。复杂表单推荐 Reactive Forms（模型在 TypeScript 中，易测试）。涵盖 FormGroup\u002FFormArray、Typed Forms、自定义校验器、最佳实践与常见坑。",{"path":1479,"title":1480,"description":1588,"category":10,"score":194},"Angular 没有强制单一状态库：服务 + DI、Signal、RxJS 即可覆盖多数场景；大型应用可选用 NgRx。涵盖选型、服务作为状态容器、跨组件共享、NgRx 与 SignalStore、常见坑。",{"path":1590,"title":1591,"description":1592,"category":10,"score":194},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fhttp","HTTP 与数据","Angular 通过 HttpClient（@angular\u002Fcommon\u002Fhttp）发起 HTTP 请求，返回 RxJS Observable。见 RxJS 互操作、依赖注入。",{"path":1594,"title":1595,"description":1596,"category":10,"score":194},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fangular\u002Fperformance\u002Fchange-detection","变更检测","Angular 在数据变化后更新 DOM 的过程称为 变更检测（Change Detection）。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同，有助于性能调优。运行时技巧见 runtime。",{"series":1598,"title":13,"items":1599,"index":113,"prev":1603,"next":1604},"booklet\u002Fframe\u002Fangular\u002Fsignals",[1600,1602],{"path":1472,"title":1473,"date":1601,"pinned":16},"2026-06-12",{"path":7,"title":8,"date":15,"pinned":16},{"path":1472,"title":1473,"date":1601,"pinned":16},null]