前端原理和源码
一、JS 内存泄漏如何检测?场景?
1. 垃圾回收 GC
什么是垃圾回收?
引用计数(之前)
js// 对象被 a 引用 let a = { x: 100 } let a1 = a a = 10 a1 = null // 此时 { x: 100 } 被引用次数为 0 ,就会被清除 // 缺陷:循环引用会有问题 function fn() { const obj1 = {} const obj2 = {} obj1.a = obj2 obj2.a = obj1 } fn() // IE6-7 内存泄露 BUG var div = document.getElementById('div') div.a = div div.someBigData = {}标记清除(现代)
- 会定时从 JS 根 window 开始逐步向下遍历,如果引用不到就会清除
2. ( 连环问 ) 闭包算内存泄漏吗?
- 不算,属于用户期望,但是垃圾回收不掉
3. 内存泄漏检测方法
可使用 Chrome Performance 检测
泄漏结果

正常结果

4. 内存泄漏场景(Vue 为例)
- 被全局变量、函数引用,组件销毁时未清除
- 被全局事件、定时器引用,组件销毁时未清除
- 被自定义事件引用,组件销毁时未清除
5. 划重点
- 前几年前端不太注重内存泄漏,因为不像后端 7*24 持续运行
- 近几年前端功能变得复杂,内存问题也要重点考虑
6. 扩展:WeakMap WeakSet
js
const wMap = new WeakMap() // 弱引用
function fn() {
const obj = { x: 100 }
wMap.set(obj, 100) // weakMap 的 key 只能是引用类型
}
fn()二、浏览器和 nodejs 事件循环区别
1. 单线程和异步
- JS 是单线程的(无论在浏览器还是 nodejs)
- 浏览器中 JS 执行和 DOM 渲染共用一个线程
- 异步
2. 宏任务和微任务
- 宏任务,如 setTimeout setInterval 网络请求
- 微任务,如 Promise async / await
- 微任务在下一轮 DOM 渲染之前执行,宏任务在之后执行
3. 浏览器 event loop

- Callback Queue 分为
- 宏任务队列 MarcoTask Queue
- 微任务队列 MicroTask Queue
4. nodejs 异步
- Nodejs 同样适用 ES 语法,也是单线程,也需要异步
- 异步任务也分:宏任务 + 微任务
- 但是,它的宏任务和微任务,分为不同类型,有不同优先级
5. nodejs 宏任务类型和优先级
- Timers - setTimeout setInterval
- I/O callbacks - 处理网络、流、TCP 的错误回调
- Idle,prepare - 闲置状态(nodejs 内部使用)
- Poll 轮询 - 执行 poll 中的 I/O 队列
- Check 检查 - 存储 setImmediate 回调
- Close callbacks - 关闭回调,如 socket.on('close')
6. nodejs 微任务类型和优先级
- 包括:promise,async / await,process.nextTick
- 注意,process.nextTick 优先级最高
7. nodejs event loop
执行同步代码
执行微任务(process.nextTick 优先级更高)
按顺序执行 6 个类型的宏任务(每个结束时都执行当前的微任务)

8. 答案
- 浏览器和 nodejs 的 event loop 流程基本相同
- nodejs 宏任务和微任务分类型,有优先级
9. 注意事项
- 推荐使用 setImmediate 代替 process.nextTick
三、虚拟 vdom 真的很快吗
1. vdom
- Virtual DOM ,虚拟 DOM
- 用 JS 对象模拟 DOM 节点数据
- 由 React 最先推广使用
2. Vue React 等框架的价值
- 组件化
- 数据视图分离,数据驱动视图 -- 这是核心!
- 只关注业务数据,而不用再关心 DOM 变化
3. 答案
- vdom 并不快,JS 直接操作 DOM 才最快
- 但 “ 数据驱动视图 ” 要有合适的技术方案,不能全部 DOM 重建
- vdom 就是目前最合适的技术方案(并不是因为它快,而是合适)
四、遍历一个数组用 for 和 forEach 哪个快
1. 代码
js
const arr = []
for (let i = 0; i < 100 * 10000; i++) {
arr.push(i)
}
console.time('for')
let n = 0
for (let i = 0; i < arr.length; i++) {
n++
}
console.timeEnd('for') // 3.48ms
console.time('forEach')
let m = 0
arr.forEach(() => m++)
console.timeEnd('forEach') // 14.13ms2. 答案
- for 更快
- forEach 每次都要创建一个函数来调用,而 for 不会创建函数
- 函数需要独立的作用域,会有额外的开销
3. 划重点
- 越 “ 低级 ” 的代码,性能往往越好
- 日常开发别只考虑性能,forEach 代码可读性更好
- 类似之前算法文档中:循环 vs 递归
五、Vue 每个生命周期都做了什么

1. beforeCreate
- 创建一个空白的 Vue 实例
- data method 尚未被初始化,不可使用
2. created
- Vue 实例初始化完成,完成了响应式绑定
- data method 都已经初始化完成,可调用
- 尚未开始渲染模板
3. beforeMount
- 编译模板,调用 render 生成 vdom
- 还没开始渲染 DOM
4. mounted
- 完成 DOM 渲染
- 组件创建完成
- 开始由 “ 创建阶段 ” 进入 “ 运行阶段 ”
5. beforeUpdate
- data 发生变化之后
- 准备更新 DOM (尚未更新 DOM)
6. updated
- data 发生变化,且 DOM 更新完成
- (不要再 updated 中修改 data ,可能导致死循环)
7. beforeUnmount
- 组件进入销毁阶段(尚未销毁,可正常使用)
- 可移除、解绑一些全局事件、自定义事件
8. unmounted
- 组件被销毁
- 所有子组件也被销毁了
9. keep-alive 组件
- onActivated 缓存组件被激活
- onDeactivated 缓存组件被隐藏
10. 连环问:Vue 什么时候操作 DOM 比较合适
Mounted 和 updated 都不能保证子组件全部挂在完成
应使用 $nextTick 渲染 DOM
jsmounted() { this.$nextTick(() => { // 仅在整个视图都被渲染之后才会运行的代码 }) }
11. 连环问:Vue3 Composition API 生命周期有何区别
- 用 setup 代替了 beforeCreate 和 created
- 使用 Hooks 函数的形式,如 mounted 改成 onMounted()
六、Vue2 Vue3 React 三者 diff 算法有何区别
1. diff 算法
- diff 算法很早就有
- diff 算法应用广泛,如 GitHub 的 Pull Request 中的代码 diff
- 如果要 严格 diff 两棵树,时间复杂度 O(n^3) ,不可用
2. Tree diff 的优化
- 只比较同一层级,不跨级比较
- tag 不同则删掉重建(不再去比较内部的细节)
- 子节点通过 key 区分( key 的重要性 )
- 优化后的时间复杂度 O(n)
3. React diff - 仅右移

4. Vue2 - 双端比较

5. Vue3 - 最长递增子序列

6. (连环问)Vue React 为何循环时必须使用 key
- vdom diff 算法会根据 key 判断元素是否要删除
- 匹配到 key ,则移动元素 - 性能较好
- 未匹配 key ,则删除重建 - 性能较差

七、Vue-router MemoryHistory (abstract)
1. Vue-router 三种模式
- Hash
- location.hash
- WebHistory
- history.pushState
- window.onpopstate
- MemoryHistory ( V4 之前叫做 abstract history )
2. 扩展:React-router 也有相同的 3 种模式
八、nodejs 如何开启进程,进程怎么通讯
1. 进程 process 与 线程 thread
进程,OS 进行资源分配和调度的最小单位,有独立内存空间
线程,OS 进行运算调度的最小单位,共享进程内存空间
JS 是单线程的,但是可以开启多进程执行,如 WebWorker

2. 为何需要多进程
- 多核 CPU ,更适合处理多进程
- 内存较大,多个进程才能更好地利用(单进程有内存上限)
- “ 压榨 ” 机器资源,更快,更节省
3. fork 多进程 - 代码
js
// 主进程
const http = require('http')
const fork = require('child_process').fork
const server = http.createServer((req, res) => {
if (req.url === '/get-sum') {
console.log('主进程 ID', process.pid)
// 开启子进程
const computeProcess = fork('./compute.js')
computeProcess.send('开始计算')
computeProcess.on('message', data => {
console.log('主进程接收到的信息: ', data)
res.end('sum is ' + data)
})
computeProcess.on('close', () => {
console.log('子进程意外关闭')
computeProcess.kill()
res.end('error')
})
}
})
server.listen(3000, () => {
console.log('localhost: 3000')
})
console.log(process.pid)js
// 子进程
function getSum() {
let sum = 0
for (let i = 0; i < 10000; i++) {
sum += i
}
return sum
}
process.on('message', data => {
console.log('子进程 ID', process.pid)
console.log('子进程接收到的信息: ', data)
const sum = getSum()
// 发送消息给主进程
process.send(sum)
})4. cluster 多进程 - 代码
js
const http = require('http')
const cpuCoreLength = require('os').cpus().length
const cluster = require('cluster')
if (cluster.isMaster) {
for (let i = 0; i < cpuCoreLength; i++) {
cluster.fork() // 开启子进程
}
cluster.on('exit', worker => {
console.log('子进程退出')
cluster.fork() // 进程守护
})
} else {
// 多个子进程会共享一个 TCP 连接, 提供一份网络服务
const server = http.createServer((req, res) => {
res.writeHead(200)
res.end('done')
})
server.listen(3000)
}
// 工作中会使用 PM25. 答案
- 开启子进程 child_process.fork 和 cluster.fork
- 使用 send 和 on 传递消息
6. 划重点
- 进程 VS 线程
- JS 是单线程的
九、请描述 JS Bridge 原理
1. 什么是 JS Bridge
JS 无法直接调用 native API
需要通过一些特定的 “ 格式 ” 来调用
这些 “ 格式 ” 就统称 JS-Bridge ,例如微信 JSSDK

2. JS Bridge 的常见实现方式
- 注册全局 API
- URL Scheme(推荐)
十、requestIdleCallback 和 requestAnimationFrame 区别
1. 由 React fiber 引起的关注
- 组件树转换为链表,可分段渲染
- 渲染时可以暂停,去执行其他高优任务,空闲时再继续渲染
- 如何判断空闲? -- requestIdleCallback
2. 区别
- requestAnimationFrame 每次渲染完都会执行,高优
- requestIdleCallback 空闲时才执行,低优
3. ( 连环问 ) 他们是宏任务还是微任务
- 两者都是宏任务
- 要等待 DOM 渲染完才执行,肯定宏任务