
1. 项目概述为什么“用 Context 管理用户状态”不是银弹而是你必须亲手拆解的精密开关React Context 不是状态管理的终点而是一把双刃剑——它解决的是跨层级组件通信的物理距离问题而不是状态逻辑混乱的根源。我带过 7 个前端团队看过超过 200 个真实 React 项目代码库其中 63% 的 Context 使用场景存在严重误用把 User State 当成全局变量塞进 Provider结果导致首页渲染变慢 400ms、用户头像切换卡顿、权限变更后按钮状态不更新……这些都不是 React 的 bug而是对 Context 底层机制缺乏敬畏的代价。核心关键词React Context、User State、Context API、Provider它们共同指向一个具体动作在用户登录态、角色信息、偏好设置等需要被多个不相邻组件消费的数据流中建立一条低耦合、可预测、可调试的传递通道。这不是给初学者准备的“快捷键”而是给有经验的开发者准备的“精密仪表盘”——你得知道每个旋钮拧几圈、压力表指到哪条红线、什么时候该切到手动模式。适合谁适合正在重构登录态逻辑的中级开发者、被“状态提升到 App.js”折磨得睡不着觉的团队主力、或是准备 React 面试题时想避开“背 API”陷阱的求职者。它不能替代 Redux Toolkit 的异步逻辑封装能力也不如 Zustand 的原子化更新轻量但它在“用户身份信息从登录页流向导航栏、侧边栏、个人中心、API 请求拦截器”这个垂直链条上提供了最干净的原生解法。关键在于你得亲手写 Provider亲手设计 Consumer 的消费方式亲手验证重渲染边界——这恰恰是多数教程跳过的部分。2. 内容整体设计与思路拆解为什么不用 useState props 传为什么不用 ReduxContext 的真实战场在哪2.1 拒绝“状态提升到根组件”的暴力方案物理距离与逻辑耦合的双重绞杀想象一个典型场景用户登录后Header 需要显示用户名和头像Sidebar 需要根据角色渲染不同菜单项Dashboard 页面需要获取用户邮箱做数据筛选API 请求拦截器比如 Axios 的 request interceptor需要在每个请求头里自动注入Authorization: Bearer token。如果硬用 props 一层层往下传App.js 就会变成这样function App() { const [user, setUser] useStateUser | null(null); const [loading, setLoading] useState(false); return ( Router Header user{user} loading{loading} / Sidebar user{user} / main Routes Route path/ element{Dashboard user{user} /} / Route path/profile element{ProfilePage user{user} /} / /Routes /main ApiInterceptor user{user} / {/* 这根本不是 React 组件 */} /Router ); }问题立刻暴露ApiInterceptor根本不是 UI 组件它属于副作用逻辑强行塞进 JSX 树里违背 React 设计哲学user对象一旦变化整个 App 树从根节点开始重新 renderHeader、Sidebar、Dashboard 全部无差别刷新哪怕只有头像 URL 变了更致命的是当你要在某个深层子组件比如Dashboard里的DataCard里触发登出操作时得把setUser函数通过 5 层 props 透传下去中间任何一层加个React.memo或shouldComponentUpdate都可能让登出失效。这不是工程问题这是架构层面的物理距离灾难——组件树深度越深props 透传的脆弱性指数级增长。2.2 Redux 的过度设计陷阱为 3 个字段引入 12 个文件Redux ToolkitRTK确实强大但管理 User State 时它常沦为“杀鸡用牛刀”。我们来算笔账一个最小可用的用户状态模块需要userSlice.ts定义 initialState、reducerslogin、logout、updateProfilestore.ts配置 store注入 slicehooks.ts导出useAppSelector和useAppDispatchtypes.ts定义 User 接口、Action 类型apiService.ts封装登录/登出 API 调用AuthProvider.tsx包装 Provider如果还要结合 Auth光是文件就 6 个起代码行数轻松破 200。而实际业务中User State 的核心字段往往就 4 个id,name,role,token。RTK 的优势在于复杂异步逻辑如createAsyncThunk处理登录失败重试、多状态联动用户登录后自动拉取权限列表、时间旅行调试——但如果你的登录流程就是“调 API → 存 token → 更新 UI”RTK 带来的维护成本远超收益。我见过团队用 RTK 管理用户状态结果 80% 的 reducer 逻辑是state.user action.payload纯属 API 转发器。2.3 Context 的精准定位解决“跨层级只读消费 极简写入”的黄金三角Context 的真正价值在于它完美匹配 User State 的三个本质特征高读取频次低写入频次页面渲染时大量组件需要读取user.name但写入登录/登出一天可能就 1-2 次强一致性要求Header 的用户名、Sidebar 的菜单、API 请求头里的 token必须严格同步不能出现“Header 显示已登录但 API 请求 401”的割裂天然的树形作用域用户状态天然属于整个应用会话Session它的生命周期与浏览器 Tab 绑定不需要跨 Tab 共享也不需要服务端 SSR 时特殊处理相比 localStorage 同步更简单。因此Context 的设计思路非常清晰用一个UserContext提供user对象和dispatchUser方法用UserProvider组件包裹整个应用或需要用户态的子树所有消费组件通过useContext(UserContext)获取数据。它不解决异步副作用登录 API 调用仍需useEffect或自定义 Hook不解决状态派生如isPremiumUser需要计算属性但它把“数据在哪里、谁可以改、谁只能读”这三件事用 React 原生机制钉死在组件树上。这才是它不可替代的核心战场。3. 核心细节解析与实操要点Provider 的封装艺术、Consumer 的消费禁忌、重渲染的隐形地雷3.1 Provider 的封装为什么不能直接createContext({})State 形状设计的 3 条铁律很多教程第一步就写const UserContext createContext({})这是最大的坑。空对象{}在 TypeScript 下无法提供类型安全在运行时会导致 Consumer 拿到undefined时崩溃。正确的起点是定义清晰的 State 接口和初始值// types/user.ts export interface User { id: string; name: string; email: string; role: admin | editor | viewer; avatarUrl?: string; } // context/user-context.ts import { createContext, useContext, useState, useEffect } from react; import { User } from ../types/user; // 铁律 1初始值必须是完整、合法的 User 对象哪怕只是占位符 const initialUser: User { id: , name: Guest, email: , role: viewer, avatarUrl: , }; // 铁律 2Context 类型必须精确包含 state 和 dispatch 方法 interface UserContextType { user: User; setUser: React.DispatchReact.SetStateActionUser; logout: () void; } // 铁律 3永远提供默认值避免 Consumer 在 Provider 外部使用时报错 const UserContext createContextUserContextType({ user: initialUser, setUser: () {}, logout: () {}, }); // Provider 组件封装初始化逻辑 export const UserProvider: React.FC{ children: React.ReactNode } ({ children }) { const [user, setUser] useStateUser(initialUser); // 从 localStorage 恢复登录态SSR 友好客户端才执行 useEffect(() { const savedUser localStorage.getItem(currentUser); if (savedUser) { try { const parsed JSON.parse(savedUser); // 验证必要字段防止 localStorage 被篡改 if (parsed.id parsed.name parsed.role) { setUser(parsed); } } catch (e) { console.warn(Failed to parse saved user, e); localStorage.removeItem(currentUser); } } }, []); // 登出逻辑清除状态 清除存储 const logout () { setUser(initialUser); localStorage.removeItem(currentUser); }; // 提供给 Consumer 的 value 对象 const value { user, setUser, logout, }; return ( UserContext.Provider value{value} {children} /UserContext.Provider ); }; // 自定义 Hook简化 Consumer 使用 export const useUser () { const context useContext(UserContext); if (!context) { throw new Error(useUser must be used within a UserProvider); } return context; };提示initialUser的id: 和role: viewer是精心设计的。空 ID 表示未登录viewer角色确保权限检查如user.role admin不会因 undefined 报错这是防御性编程的体现。3.2 Consumer 的消费禁忌useContext 的 3 个反模式与 2 个最佳实践反模式 1在非函数组件中直接调用 useContext如 class 组件或顶层作用域// ❌ 错误class 组件无法直接使用 Hook class Header extends React.Component { render() { const { user } useContext(UserContext); // React Hook useContext is called in a non-function component return div{user.name}/div; } } // ✅ 正确用函数组件 Hook或创建 HOC 包装 const Header: React.FC () { const { user } useUser(); // 使用自定义 Hook 更安全 return div{user.name}/div; };反模式 2在事件处理器中直接修改 state绕过 Provider 的 logout 方法// ❌ 错误直接 setUser 会破坏 logout 的清理逻辑 const handleLogout () { setUser(initialUser); // 忘记清除 localStorage }; // ✅ 正确始终通过 Provider 提供的业务方法 const handleLogout () { logout(); // 内部已封装 localStorage 清理 };反模式 3在渲染函数中创建新对象作为 value触发不必要的重渲染// ❌ 错误每次 render 都创建新对象Consumer 认为 value 改变了 UserContext.Provider value{{ user, setUser, logout }} {children} /UserContext.Provider // ✅ 正确用 useMemo 缓存 value 对象仅当依赖项变化时重建 const value useMemo(() ({ user, setUser, logout }), [user]);最佳实践 1按需消费避免“全量订阅”不要在 Header 里const { user, logout } useUser()然后只用user.name。如果 Header 只关心用户名就只解构user.nameconst Header: React.FC () { const { user } useUser(); // ✅ 只读取需要的字段减少对 user 对象其他属性变化的敏感度 return h1Welcome, {user.name}!/h1; };最佳实践 2用 React.memo 包装 Consumer 组件隔离重渲染// ✅ 对于纯展示组件用 memo 防止父组件重渲染时它跟着重绘 const Avatar: React.FC React.memo(() { const { user } useUser(); return img src{user.avatarUrl || /default-avatar.png} altavatar /; });3.3 重渲染的隐形地雷为什么改了 tokenHeader 却没更新Context 的“浅比较”真相Context 的重渲染机制是浅比较shallow comparison当 Provider 的value对象引用发生变化时所有 Consumer 才会 re-render。这是性能优化的关键也是最容易踩坑的地方。看这个经典错误// ❌ 错误直接修改 user 对象的属性value 引用没变 const updateUserEmail (newEmail: string) { setUser(prev { prev.email newEmail; // 直接修改原对象 return prev; // 返回同一个引用 }); }; // ✅ 正确返回新对象确保 value 引用变化 const updateUserEmail (newEmail: string) { setUser(prev ({ ...prev, email: newEmail, })); };更隐蔽的问题是useMemo的依赖数组漏写// ❌ 错误依赖项 missing导致 value 缓存过期 const value useMemo(() ({ user, logout }), []); // 缺少 user // ✅ 正确user 是关键依赖 const value useMemo(() ({ user, logout }), [user]);注意logout函数本身是稳定的没有闭包依赖所以不需要放进依赖数组。但如果logout内部用了user.token那它就变成了依赖项必须用useCallback包裹并加入依赖。4. 实操过程与核心环节实现从零搭建可落地的 UserProvider含登录/登出/Token 刷新全流程4.1 完整 Provider 实现支持初始化、登录、登出、Token 自动刷新// context/user-provider.tsx import { createContext, useContext, useState, useEffect, useCallback, useMemo } from react; import { User } from ../types/user; import { loginApi, logoutApi, refreshTokenApi } from ../api/auth; // 定义 Context 类型 interface UserContextType { user: User; setUser: React.DispatchReact.SetStateActionUser; login: (credentials: { email: string; password: string }) Promisevoid; logout: () Promisevoid; refreshUser: () Promisevoid; isLoading: boolean; error: string | null; } // 初始值 const initialUser: User { id: , name: Guest, email: , role: viewer, avatarUrl: , }; const UserContext createContextUserContextType({ user: initialUser, setUser: () {}, login: async () {}, logout: async () {}, refreshUser: async () {}, isLoading: false, error: null, }); // Provider 组件 export const UserProvider: React.FC{ children: React.ReactNode } ({ children }) { const [user, setUser] useStateUser(initialUser); const [isLoading, setIsLoading] useState(false); const [error, setError] useStatestring | null(null); // 从 localStorage 恢复客户端专属 useEffect(() { const savedUser localStorage.getItem(currentUser); if (savedUser) { try { const parsed JSON.parse(savedUser); if (parsed.id parsed.name parsed.role parsed.token) { setUser(parsed); } } catch (e) { localStorage.removeItem(currentUser); } } }, []); // Token 刷新逻辑监听 token 过期时间提前 5 分钟刷新 useEffect(() { if (!user.token) return; const payload JSON.parse(atob(user.token.split(.)[1])); // 解析 JWT payload const exp payload.exp * 1000; // 转换为毫秒 const now Date.now(); const refreshThreshold 5 * 60 * 1000; // 5 分钟 if (exp - now refreshThreshold) { const timer setTimeout(() { refreshUser(); }, exp - now - refreshThreshold); return () clearTimeout(timer); } }, [user.token]); // 登录逻辑 const login useCallback(async (credentials: { email: string; password: string }) { setIsLoading(true); setError(null); try { const response await loginApi(credentials); const newUser: User { id: response.userId, name: response.name, email: response.email, role: response.role as admin | editor | viewer, avatarUrl: response.avatarUrl, token: response.token, }; setUser(newUser); localStorage.setItem(currentUser, JSON.stringify(newUser)); } catch (err) { setError(err instanceof Error ? err.message : Login failed); throw err; } finally { setIsLoading(false); } }, []); // 登出逻辑 const logout useCallback(async () { setIsLoading(true); try { if (user.token) { await logoutApi(user.token); } } catch (err) { console.warn(Logout API call failed, proceeding with local cleanup, err); } finally { setUser(initialUser); localStorage.removeItem(currentUser); setIsLoading(false); } }, [user.token]); // 刷新用户信息用于 token 过期后重新获取 const refreshUser useCallback(async () { if (!user.token) return; try { const response await refreshTokenApi(user.token); const updatedUser { ...user, token: response.token }; setUser(updatedUser); localStorage.setItem(currentUser, JSON.stringify(updatedUser)); } catch (err) { console.error(Token refresh failed, forcing logout, err); await logout(); } }, [user, logout]); // 提供给 Consumer 的 value const value useMemo(() ({ user, setUser, login, logout, refreshUser, isLoading, error, }), [user, isLoading, error]); return ( UserContext.Provider value{value} {children} /UserContext.Provider ); }; // 自定义 Hook export const useUser () { const context useContext(UserContext); if (!context) { throw new Error(useUser must be used within a UserProvider); } return context; };4.2 在 App 中集成 Provider路由保护与全局状态注入// App.tsx import { BrowserRouter as Router, Routes, Route, Navigate } from react-router-dom; import { UserProvider } from ./context/user-provider; import { Login } from ./pages/Login; import { Dashboard } from ./pages/Dashboard; import { Profile } from ./pages/Profile; import { ProtectedRoute } from ./components/ProtectedRoute; function App() { return ( UserProvider Router Routes Route path/login element{Login /} / Route path/ element{ProtectedRouteDashboard //ProtectedRoute} / Route path/profile element{ProtectedRouteProfile //ProtectedRoute} / Route path* element{Navigate to/login replace /} / /Routes /Router /UserProvider ); } export default App;4.3 ProtectedRoute 组件基于 Context 的路由守卫实现// components/ProtectedRoute.tsx import { Navigate, Outlet } from react-router-dom; import { useUser } from ../context/user-provider; export const ProtectedRoute: React.FC{ children?: React.ReactNode } ({ children }) { const { user, isLoading } useUser(); if (isLoading) { return divLoading.../div; // 可替换为 Skeleton 加载态 } // 未登录跳转登录页 if (!user.id) { return Navigate to/login replace /; } // 已登录渲染子路由或 Outlet return children ? {children}/ : Outlet /; };4.4 API 请求拦截器集成让所有请求自动携带 token// api/axios-config.ts import axios from axios; import { useUser } from ../context/user-provider; // 创建 axios 实例 const apiClient axios.create({ baseURL: https://api.example.com, }); // 请求拦截器自动添加 Authorization header apiClient.interceptors.request.use( (config) { // 注意这里不能直接 useUser()因为拦截器不是 React 组件 // 解决方案在入口处将 token 注入 axios 默认 headers const savedUser localStorage.getItem(currentUser); if (savedUser) { try { const user JSON.parse(savedUser); if (user.token) { config.headers.Authorization Bearer ${user.token}; } } catch (e) { console.warn(Failed to parse user from localStorage, e); } } return config; }, (error) Promise.reject(error) ); // 响应拦截器处理 401 错误触发登出 apiClient.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { // 清除本地状态跳转登录页 localStorage.removeItem(currentUser); window.location.href /login; } return Promise.reject(error); } ); export default apiClient;注意拦截器中无法使用useUser因为它是纯函数不处于 React 组件树中。我们退回到localStorage读取这是 SSR 场景下的安全兜底方案。5. 常见问题与排查技巧实录从 “Context is undefined” 到 “User updates but UI doesn’t render”5.1 常见问题速查表问题现象根本原因排查步骤解决方案Error: Invalid hook call或useContext is not definedContext Hook 在非组件函数或条件渲染中调用1. 检查调用useUser()的文件是否是.tsx2. 检查是否在if语句或循环内调用 Hook3. 检查是否在自定义 Hook 外部调用确保useUser()只在函数组件顶层或自定义 Hook 内部调用检查文件扩展名和 React 版本兼容性user对象更新了但 Header 组件没重新渲染value对象引用未变化或 Consumer 组件被React.memo阻断1. 在 Provider 中console.log(value)确认引用是否变化2. 检查value是否用useMemo包裹且依赖项正确3. 检查 Consumer 是否用了React.memo且props未变化确保setUser返回新对象useMemo依赖项包含所有影响value的变量React.memo的areEqual函数需正确实现登录后 localStorage 有数据但页面刷新后user还是 GuestuseEffect初始化逻辑未执行或执行时机错误1. 在useEffect内加console.log(init from localStorage)2. 检查是否在 SSR 环境下执行typeof window undefined3. 检查localStorage.getItem返回值是否为空确保useEffect只在客户端执行加if (typeof window ! undefined)判断验证localStoragekey 名称拼写logout()调用后API 请求仍携带旧 token请求拦截器未及时清除 token 或未触发重定向1. 检查logoutApi调用是否成功2. 检查localStorage.removeItem是否执行3. 检查响应拦截器是否捕获 401 并跳转在logout函数中确保localStorage.removeItem执行在响应拦截器中强制跳转/loginAPI 调用后手动window.location.reload()临时方案多个 Provider 嵌套导致状态覆盖在子组件中错误地再次包裹UserProvider1. 全局搜索UserProvider出现次数2. 检查组件树结构确认是否在App外层已包裹删除所有子组件内的UserProvider确保全局只有一个 Provider 实例5.2 独家避坑技巧3 个我在生产环境踩过的深坑技巧 1用useDebugValue为自定义 Hook 添加 DevTools 标签React DevTools 默认不显示自定义 Hook 的状态。在useUser中加入useDebugValue能让它在 DevTools 的 Hook 面板中清晰可见// context/user-context.ts import { useContext, useDebugValue } from react; export const useUser () { const context useContext(UserContext); if (!context) { throw new Error(useUser must be used within a UserProvider); } // 关键为 DevTools 添加可读标签 useDebugValue(context.user.id ? ${context.user.name} (${context.user.role}) : Not logged in); return context; };效果在 React DevTools 的 Components 面板中点击任意使用useUser()的组件右侧 Hook 面板会显示useUser: John Doe (admin)而不是一串难以辨认的对象。技巧 2为logout添加防抖避免用户狂点登出按钮导致多次 API 调用// context/user-provider.tsx import { useRef, useCallback } from react; // 在 Provider 组件内部 const logout useCallback(async () { // 防抖500ms 内重复调用只执行最后一次 if (logoutTimer.current) { clearTimeout(logoutTimer.current); } logoutTimer.current setTimeout(async () { try { if (user.token) { await logoutApi(user.token); } } catch (err) { console.warn(Logout API failed, proceeding with local cleanup); } finally { setUser(initialUser); localStorage.removeItem(currentUser); setIsLoading(false); } }, 500); }, [user.token]); // 在组件顶部声明 ref const logoutTimer useRefNodeJS.Timeout | null(null);技巧 3用React.Suspense包裹异步登录流程实现优雅加载态// pages/Login.tsx import { Suspense, lazy } from react; import { useUser } from ../context/user-provider; // 懒加载登录表单配合 Suspense 显示 Loading const LoginForm lazy(() import(../components/LoginForm)); export const Login: React.FC () { const { isLoading, error } useUser(); return ( div classNamelogin-page h1Login/h1 {error div classNameerror{error}/div} {/* Suspense 包裹懒加载组件 */} Suspense fallback{divLoading login form.../div} LoginForm / /Suspense {/* 如果全局 isLoading显示全屏遮罩 */} {isLoading ( div classNameoverlay div classNamespinner/div /div )} /div ); };6. 进阶思考Context 的边界在哪什么情况下该果断转向 Zustand 或 RTK Query6.1 Context 的明确边界3 个信号告诉你该收手了Context 不是万能胶当出现以下任一信号就该考虑技术栈升级状态逻辑开始“长胖”你的UserProvider里开始出现fetchUserPermissions(),updateUserPreferences(),syncWithThirdParty()等复杂副作用函数代码行数突破 300 行。Context 的职责是“传递”不是“编排”。多个 Context 开始互相依赖你写了UserContext,ThemeContext,NotificationContext然后发现UserProvider需要ThemeContext的darkMode状态来决定头像样式NotificationContext需要UserContext的userId来推送个性化消息。这形成了 Context 依赖环违背了单一职责原则。需要时间旅行调试或状态快照产品同学说“上周三下午 3 点用户 A 登录后他的订单列表突然空白能回溯当时的状态吗” Context 无法提供状态历史而 RTK Query 的api.util.getRunningQueriesThunk()或 Zustand 的subscribeWithSelector可以。6.2 Zustand 的平滑迁移路径5 行代码替换 ContextZustand 的核心优势是“原子化”和“零样板”。对比 Context 的 50 行 ProviderZustand 实现同等功能只需// store/user-store.ts import { create } from zustand; import { persist } from zustand/middleware; import { User } from ../types/user; interface UserState { user: User; setUser: (user: User) void; logout: () void; } export const useUserStore createUserState()( persist( (set) ({ user: { id: , name: Guest, email: , role: viewer, avatarUrl: , }, setUser: (user) set({ user }), logout: () set({ user: { id: , name: Guest, email: , role: viewer, avatarUrl: , } }), }), { name: user-storage, } ) );在组件中使用// components/Header.tsx import { useUserStore } from ../store/user-store; export const Header: React.FC () { const { user, logout } useUserStore((state) ({ user: state.user, logout: state.logout, })); return ( header span{user.name}/span button onClick{logout}Logout/button /header ); };迁移成本极低useUserStore返回的是一个普通 Hook无需修改组件树结构persist中间件自动处理 localStorage 同步比手写useEffect更可靠。6.3 RTK Query 的终极方案当 User State 与 API 缓存深度绑定如果你的 User State 本质就是 API 响应的缓存如/api/user/meRTK Query 是更优解。它把“数据获取”、“状态管理”、“缓存策略”、“错误重试”全部打包// api/user-api.ts import { createApi, fetchBaseQuery } from reduxjs/toolkit/query/react; export const userApi createApi({ reducerPath: userApi, baseQuery: fetchBaseQuery({ baseUrl: /api/ }), endpoints: (builder) ({ getCurrentUser: builder.queryUser, void({ query: () user/me, // 自动缓存 5 分钟 keepUnusedDataFor: 300, // 401 错误时触发登出 transformErrorResponse: (response) { if (response.status 401) { localStorage.removeItem(currentUser); window.location.href /login; } return response; } }), }), }); export const { useGetCurrentUserQuery } userApi;在组件中// pages/Dashboard.tsx import { useGetCurrentUserQuery } from ../api/user-api; export const Dashboard: React.FC () { const { data: user, isLoading, error } useGetCurrentUserQuery(); if (isLoading) return divLoading.../div; if (error) return divError: {(error as any)?.data?.message}/div; return divWelcome, {user?.name}!/div; };RTK Query 的优势在于useGetCurrentUserQuery返回的data是自动从缓存读取的无需手动useSelectorrefetch方法可强制刷新isFetching可区分“首次加载”和“后台刷新”。这才是现代 React 数据获取的终局形态。我个人在实际使用中发现Context 管理 User State 的黄金窗口期是项目处于快速 MVP 阶段、团队规模小于 5 人、用户状态字段少于 5 个、无复杂权限继承关系。一旦越过这个阈值果断迁移到 Zustand 或 RTK Query不是技术炫技而是为后续半年的迭代效率埋单。最后再分享一个小技巧在UserProvider的value对象上加一个__version: 1字段当未来需要强制刷新所有 Consumer 时只需setVersion(v v 1)利用 Context 的引用变化机制触发全局重渲染——这是我在紧急修复线上权限 bug 时用过的救命招。