Kc's blog Kc's blog
首页
分类
标签
Timeline
收藏夹
关于
GitHub (opens new window)

kcqingfeng

前端小学生
首页
分类
标签
Timeline
收藏夹
关于
GitHub (opens new window)
  • 提前下班小妙招

  • node版本的切换

  • 面试

    • 面试问答
      • 面试随记
      • googleNotebooklm
      • 虚拟列表实现原理与优化方案
      • 环保相关
      • 中安科技面试题
    • 收藏
    • 面试
    kc_shen
    2026-03-31
    目录

    面试问答

    面试问答,

    别想转行了,自己的行业都干不了,别的行业也干不了
    
    1

    # 1. 都使用过哪些框架写项目

    常用过的前端框架包括:Vue.js、React、Angular。后端框架有:Express、Koa、Django、Spring Boot 等,针对项目的需求选择合适的框架。

    # 2. 对 Vue 的数据双向绑定怎么理解

    Vue 实现数据双向绑定主要通过 v-model 指令和 Object.defineProperty(Vue 2)或 Proxy(Vue 3)。它使得数据的变化能够自动反映到视图层,反之,用户的输入也能更新数据。这是通过 Vue 的响应式系统来实现的,当数据变化时,会触发视图更新。

    # 3. 你对 Vue 的 MVVM 有什么理解

    MVVM(Model-View-ViewModel)是 Vue 的架构模式。它将应用程序的视图(View)与业务逻辑(Model)分离,ViewModel 是一个连接 View 和 Model 的桥梁,处理视图和数据之间的同步。Vue 通过 data 和计算属性等来实现 ViewModel 的功能,确保数据变化能够自动驱动视图更新。

    # 4. JS 的事件循环机制说一下

    JavaScript 是单线程执行的,但事件循环机制使得它可以处理异步操作。事件循环的核心概念是:

    1. 调用栈:执行当前任务。
    2. 任务队列:保存待执行的异步任务。
    3. 事件循环:在调用栈为空时,事件循环会将任务队列中的任务推入调用栈,执行它们。

    通过这机制,JavaScript 可以处理如 setTimeout、Promises 等异步操作。

    # 5. H5 那边的节流和防抖

    • 防抖:防止短时间内多次触发事件,只有事件停止触发一段时间后才会执行。常用于用户输入或窗口调整大小等场景。
    • 节流:限制单位时间内事件触发的次数,常用于高频率触发的事件,如滚动、resize 等。节流保证在一定时间内事件只会被执行一次。

    # 6. **你能大概的说一下 Vue 的生命周期吗

    选项式 API (Vue2) 组合式 API (Vue3 setup) 触发时机 常用场景
    beforeCreate 不需要 组件初始化完成,data 观测之前 几乎不用 (setup 中直接写代码即可)
    created 不需要 组件初始化完成,data 观测之后 几乎不用 (setup 中直接写代码即可)
    beforeMount onBeforeMount 挂载开始前,DOM 还没生成 极少用
    mounted onMounted DOM 渲染完成 最常用:发送请求、操作 DOM、初始化图表
    beforeUpdate onBeforeUpdate 数据更新,视图未更新 监听数据变化前后的状态
    updated onUpdated 数据 + 视图 都更新完成 处理更新后的 DOM 操作
    beforeUnmount onBeforeUnmount 组件卸载(销毁)之前 最常用:清除定时器、解绑事件、取消订阅
    unmounted onUnmounted 组件卸载完成 清理遗留资源

    # 7. 跨域这边你们前端是如何解决的

    前端跨域问题可以通过以下几种方式解决:

    • CORS:通过服务器设置响应头允许跨域访问。
    • JSONP:通过动态加载 script 标签来绕过同源策略。
    • 代理服务器:通过设置开发环境的代理服务器,将请求代理到不同域上。
    • iframe + postMessage:通过嵌套 iframe 进行跨域通信。

    # 8. ES7 里面 async/await 有使用过吗

    是的,async/await 是 ES7 引入的异步编程语法,使用它可以避免回调地狱,写出更简洁的异步代码。async 标记一个函数为异步,await 用于等待 Promise 解析结果。

    async function fetchData() {
      try {
        const response = await fetch("/data");
        const data = await response.json();
        return data;
      } catch (error) {
        console.error(error);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 9. 你能讲一个你比较熟悉的项目, 大致说一下它的业务

    可以讲述一个熟悉的项目,假如是开发一个新闻信息流广告平台,主要业务是通过新闻内容匹配相关广告,向用户展示个性化广告。这涉及到广告的精准投放、用户行为分析、后台管理系统等多个模块。

    # 10.性能优化

    1. 构建层面
      1. Tree Shaking
      2. 代码分割(Code Splitting)大代码块分割为多个小代码块,实现懒加载
      3. 代码的压缩混淆
    2. 代码层面
      1. 关键资源优先加载,非关键的异步加载
      2. 减少重排 (Reflow) 与重绘 (Repaint),批量修改 DOM,动画,优先使用 CSS 的 transform 和 opacity 属性,因为它们可以利用 GPU 加速,避免触发重排
      3. 事件处理优化,对于 scroll、resize、input 等高频触发的事件,使用防抖 (Debounce) 和节流 (Throttle) 技术来控制事件处理函数的执行频率
    3. 资源层面
      1. 图片懒加载
      2. CDN
      3. gzip
      4. 使用现代图片格式,webP,图片压缩,字体压缩
      5. 合并小图标,使用字体文件等
      6. 开启浏览器缓存,(强缓存:强缓存是性能最优的缓存方式。它的核心逻辑是:浏览器在判断缓存有效后,会直接使用本地资源,完全不向服务器发送任何请求。协议缓存:当强缓存失效(例如 max-age 过期)或被禁用(设置了 no-cache)时,浏览器会进入协商缓存流程。它的核心逻辑是:浏览器会向服务器发送一个请求,询问“我本地的资源是否还有效?)

    # 11.var,let,const

    • var 是函数作用域:它只关心函数边界,不关心 {} 块边界(除非是函数块)。
    • let 和 const 是块级作用域:它们只在大括号 {} 内有效(如 if、for 循环)。

    # 12.setup 的作用

    
    - **自动暴露**:顶层声明的变量、函数等会自动在模板中可用,无需 `return`。
    - **更简洁**:减少了大量样板代码。
    - **更好的 TypeScript 支持**。
    - **this执行underfined**
    
    1
    2
    3
    4
    5

    # 13.开发过程中遇到的问题,你是如何解决的

    1. 浏览器兼容性问题(使用 Babel 将 ES6+ 代码转译为 ES5,配合 core-js 进行 Polyfill,填补旧浏览器的功能缺失)
    2. 性能优化问题 (首屏加载慢,滚动/操作卡顿,内存泄漏)
    3. 移动端与响应式适配
    4. 网络请求与安全 (跨域问题 ,安全漏洞,弱网请求失败;设置超时时间)

    # 14.事件循环

    js 是一个单线程的机制,事件循环负责调度,防止页面卡死

    宏任务 (Macro Task) setTimeout, setInterval, I/O 操作, UI 渲染 耗时相对较长,优先级较低
    微任务 (Micro Task) Promise.then/catch/finally, queueMicrotask, async/await 耗时短,优先级非常高
    console.log('1. 同步代码 start'); // 同步代码
    
    setTimeout(() => {
      console.log('2. setTimeout 回调'); // 宏任务
    }, 0);
    
    Promise.resolve().then(() => {
      console.log('3. Promise.then 回调'); // 微任务
    });
    
    console.log('4. 同步代码 end'); // 同步代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    1. 执行全局脚本(宏任务)
      • 首先执行同步代码,打印 1. 同步代码 start。
      • 遇到 setTimeout,将其回调函数放入宏任务队列,等待下一轮循环。
      • 遇到 Promise.resolve().then(),将其回调函数放入微任务队列。
      • 继续执行同步代码,打印 4. 同步代码 end。
    2. 清空微任务队列
      • 当前宏任务(全局脚本)的同步代码执行完毕。
      • 事件循环检查微任务队列,发现有一个 Promise 回调,立即执行它,打印 3. Promise.then 回调。
      • 微任务队列被清空。
    3. 尝试渲染
      • 浏览器会尝试进行一次 UI 更新(如果有的话)。
    4. 执行下一个宏任务
      • 事件循环从宏任务队列中取出 setTimeout 的回调并执行,打印 2. setTimeout 回调。

    因此,最终的输出顺序是:1 → 4 → 3 → 2。

    # async/await 与事件循环

    async/await 是 Promise 的语法糖,它让异步代码看起来像同步代码。理解它的关键在于 await 关键字。

    await 表达式会暂停 async 函数的执行,等待 Promise 完成。await 后面的代码,会被当作一个微任务(类似于 .then() 的回调)来处理。

    async function asyncFn() {
      console.log('a. async 函数开始');
      await Promise.resolve(); // 等待一个 Promise
      console.log('b. await 之后的代码'); // 这部分代码是微任务
    }
    
    console.log('1. 同步代码 start');
    asyncFn();
    console.log('2. 同步代码 end');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 15.节流/防抖

    两者都依赖闭包来持久化状态(如定时器 ID 或上次执行时间)。

    # 节流:

    固定时间执行一次。(多用于按钮)

    追问:

    • 立即执行版本怎么实现?
    • Vue 场景怎么用?

    节流有多种实现方式,这里介绍最常用的时间戳版本,它通过比较当前时间和上次执行时间来判断是否执行函数。

    /**
     * 节流函数 (时间戳版)
     * @param {Function} fn - 需要执行的函数
     * @param {Number} interval - 间隔时间(毫秒)
     * @returns {Function} - 包装后的函数
     */
    function throttle(fn, interval) {
      let lastTime = 0; // 通过闭包保存上次执行的时间戳
      return function(...args) {
        const context = this;
        const now = Date.now();
        // 如果当前时间与上次执行时间的差值大于等于间隔,则执行
        if (now - lastTime >= interval) {
          fn.apply(context, args);
          lastTime = now;
        }
      };
    }
    
    // 使用示例:页面滚动加载
    const handleScroll = throttle(() => {
      console.log('检查是否加载更多内容');
    }, 200);
    window.addEventListener('scroll', handleScroll);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 防抖:

    触发后延迟执行。事件持续触发时,只在最后一次触发后执行(多用于输入框)

    # 防抖 (Debounce) 实现

    防抖的核心是维护一个定时器,每次事件触发时都清除上一次的定时器并重新设置。

    /**
     * 防抖函数
     * @param {Function} fn - 需要执行的函数
     * @param {Number} delay - 延迟时间(毫秒)
     * @returns {Function} - 包装后的函数
     */
    function debounce(fn, delay) {
      let timer = null; // 通过闭包保存定时器ID
      return function(...args) {
        const context = this;
        // 清除上一次的定时器,重新计时
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(context, args);
        }, delay);
      };
    }
    
    // 使用示例:搜索框输入
    const searchInput = document.getElementById('search');
    const handleSearch = debounce((e) => {
      console.log('发送搜索请求:', e.target.value);
    }, 500);
    searchInput.addEventListener('input', handleSearch);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 16.vue3 响应式原理

    使用 es6 的 proxy 构建

    特性 Vue 2 (Object.defineProperty) Vue 3 (Proxy)
    监听对象 只能劫持对象的已有属性 代理整个对象,可拦截 13 种操作
    新增/删除属性 无法检测,需使用 Vue.set / Vue.delete 自动检测,原生支持
    数组索引/长度 需特殊处理(重写变异方法) 原生支持,无需特殊处理
    性能 初始化时递归遍历所有属性,开销大 懒代理,访问时才代理嵌套对象,性能更优
    1. Proxy (拦截器)
      • 它是响应式系统的核心。reactive 函数会创建一个 Proxy 来包裹原始数据对象。
      • 它负责拦截对数据的所有操作,最核心的是 get (读取) 和 set (修改) 操作。
    2. Reflect (反射器)
      • 它是一套与 Proxy 拦截器方法对应的工具函数。
      • 在 Proxy 的拦截器内部,使用 Reflect.get 或 Reflect.set 来执行对原始对象的默认操作。这样做可以确保 this 指向的正确性,并保留原生对象的行为。
    3. Effect (副作用函数)
      • 它代表了依赖响应式数据的代码,例如组件的渲染函数、watch 的回调函数等。
      • 当 effect 执行时,它会读取响应式数据,从而触发依赖收集。当数据变化时,effect 会被重新执行,从而更新视图或执行相应逻辑。

    # 工作流程:依赖收集与触发更新

    整个响应式系统的工作流程是一个完美的闭环,分为两个关键阶段:

    # 1. 依赖收集 (Track)

    这个过程发生在读取响应式数据时。

    1. 当一个副作用函数 (effect) 执行时(例如组件首次渲染),它会访问响应式对象的属性。
    2. 这个访问操作会触发 Proxy 的 get 拦截器。
    3. 在 get 拦截器中,Vue 会调用 track 函数,记录下“当前正在执行的 effect”依赖于“这个属性”。
    4. 这个依赖关系会被存储在一个“依赖映射表”中,建立 数据属性 -> effect 的关联。

    # 2. 触发更新 (Trigger)

    这个过程发生在修改响应式数据时。

    1. 当响应式对象的属性被修改(或新增、删除)时,会触发 Proxy 的 set (或 deleteProperty) 拦截器。
    2. 在拦截器中,Vue 会调用 trigger 函数。
    3. trigger 函数会根据被修改的属性,从之前建立的“依赖映射表”中找到所有依赖于该属性的 effect。
    4. 最后,将这些 effect 重新执行一遍,从而实现视图的自动更新。
    # Add cloudflare gpg key
    sudo mkdir -p --mode=0755 /usr/share/keyrings
    curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null
    
    # Add this repo to your apt repositories
    echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
    
    # install cloudflared
    sudo apt-get update && sudo apt-get install cloudflared
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    http://113.207.49.134:22066/api/v1/client/subscribe?token=801e0f0d1e8c4a1b8ea658c2370aca87

    # 17.UniApp 的 JS 能力有限,如果业务需要调用原生功能(如复杂的蓝牙交互或特定的 SDK),你会怎么做?

    我会选择开发或集成“原生插件”。核心是利用 UniApp 提供的桥接层。JS 调用 uni.requireNativePlugin 获取原生模块实例,然后调用其方法。

    # 18.Promise

    # Promise.all() —— “全员通过才算过”

    • 逻辑:接收一组 Promise,只有当所有 Promise 都成功时,才返回成功结果(数组);只要有一个失败,就立即返回失败。

    # Promise.race() —— “谁快听谁的”

    • 逻辑:接收一组 Promise,第一个改变状态(无论成功或失败)的 Promise 决定最终结果。

    # Promise.allSettled() —— “不管成败,我都要结果”

    • 逻辑:等待所有 Promise 完成,返回一个包含所有结果(状态和值/原因)的数组。

    # Promise.any() —— “只要有一个人成功就行”

    • 逻辑:只要有一个 Promise 成功,就立即返回成功;只有当所有 Promise 都失败时,才返回失败。

    # 19.数组的常用方法

    # 1. 增删改查(操作方法)

    这类方法主要用于改变数组的内容。

    # 增(添加)
    • push(item1, ...):在数组末尾添加元素。返回新数组的长度。
    • unshift(item1, ...):在数组开头添加元素。返回新数组的长度。
    • concat(arr1, ...):合并数组。不修改原数组,返回一个新数组。
    # 删(删除)
    • pop():删除并返回数组最后一个元素。
    • shift():删除并返回数组第一个元素。
    # 改(增删改一体)
    • splice(start, deleteCount, ...items)

      :这是数组的“手术刀”。

      • start:开始位置。
      • deleteCount:删除多少个(0 表示不删)。
      • items:要插入的新元素。
      • 注意:它会修改原数组,返回被删除的元素数组。
    # 查(查找)
    • indexOf(item):返回元素索引,找不到返回 -1。
    • includes(item):返回 true 或 false,判断是否存在。
    • slice(start, end):截取数组片段。不修改原数组,返回新数组(常用于实现浅拷贝)。

    # 2. 排序与填充

    • sort(compareFn)
      • 修改原数组。
      • 坑:默认按字符编码排序([10, 5, 40, 25] 会变成 [10, 25, 40, 5])。
      • 解法:必须传入比较函数,如 arr.sort((a, b) => a - b) 进行升序排列。
    • reverse():反转数组顺序,修改原数组。
    • fill(value):用固定值填充数组,修改原数组。

    # 3. 迭代方法(高阶函数)

    这是现代开发(尤其是 React/Vue)中最常用的部分,它们不会修改原数组(除了回调函数内部手动修改对象属性外)。

    # forEach —— 纯遍历
    • 用途:执行副作用(如打印日志、操作 DOM、修改外部变量)。
    • 返回值:undefined。
    • 注意:无法使用 break 或 return 中断循环。
    # map —— 映射/转换
    • 用途:将数组中的每一项转换成新格式。
    • 返回值:新数组(长度与原数组一致)。
    • 场景:[1, 2, 3] → [2, 4, 6] 或 对象数组提取 ID 列表。
    # filter —— 过滤
    • 用途:筛选符合条件的项。
    • 返回值:新数组(长度 ≤ 原数组)。
    • 场景:删除列表中的无效数据,或搜索功能。
    # reduce —— 归并/汇总
    • 用途:将数组“压缩”成一个值(数字、对象、数组等)。
    • 语法:arr.reduce((累加器, 当前值) => { ... }, 初始值)。
    • 场景:数组求和、计算最大值、将数组转为对象(分组)。
    # 判断与查找
    • find(callback):返回第一个符合条件的元素。
    • findIndex(callback):返回第一个符合条件的索引。
    • some(callback):只要有一个符合条件就返回 true(类似“或”逻辑)。
    • every(callback):所有都符合条件才返回 true(类似“且”逻辑)。

    # 20.闭包

    函数 + 它能访问的作用域环境。

    一般是一个函数返回了一个函数的情形,

    1. 返回的函数,仍然可以 i 访问外层函数的局部变量即使外层函数已经执行结束
    2. 这种访问保持了外层变量的状态,可以实现私有变量或累加器等功能
    3. 并不是所有返回函数的场景都是闭包,关键是返回的函数引用了外层函数的变量

    闭包

    常见应用:

    • 防抖节流
    • 模块封装
    • 私有变量

    # 21.uniapp 开发小程序遇到的坑

    1.vue2和vue3区别;:style="[object]",在vue2中识别成这样的了, 改成 :style="[styleObj]"
    2.ios和安卓的区别;ios时间只能识别“/”,需要把“-”转换为“/”
    3.字体渲染差异,需要统一字体,设置line-height:1.5
    4.录音格式兼容,ios默认aac,安卓默认silk
    5.ios自带bounce,下拉页面留白
    
    
    1
    2
    3
    4
    5
    6

    # 22.项目中对接了 ai,你都遇到什么问题

    ai 输出的内容不可控,格式不规范,内容可能逻辑不通,慢,需要进行

    prmopt 优化;

    定义角色,说明任务,禁止什么,给出示例模版

    严格的 json 教研,自动补全,加上流式打字机效果,

    # 23.设计基于 RBAC 的权限模型,实现页面权限 + 按钮权限 + 数据权限三层控制

    页面路由:静态路由和动态路由(通过 addRouter 添加进去);有可能刷新丢失,在路由守卫 beforeEach 里面判断是否添加了权限

    按钮权限:使用自定义指令;按钮绑定权限码(add,edit)等,起 pinia 里面判断权限数组是否包含

    数据权限:前端只要是传递 scope 参数,后端数据过滤

    # 24.对外平台采用“多租户数据隔离”思路,所有接口均基于合作商唯一标识进行数据过滤,避免跨 租户访问风险

    所有接口统一通过添加合作商唯一 id 进行强制数据过滤,放置数据安全

    # 25.优化生成性能与交互体验,减少生成等待时间

    简化 prompt,流式返回 sse,websocket,骨架屏,防抖防止重复点击,异步加载,离开页面

    # 26.虚拟列表

    如果是定高列表很好处理,但在实际业务中(比如评论区),高度往往是不固定的。我的处理策略是**‘预估 + 测量 + 缓存’**:

    • 预估:先给所有未渲染的项设置一个默认高度(比如 50px),保证初始滚动条能显示。
    • 测量:当数据渲染上屏后,利用 ResizeObserver 或 getBoundingClientRect 获取它的真实高度。
    • 缓存与修正:把真实高度存到一个缓存数组里(heightCache[index] = realHeight)。下次滚动计算位置时,优先使用缓存的高度。

    这样可以保证滚动过程中,位置计算越来越精准,同时避免了重复测量带来的性能损耗。”

    性能和优化

    1. 滚动性能优化:滚动事件触发频率极高,我会使用 requestAnimationFrame 对滚动回调进行节流,确保每帧只计算一次位置,保证 60fps 的流畅度。
    2. 定位查找优化:如果是超长列表(如 10 万条),计算 startIndex 时如果遍历高度数组会很慢。我会维护一个累积高度数组,利用二分查找来快速定位当前滚动位置对应的索引,将时间复杂度从 O(N) 降到 O(logN)。
    3. DOM 复用:在框架(如 Vue/React)中,我会确保 key 的稳定性,或者使用对象池的思想复用 DOM 节点,减少 Diff 算法的开销。”

    # 27.http 请求三次握手,四次挥手

    作用:建立可靠的 TCP 连接,确保双方收发能力正常。 第一次握手(SYN) 客户端 → 服务端:发送 SYN 包,请求建立连接。 第二次握手(SYN+ACK) 服务端 → 客户端:回复 SYN+ACK,同意连接并确认。 第三次握手(ACK) 客户端 → 服务端:回复 ACK,连接建立成功。 核心答案:客户端发 SYN,服务端回 SYN+ACK,客户端再回 ACK,三次握手完成,TCP 连接建立。

    第一次挥手(FIN):主动方发 FIN,请求关闭。 第二次挥手(ACK):被动方回 ACK,确认收到。 第三次挥手(FIN):被动方发 FIN,同意关闭。 第四次挥手(ACK):主动方回 ACK,连接关闭。

    # 28.浏览器渲染页面的流程

    1. DNS 解析:域名 → IP 地址。
    2. 建立 TCP 连接:三次握手。
    3. TLS 握手(HTTPS):加密通道建立。
    4. 发送 HTTP/HTTPS 请求。
    5. 服务端处理并返回响应。
    6. 解析 HTML,构建 DOM 树。
    7. 解析 CSS,构建 CSSOM 树。
    8. 合成 Render Tree(渲染树)。
    9. 布局(Layout/Reflow):计算位置大小。
    10. 绘制(Paint):绘制像素。
    11. 合成(Composite):图层合并,页面展示。
    12. 关闭 TCP 连接:四次挥手。

    # 29.开发中遇到印象最深的问题

    # 30.调试起来最麻烦的一个bug

    1. 在table中使用表格,span里面的空格无法自动去除,因为vxe-table中设置了一个样式:white-space:pre-line;最后实现方法:options.compilerOptions.whitespace="condense"

    2. form表单内数字的输入框type=number,页面滑动的时候会触发他的值的变化,因为vxe-table自动添加了一个throller 节流导致,修改方法就是@wheel.native.prevent,去除滚轮的原生事件,或者是不使用ei-input,使用el-number-input

    3. 页面上有 <> 这种符号的情况,会转义

    4. 横向滚动条不好用,2.7的bug,更新到2.10就好了,因为源码中在处理纵向无限滚动的时候,把源码中的动态的宽高设置成了固定的高度

    5. 表格在某一个宽度的时候会出现抖动,情况复现:就是最后一列操作是宽度固定的,前面的宽度都是自适应的,elemetui设置的最小默认值是80,除了第一列,其他的列是平均分的,除了第一列其他的都是向下取整,会导致第一列的宽度变窄,会换行,然后导致页面出现纵向滚动条,这个时候许呀哦重新计算每一列的宽度,计算之后第一列的宽度又变大了

    6. # 1. ElementUI Table 列宽分配规则(你遇到的版本)
      • 最后一列:固定宽度(如 120px)

      • 其他列:不设 width,只受 min-width=80 约束

      • 剩余宽度分配逻辑:

        1. 总可用宽度 = 表格宽度 − 固定列宽
        2. 动态列按 min-width 权重 分配剩余空间
        3. 除第一列外,其他列宽度都向下取整
        4. 最后把 剩下的零头全部给第一列

      结果:

      • 中间列都是整数宽度
      • 第一列宽度是小数 / 不固定
      # 2. 抖动触发链(关键)
      1. 临界宽度:表格缩到某一宽度

      2. 第一列过窄 → 内容换行 → 行高变高 → 出现纵向滚动条

      3. 滚动条出现 → 表格可用宽度变少(减去滚动条 15~17px)

      4. ElementUI 检测到宽度变化 → 重新计算列宽

      5. 重新计算后:

        • 可用宽度变多一点
        • 第一列分到更多零头 → 变宽 → 不换行 → 行高恢复 → 滚动条消失
      6. 滚动条消失 → 可用宽度又变大 → 再次触发重算 → 第一列又变窄 → 换行 → 滚动条出现

      7. 循环往复 → 视觉抖动

      一句话:

      滚动条的出现 / 消失 ↔ 列宽重算 ↔ 换行 / 不换行 → 死循环抖动

      解决方法:

      1. :show-overflow-tooltip="true"
      2. padding-right:20px;预留出来滚动条的宽度
      3. 设置表格为fixed

    # 31.在使用element组件中,有对源代码进行过修改吗

    项目中不会直接修改 Element 源码,避免依赖不可维护、升级失效。

    样式用 :deep () 深度选择器覆盖;

    禁用 / 修改组件原生逻辑,通过组件事件 return false、封装组件、props 配置实现。

    .native 只能监听根 DOM 原生事件,无法拦截组件内部逻辑,不能用来禁用组件原生方法,所以一般不用 native 做这件事。

    # 32.Echarts踩坑

    窗口缩放后图表变形、不自适应,监听resize,并且还要加防抖, 切换页面之后需要销毁实例

    “在容器节点被销毁时,总是应调用 echartsInstance.dispose 以销毁实例释放资源,避免内存泄漏。”

    onUnmounted(() => {
      chart?.dispose()
      chart = null
    })
    
    1
    2
    3
    4

    echarts legend头部有遮挡、上半部分显示不全,像被截取了一块;

    legend-textStyle中加入lineHeight即可
    
    1

    不要用 ref 存实例,用 shallowRef

    坑:

    const chart = ref(null) → Vue 会深度 Proxy 监听 ECharts 巨大对象,巨卡。

    import { shallowRef } from 'vue'
    const chart = shallowRef(null)
    只存引用,不做响应式劫持。
    
    1
    2
    3

    大数据量关闭动画、开启 large 模式、做数据降采样,用 Canvas 渲染。

    采用按需引入 ECharts 模块,减少包体积与初始化开销。

    # 33.WebSocket频繁更新echats,会有什么问题吗

    1. 节流 + 批量更新

      • 把 WebSocket 数据先缓存,按 300~500ms 批量更新一次
    2. 限制数据长度(滑动窗口)

      • 只保留最近 N 条数据,避免无限增长,限制MAX,从前删除数字
    3. 使用 appendData 替代 setOption

      • 减少 diff 开销
    4. 关闭或降低动画

      • 提升实时场景下的流畅性
    5. 必要时做数据采样(sampling)

      • 控制渲染点数量

      在大屏项目中,由于 WebSocket 持续推送数据,如果不做控制会导致图表卡顿。 我们通过“采样 + 时间窗口”优化:

      • 采样:减少数据点数量,比如按间隔或聚合处理
      • 时间窗口:只保留最近 N 条数据,旧数据删除 同时配合节流更新和 ECharts 的增量更新,提高渲染性能。

    大屏一般不会用全量数据,因为数据量过大不仅影响性能,而且在有限屏幕上也没有展示价值。通常会通过“窗口”控制数据范围,比如只保留最近一段时间的数据,同时通过“采样”减少渲染点数量,比如使用 ECharts 的 LTTB 算法,在保证趋势和峰值的前提下降低渲染压力,从而在性能和展示效果之间做平衡。

    首屏加载优化

    1. 分批异步加载,不要同时占满主线程
    2. 只加载可视区域内的

    减少一次性渲染压力 + 控制数据量 + 降低更新频率 + 提升用户感知速度

    # 34.异步组件

    <!-- 父页面:使用异步组件的页面 -->
    <template>
      <div class="page">
        <h2>Vue3 异步组件(完整带注释版)</h2>
    
        <!-- 按钮控制:控制异步组件是否渲染 -->
        <button @click="show = true">点击加载图表组件</button>
    
        <!--
          异步组件使用:
          1. 只有 show = true 时才会开始加载
          2. 加载中自动显示 Loading
          3. 加载失败自动显示 Error
          4. 加载完成后触发 @loaded 事件
        -->
        <AsyncChart
          v-if="show"
          title="首屏加载优化图表"
          @loaded="handleChartLoaded"
        />
      </div>
    </template>
    
    <script setup>
    import { defineAsyncComponent, ref } from 'vue'
    
    // 1. 加载中组件(可以单独拆文件,这里写简易版)
    const LoadingComponent = {
      template: '<div style="color:#1677ff">组件加载中...</div>'
    }
    
    // 2. 加载失败组件
    const ErrorComponent = {
      template: '<div style="color:red">组件加载失败!</div>'
    }
    
    // ==============================================
    // 3. 核心:定义异步组件(所有参数都在这里)
    // ==============================================
    const AsyncChart = defineAsyncComponent({
      // --------------------------
      // 必填:加载器(异步导入组件)
      // 返回 import() 函数
      // --------------------------
      loader: () => import('./Chart.vue'),
    
      // --------------------------
      // 可选:加载中显示的组件
      // --------------------------
      loadingComponent: LoadingComponent,
    
      // --------------------------
      // 可选:加载失败显示的组件
      // --------------------------
      errorComponent: ErrorComponent,
    
      // --------------------------
      // 可选:延迟 200ms 再显示 loading
      // 作用:加载快的话不闪屏
      // 默认:200
      // --------------------------
      delay: 200,
    
      // --------------------------
      // 可选:超时时间(毫秒)
      // 10 秒加载失败算错误
      // --------------------------
      timeout: 10000,
    
      // --------------------------
      // 可选:失败后是否支持重试
      // --------------------------
      retry: true,
    
      // --------------------------
      // 可选:配合<Suspense>使用
      // 普通项目不用改
      // --------------------------
      suspensible: false
    })
    
    // 控制异步组件显示/隐藏
    const show = ref(false)
    
    // 监听:异步组件加载完成
    function handleChartLoaded() {
      console.log('✅ 异步组件加载 + 渲染完成!')
      alert('图表组件加载成功!')
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90

    # 35.地图相关

    新闻h5中对接过,高德的ip定位

    支付宝小程序对接过周边查询

    绘制轨迹图;

    // 1. 初始化地图
    const map = new AMap.Map('map', {
      zoom: 14,
      center: [121.4737, 31.2304] // 轨迹中心点
    })
    
    // 2. 运动轨迹经纬度数组(示例:一串点)
    const path = [
      [121.465, 31.235],
      [121.468, 31.233],
      [121.470, 31.231],
      [121.473, 31.229],
      [121.475, 31.227]
    ]
    
    // 3. 绘制轨迹线
    const polyline = new AMap.Polyline({
      path: path,
      strokeColor: '#1677ff', // 轨迹颜色
      strokeWeight: 6,        // 轨迹宽度
      strokeOpacity: 0.8,     // 透明度
      zIndex: 50
    })
    polyline.addTo(map)
    
    // 4. 标记起点(绿色)
    new AMap.Marker({
      position: path[0],
      icon: 'https://webapi.amap.com/theme/v1/markers/markers_nolabel.png',
      title: '起点',
      map: map
    })
    
    // 5. 标记终点(红色)
    new AMap.Marker({
      position: path[path.length - 1],
      icon: 'https://webapi.amap.com/theme/v1/markers/markers_red_nolabel.png',
      title: '终点',
      map: map
    })
    
    // 自动调整视野,包含整条轨迹
    map.setFitView([polyline])
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    # 36.vue降级处理

    在 Vue3 项目中,我主要做过两类降级处理:

    一是浏览器兼容降级,通过 Babel 转译、core-js 垫片、环境判断,保证老旧设备不白屏;

    二是低性能设备与弱网降级,通过懒加载、虚拟滚动、关闭非必要动画、接口缓存等方式,保证基础功能可用;

    同时对异常场景做容错降级,使用错误边界和接口兜底,避免局部问题影响整个页面。

    # 37.遇到的最大的挑战

    解构导致响应式丢失,要使用 toRefs包一下

    编辑 (opens new window)
    上次更新: 2026/04/17, 9:04:00
    node版本管理工具-n-
    面试随记

    ← node版本管理工具-n- 面试随记→

    最近更新
    01
    openspec使用指南
    04-17
    02
    上海购买摩托车指南
    04-13
    03
    Cloudflare免费节点搭建详细教程
    04-12
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 kc shen | MIT License 豫ICP备2024074563号-3
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式