Skip to content

一、前端知识深度-原理和源码

1. JS 内存泄漏如何检测?场景有哪些?

  • 什么是垃圾回收

    html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>gc</title>
    </head>
    <body>
        <p> Garbage collection </p>
        <script>
            function fn1() {
              const a = 'aa'
              console.log(a)
    
              const obj = { x: 100}
              console.log(obj)
            }
            // 执行完该函数,函数中的变量会被回收
            fn1()
    
            function fn2() {
              const obj = { x: 100 }
              // 回收不了,这是用户期望的
              window.obj = obj;
            }
            fn2()
    
            function getDataFns() {
              const data = {}; // 闭包的数据永远都是常驻内存的
              return {
                get(key) {
                  return data[key]
                },
                set(key, value) {
                  data[key] = value
                }
              }
            }
            const { get, set } = getDataFns();
            set('x', 100);
            get('x');
    
            // 对象被 a 引用
            let a = { x: 100 };
            let a1 = a;
            a = 10;
            a1 = null;
    
            // 引用计数缺陷:循环引用
            function fn3() {
              const obj1 = {};
              const obj2 = {};
              obj1.a = obj2;
              obj2.a = obj1;
            }
            fn3()
    
            // JS 根 window 遍历属性
        </script>
    </body>
    </html>
  • JS 垃圾回收的算法

    • 引用计数(之前)
      js
        // 对象被 a 引用
        let a = { x: 100 };
        let a1 = a;
        a = 10;
        a1 = null;
    • 标记清除(现代)
      js
      // JS 根 window 遍历属性
  • 闭包是内存泄漏吗?

    • 闭包严格意义上不算内存泄漏
  • 如何检测泄漏

    • 使用 Chrome devTools 的 Performance 和 Memory 工具检测 js 内存 memory.png
    • 扩展:wangEditor 检测内存泄漏
    • 内存泄漏的场景(Vue 为例)
      • 被全局变量、函数引用,组件销毁时未清除

        vue
        <template>
        </template>
        
        <script>
        export default {
          name: 'MemoryLeak',
          data() {
            return {
              arr: [10, 20, 30]
            }
          },
          mounted() {
            window.arr = this.arr;
            window.printArr = () => {
              console.log(this.arr)
            }
        
          },
          beforeUnmount() {
            window.arr = null;
            window.printArr = null
          },
          methods: {
        
          }
        }
        </script>
        
        <style scoped lang='less'>
        </style>
      • 被全局事件、定时器引用,组件销毁时未清除

        vue
        <template>
        </template>
        
        <script>
        export default {
          name: 'MemoryLeak',
          data() {
            return {
              arr: [10, 20, 30], // 数组、对象
              intervalId: 0
            }
          },
          mounted() {
            this.intervalId = setInterval(() => {
              console.log(this.arr)
            }, 100)
          },
          beforeUnmount() {
            if (this.intervalId) {
              clearInterval(this.intervalId)
            }
          }
        }
        </script>
        
        <style scoped lang='less'>
        </style>
        vue
        <template>
        </template>
        
        <script>
        export default {
          name: 'MemoryLeak',
          data() {
            return {
              arr: [10, 20, 30], // 数组、对象
            }
          },
          mounted() {
            window.addEventListener('resize',  this.printArr)
          },
          beforeUnmount() {
            window.removeEventListener('resize', this.printArr)
          },
          methods: {
            printArr() {
              console.log(this.arr)
            }
          }
        }
        </script>
        
        <style scoped lang='less'>
        </style>
      • 被自定义事件引用,组件销毁时未清除

        • 使用 eventBus 的时候,on 绑定事件,需要在组件销毁的生命周期函数中使用 off 进行解绑。
    • 扩展 WeakMap WeakSet
      • WeakMap 的 key 只能是引用类型

2. 浏览器和 node.js 的事件循环有什么区别?

答案:

  • 浏览器和 nodejs 的 event loop 流程基本相同。

  • nodejs 宏任务和微任务分类型,有优先级 注意:

  • 推荐使用 setImmediate 代替 process.nextTick

  • nodejs 低版本可能会有不同

  • 单线程和异步

    • JS 是单线程的(无论在浏览器还是nodejs)。
    • 浏览器中 JS 执行和 DOM 渲染公用一个线程。
    • 异步
      • 宏任务和微任务
        • 宏任务:如 setTimeout setInterval 网络请求。
        • 微任务:如 promise async/await。
          • 优先执行微任务,再执行宏任务
            html
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <title>event-loop</title>
            </head>
            <body>
            <p>event-loop</p>
            <script>
              console.log('start');
              setTimeout(() => {
                console.log('timeout')
              })
              Promise.resolve().then(() => {
                console.log('promise then')
              })
              console.log('end')
            </script>
            
            </body>
            </html>
            执行顺序:start-->end--->promise then--->timeout
        • 微任务在下一轮 DOM 渲染之前执行,宏任务在之后执行。
          html
          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>event-loop</title>
          </head>
          <body>
          <p>event-loop</p>
          <script>
              const p = document.createElement('p');
              p.innerHTML = 'new paragraph';
              document.body.appendChild(p);
              const ps = document.getElementsByTagName('p');
              console.log('ps--->', ps.length)
          
              console.log('start');
              // 渲染之前触发
              setTimeout(() => {
                const ps = document.getElementsByTagName('p');
                console.log('ps timeout--->', ps.length)
                alert('阻塞 timeout');
              })
              // 渲染之后触发
              Promise.resolve().then(() => {
                const ps = document.getElementsByTagName('p');
                console.log('ps promise then--->', ps.length)
                alert('阻塞 promise');
              })
              console.log('end');
          </script>
          
          </body>
          </html>
          event-loop.png
          • 宏任务队列 MarcoTask Queue
          • 微任务队列 MircoTask Queue
      • nodejs异步
        • Nodejs 同样使用 ES 语法,也是单线程,也需要异步。
        • 异步任务:宏任务、微任务。
        • 但是,它的宏任务和微任务,分不同类型,有不同优先级。
          • nodejs 宏任务类型和优先级
            • Timers: setTimeout setInterval
            • I/O callbacks: 处理网络、流、TCP 的错误回调
            • Idle, prepare: 闲置状态(nodejs 内部使用)
            • Poll 轮询: 执行 poll 中的 I/O 队列
            • Check 检查: 存储 setImmediate 回调
            • Close callbacks: 关闭回调,如 socket.on('close')
          • nodejs 微任务类型和优先级
            • promise async/await process.nextTick
            • 注意:process.nextTick 优先级最高
              js
              console.log('start')
              setImmediate(() => {
                console.log('setImmediate')
              })
              setTimeout(() => {
                console.log('setTimeout')
              })
              Promise.resolve().then(() => {
                console.log('Promise then')
              })
              process.nextTick(() => {
                console.log('nextTick')
              })
              console.log('end')
          • nodejs event loop
            • 执行同步代码
            • 执行微任务(process.nextTick 优先级最高)
            • 按顺序执行 6 个类型的宏任务(每当开始之前都执行当前的微任务) nodejs-event-loop.png

3. vdom 真的很快吗?

答案:

  • vdom 并不快, JS 直接操作 DOM 才是最快的

  • 但 "数据驱动视图"要有合适的技术方案,不能全部 DOM 重建

  • vdom 就是目前最合适的技术方案(并不是因为它快,而是合适)

  • vdom

    • Virtual DOM, 虚拟 DOM
    • 用 JS 对象模拟 DOM 节点数据
    • 由 React 最新推广使用
  • Vue React 等框架的价值

    • 组件化
    • 数据视图分离,数据驱动视图(核心)
    • 只关心业务数据,而不用再关心 DOM 变化
  • 数据驱动视图,技术方案:vdom

    • data 变化
    • diff 算法 vnode oldVnode
    • 更新 DOM
  • 扩展:svelte 就不用 vdom svelte.png

4. 遍历数组,for 和 forEach 哪个快?

答案:

  • for 更快
  • forEach 每次都要创建一个函数来调用,而 for 不会创建函数
  • 函数需要独立的作用域,会有额外的开销
    html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>for-foreach</title>
    </head>
    <body>
        <script>
            const arr = [];
            for (let i = 0; i < 100 * 10000; i++) {
              arr.push(i)
            }
            const length = arr.length
            console.time('for')
            let n1 = 0;
            for (let i = 0; i < length; i++) {
                n1++
            }
            console.timeEnd('for') // 3.7ms
    
            console.time('forEach')
            let n2 = 0;
            arr.forEach(() => n2++)
            console.timeEnd('forEach') // 15.1ms
        </script>
    </body>
    </html>

5. Node.js 如何开启进程,进程如何通讯?

答案:

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

涉及相关知识点:

  • 进程 process vs 线程 thread

    • 进程,OS 进行资源分配和调度的最小单位,有独立内存空间。
    • 线程,OS 进行运算调度的最小单位,共享进程内存空间。
    • JS 是单线程的,但可以开启多线程执行,如 WebWorker
  • 为何需要多进程?

    • 多核 CPU,更适合处理多进程
    • 内存较大,多个进程才能更好的利用(单进程有内存上限)
    • 总之,"压榨"机器资源,更快,更节省
  • 代码演示

    • node 进程
      js
      const http = require('http');
      
      const server = http.createServer();
      server.listen(3000, () => {
        console.log('localhost: 3000')
      })
      console.info(process.pid)
    • node child_process.fork
      js
      const http = require('http');
      const fork = require('child_process').fork
      
      const server = http.createServer((req, res) => {
        if (req.url === '/get-sum') {
          console.info('主进程 id', process.pid)
          // 开启子进程
          const computeProcess = fork('./compute.js');
          computeProcess.send('开始计算');
          computeProcess.on('message', data => {
            console.info('主进程接受到的信息:', data)
            res.end('sum is' + data)
          })
          computeProcess.on('close', () => {
            console.info('子进程因报错而退出');
            computeProcess.kill();
            res.end('error')
          })
        }
      });
      server.listen(3000, () => {
        console.info('localhost: 3000')
      })
    • node cluster.fork
      js
      const http = require('http');
      const cpuCoreLength = require('os').cpus().length;
      const cluster = require('cluster');
      
      if (cluster.isPrimary) {
        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
  • 重点:

    • 进程 VS 线程
    • JS 是单线程的
    • 为何需要多进程?

6. 请描述 JS Bridge 原理

  • 什么是JS Bridge

    • JS 无法直接调用 native API
    • 需要通过一些特定的"格式"来调用
    • 这些"格式"就统称 JS Bridge,例如微信JS-SDK js-bridge.png
  • JS Bridge 的常见实现方式

    • 注册全局 API
    • URL Scheme
    • 代码演示
      html
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>js-bridge</title>
      </head>
      <body>
          <p>js-bridge</p>
          <iframe id="iframe1"></iframe>
          <script>
              // 全局注册
              // const version = window.getVersion()
      
              // URL Scheme 通过自定义协议来提供相应的能力 异步
              // 简单用法
              const iframe1 = document.getElementById('iframe1');
              iframe1.onload = () => {
                const content = iframe1.contentWindow.document.body.innerHTML;
              }
              iframe1.src = 'my-app-name://api/getVersion'
      
              // 封装 JS-Bridge
              const sdk = {
                  invoke(url, data = {}, onSuccess, onError) {
                    const iframe = document.createElement('iframe');
                    iframe.style.visibility = 'hidden';
                    document.body.appendChild(iframe);
      
                    iframe.onload = () => {
                      const content = iframe.contentWindow.document.body.innerHTML;
                      onSuccess(JSON.parse(content));
                      iframe.remove();
                    }
      
                    iframe.onerror = () => {
                      onError();
                      iframe.remove();
                    }
      
                    iframe.src = `my-app-name://${url}?data=${JSON.stringify(data)}`
                  },
      
                  fn1(data, onSuccess, onError) {
                    sdk.invoke('api/fn1', data, onSuccess, onError)
                  },
                  fn2(data, onSuccess, onError) {
                    sdk.invoke('api/fn2', data, onSuccess, onError)
                  }
              }
              sdk.fn1()
          </script>
      </body>
      </html>

7. 是否了解过 requestIdleCallback? 和 requestAnimationFrame 有什么区别?

答案:

  • requestAnimationFrame 每次渲染完都会执行,高优

  • requestIdleCallback 空闲时才执行,低优

  • 由 React fiber 引起的关注

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

    html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>requestAnimationFrame</title>
    </head>
    <body>
        <p>requestAnimationFrame</p>
        <button id="btn">change</button>
        <div id="box"></div>
        <script>
            const box = document.getElementById('btn');
            document.getElementById('btn').addEventListener('click', () => {
              let curWidth = 100;
              const maxWidth = 400;
    
              function addWidth() {
                curWidth = curWidth + 3;
                box.style.width = `${curWidth}px`
                if (curWidth < maxWidth) {
                  // window.requestAnimationFrame(addWidth);
                  window.requestIdleCallback(addWidth);
                }
              }
              addWidth()
            })
        </script>
    </body>
    </html>
  • 他们是宏任务还是微任务

    • 两者都是宏任务
    • 要等待 DOM 渲染完成之后才执行,肯定是宏任务。
      html
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>requestIdleCallback</title>
      </head>
      <body>
          <script>
              // 顺序:start end setTimeout requestAnimationFrame requestIdleCallback
              window.onload = () => {
                console.log('start')
                setTimeout(() => {
                  console.log('setTimeout')
                })
                window.requestAnimationFrame(() => {
                  console.log('requestAnimationFrame')
                })
                window.requestIdleCallback(() => {
                  console.log('requestIdleCallback')
                })
                console.log('end')
              }
          </script>
      </body>
      </html>

8. Vue 每个生命周期都做了什么?

  • 重点

    • Vue 生命周期必须掌握
    • 连环问:Vue 什么时候操作 DOM 比较合适
      • mounted 和 updated 都不能保证子组件全部挂载完成
      • 使用 $nextTick 渲染 DOM
        vue
        <template>
        </template>
        
        <script>
          export default {
            name: "NextTick",
            mounted() {
              this.$nextTick(function () {
                // 仅在整个视图都被渲染之后才会运行的代码
              })
            }
          }
        </script>
        
        <style scoped>
        </style>
    • 连环问:Ajax 应该在哪个生命周期?
      • 有两个选择:created 和 mounted
      • 推荐:mounted
    • 连环问:Vue3 Composition API 生命周期有何区别?
      • 用 setup 代替了 beforeCreate 和 created
      • 使用 Hooks 函数的形式,如 mounted 改为 onMounted()
  • beforeCreate

    • 创建一个空白的 Vue 实例
    • data method 尚未被初始化,不可使用
  • created

    • Vue 实例初始化完成,完成响应式绑定。
    • data method 都已经初始化完成,可调用。
    • 尚未开始渲染模板
  • beforeMount

    • 编译模板,调用 render 生成 vdom
    • 还没有开始渲染 DOM
  • mounted

    • 完成 DOM 渲染
    • 组件创建完成
    • 开始由 "创建阶段" 进入 "运行阶段"
  • beforeUpdate

    • data 发生变化之后
    • 准备更新 DOM(尚未更新 DOM)
  • updated

    • data 发生变化,且 DOM 更新完成
    • (不要在 updated 中修改 data, 可能会导致死循环)
  • beforeUnmount

    • 组件进入销毁阶段(尚未销毁,可正常使用)
    • 可移除、解绑一些全局事件、自定义事件
  • unmounted

    • 组件被销毁了
    • 所有子组件也都被销毁了
  • keep-alive 组件

    • onActivated 缓存组件被激活
    • onDeactivated 缓存组件被隐藏

9. Vue2 Vue3 React 三者 diff 算法有何区别?

  • 介绍 diff 算法

    • diff 算法很早就有。
    • diff 算法应用广泛,例如 github 的 Pull Request 中的代码 diff。
    • 如果要严格 diff 两棵树,时间复杂度 O(n^3) 不可用。
  • diff 算法 优化 O(n)

    • 只比较同一层级,不跨级比较
    • tag 不同则删掉重建(不再去比较内部的细节)
    • 子节点通过 key 区分(key 的重要性)
  • React diff 仅右移 react-diff.png

  • Vue2 diff 双端比较

    vue2-diff.png

  • Vue3 diff 最长递增子序列 vue3-diff.png

  • 连环问:Vue React 为何循环时必须使用 key?

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

10. Vue-router MemoryHistory(abstract)

  • Vue-router 三种模式

    • Hash
    • WebHistory
    • MemoryHistory(V4 之前叫做 abstract history)
  • 代码

    • createWebHistory 的实现方式

      • history.pushState
      • window.onpopstate
    • createWebHashHistory 的实现方式

      • location.hash
    • createMemoryHistory 的实现方式

      • 不可前进、后退
      • 只在一个页面中进行路由的切换

Released under the MIT License.