字节跳动前端一面面经深度解析
面经原文内容
📍面试公司:字节跳动
🕐面试时间:10 月 9 日
💻面试岗位:前端
⏱️面试时长:未提及
❓面试问题:
基础与项目
- 自我介绍
- 前端学习路径
- 项目相关
框架对比
- Vue 与 React 的区别
- Vue 双向绑定原理
- 自定义 Hooks 的实现思路
- 虚拟 DOM 的作用
- Fiber 架构的作用
跨端开发
- RN 开发的'坑'
JS 核心
- ES6 新特性
- Promise 异常处理
- import 与 require 的区别
- finally 的作用
浏览器与性能
- 资源缓存策略,强缓存 vs 协商缓存的适用场景
- 协商缓存的流程
- 资源更新问题:如何让用户加载新的 JS 资源
- 列表滚动卡顿怎么排查
📝 字节跳动前端一面·面经深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 部门定位 | 字节跳动 - 未明确部门 |
| 面试风格 | 框架对比型 + 原理深入型 + 场景排查型 |
| 难度评级 | ⭐⭐⭐⭐(四星,覆盖面广且深入) |
| 考察重心 | Vue/React 对比、跨端经验、缓存策略、性能排查 |
💡 面经关键点解读
面试官的潜台词:这场面试覆盖了从框架原理到跨端实践,从 JS 基础到性能排查的全链路。每个问题都在考察你是否真的'用过'并且'懂原理'。特别是 RN 的'坑'和滚动卡顿排查,是字节业务场景中真实会遇到的问题。
🔍 逐题深度解析
一、Vue 与 React 的区别
问题:Vue 与 React 的区别
| 维度 | Vue | React |
|---|---|---|
| 设计哲学 | 渐进式框架,易上手 | 纯 UI 库,更灵活 |
| 模板语法 | 单文件组件 (template+script+style) | JSX(JS 中写 HTML) |
| 响应式原理 | 数据劫持 (Proxy/Object.defineProperty) | 手动 setState 触发更新 |
| 更新粒度 | 组件级自动追踪 | 根节点开始 diff |
| 状态管理 | Vuex/Pinia(官方) | Redux/Mobx(社区) |
| 学习曲线 | 平缓 | 较陡 (需要理解 JSX、Hooks) |
// Vue 响应式:数据变化自动更新
function data(){return{count:0}}
function methods(){increment(){this.count++ // 自动触发视图更新}}
// React:需要手动触发
const [count, setCount] = useState(0)
const increment = () => {
setCount(count + 1) // 显式调用 setter
}
适用场景对比
- Vue 适合:快速开发、中小型项目、需要模板语法的团队
- React 适合:大型应用、需要高度灵活性的项目、跨端开发 (React Native)
二、Vue 双向绑定原理
问题:Vue 双向绑定原理
// v-model 是语法糖
<input v-model="message">
// 等价于
<input :value="message" @input="message = $event.target.value">
// 原理:数据劫持 + 发布订阅
class Vue {
constructor(options) {
this.$data = options.data
this.observe(this.$data)
this.compile(options.el)
}
// 1. 数据劫持
observe(data) {
Object.keys(data).forEach(key => {
let value = data[key]
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
if (Dep.target) { dep.addSub(Dep.target) // 依赖收集 }
return value
},
set(newVal) {
if (newVal !== value) {
value = newVal
dep.notify() // 触发更新
}
}
})
})
}
// 2. 模板编译
compile(el) {
// 解析指令,绑定更新函数
}
}
// 3. 发布订阅
class Dep {
constructor() { this.subs = [] }
addSub(sub) { this.subs.push(sub) }
notify() { this.subs.forEach(sub => sub.update()) }
}
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
this.vm[this.key] // 触发 get,收集依赖
Dep.target = null
}
update() {
this.cb.call(this.vm, this.vm[this.key])
}
}
三、自定义 Hooks 的实现思路
问题:自定义 Hooks 的实现思路
// 自定义 Hook:封装复用逻辑的函数,以 use 开头
// 1. 基础思路
function useCustomHook(initialValue) {
const [state, setState] = useState(initialValue)
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑
}
}, [state])
const updateState = (newValue) => {
setState(newValue)
}
return [state, updateState]
}
// 2. 实际例子:useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
return initialValue
}
})
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} (error) {
.(error)
}
}
[storedValue, setValue]
}
() {
[data, setData] = ()
[loading, setLoading] = ()
[error, setError] = ()
( {
= () => {
{
()
response = (url)
result = response.()
(result)
} (err) {
(err)
} {
()
}
}
()
}, [url])
{ data, loading, error }
}
() {
{ data, loading } = ()
[theme, setTheme] = (, )
}
自定义 Hooks 的要点
- 命名必须以 use 开头(React 识别规则)
- 内部可以使用其他 Hooks
- 每次调用都是独立的(状态隔离)
- 返回值可以是任意类型(数组/对象/函数)
四、虚拟 DOM 的作用
问题:虚拟 DOM 的作用
// 虚拟 DOM:用 JS 对象描述真实 DOM 的结构
// 真实 DOM
<div class="container"><p>Hello</p></div>
// 虚拟 DOM (VNode)
{
type: 'div',
props: { class: 'container' },
children: [
{ type: 'p', props: {}, children: ['Hello'] }
]
}
核心作用
| 作用 | 说明 | 收益 |
|---|---|---|
| 性能优化 | 批量操作 DOM,减少重排重绘 | 提升更新性能 |
| 跨平台 | 同一套 VNode 可渲染到不同平台 | Web/小程序/Native |
| 声明式编程 | 开发者只需描述 UI 状态 | 降低心智负担 |
| diff 计算 | 找出最小更新范围 | 避免全量更新 |
// 无虚拟 DOM:直接操作 document.querySelector('.count').innerText = count
// 有虚拟 DOM
// 1. 生成新 VNode
// 2. diff 找出变化
// 3. 批量更新真实 DOM
虚拟 DOM 真的更快吗?
- 不是:直接操作 DOM 在某些场景更快
- 是:在复杂 UI 更新中,虚拟 DOM+diff 可以减少不必要的 DOM 操作
- 本质:用 JS 计算换 DOM 操作,在大多数场景下是更优解
五、Fiber 架构的作用
问题:Fiber 架构的作用
// Fiber:React16 引入的新架构
// 旧架构(Stack Reconciler)的问题
// - 递归遍历虚拟 DOM,一旦开始无法中断
// - 如果组件树很大,会阻塞主线程
// - 用户输入、动画会出现卡顿
// Fiber 架构的核心:可中断的渲染
Fiber 的作用
| 作用 | 说明 | 实现方式 |
|---|---|---|
| 时间切片 | 将渲染工作拆分成小单元 | requestIdleCallback |
| 优先级调度 | 高优先级任务可打断低优先级 | lanes 模型 |
| 并发模式 | 同时准备多个版本的 UI | 双缓冲技术 |
| 异常边界 | 错误隔离,不影响整体 | Error Boundaries |
// Fiber 节点结构(简化)
{
tag: 1, // 组件类型
key: null, // 唯一标识
elementType: 'div', // 元素类型
stateNode: DOM 节点, // 真实 DOM
return: Fiber 父节点,// 父节点
child: Fiber 子节点, // 第一个子节点
sibling: Fiber 兄弟节点,// 下一个兄弟节点
pendingProps: {}, // 新 props
memoizedProps: {}, // 当前 props
memoizedState: {}, // 当前 state
effectTag: 'UPDATE', // 操作类型
nextEffect: null // 下一个副作用
}
渲染流程
// 1. render 阶段(可中断)
// - 从根节点开始遍历 Fiber 树
// - 收集变化,打上 effectTag
// - 可被高优先级任务打断
// 2. commit 阶段(不可中断)
// - 一次性提交所有变化到 DOM
// - 执行生命周期钩子
// - 处理副作用
六、RN 开发的'坑'
问题:RN 开发的'坑'
// React Native:用 JS 写移动端应用,渲染原生组件
// 坑 1:样式系统不一致
// Web <div style={{flex:1,boxShadow:'0 2px 4px black'}}/>
// RN <View style={{flex:1,shadowColor:'black'}}/>
// - 没有 CSS 所有属性,只有 RN 支持的样式
// - 继承规则不同(Text 组件内才有文字样式)
// 坑 2:导航系统复杂
// Web:浏览器自带前进后退
// RN:需要集成 react-navigation 或 react-native-navigation
// - 嵌套导航器的状态管理复杂
// - 和原生交互可能出问题
// 坑 3:性能问题
// - 长列表用 FlatList(不是 ScrollView)
// - 大量图片需要缓存策略
// - 动画用 Animated 或 react-native-reanimated
// 坑 4:第三方库兼容性
// - 有些库不支持 RN
// - 原生模块需要链接(autolinking 也可能出问题)
// 坑 5:调试困难
// - 原生崩溃需要看 Xcode/Android Studio 日志
// - 桥接层问题定位难
// 坑 6:版本升级
// - 大版本升级可能改动大
// - 依赖库需要同步升级
// 坑 7:热更新
// - iOS 禁止热更新(影响代码)
// - 只能用 CodePush 更新 JS bundle 和资源
// 解决方案
// - 提前调研技术选型
// - 做好性能监控
// - 建立错误上报机制
// - 原生模块开发能力储备
七、ES6 新特性
问题:ES6 新特性
| 类别 | 特性 | 示例 |
|---|---|---|
| 变量声明 | let/const | let a = 1; const PI = 3.14 |
| 箭头函数 | 简写、this 绑定 | () => {} |
| 解构赋值 | 数组/对象解构 | const { name } = user |
| 展开运算 | … | const newArr = [...arr, 4] |
| 模板字符串 | 字符串插值 | `${name}` |
| 类 | class 语法 | class Person {} |
| 模块 | import/export | import React from 'react' |
| Promise | 异步编程 | fetch().then().catch() |
| 迭代器 | for…of | for (let item of arr) {} |
| Set/Map | 新数据结构 | new Set(), new Map() |
| Proxy | 代理 | new Proxy(target, handler) |
| Symbol | 唯一值 | Symbol('description') |
八、Promise 异常处理
问题:Promise 异常处理
// 1. catch 捕获
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error))
// 2. try/catch(async/await)
async function fetchData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
console.log(data)
} catch (error) {
console.error('请求失败:', error)
}
}
// 3. finally(无论成功失败都执行)
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => {
.()
()
})
.(, {
.(, event.)
})
.()
.( {
()
})
.( {
})
.( {
.(, error.)
})
九、import 与 require 的区别
问题:import 与 require 的区别
| 维度 | import | require |
|---|---|---|
| 规范 | ES6 模块标准 | CommonJS 规范 |
| 加载时机 | 编译时加载(静态) | 运行时加载(动态) |
| 语法 | 关键字 | 函数 |
| 导出 | export/export default | module.exports |
| 动态导入 | import() | 直接变量拼接 |
| this 指向 | undefined | 当前模块 |
| 值传递 | 只读引用 | 值的拷贝(基本类型) |
// require (CommonJS)
const fs = require('fs')
const { readFile } = require('fs')
// 动态加载
const moduleName = 'lodash'
const lib = require(moduleName) // 可以
// import (ES Module)
import fs from 'fs'
import { readFile } from 'fs'
// 动态加载
import(moduleName).then(module => {
// 使用 module
})
// 混合使用注意
// - require 不能用于 import 模块(除非转译)
// - import 不能用于条件语句顶层
十、finally 的作用
问题:finally 的作用
// finally:无论 Promise 成功还是失败都会执行
// 1. 清理资源
let loading = true
fetchData()
.then(data => render(data))
.catch(error => showError(error))
.finally(() => {
loading = false // 无论成功失败,关闭 loading
hideLoadingSpinner()
})
// 2. 释放连接
let dbConnection = null
openDatabase()
.then(conn => {
dbConnection = conn
return conn.query(sql)
})
.then(results => processResults(results))
.catch(error => handleError(error))
.finally(() => {
if (dbConnection) {
dbConnection.close() // 无论成功失败,关闭连接
}
})
// 3. 日志记录
function trackUserAction(action) {
return executeAction(action)
.then(result => {
return result
})
.catch(error => {
error
})
.( {
.()
analytics.(action)
})
}
{
} (error) {
} {
}
十一、资源缓存策略
问题:强缓存 vs 协商缓存的适用场景
// 强缓存:不发请求,直接读本地
// 响应头 Cache-Control: max-age=3600
// 缓存 1 小时
// Expires: Wed, 21 Oct 2025 07:28:00 GMT
// 适用场景:
// - 静态资源(图片、CSS、JS)
// - 长期不变的文件(带 hash 的打包文件)
// - 用户头像等不敏感资源
// 协商缓存:发请求,服务器判断是否可用
// 响应头 Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
// ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 请求头(下次请求) If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
// If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 适用场景:
// - HTML 文件
// - API 接口数据
// - 可能变化但不想每次都重新下载的资源
协商缓存的流程
// 第一次请求 浏览器 → 服务器 (请求资源)
// 服务器 ← 返回资源 + Last-Modified/ETag
// 第二次请求 浏览器 → 服务器 (请求头带 If-Modified-Since/If-None-Match)
// 服务器判断资源是否修改:
// - 未修改 → 返回 304,不返回 body
// - 已修改 → 返回 200 和新资源
// 流程图
// 1. 浏览器请求资源
// 2. 服务器返回资源和缓存标记
// 3. 浏览器缓存资源和标记
// 4. 下次请求时带上标记
// 5. 服务器对比标记
// 6. 304 则用缓存,200 则更新缓存
十二、资源更新问题
问题:如何让用户加载新的 JS 资源
// 问题:浏览器缓存了老的 JS,新功能没生效
// 1. 文件指纹(最推荐)
// 打包时生成带 hash 的文件名 app-8f3c9d.js
// 内容变化,hash 变化 vendor-3a2b5c.js
// HTML 引用新 hash 的文件
// <script src="/static/js/app-8f3c9d.js"></script>
// 2. 强制刷新(不推荐)
// window.location.reload(true) // 已废弃
// 改为 window.location.reload()
// 配合缓存策略
// 3. 版本号查询参数
// <script src="/static/js/app.js?v=2.1.0"></script>
// 发版时修改版本号
// 4. 元数据控制
// <meta http-equiv="Cache-Control" content="no-cache">
// <meta http-equiv="Pragma" content="no-cache">
// 5. Service Worker 控制
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v2').then(cache => {
return cache.addAll(['/js/app.js', // 新版本资源])
})
)
})
// 6. 请求头控制
fetch('/api/data', {
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
// 最佳实践
// - HTML:协商缓存(no-cache)
// - JS/CSS/图片:强缓存 + 文件名 hash
// - 发版时:更新 HTML 引用的文件名
十三、列表滚动卡顿排查
问题:列表滚动卡顿怎么排查
// 1. 使用 Performance 面板记录
// Chrome DevTools → Performance → 开始录制 → 滚动 → 停止
// 2. 观察指标
// - FPS(帧率):低于 30 说明卡顿
// - 长任务(Long Task):>50ms 的任务
// - 重排重绘(Layout/Recalc Style)
// 3. 常见原因及排查
// 4. 虚拟滚动实现(解决大量 DOM)
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0)
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(
Math.floor((scrollTop + containerHeight) / itemHeight),
items.length - 1
)
const visibleItems = items.slice(startIndex, endIndex + 1)
const offsetY = startIndex * itemHeight
return (
<div style={{ height: containerHeight, overflow: 'auto' }} onScroll={(e) => setScrollTop(e.target.scrollTop)}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
<div style= `(${})` }}>
{visibleItems.map(item => (
{item}
))}
)
}
.-item {
: ();
will-: transform;
}
ticking =
.(, {
(!ticking) {
( {
()
ticking =
})
ticking =
}
})
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| Vue/React 区别 | 设计哲学、响应式、模板、学习曲线 |
| 双向绑定 | v-model 语法糖、数据劫持、发布订阅 |
| 自定义 Hooks | use 开头、封装逻辑、状态隔离 |
| 虚拟 DOM | JS 对象描述 DOM、批量更新、跨平台 |
| Fiber 架构 | 可中断渲染、时间切片、优先级调度 |
| RN 坑点 | 样式不一致、导航复杂、性能问题 |
| ES6 | let/const、箭头函数、解构、模块 |
| Promise 异常 | catch、try/catch、finally、全局捕获 |
| import/require | 静态 vs 动态、编译时 vs 运行时 |
| finally | 总是执行、清理资源、关闭 loading |
| 强缓存 | max-age、不请求、静态资源 |
| 协商缓存 | Last-Modified/ETag、304、HTML |
| 资源更新 | hash 文件名、版本号、Service Worker |
| 滚动卡顿 | Performance、虚拟滚动、GPU 加速 |


