前端老铁都在用的可观察对象:这玩意到底能干啥?(附实战套路)
- 前端老铁都在用的可观察对象:这玩意到底能干啥?(附实战套路)
- 先别急着翻篇,我给你讲个真事儿
- 为什么前端越来越离不开 Observable?
- 可观察对象不是 RxJS 的专利!
- 1. Vue 3 的 `reactive` + `watchEffect`
- 2. Svelte 的 `store`
- 3. 原生 JS 也能搓简易版
- 这些场景不用 Observable 真的会哭
- 1. 搜索框防抖 + 请求去重
- 2. 多源数据“谁先回来用谁”
- 3. WebSocket 断线重连 + 心跳
- 内存泄漏:凌晨三点的幽灵
- 故事续集
- 冷热 Observable:别在雪地里等公交
- 冷 Observable(Cold)
- 热 Observable(Hot)
- 调试 Observable 的野路子
- 几个让你少加班的骚操作
- 1. shareReplay 缓存 HTTP
- 2. switchMap 自动取消旧请求
- 3. 表单验证 = valueChanges + 异步校验
- 4. localStorage 当数据源,持久化状态流
- 当你发现代码里全是 `.pipe(...)` 别慌
- 彩蛋:我把文章写成 Observable 了
前端老铁都在用的可观察对象:这玩意到底能干啥?(附实战套路)
友情提示:本文超长,代码量巨大,建议先收藏再蹲坑慢慢看。
如果你边看边骂“这孙子写得真啰嗦”,恭喜你,你已经掌握了人类技术博客的真谛。
先别急着翻篇,我给你讲个真事儿
去年双十一,我们组负责的商品筛选页在 0 点前 5 分钟直接白屏。
运营在群里疯狂 @ 全员,老板把键盘敲得跟打鼓似的。
我蹲在地上 Debug,发现罪魁祸首是——搜索框防抖写劈叉了。
对,就是setTimeout+clearTimeout那种“土味防抖”,在疯狂点击下直接内存飙红。
那一刻我深刻体会到:不会 Observable,连购物车都欺负你。
为什么前端越来越离不开 Observable?
说人话:
以前页面是“点一下 -> 发请求 -> 渲染”,现在呢?
用户一边输入,一边联想,一边语音,一边拖拽,一边还切后台。
数据流像早高峰的地铁,不找根管子疏导,那就等着爆炸。
Observable 就是这根管子:
- 想什么时候推数据就推,想什么时候停就停;
- 能把多个来源的“脏水”拧成一股“纯净水”;
- 还能在管子中途加滤网(filter)、加延时(debounce)、加开关(takeUntil)。
用一句话总结:它让异步数据从“回调地狱”变成“乐高积木”。
可观察对象不是 RxJS 的专利!
一提 Observable 就想到 RxJS,就像一提泡面就想到康师傅——其实老坛酸菜也挺好吃的。
1. Vue 3 的reactive+watchEffect
// Store 文件import{reactive,watchEffect}from'vue'exportconstcartStore=reactive({list:[],gettotalPrice(){returnthis.list.reduce((s,it)=>s+it.price*it.count,0)}})// 组件里直接用watchEffect(()=>{console.log('当前总价',cartStore.totalPrice)})没有 RxJS 的影子,但思想一模一样:数据变 -> 副作用自动跑。
2. Svelte 的store
// store.jsimport{writable}from'svelte/store'exportconstcount=writable(0)// 组件里count.subscribe(v=>console.log('count 现在是',v))语法糖甜到齁,编译期帮你把订阅/取消订阅全搞定,内存泄漏?不存在的。
3. 原生 JS 也能搓简易版
// 用 AbortController + async generator 实现“点按钮 -> 间隔发数 -> 随时停”functioncounter$(step=1000){constac=newAbortController()conststream=(asyncfunction*(){leti=0while(!ac.signal.aborted){yieldi++awaitnewPromise(r=>setTimeout(r,step))}})()return{stream,cancel:()=>ac.abort()}}const{stream,cancel}=counter$();(async()=>{forawait(constnofstream){console.log('原生流',n)if(n===5)cancel()// 5 秒后自爆}})()一行 RxJS 都没引,但思想还是“数据流+取消”那一套。
这些场景不用 Observable 真的会哭
1. 搜索框防抖 + 请求去重
需求:用户疯狂输入,300 ms 内不敲键盘再发请求;如果上一次请求没回来,直接 cancel。
土味写法:
lettimer=nullletlastController=nullfunctionsearch(keyword){clearTimeout(timer)lastController?.abort()timer=setTimeout(()=>{lastController=newAbortController()fetch(`/search?q=${keyword}`,{signal:lastController.signal}).then(r=>r.json()).then(render)},300)}Observable 写法:
import{fromEvent}from'rxjs'import{ajax}from'rxjs/ajax'import{debounceTime,distinctUntilChanged,switchMap,map,catchError}from'rxjs/operators'constsearch$=fromEvent(inputDom,'input').pipe(map(e=>e.target.value.trim()),debounceTime(300),distinctUntilChanged(),// 输入值没变就跳过switchMap(keyword=>// 自动取消上一次未完成的 ajaxajax.getJSON(`/search?q=${keyword}`).pipe(catchError(err=>of({list:[],err})))))search$.subscribe(render)switchMap 会自动帮你把旧请求扔掉,代码量砍半, bug 率砍 80%。
2. 多源数据“谁先回来用谁”
需求:商品详情页,优先走缓存,缓存 1 秒没回就请求网络,谁先到用谁。
import{of,timer,race}from'rxjs'import{map,timeout,catchError}from'rxjs/operators'constcache$=of(JSON.parse(localStorage.getItem('detail_'+id))).pipe(filter(_=>!!_),// 缓存空就直接无视timeout(1000),// 1 秒没发值就报错catchError(()=>EMPTY)// 报错就流废掉,让 race 继续等网络)constnet$=ajax.getJSON(`/api/detail/${id}`).pipe(tap(res=>localStorage.setItem('detail_'+id,JSON.stringify(res))))race(cache$,net$).subscribe(render)race 就是“谁先冲线谁赢”,缓存快就省流量,缓存慢也不卡用户。
3. WebSocket 断线重连 + 心跳
需求:聊天室,掉线后指数退避重连,连上后每 30 秒 ping 一次,服务端 5 秒不回 pong 就重连。
import{webSocket}from'rxjs/webSocket'import{retryWhen,delay,takeUntil,repeat,share}from'rxjs/operators'import{timer,EMPTY}from'rxjs'functionmakeSocket(url){constws$=webSocket(url)constheartbeat$=timer(0,30_000).pipe(switchMap(()=>ws$.multiplex(()=>({type:'ping'}),()=>({type:'pong'}),msg=>msg.type==='pong')),takeUntil(ws$.pipe(filter(m=>m.type==='error'))),repeat()// 出错后重新订阅心跳)return{messages$:ws$.pipe(share()),// 业务消息heartbeat$// 心跳流,内部自己管理}}const{messages$,heartbeat$}=makeSocket('wss://chat.example')messages$.subscribe(renderMessage)heartbeat$.subscribe()retryWhen + 指数退避把重连逻辑包得明明白白,share 保证多订阅只连一次,谁用谁知道。
内存泄漏:凌晨三点的幽灵
故事续集
上文搜索框防抖上线后,QA 妹子说:“页面切 10 次,Chrome 任务管理器里 JS 事件监听器 +2000。”
我头皮发麻,打开 Performance 一看,每次切路由都重新 subscribe,旧监听没清。
根因:
// ❌ 组件里随手写created(){search$.subscribe(this.render)}正确姿势:
// ✅ Vue 3import{watchEffect}from'vue'letstop=nullonMounted(()=>{stop=watchEffect(()=>search$.subscribe(render))})onUnmounted(()=>stop&&stop())// ✅ ReactuseEffect(()=>{constsub=search$.subscribe(render)return()=>sub.unsubscribe()// 清理函数必写},[])// ✅ Angularprivatedestroy$=newSubject<void>()ngOnInit(){search$.pipe(takeUntil(this.destroy$)).subscribe(render)}ngOnDestroy(){this.destroy$.next()this.destroy$.complete()}一句话:只要你 subscribe,就必须想好怎么 unsubscribe。
否则内存泄漏不是 bug,是定时炸弹。
冷热 Observable:别在雪地里等公交
冷 Observable(Cold)
“每次订阅都从头开始放电影”
constcold$=newObservable(sub=>{console.log('开始放电影')sub.next(1)sub.next(2)sub.complete()})cold$.subscribe(v=>console.log('观众 A 看到',v))cold$.subscribe(v=>console.log('观众 B 看到',v))// 输出两遍“开始放电影”热 Observable(Hot)
“现场直播,错过就错过”
constlive$=newSubject()live$.next('开场')// A 还没来,错过了live$.subscribe(v=>console.log('观众 A 看到',v))live$.next('高潮')live$.subscribe(v=>console.log('观众 B 看到',v))live$.next('彩蛋')HTTP 请求默认是冷流,所以两次 subscribe 会打两次接口;
WebSocket、Subject、share 后是热流,早订阅早享受,晚订阅没回头。
调试 Observable 的野路子
tap插针
source$.pipe(tap(v=>console.log('%c 原始值','color:green',v)),map(expensiveCalc),tap(v=>console.log('%c 计算后','color:red',v)))timestamp看时序
source$.pipe(timestamp(),tap(({value,timestamp})=>console.log(`值${value}在${newDate(timestamp).toLocaleTimeString()}到达`)))finalize捕获结束
ajax.post('/save',data).pipe(finalize(()=>console.log('请求结束,无论成功失败')))- 断点调试
在tap里写debugger,比打一堆 console 更精准。
tap(v=>{if(v.id==='buggy')debugger})几个让你少加班的骚操作
1. shareReplay 缓存 HTTP
constprofile$=ajax.getJSON('/me').pipe(shareReplay({bufferSize:1,refCount:true})// 1 秒内复用,无人订阅自动释放)// 组件 A、B、C 同时注入,只发一次请求2. switchMap 自动取消旧请求
上文搜索框已演示,一句话:网络 race 条件杀手。
3. 表单验证 = valueChanges + 异步校验
fromEvent(input,'input').pipe(map(e=>e.target.value),debounceTime(300),distinctUntilChanged(),switchMap(v=>ajax.post('/check-username',{username:v}).pipe(map(res=>res.available?null:'用户名被占用'),catchError(()=>of('网络错误'))))).subscribe(error=>{errorEl.textContent=error||''input.setCustomValidity(error||'')})实时、防抖、异步、错误处理,全在 10 行里。
4. localStorage 当数据源,持久化状态流
functionpersistent(key,init){constraw=localStorage.getItem(key)constsubject$=newBehaviorSubject(raw?JSON.parse(raw):init)subject$.pipe(debounceTime(500)).subscribe(v=>localStorage.setItem(key,JSON.stringify(v)))return{get:()=>subject$.value,set:v=>subject$.next(v),stream:()=>subject$.asObservable()}}// 使用consttheme$=persistent('theme','light')theme$.set('dark')theme$.stream().subscribe(v=>document.body.className=v)刷新页面状态还在,堪称低配版 Vuex/Pinia。
当你发现代码里全是.pipe(...)别慌
说明你已经从“命令式”进化到“响应式”。
但记住三句话:
- 简单一次性请求,
fetch + await更清爽; - 业务流复杂、多源、需要组合/取消,再上 Observable;
- 不要为了炫技把 if else 全部换成流,否则三个月后你自己都看不懂。
彩蛋:我把文章写成 Observable 了
constarticle$=of('前端老铁都在用的可观察对象').pipe(concatMap(()=>from(['为啥离不开','不是 RxJS 专利','实战场景','内存泄漏','调试野路子','骚操作'])),map(section=>`---\n##${section}\n(此处省略 1000 字)\n`),reduce((a,b)=>a+b),finalize(()=>console.log('文章结束,点个赞再跑!')))article$.subscribe(console.log)- 参考资料
- Vue.observable 实战案例
- Angular 路由守卫异步技巧
- Redux-Observable 深度解析
- Chrome 新 Observable API 提案
- Vue 状态管理最佳实践
- Vue.observable 轻量级状态管理方案
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 | |
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!