Skip to content

前端原理和源码


一、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 = {}
  • 标记清除(现代)

    1. 会定时从 JS 根 window 开始逐步向下遍历,如果引用不到就会清除

2. ( 连环问 ) 闭包算内存泄漏吗?

  • 不算,属于用户期望,但是垃圾回收不掉

3. 内存泄漏检测方法

  • 可使用 Chrome Performance 检测

    1. 泄漏结果

      image-20220316155504373

    2. 正常结果

      image-20220316155945765

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

image-20220317100749714

  • Callback Queue 分为
    1. 宏任务队列 MarcoTask Queue
    2. 微任务队列 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 个类型的宏任务(每个结束时都执行当前的微任务)

    image-20220317103505024

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.13ms

2. 答案

  • for 更快
  • forEach 每次都要创建一个函数来调用,而 for 不会创建函数
  • 函数需要独立的作用域,会有额外的开销

3. 划重点

  • 越 “ 低级 ” 的代码,性能往往越好
  • 日常开发别只考虑性能,forEach 代码可读性更好
  • 类似之前算法文档中:循环 vs 递归

五、Vue 每个生命周期都做了什么

image-20220317170131288

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

    js
    mounted() {
        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 - 仅右移

image-20220318105321872

4. Vue2 - 双端比较

image-20220318105519337

5. Vue3 - 最长递增子序列

image-20220318110337257

6. (连环问)Vue React 为何循环时必须使用 key

  • vdom diff 算法会根据 key 判断元素是否要删除
  • 匹配到 key ,则移动元素 - 性能较好
  • 未匹配 key ,则删除重建 - 性能较差

image-20220318110807084


七、Vue-router MemoryHistory (abstract)

1. Vue-router 三种模式

  • Hash
    1. location.hash
  • WebHistory
    1. history.pushState
    2. window.onpopstate
  • MemoryHistory ( V4 之前叫做 abstract history )

2. 扩展:React-router 也有相同的 3 种模式


八、nodejs 如何开启进程,进程怎么通讯

1. 进程 process 与 线程 thread

  • 进程,OS 进行资源分配和调度的最小单位,有独立内存空间

  • 线程,OS 进行运算调度的最小单位,共享进程内存空间

  • JS 是单线程的,但是可以开启多进程执行,如 WebWorker

    image-20220321111642479

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)
}
// 工作中会使用 PM2

5. 答案

  • 开启子进程 child_process.fork 和 cluster.fork
  • 使用 send 和 on 传递消息

6. 划重点

  • 进程 VS 线程
  • JS 是单线程的

九、请描述 JS Bridge 原理

1. 什么是 JS Bridge

  • JS 无法直接调用 native API

  • 需要通过一些特定的 “ 格式 ” 来调用

  • 这些 “ 格式 ” 就统称 JS-Bridge ,例如微信 JSSDK

    image-20220321151555261

2. JS Bridge 的常见实现方式

  • 注册全局 API
  • URL Scheme(推荐)

十、requestIdleCallback 和 requestAnimationFrame 区别

1. 由 React fiber 引起的关注

  • 组件树转换为链表,可分段渲染
  • 渲染时可以暂停,去执行其他高优任务,空闲时再继续渲染
  • 如何判断空闲? -- requestIdleCallback

2. 区别

  • requestAnimationFrame 每次渲染完都会执行,高优
  • requestIdleCallback 空闲时才执行,低优

3. ( 连环问 ) 他们是宏任务还是微任务

  • 两者都是宏任务
  • 要等待 DOM 渲染完才执行,肯定宏任务