:store.watch如何实现状态「时间旅行」,一个布尔值巧妙打破无限循环)
vuex-router-sync实现原理下store.watch如何实现状态「时间旅行」一个布尔值巧妙打破无限循环【免费下载链接】vuex-router-syncEffortlessly keep vue-router and vuex store in sync.项目地址: https://gitcode.com/gh_mirrors/vu/vuex-router-syncvuex-router-sync 是 Vue 生态中让 vue-router 与 Vuex 保持同步的经典小库。上篇讲了它如何用router.afterEach把路由变化写进 store本篇深入另一半store.watch如何监听状态变化、把路由推回浏览器从而解锁 Vuex 的「时间旅行」能力——以及它只用一个布尔标志加一个路径记录就巧妙切断了双向同步的天敌无限循环。一、先回顾双向同步的两条通道vuex-router-sync 的核心入口只有一个sync()函数位于 src/index.ts它注册一个名为route的 Vuex 模块然后建立两条反向通道同步通道监听手段方向作用router.afterEach路由导航钩子router → store路由变了更新 store 里的route模块store.watchVuex 状态监听store → router状态变了把浏览器推到对应路由第一条通道上篇文章已拆解过本篇聚焦第二条store.watch。正是它让时间旅行成为可能。二、store.watch时间旅行的核心实现Vuex 的 DevTools 有一个招牌功能——时间旅行Time Travel撤销undo一个 mutation整个 store 的状态就会回到过去。问题在于如果只是store.state.route回滚了浏览器地址栏并不会跟着变旅行就不完整。vuex-router-sync 的做法是既然 route 模块被写进了 store那它也会随时间旅行一起回滚——而一个 watch 就能抓住这次回滚。对应的核心代码在 src/index.ts#L45-L59const storeUnwatch store.watch( (state) state[moduleName], // 监听整个 route 模块 (route) { const { fullPath } route if (fullPath currentPath) return // 防御一路径没变直接跳过 if (currentPath ! null) { isTimeTraveling true // 打标这次 push 源自 store router.push(route) // 关键一步状态 → 浏览器 } currentPath fullPath }, { sync: true } // 同步回调保证时序可控 )三个设计点值得新手注意getter 监听整个route模块。Vuex 的 watch 基于响应式依赖收集只要state.route被替换mutation 里整个对象被cloneRoute重建回调就会触发——无论是正常导航、还是 DevTools 撤销。router.push(route)是时间旅行的落点。当 undo 把route.fullPath从/about打回/homewatch 回调里这次 push 就真正驱动浏览器回到了/home。状态回滚 → 页面回滚闭环完成。{ sync: true }让回调同步执行。默认情况下 watch 回调会进入 Vue 的异步更新队列强制同步后检测路径 → 打标 → push 在状态变更的当个瞬间完成时序清晰这也是后续防循环逻辑成立的前提。三、暗藏的风险双向同步 无限循环把两条通道摆在一起一个致命问题就浮现了router.push→ 触发afterEach→store.commit→ route 状态变化 → 触发store.watch→ 又router.push→ ……两条通道互为因果任何一条触发都可能被另一条再次接住形成无限循环轻则死循环卡死页面重则无限 push 刷爆历史栈。vuex-router-sync 的解法极简一个布尔值isTimeTraveling 一个变量currentPath分别装在两个回调的入口处各挡一刀。防御一currentPath 比对——没变化就别动watch 回调的第一行就检查fullPath currentPath相同则直接return。这招挡住的是无意义的自我触发当 store 的更新其实是由 router 导航引起时正常流程下afterEach已经先把currentPath记成了目标路径随后 watch 醒来发现路径没变就安静退场不会再 push 一次。防御二isTimeTraveling 标志——我发起的别再回写标志位定义在 src/index.ts#L41-L42在两个回调中形成接力棒式的配合store → router 方向watch 回调里一旦确认要router.push先执行isTimeTraveling truesrc/index.ts#L52-L55意思是这次导航是我store发起的。router → store 方向afterEach钩子src/index.ts#L62-L69醒来第一件事就是看标志——router.afterEach((to, from) { if (isTimeTraveling) { isTimeTraveling false return // 是我发起的 push不再回写 store } currentPath to.fullPath store.commit(moduleName /ROUTE_CHANGED, { to, from }) })标志为真就复位并直接 return跳过 commit——循环在回写这一步被斩断。两条时序线各走一遍就懂了场景 A用户在 DevTools 里 undo时间旅行undo 生效state.route回滚到/homewatch 同步触发/home ≠ currentPath(/about)→isTimeTraveling true→router.push(/home)导航完成afterEach触发发现标志为真 →复位标志直接 return不 commit✅ 循环止步浏览器停留在/home且 store 状态与之一致它本来就回滚过去了场景 B用户手动router.push(/about)afterEach触发标志为假 → 记录currentPath /about→ commit 更新 storewatch 醒来发现fullPath currentPath→直接 return不再 push✅ 循环止步路由与状态各自就位两次防御一次各管一个方向配合得天衣无缝。四、细节收尾冻结与解绑除了防循环还有两处细节保证了同步的健壮性route 状态不可变cloneRoute在 src/index.ts#L87-L103 中对克隆出的路由对象执行Object.freeze。README 也明确强调URL 才是路由的唯一事实来源修改 store 里的 route 不应反过来驱动导航——冻结正是把这条约定固化进了运行时。unsync 干净解绑sync()返回的unsync()函数src/index.ts#L71-L84会依次移除afterEach钩子、解除 store 的 watch、注销 route 模块。相关清理逻辑在 test/index.spec.ts 的unsync用例中被逐项验证避免应用局部卸载后留下幽灵监听。五、小结vuex-router-sync 全文不到 100 行核心见 src/index.ts却完整演示了一个经典双向绑定难题的解法store.watch监听 route 模块把状态变化翻译成router.push让 Vuex 的时间旅行真正延伸到浏览器地址栏currentPath比对拦截无变化的冗余触发isTimeTraveling标志拦截自己发起的导航的回写两个变量一记一跳双向同步从此告别无限循环。这套标志位 基线比对的思路对任何需要双向同步的场景表单 ↔ 状态、组件 ↔ 服务都有直接借鉴价值。【免费下载链接】vuex-router-syncEffortlessly keep vue-router and vuex store in sync.项目地址: https://gitcode.com/gh_mirrors/vu/vuex-router-sync创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考