# 饿了么(25 题)
# 1. 小程序里面开页面最多多少
30 秒速记
- 微信小程序页面栈最多
10层,满了再navigateTo会走fail navigateTo入栈 /redirectTo替换当前页 /reLaunch清空重建 /navigateBack出栈tabBar页用switchTab切换,也可用reLaunch重建;navigateTo和redirectTo都打不开- 详情页套详情页(推荐商品、相关文章)是最容易撞上限的场景,该替换就替换
- 分享卡片冷启动进来时栈里只有一页,返回按钮要自己兜底回首页
微信小程序的页面栈最多 10 层,超过以后 navigateTo 直接失败。 可以把页面栈想成浏览器的历史记录,只不过平台为它设了层数上限,页面及其状态也会占用资源;具体渲染方式取决于 WebView 或 Skyline 等渲染引擎。真碰到上限一般不是数字的问题,而是导航设计有问题,比如商品详情里点推荐商品一路往下点。我会封装一个跳转方法,用 getCurrentPages().length 判断深度,接近上限时改成 redirectTo 替换当前页;回首页这类动作用 reLaunch 或 switchTab,不往栈里堆。
原理拆开看
微信小程序同时打开的页面栈通常最多 10 层,超过后不能继续通过 navigateTo 入栈;返回用 navigateBack,切换 tab 用 switchTab。这个值属于平台限制,面试时还应补一句:开发要以当前官方文档和实际基础库为准,页面层级过深本身也说明导航设计需要收敛。 小程序页面栈可以理解为一段受平台管理的导航历史:navigateTo 新增普通页,navigateBack 回退,redirectTo 替换当前页,reLaunch 重建整条栈,switchTab 切换到 tabBar 页面。具体层数是平台能力,不应把“能开多少层”当作导航设计目标。
最小实现
function openDetail(id) {
const pages = getCurrentPages()
if (pages.length >= 9) {
wx.redirectTo({ url: `/pages/detail/index?id=${id}` })
return
}
wx.navigateTo({ url: `/pages/detail/index?id=${id}` })
}
// tabBar 页面必须使用 switchTab
function goHome() {
wx.switchTab({ url: '/pages/home/index' })
}
普通情况下用 navigateTo 保留返回路径;当页面栈已经很深时,示例改用 redirectTo 替换当前页,避免继续入栈。tabBar 页面不能用普通入栈 API,必须 switchTab。生产还应根据流程语义决定是否替换,不能只按数字硬切。
边界与排查
开发时先区分普通页和 tabBar 页,再根据用户是否需要返回选择入栈、替换或重建。可以用 getCurrentPages 查看当前栈深度,在接近上限前收敛跳转;订单、支付等流程还要避免重复入栈并处理从分享卡片直接进入中间页的情况。 基础库版本、tabBar 限制、分享冷启动和业务流程过深,都会影响导航行为。
面试官追问
追问 1商品详情里点推荐商品,连点十次就打不开了,怎么改?
详情跳详情用 redirectTo 替换当前页,或者干脆在当前页改参数重新拉数据,不再入栈。用户点返回会回到列表页,这个行为大多数电商是能接受的。
追问 2用户从分享卡片直接进了订单详情,左上角返回没反应,怎么处理?
这时 getCurrentPages().length === 1,没有可返回的页面。页面里判断一下,栈深为 1 时显示「回首页」按钮;首页是 tabBar 页就调 wx.switchTab,否则用 wx.reLaunch 打开首页。
追问 3同事用 navigateTo 跳 tabBar 页面,模拟器偶尔行真机不行,为什么?
navigateTo 和 redirectTo 不支持打开 tabBar 页,切换标签页应使用 switchTab;清空页面后重新打开 tabBar 页也可以用 reLaunch。而且 switchTab 的 url 不能带 ?id=1 这种参数,要传数据得走全局状态或缓存。
追问 4reLaunch 和 switchTab 都能回首页,有什么区别?
reLaunch 会关掉所有页面再打开目标页,新页面会走 onLoad;switchTab 关闭非 tabBar 页,切到已加载的标签页通常只触发 onShow,首次打开仍会触发 onLoad。支付后刷新数据可在 onShow 检查失效标记后请求,不必为刷新而重建全部页面。
# 2. React 父子组件之间如何传值
30 秒速记
- 父传子:
props;子传父:父组件传一个回调下去,子组件调用它 - 兄弟组件:状态提升到最近的共同父组件,再分别往下传
- 跨很多层:
Context;跨页面、更新频繁的共享状态再上zustand/Redux - 父组件要调子组件方法:
ref+useImperativeHandle,少用,优先用props驱动 - 单向数据流:子组件不改
props,只「通知」父组件改
React 是单向数据流:数据通过 props 往下传,子组件想改数据就调用父组件传下来的回调。 打个比方,父组件是数据的主人,子组件只能借来看,想改得跟主人说一声。比如父组件写 <Counter value={count} onChange={setCount} />,子组件里点按钮调 onChange(value + 1)。两个兄弟要共享数据,就把状态挪到它们共同的父组件里;层级太深、一路透传 props 很烦的时候再用 Context。我一般不会一开始就把状态丢进全局 store,放得越高,牵连重渲染的组件越多。
原理拆开看
父传子用 props,子通知父由父组件下发回调;共享状态要提升到最近公共父级。层级很深再考虑 Context,跨页面且更新复杂才引入外部状态库,避免所有状态一上来就全局化。 React 题要先分清 render 阶段和 commit 阶段:前者计算下一棵界面树,可以被打断或重做;后者才真正修改宿主环境并执行布局相关副作用。组件的输入是 props、state、context 与外部订阅,优化的前提是先确认哪一个输入真的导致了重复工作。
最小实现
function Parent() {
const [count, setCount] = React.useState(0)
return <Counter value={count} onChange={setCount} />
}
function Counter({ value, onChange }) {
return <button onClick={() => onChange(value + 1)}>{value}</button>
}
状态由真正拥有它的父组件保存,通过 props 向下传;子组件不直接改父状态,而是调用回调表达“希望发生什么”。兄弟组件共享时把状态提升到最近公共父级,跨很多层再考虑 Context 或外部 store。
边界与排查
排查时先用 React DevTools Profiler 录制一次真实交互,再看是哪一层组件重复 render、提交耗时落在哪里。不要一上来堆 memo;引用每次都重建、状态放得太高或 effect 依赖不完整,都会让表面优化失效。涉及生命周期和事件时还要主动说明 React 16、17、18+ 的版本边界。 版本、并发渲染、组件卸载和 Strict Mode 重复检查,是这类题最常被追问的边界。
面试官追问
追问 1子组件里直接 props.list.push(item),页面为什么没更新?
props 是只读的,push 改的是同一个数组引用,父组件的 state 没有经过 setState,React 根本不知道变了。要让子组件调 onAdd(item),父组件里 setList(prev => [...prev, item])。
追问 2表单弹窗里,父组件想点按钮让子组件重置表单,怎么写?
子组件用 forwardRef + useImperativeHandle(ref, () => ({ reset })) 暴露一个方法,父组件调 ref.current.reset()。React 19 里函数组件可以直接把 ref 当普通 prop 收,不用再包 forwardRef。不过能用 key 换一个新实例来重置的,我更倾向 key。
追问 3props 透传了四五层,中间组件根本不用这个值,怎么办?
先考虑组件组合:把子组件当 children 直接传下去,中间层就不用转手了。真是全局性的东西(主题、当前用户)再用 Context。
追问 4用了 Context 之后,改一个值整个页面都在重渲染,怎么优化?
所有用到这个 Context 的组件都会在 value 变化时重渲染。按更新频率拆成多个 Context,value 用 useMemo 包住,避免父组件每次渲染都生成新对象;更新特别频繁的状态换成 zustand 这种能按字段订阅的库。
# 3. Emit事件怎么发,需要引入什么
30 秒速记
