1. 为什么 React 值得投入时间:先看懂这轮的更新逻辑
说实话,这两年每次有人让我推荐前端入门框架,我大概率推荐 React。不因为 React 是万能的,而是因为它的设计思路确实经得起时间考验——从 Facebook 内部项目走到今天,整个生态已经相当成熟,无论是做中后台、跨端小程序还是原生体验的 App,React 都有对应的技术方案。市面上关于 React 的资料多得能淹没一座图书馆,但真正能帮到人的往往是那些从实战里长出来的经验,而不是复述官方文档。这篇文章源自最近做的一轮 React 项目复盘,从 18 的批处理机制一直聊到路由选型、React Native 白屏、Taro 跨端,再到面试高频考点。我尽量把那些文档里没有明说、但实际开发中一定会遇到的问题都讲透,让知识串成一条线。无论你是刚打算入门 React 的新人,还是已经在写业务代码但想看透背后原理的开发者,读完应该都会有些收获。
先说清楚 React 到底解决了什么问题。用最直白的话讲,React 是一个用来构建用户界面的 JavaScript 库,核心思想是当你把界面拆成一个个小小的组件,再用状态(state)驱动界面更新,事情就变得可预测了。你不用再像老式 jQuery 时代那样手动去操作 DOM,而是声明"这个页面的数据长什么样",React 帮你把界面渲染出来。这种思路最大的优势在于:代码逻辑清晰、团队协作方便、遇到复杂交互时不容易写成一团乱麻。
而 React 18 的出现,又把这套运行机制往更深处推了一步。很多人在刚开始学习 React 时会有个困惑:React 不是号称性能很好的吗?为什么我每次 setState 都要造成一次页面重新渲染?这里就牵扯到一个核心概念——批处理(Batching)。批处理并不是 React 18 才有的东西,只是 18 把它升级到了"自动批处理"(Automatic Batching),覆盖范围更广,行为也更符合直觉。如果这一层没吃透,你在写 React 时就会频繁地踩到"咦,我明明只更新了一次状态,页面怎么闪了好几下"或者"为什么这里的状态更新没有立刻看到效果"这类坑。所以我想先从批处理机制开始,把 React 18 的变化聊透。
提示:React 18 的自动批处理是面试中常考的深水区,也是实际开发中很多性能问题的根源,建议仔细读完这一节再往下走。
2. 核心机制拆解:React 18 的自动批处理到底改了什么
2.1 先理解"批处理"的起点:为什么 setState 会合并
在 React 的老版本(React 17 及之前)里,批处理这个概念就已经存在了,只不过它的生效范围非常有限。我举一个最常见的场景:你在一个 onClick 事件里连续调用了三次 setState,比如这样:
function handleClick() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }你觉得页面会重新渲染几次?如果凭直觉回答"三次",那说明你对 React 的渲染机制还有误解。实际上,在 React 17 及之前,React 会把这三次 setState 合并成一次渲染,最终的 count 也只会加 1,而不是加 3。原因很简单:React 的更新调度机制认为,同一个事件处理函数里发生的状态变更,应该作为一个原子更新来处理,没必要把一份 UI 重复渲染三遍。
那为什么实际加到的是 1 不是 3?因为这里的 setCount 传入的是一个具体的数值,React 会在批处理过程中"后写的覆盖先写的",最后一次加 1 胜出。如果你希望从当前值累加三次,就必须传入函数式的更新方式:
setCount((prev) => prev + 1); setCount((prev) => prev + 1); setCount((prev) => prev + 1);React 会把这三个函数依次执行,最后 count 加 3。这个差异在实际开发里非常容易翻车,尤其是那种"循环里 setState"的场景。我在不少项目里见过类似的 bug,表面上看代码逻辑没问题,但最终状态总是比预期少一截,就是因为把函数式更新和值更新混用了。
2.2 React 18 自动批处理的覆盖范围变化
React 18 的核心升级点在于:批处理不再只局限于 React 自身的事件系统内,而是把范围扩大到了 Promise、setTimeout、原生事件监听器等更多异步场景中。这个变化有一个非常直观的体验改善:以前你做个请求数据再设置状态的逻辑,可能会因为多次 setState 导致界面闪烁或者布局跳动,而现在 React 会把同一个"异步代码块"里的所有更新自动合并成一次渲染。
举个例子,在 React 17 中,下面这段代码的渲染次数是两次:
function fetchData() { // 模拟异步请求 Promise.resolve().then(() => { setLoading(false); setData(result); }); }因为 Promise 回调不在 React 的事件系统内,React 17 不会对它做批处理,所以每调用一次 setState 就会触发一次渲染。到了 React 18,这段代码会非常自然地合并为一次渲染。这看起来是个小优化,但在真实项目中效果非常明显——尤其是列表页同时更新 loading、刷新列表数据、更新分页信息时,自动批处理能让整个页面只重绘一次,流畅度和布局稳定性都会好很多。
不过要注意,自动批处理并不是所有场景下都生效。React 官方给出的不生效情况主要在 Suspense 的 fallback 展示阶段,还有一些极特殊的高优先级更新场景里。这块我在 2.3 里详细说。
2.3 批处理失效的场景与原因分析
我刚提到 React 18 自动批处理覆盖面更广,但实际使用中仍然存在三种常见情况,React 不会老老实实地合并更新:
第一种,使用了 flushSync。React 18 专门提供了一个 API 来"强制同步刷新",它会立刻把当前队列里的所有更新推送给浏览器渲染。这在某些极少数场景下是有用的,比如你需要在 setState 之后立刻读取 DOM 的尺寸或位置,但代价就是失去批处理的性能优势。实际项目里能用 flushSync 的地方非常少,我见过不少同学因为对更新时机不熟悉,用 flushSync 来"修复"逻辑问题,结果只是把问题藏得更深了。真正需要 flushSync 的场景应该是对渲染时序有硬性要求的操作,比如某些拖拽类组件在 dragging 状态切换时需要同步读取布局信息。
第二种,在脱离 React 上下文的环境里手动调用 setState,比如把 setState 函数传递给了第三方非 React 库,这个库的异步回调在更新的复杂调度下可能不会触发自动批处理。这种情况在 React 18 中已经大大减少,但严谨地说,只要你把更新函数抛给了外部系统,React 就失去了对调度上下文的完整控制,行为会变得不可预测。所以一个常见的最佳实践是:尽量把外部回调统一封装成 React 内部的自定义 Hook,而不是散落到组件各处。
第三种,独立根节点的更新。如果应用中同时使用了两个以上的 React 根节点(例如多个 createRoot 创建的应用实例),React 并没有跨根节点的调度能力,每个根节点各自维护自己的更新队列,批处理也就无法跨根合并。这在微前端场景下尤其明显,主应用和子应用各自挂在不同的 React 根上,更新互不相通,性能优化时往往得从微前端框架层面去想办法。
注意:面试时你完全可以这样回答 React 18 自动批处理的边界——它不是万灵药,设计本质是"合并可合并的更新,保证关键更新的即时性"。主动用 flushSync 会破坏批处理,但这是有意识地选择,不是 bug。
2.4 从源码层面理解批处理的调度时机
要真正搞懂批处理,光看官方文档是不够的,还得稍微瞄一眼源码原理。React 18 的更新调度不再直接依赖任务队列的"宏任务/微任务"概念,而是实现了自己的调度器(Scheduler),它以优先级为单位管理更新任务。在这个调度器之上,React 把一次更新分为两个阶段:render 阶段和 commit 阶段。
批处理发生在 render 阶段之前。当你触发 setState 时,React 先把这个更新加入更新队列,然后通过 scheduleUpdateOnFiber 函数判断当前有没有正在执行的渲染任务。如果没有,它会把这一次更新调度成一次新的渲染。如果在同一轮事件循环里触发了多个 setState,它们会依次进入同一个调度队列,然后由 Scheduler 统一安排,最终只进入一次 render。这么设计的好处是,即使更新来源五花八门(点击事件、定时器、网络回调),React 也能根据优先级合并处理,把浏览器主线程的空闲时间利用起来。
从面试角度讲,能把"批处理本质是 React 调度器对更新队列的合并策略"这句话说清楚,就已经超过大多数候选人。大多数面经里只会背"React 18 支持自动批处理",但说不出为什么。把这个机制的知识点记牢,无论你做项目还是面试,都明显更有底气。
3. 路由场景选型:React Router 与 Vue Router 的核心差异
3.1 设计思想的分野:组件化声明式与配置中心化的博弈
路由是前端项目绕不开的技术选型问题,尤其是很多团队同时维护着 React 和 Vue 两套技术栈,比较二者路由设计的差异就成了非常实际的需求。我在做技术方案时最直观的感受是:React Router 把路由当成 React 组件体系的一部分来设计,而 Vue Router 更倾向于集中式配置路由表。这个根本差异决定了后续所有的开发体验。
React Router 的写法非常声明式,路由本身就是放在组件树里的一个嵌套结构。以 React Router v6 为例子,最常见的写法是用 createBrowserRouter 或 Routes/Route 组合:
const router = createBrowserRouter([ { path: '/', element: <Layout />, children: [ { index: true, element: <Home /> }, { path: 'user/:id', element: <UserDetail /> }, ], }, ]);这种写法的好处在于——路由和 React 组件的渲染逻辑天然统一,你的路由跳转其实就是组件树的切换,可以非常方便地在路由层级中共享布局组件,比如父路由渲染一个带导航栏的 Layout,子路由渲染具体页面。再加上 React Router 提供的高阶组件或 Hooks(useNavigate、useParams、useLocation),开发体验非常顺手。
Vue Router 则把路由表单独抽出来,在 main.js 里集中注册,页面组件通过 RouterView 占位渲染。它的核心优势在配置清晰,整个项目的路由权限集中管理,改起来一目了然。Vue Router 内置的导航守卫(beforeEach、beforeResolve 等)在权限控制场景下非常方便,直接写在路由定义里,配合后端的用户角色信息,可以比较轻松地实现菜单级别的路由拦截。
3.2 路由守卫:Vue 自带拦截器,React 需要自己封装
这是两者在使用体验上最大的差异之一。Vue Router 的路由守卫是框架特性,我可以在路由跳转前后做各种拦截逻辑,比如"用户未登录则跳转到登录页""进入管理后台前先拉取用户权限"。它相当于在请求的路口设置了路障,每个导航都必须通过路障检查才能继续。
React Router 没有内置的路由守卫概念。你当然能实现相同的效果,但方式要"React 化"一点。常见的做法是封装一个 RequireAuth 组件,包裹需要鉴权的页面:
function RequireAuth({ children }) { const { user } = useAuth(); const navigate = useNavigate(); useEffect(() => { if (!user) { navigate('/login', { replace: true }); } }, [user, navigate]); return user ? children : null; } // 使用方式 <Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />这种封装方式写出来的路由代码更加贴近 React 组件思维:鉴权逻辑是组件的一部分,而不是框架的额外机制。但也正因为如此,团队的编码规范约束就很重要。如果团队里每个开发都按自己的思路实现鉴权,很容易出现逻辑不一致的维护噩梦。我的建议是,在 React 项目中把路由权限控制抽象成一个统一高阶组件或者 Context Provider,保证全局只有一种实现方式。
3.3 动态路由与嵌套路由的实现差异
动态路由指的是在路由配置中带参数传递,比如用户详情页的 ID、商品详情页的 SKU。两者都有对应的设计,React Router 用的是 :id 语法,Vue Router 用的也是 :id 语法,但取参数的 API 不一样。React Router 里我用 useParams 拿到参数,Vue Router 里在组件里通过 useRoute 取。看起来都很简单,但实际业务里动态路由往往和权限系统、面包屑导航绑定在一起,这时候差异就显现出来了。
Vue Router 的 meta 字段可以让开发者很方便地在路由配置里附加面包屑文本、窗口标题、页面图标等元信息,集中管理极其清晰。React Router v6 没有原生的 meta 概念,但你完全可以在路由对象里自定义字段,因为 createBrowserRouter 接收的本质上就是一个普通对象数组,多塞几个 field 完全合法。比如我可以这样写:
{ path: '/user/:id', element: <UserDetail />, handle: { title: '用户详情', breadcrumb: '用户管理 / 用户详情', permission: 'user:view', }, }然后在 Layout 组件里通过 useMatches 读取 handle 信息来动态生成面包屑。这种方式跟 Vue Router 的 meta 没有本质区别,灵活度甚至更高,但也意味着团队需要有意识地在业务代码里约定好这些自定义字段的命名和用途,否则就容易变成各自飞。
3.4 不同业务场景下的选型建议
我在技术方案评审时经常被问到:"React 和 Vue 到底怎么选?"我的答案一直都很实际:看团队与项目类型,不要盲目跟风。
如果项目是大型中后台管理系统,这类系统往往有复杂的嵌套菜单、路由级权限和大量的表单页面。此时 React Router 的组件化能力配合状态管理库(Redux Toolkit 或 Zustand)非常顺手。如果你团队本来就是 React 技术栈,更加不用犹豫。如果项目是面向 C 端的移动端 H5 或偏营销侧的轻交互页面,Vue 的模板语法上手快、开发效率高,Vue Router 的集中式配置也让后续维护更加简单直观。尤其是小团队、新团队、产品需求频繁调整的情境下,Vue 的易读性优势很难被替代。
还有一种常见场景是混合技术栈团队,同一个部门既有 React 项目又有 Vue 项目。这种情况下我强烈建议不要为了统一而强行迁移,而是在路由层保持各技术栈的最佳实践,但在请求层(axios 封装、错误码处理)和数据层(统一的状态管理规范)做统一约束。这样团队协作效率更高,也不会让某一边的开发被迫做出不合理的妥协。
我把这两种主流技术栈的路由对比整理成一个表格,方便面试和选型时直接参考:
| 对比维度 | React Router v6 | Vue Router 4 | 选型建议 |
|---|---|---|---|
| 设计理念 | 组件化、声明式 | 集中式路由配置、Hook API | 理解团队偏好 |
| 路由守卫 | 需自行封装高阶组件 | 内置完整导航守卫 | 快速权限控制选 Vue |
| 动态路由参数 | useParams() | useRoute().params | 差异不大 |
| 元信息 meta | 自定义 handle 字段 | 自带 meta 字段 | 中后台偏好 Vue |
| 与状态管理集成 | 常配合 Redux/Zustand | 常配合 Pinia/Vuex | 看技术栈 |
| 学习曲线 | 逐渐理解"路由即组件" | 配置式更直白 | 新人友好选 Vue |
4. React Native 实战:启动白屏排查与循环滚轮组件实现
4.1 启动白屏的成因:JS 引擎到原生 UI 的整个链路
React Native(简称 RN)是 React 技术栈跨端到移动端的代表方案,但跨端方案永远有跨端的烦恼,排第一的就是启动白屏。很多刚接触 RN 的开发者都会遇到一个体验非常惊悚的场面——App 正常安装,打开后屏幕一片空白,等了半天才有界面出来,甚至永远都是白屏。我先帮你把这条链路的每个环节理清楚,白屏的根因自然就清楚了。
RN 的启动链路从原生代码开始,依次经过:原生 Activity/Fragment 创建 → 初始化 ReactRootView → JS 引擎启动(JSC 或 Hermes)→ 加载 JS Bundle → 执行 JS 代码 → render 渲染出 Native 视图。任何一个环节卡住了,画面就可能停在空白状态。白屏可以分为两种,一种是启动阶段的白屏,另一种是页面切换时的白屏。启动白屏更多是 JS Bundle 太大或者初始化耗时过长;页面切换白屏则往往是导航配置错误或页面组件报错了。
如果你用 Android 跑 RN,打开应用后白屏一闪而过,可以优先查看 Logcat 里有没有红色的异常堆栈,最常见的一类是 JavaScriptException 或 RuntimeException。iOS 上则看 Xcode 控制台。只要能看到异常栈,问题其实就解决了一半。真正难排查的是那种没有任何异常、静默白屏的情况,这往往是 JS 执行没成功,或者渲染结果被某个原生层遮挡了。
4.2 白屏排查步骤:从 Bundle 到渲染层的分层检查
我记得上一次遇到严重白屏问题是在一个用 Hermes 引擎的项目里,现象是 Release 包刚装时启动正常,在弱网或冷启动较慢时容易白屏,且没有任何报错。最后定位到是启动时机问题:原生给 JS 的初始化数据还没准备好,JS 侧就开始渲染了,导致首屏渲染依赖的数据缺失,界面渲染不出来。当时我把启动流程重新梳理了一遍,按照下面这套分层检查法,一步步排查才定位到根因。
这套排查流程可以归纳成四个步骤:
检查 Bundle 是否加载成功。最简单的做法是在 React Native 里打印全局变量DEV或者用 global.console.log 输出一条启动日志。如果日志输出了,说明 JS 引擎和 Bundle 没问题,否则问题出在这层。也可以用 Chrome DevTools 或 React Native DevTools 连接调试,看 Network 面板里 Bundle 的加载情况。
检查原生视图层是否被遮挡。有些项目会在原生层叠加了一个启动闪屏 View,如果闪屏 View 没有在 JS 渲染完成后手动隐藏,就会一直盖在上面,看起来就是白屏。这种情况可以通过在原生侧加一个定时器,延迟隐藏闪屏来试探。
检查首次渲染的数据是否为空。如果你的首页 UI 依赖异步请求的数据,而请求失败或者接口慢,页面很可能就是空白的。建议在页面根组件加一个边界的 loading/error 状态,不要直接返回 null。
检查导航容器的初始化。如果你用了 React Navigation,Stack.Navigator 的 initialRouteName 如果写错,又恰好没有做兜底路由,也会出现白屏。用 navigation 时务必设置 initialRouteName,并在路由表外层加一层"空页面兜底"。
注意:RN 白屏问题往往是多个因素叠加造成的。我的建议是不要一上来就猜是引擎问题或者框架 bug,先按上面四步做分层排除,90% 的情况都是配置或者数据问题。
4.3 循环滚轮组件:两种主流实现做一次选型
React Native 社区里实现"循环滚轮"(picker wheel)是一个经典需求,比如实现日期选择、城市选择、自定义评分选择器。这类组件的核心难点在于——滚轮需要平滑滚动,并且支持无限循环(滚到末尾会回到开头)。在原生 App 里这是小菜一碟,因为 iOS 的 UIPickerView 和 Android 的 WheelView 都是现成的;但在 React Native 里,你想同时兼容 iOS 和 Android,就得仔细选型。
第一类方案是使用现成的第三方库,比如 @react-native-picker/picker 或者 react-native-wheel-picker-android。前者在 iOS 上表现不错,后者主要针对 Android,跨端需要做适配。优点是开发速度极快,缺点是自定义程度有限,UI 风格不够灵活,遇到比较特殊的设计稿往往很难满足。
第二类方案是基于 ScrollView 或者 FlatList 手动模拟滚轮。以 FlatList 为例,思路是把数据列表渲染成横向或纵向的列表,设定一个固定的行高,然后监听滚动事件,在滚动停止时计算出当前选中的行索引。循环的核心逻辑在于——当滚到末尾时,通过 scrollToIndex 瞬间跳转到"复制出来的第 N 段",给用户造成无限滚动的视觉错觉。这种方式实现起来成本中等,但 UI 完全可控,适合定制需求强的团队。
我自己的建议分场景:如果项目里只是简单用一下选择器,用现成库更稳妥;如果这个滚轮是产品主视觉的一部分,或者设计要求非常高,那就直接自己封装一个 FlatList 版本,长期收益更高。要注意的是,滚动列表必须设置 getItemLayout 或者行高固定,否则滚动距离计算容易出错。
4.4 封装滚轮时的 5 个避坑细节
封装滚轮时最容易踩到以下几个坑,我一条条解释清楚。
第一,不要用 onScroll 做实时状态更新。你可能会想在滚动时实时更新当前选中值,但 onScroll 的触发频率非常高,如果同步 setState,很快就会卡顿。正确做法是在 onMomentumScrollEnd 或者 onScrollEndDrag 事件里计算最终位置,再把选中值同步到 state。
第二,循环跳转必须禁用动画。当你滚到列表末尾,需要瞬间跳到复制的列表头部时,必须使用 scrollToOffset(或 scrollToIndex)并且设置 animated: false。如果开了动画,用户就会看到列表像"嗖"地一下往回滚,非常出戏。
第三,选中项的高亮框要放在列表外面。不要试图在列表项内部绘制高亮背景,否则滚动时每个 item 都要根据位置计算样式,性能差且逻辑复杂。正确方案是把高亮框做成一个绝对定位的覆盖层,固定在容器的中间,列表只负责滚动。
第四,item 的点击事件要额外处理。因为高亮框是固定的,用户可能没滚轮、直接点击某一项来选中,这时候需要在 FlatList 的 onPressItem 回调里先做滚动动画到目标位置,再更新选中值。如果把点击和滚动手动更新逻辑耦合在一起,很容易出现选中值和 UI 不一致的情况。
第五,Android 上的滚动触摸要设 overScrollMode="never"。Android 的默认弹性滚动效果和滚轮 UI 在一起看起来非常奇怪,建议在 ScrollView/FlatList 上显式关闭 overScroll 模式。iOS 端 bounces 属性也要根据设计稿决定是否关闭,否则滚到边界会有橡皮筋效果,影响滚轮整体质感。
5. 跨端方案避坑:React + Taro 开发必须记牢的技能清单
5.1 为什么 React 团队做小程序会选择 Taro
除了 React Native 这种移动端方案,React 生态里还有一条非常重要、也特别容易被新手忽略的路线——使用 Taro 开发小程序。Taro 是由京东凹凸实验室开源的一个多端统一开发框架,核心卖点是一套 React 代码可以同时编译到微信小程序、支付宝小程序、H5、React Native 等多个平台。如果你所在团队有强烈的多端复用诉求,Taro 几乎是绕不开的选择。
从我实际使用体验来看,Taro 最大的价值在于让前端工程师能复用已有的 React 知识。写 Taro 组件的时候,组件生命周期的写法、状态管理的方式、JSX 的语法,和普通 React 项目几乎一致。这对团队来说意味着非常低的上手成本——招一个 React 开发者,基本可以直接上手 Taro 业务,不需要专门去学小程序原生的 WXML 和 WXSS。但 Taro 毕竟是编译方案,跟纯 React 运行在 Web 端还是有本质区别,这些区别就是踩坑的根源。
5.2 生命周期与 useEffect 的执行时机差异
在普通 React Web 项目中,useEffect 的语义非常清晰:组件渲染提交到屏幕后执行副作用。但在 Taro 编译到小程序时,useEffect 的执行时机并不总跟 Web 端表现一致,尤其是在组件初次加载的数据请求场景下,容易出现"请求还没发,页面就渲染完"或者"useEffect 根本没有执行"的情况。
原因在于 Taro 的编译机制:小程序本身没有 React 这种调度系统,Taro 是在运行时模拟了一套组件树的更新。你写的 useLayoutEffect 在小程序端的表现和 Web 端会有差异,因为小程序本身没有同步布局的完整能力。这个属于底层平台限制,避不开,只能在上层做预防。我的经验是:在 Taro 项目里,页面初次的数据请求尽量写在 useDidShow(Taro 提供)或者组件类里的 componentDidShow,而不是 useEffect。因为 useDidShow 语义和微信小程序的 onShow 对齐,每次页面展示都会触发,更符合小程序交互模型。
5.3 样式与动画的跨端差异:不是所有 CSS 都有效
CSS 在小程序里是存在"降级"的。很多在 Web 端用得飞起的样式特性,到小程序端就用不了,编译过程中也不会报错,只能运行时静默失效,这类问题是 Taro 开发中最容易让人崩溃的。最常见的几个:通配符选择器 * 在小程序里不支持,全局样式需要手动写在 app.scss 里而不是靠通配符;position: fixed 在部分安卓小程序下的表现和 Web 端不同,尤其配合软键盘弹出时,可能会被顶起来或遮挡;还有 CSS 变量(自定义属性)的兼容性在小程序端并不完全,编译到某些平台时会丢失。
动画这边也容易踩坑。CSS animation 在大部分小程序里是支持的,但如果你用 React 的 state 频繁切换 class 名来触发动画,可能会遇到动画不生效的问题,因为小程序端对 class 切换的渲染优化跟浏览器不同。更好的做法是直接使用 Taro 提供的动画 API,或者安装官方推荐的上层方案,确保跨端一致性。
5.4 自定义组件与原生组件的混用边界
Taro 支持自定义组件,也支持跳回小程序原生自定义组件。但这两者的混用有一个非常隐蔽的坑:原生组件的层级问题。在小程序里,原生组件(比如 video、map、canvas、textarea)是原生渲染层,它们天生就盖在普通 WebView 渲染的组件之上,层级优先级非常高。如果你在 Taro 项目里使用了原生 video,然后在它上方悬浮了一个自定义弹层,你可能会发现弹层永远被视频压在下面,怎么调 z-index 都没有用。
解决思路不外乎两种:一是用小程序官方提供的 cover-view 和 cover-image 来覆盖在原生组件之上,二是避免把弹层和原生组件布局在同一区域,使用"点击视频时收起弹层"之类的交互策略,绕开层级问题。还有一个做法是直接把 video 通过第三方库替换成同层渲染组件。同层渲染是微信小程序后期推出的技术,让原生组件和普通元素共享同一个 WebView 渲染层级,解决了很多历史顽疾,但仍有部分特殊组件存在兼容问题。
5.5 React + Taro 避坑清单
我把这四年用 Taro 写业务踩过的坑集中整理成一张表,后端同学看过之后都觉得这个比文档还管用:
| 问题分类 | 具体表现 | 解决方案 |
|---|---|---|
| 生命周期 | useEffect 在某些基础库版本下不执行 | 用 useDidShow/useDidHide 代替 |
| 样式 | 通配符选择器、CSS 变量失效 | 避免使用,用类名和 scss 变量 |
| 层级 | 原生组件遮挡弹层 | 用 cover-view 或改交互方案 |
| 路由 | 小程序页面栈最多 10 层 | 跳转前用 navigateBack 或重定向 |
| 状态管理 | 跨页面共享状态易丢失 | 优先使用 Taro 的 getApp 全局态或封装好的存储 |
| 组件 | 注册自定义组件时配置错 usingComponents | 按 Taro 文档要求在页面 config 中声明 |
路由到 10 层的限制容易被人忽略。微信小程序官方限制页面栈最多 10 层,超过之后调用 navigateTo 会静默失败,用户点按钮没反应。排查了半天,发现是页面栈溢出了。解决方式是跳页面前判断一下栈深度,或者改用 redirectTo 替换当前页。
6. 高频面试考点拆解与 React 学习重点的优先级排序
6.1 高频面试题背后的真实考察点
React 面经一直是前端社区的热门话题,很多同学爱背题,但真正面试时却说不深。原因在于面试官考察的从来不是你能不能背出"虚拟 DOM 比真实 DOM 快"这句话,而是你能不能从原理角度解释清楚"为什么"。我梳理了几个最高频的高频考点,按性价比排序讲一讲。
虚拟 DOM 与 diff 算法几乎是必考题。正确的理解方式是:虚拟 DOM 是一种"跨平台的对象描述",它最大的价值并不是"快",而是让 React 可以在 JS 运行时模拟出一棵组件树,从而将变更最小化地映射到真实 DOM。diff 算法也不是什么玄学,React 基于三个假设做优化:不同类型的元素输出不同的树结构,同级元素通过 key 标识稳定性,开发者通过 key 提示如何复用节点。理解到这层,React 面经里的 diff 题基本就通了。
Fiber 架构是 React 16 之后的核心升级,它把原来同步递归的 reconciler 改成了可中断的异步调度。面试官如果问"Fiber 是什么",不要只回答"一种链表结构",要说出它的目的是解决主线程阻塞问题——把大型渲染任务切分成一个个小单元,每次处理完一个小单元就让出主线程,从而保证动画和输入框的流畅度。
单向数据流和状态管理是 React 的核心设计哲学。数据从父组件流向子组件,状态变化通过回调向上传达,这种"自上而下"的数据流让应用状态变得可追踪、可预测。面试时如果能结合一个具体业务场景讲清楚"为什么不能直接修改 props""为什么要用不可变数据"这两个问题,基本就展现了你对 React 状态管理的理解深度。
6.2 学习 React 的重点:优先级怎么排才有效
新手学 React 最容易犯的错误就是一头扎进 Redux、React Router、 TypeScript 这些外围生态,结果连 React 自己的核心概念都没搞懂。我理解原因是网络教程都在强调全家桶有多厉害,但学习路线真的有优先级。我的建议是:先把组件、JSX、props 和 state 这一组"最小核心 API"吃透,会写页面了再说别的。
第二个重点必须放在"渲染机制"上,也就是 useState 和 useEffect 这套 Hooks 体系背后的时机问题。Hooks 不只是"函数组件里可以写状态"这个表面语义,你要理解的是 React 如何管理副作用、为什么不能在条件分支里调用 Hook、为什么依赖数组需要谨慎写。这些写多了自然就会碰到,但碰到的同时一定要回头补原理,不然永远是"会写但不懂为什么"的状态。
第三个重点是学会阅读 React 官方文档的"Under the Hood"类章节,不要满足于 API 语法。很多面试问出的深挖题,其实官方文档的 Advanced Guides 里都有线索。真正的高手不在于用过多少库,而在于看到现象时能快速定位到机制层的原因,这中间的差距就是学习时的深度。
6.3 面经之外:一次完整的 React 项目实践怎么练
如果你想用 React 练手,最简单的起步项目就是把加减法计算器做出来。别小看一个计算器,它几乎涵盖了 React 的所有基础知识点:状态管理(当前值、历史值、操作符)、事件处理、条件渲染(按键逻辑)、受控组件(输入框显示)、以及组件拆分。把计算器的逻辑用 React 重写一遍,你会比单纯看十篇教程更扎实。
进阶一点的项目是仪表盘(Dashboard)。仪表盘需要你掌握三种能力:数据请求与状态缓存、图表库集成(ECharts 或 Recharts)、以及自适应布局。你可以自己造一份模拟数据,然后用 React Query 或者 SWR 这类请求库管理数据缓存,看看它们是如何处理 loading、error、refetch 这些状态的。这类项目做下来,你对"异步数据状态管理"的理解会突飞猛进,这正是中后台业务里最核心的能力。
还有一个值得一试的方向是用 React Flow 做流程编排器。React Flow 是一个专门做节点连线交互的库,使用场景包括流程图设计器、脑图工具、审批流配置界面。它能让你把 React 的拖拽、缩放、父子关系、自定义节点这些高级特性全部练一遍,是进阶阶段非常推荐的练习素材。网上关于 React Flow 的中文资料还不多,这也意味着你能抢先积累经验,形成自己的知识壁垒。
6.4 创建 GitHub 仓库与项目工程化的基本规范
项目模型训练完了,还得学会怎么用 Git 管理。很多个人项目做得很完整,但代码仓库一团糟,commit 信息全是"update""fix",这其实非常影响后续的协作和自我复盘。我自己的习惯是:新建仓库时先创建 README 文件,写清楚项目是什么、怎么跑、有哪些目录结构。README 不只是给别人看的,也是三个月后的自己"快速回忆"的入口。
在 GitHub 上创建仓库时你需要注意几个细节:一是 .gitignore 文件必须加上 node_modules、build、dist 等目录,否则把几百 MB 的依赖推上仓库会非常痛苦;二是 main 分支和 dev 分支要区分,主分支永远保持可运行;三是 commit message 用 Conventional Commits 风格(feat: 增加计算器功能 / fix: 修复除数为零报错),半年后回看 commit 记录一目了然。
如果你做的是 React 项目,我强烈建议在工程化配置上花一点时间:装 ESLint + Prettier + Husky 的 pre-commit 钩子,提交前自动做代码格式化。这套流程在团队协作中能减少大量无意义的代码风格争论,自己写个人项目也能保持代码整洁,React 生态里这套工具链已经非常成熟,配置一次几乎不用再折腾。
7. 从 React 18 到 React 生态:聊聊我看过的那些项目形态
React 的应用场景远不止 Web 页面。现在很多团队已经把 React 的组件开发模式扩展到了更多场景。React Native 负责移动端 App,Taro 负责小程序,React Flow 这类库用来做流程编辑,甚至还有一些团队在做 React 渲染到桌面端(Electron)的方案。可以说,React 不是一项孤立的技术,而是一套以组件为核心的多端渲染思想。
在我的项目经历里,React 最打动我的根本不是某个具体 API,而是它的组件化思维带来的可维护性。你可以在脑内构建一个"组件树",每个组件只负责自己的片段,数据从根部流到叶子节点,再通过事件回调向上广播变化。这种思维模式不仅适用于 React 项目,也适用于任何复杂 UI 系统的设计。今天你学 React,学到的不只是一个框架,更是一套如何拆解复杂界面的方法论。
在面试时,新手可能背得出 React 类组件生命周期有哪些,但当我问"为什么 React 从类组件走向函数组件 + Hooks"时,能给出清晰回答的人却不多。类组件的 this 绑定问题、代码复用方式,Hooks 出来后都有了更好的答案。函数式组件 + Hooks 让逻辑复用从"继承和 mixin"走向"组合",这个方向跟 React 一贯强调的声明式编程思想是完全一致的。
再往大了说,React 18 的并发渲染特性(Concurrent Mode)给复杂交互带来了更多可能。比如 startTransition 可以用来标记那些非紧急的更新,让输入框的即时响应不被大数据量更新阻塞。虽然日常业务中你可能不常用到,但理解这个能力,在很多性能优化的场景下依然能提供很高的上限。它意味着 React 正在向"让编译器自动优化"的长期主义迈进,而不是靠开发者手写一堆 memo 来保性能。
后端或者全栈开发者如果想快速掌握 React,我的建议是从一个"有数据、有表单、有列表"的真实业务页面开始。比如做一个简单的任务管理系统:左侧任务列表,右侧任务详情,支持新增、编辑、删除。这个页面虽然不大,但覆盖了请求数据、状态管理、路由跳转、受控表单、列表渲染这些全部的核心场景。做完它,你的 React 就算真正入门了;把它打磨得足够完善,你就具备了独立开发一个中后台页面的能力。