Skip to content

懂车帝-前端实习

1 流程

  • 自我介绍
  • 提问
  • 代码

2 代码

  • 并发请求 Pasted image 20251205224858.png
  • Vue数据双向绑定

3 提问

  • v-model的原理
  • Vue的数组响应式是怎么实现的
  • Vue2与Vue3之间的区别
  • Commonjs能不能摇树优化,为什么?
  • Commonjs的底层与ESM的底层实现是怎么样的
  • MVVM与MVC
  • 浏览器缓存,如果强缓存的转态码是多少
  • 存储位置,什么什么catch??
  • 图片的预加载还没有从服务器上拉下来,但是我真实要加载的图片已经发送了请求,那么这时候预加载还有效吗???这又是什么,怎么就问这些偏难怪

4 复盘反思

其实是很好的一次面试,就算我没有通过,他也给我提供了提升的方向,而不是不知道自己该学习什么。我希望自己能不端的提升自己直到成为一个优秀的前端工程师(当然是高级的前端工程师)

参考答案

一、流程

1. 自我介绍(建议模板)

> “您好,我是XXX,毕业于XX大学计算机专业,目前有X年前端开发经验。主要技术栈包括 Vue/React、TypeScript、Webpack/Vite 等,熟悉前端工程化、性能优化和跨端方案。在上一家公司参与过 XX 项目(可简述),对 MVVM 框架原理、浏览器机制、模块系统等底层知识也有一定研究。今天很高兴有机会参加贵公司的面试。”

> Tips:简洁、突出技术栈 + 项目经验 + 对原理的兴趣,契合面试官后续提问方向。


二、代码实现

1. 实现一个并发请求(控制最大并发数)

js
/**
 * 并发请求控制器,限制最大并发数量
 * @param {Array<Function>} tasks - 返回 Promise 的函数数组
 * @param {number} maxConcurrency - 最大并发数
 * @returns {Promise<Array>} 所有任务的结果
 */
function concurrentRequest(tasks, maxConcurrency = 3) {
  return new Promise((resolve) => {
    const results = [];
    let index = 0;
    let completed = 0;

    function run() {
      while (index < tasks.length && running < maxConcurrency) {
        const i = index++;
        const task = tasks[i];
        running++;

        task()
          .then((res) => {
            results[i] = res;
          })
          .catch((err) => {
            results[i] = err;
          })
          .finally(() => {
            running--;
            completed++;
            if (completed === tasks.length) {
              resolve(results);
            } else {
              run(); // 继续调度
            }
          });
      }
    }

    let running = 0;
    run();
  });
}

// 使用示例
const tasks = Array.from({ length: 10 }, (_, i) =>
  () => fetch(`/api/data/${i}`).then(res => res.json())
);

concurrentRequest(tasks, 3).then(console.log);

> 关键点:使用“滑动窗口”思想,通过 running 控制并发数,finally 中继续调度新任务。


2. 手写 Vue 数据双向绑定(Vue 2 风格)

html
<div id="app">
  <input v-model="text" />
  <p>{{ text }}</p>
</div>

<script>
class Vue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    this.observe(this.$data);
    this.compile(options.el);
  }

  observe(obj) {
    if (!obj || typeof obj !== 'object') return;
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, val) {
    // 递归监听嵌套对象
    this.observe(val);
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        this.observe(newVal); // 新值也需响应式
        dep.notify();
      }
    });
  }

  compile(el) {
    const element = document.querySelector(el);
    this.compileElement(element);
  }

  compileElement(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === 1) {
        // 元素节点
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          const attrName = attr.name;
          const attrValue = attr.value;
          if (attrName === 'v-model') {
            // 双向绑定
            node.value = this.$data[attrValue];
            node.addEventListener('input', (e) => {
              this.$data[attrValue] = e.target.value;
            });
            new Watcher(this, attrValue, (newValue) => {
              node.value = newValue;
            });
          }
        });
        // 递归子节点
        this.compileElement(node);
      } else if (node.nodeType === 3) {
        // 文本节点
        const reg = /\{\{(.+?)\}\}/g;
        let match;
        while ((match = reg.exec(node.nodeValue))) {
          const key = match[1].trim();
          new Watcher(this, key, (newValue) => {
            node.nodeValue = node.nodeValue.replace(reg, newValue);
          });
        }
      }
    });
  }
}

// 依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

// 观察者
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    Dep.target = this;
    // 触发 getter,完成依赖收集
    this.oldValue = vm.$data[key];
    Dep.target = null;
  }
  update() {
    const newValue = this.vm.$data[this.key];
    if (newValue !== this.oldValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}

// 使用
new Vue({
  el: '#app',
  data: {
    text: 'hello'
  }
});
</script>

> 说明:此实现包含: > > - Observer(Object.defineProperty) > - Watcher(观察者) > - Dep(依赖收集) > - 编译器(compile)处理 v-model > - 实现了数据 → 视图、视图 → 数据的双向绑定


三、提问详解

1. v-model 的原理

  • 本质:语法糖。

  • 在 input 上

    vue
    <input v-model="msg" />
    <!-- 等价于 -->
    <input :value="msg" @input="msg = $event.target.value" />
  • 组件上

    vue
    <custom-input v-model="searchText" />
    <!-- 等价于 -->
    <custom-input :model-value="searchText" @update:model-value="searchText = $event" />
  • Vue 2 vs Vue 3

    • Vue 2:默认监听 input 事件,prop 为 value
    • Vue 3:使用 modelValue + update:modelValue,支持多 v-model

> 核心:通过 value 绑定 + 事件监听实现双向通信。


2. Vue 的数组响应式是怎么实现的?

问题背景Object.defineProperty 无法监听数组下标赋值(如 arr[0] = 1)和长度变化。

Vue 2 解决方案

  • 重写数组原型方法:对 push, pop, shift, unshift, splice, sort, reverse 进行拦截。
  • 在这些方法内部:
    1. 调用原生方法;
    2. 获取新增元素(如有)并对其做响应式处理;
    3. 手动触发更新(dep.notify())。
js
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args);
    // 处理新增项的响应式
    let inserted;
    if (method === 'push' || method === 'unshift') {
      inserted = args;
    } else if (method === 'splice') {
      inserted = args.slice(2);
    }
    if (inserted) ob.observeArray(inserted);
    // 触发更新
    ob.dep.notify();
    return result;
  };
});

> 注意:直接通过索引设置(arr[0] = val)或修改 length不响应,需用 Vue.setsplice

Vue 3:使用 Proxy,天然支持数组索引和长度变化,无需 hack。


3. Vue2 与 Vue3 的区别

维度Vue 2Vue 3
响应式原理Object.definePropertyProxy + Reflect
组合式 API无(Options API)setup() + ref/reactive
TypeScript 支持不友好原生支持
Tree-shaking不支持(打包整个 Vue)支持(按需引入)
性能虚拟 DOM diff 较重编译时优化(静态提升、PatchFlag)
Fragment / Teleport不支持支持
自定义渲染器困难官方提供 createRenderer
IE11 支持支持不支持

> 关键升级:Proxy 解决了 Vue 2 的响应式缺陷(如数组、新增属性),组合式 API 提升逻辑复用能力。


4. CommonJS 能不能摇树优化?为什么?

不能

原因

  • CommonJS 是动态加载(运行时确定依赖),例如:

    js
    const foo = require(someCondition ? './a' : './b');
  • 摇树优化(Tree Shaking)依赖静态分析(编译时确定哪些导出未被使用)。

  • ESM 的 import/export 是静态的,构建工具(如 Webpack、Rollup)可在打包前分析依赖图,剔除未引用代码。

  • CommonJS 的 module.exportsrequire() 是赋值/调用操作,无法在编译时确定是否使用。

> 结论:只有 ESM 支持 Tree Shaking;CommonJS 无法被有效摇树。


5. CommonJS 与 ESM 的底层实现差异

特性CommonJSESM
加载时机运行时加载编译时(静态)分析
模块缓存require.cache,首次加载后缓存模块是单例,但由 JS 引擎管理
导出方式module.exports = {}(可动态修改)export 声明(静态结构,不可变)
导入方式require()(同步,返回值拷贝)import(异步,返回实时只读引用)
循环依赖返回已执行部分的 exports 对象允许,但需注意 TDZ(暂时性死区)
顶层 this指向 module.exportsundefined

> 关键区别:ESM 是静态、声明式、只读引用;CommonJS 是动态、命令式、值拷贝


6. MVVM 与 MVC 的区别

模式全称核心思想组件职责
MVCModel-View-ControllerController 作为中介,处理用户输入,更新 Model 和 View
  • Model:数据与业务逻辑
  • View:UI 展示
  • Controller:接收输入,协调 M 和 V | | MVVM | Model-View-ViewModel | ViewModel 通过数据绑定自动同步 View 和 Model |
  • Model:同上
  • View:UI,绑定到 ViewModel
  • ViewModel:暴露数据和命令,自动同步(无需手动操作 DOM) |

> 关键差异: > > - MVC 中 View 和 Model 无直接联系,靠 Controller 桥接; > - MVVM 通过双向绑定解耦 View 和 Model,ViewModel 是胶水层; > - Vue、Angular 属于 MVVM;传统后端框架(如 Rails)多用 MVC。


7. 浏览器缓存:强缓存的状态码是多少?

强缓存不会发送请求到服务器,因此没有状态码

  • 强缓存命中时,浏览器直接从内存/磁盘读取资源,Network 面板显示 (memory cache)(disk cache),状态码通常显示为 200 (from memory cache) —— 但这不是 HTTP 状态码,而是 DevTools 的提示。

> 真正的 HTTP 状态码只在请求发出时才有。强缓存阶段根本没发请求!

强缓存控制字段

  • Cache-Control: max-age=3600
  • Expires: Wed, 05 Dec 2025 15:00:00 GMT

> 若缓存失效,则进入协商缓存(会发请求,返回 304 Not Modified200)。


8. 存储位置?

你可能想问的是 Service Worker CacheHTTP 缓存存储位置

  • HTTP 强缓存/协商缓存:存储在浏览器的 Disk Cache / Memory Cache 中(开发者工具 Network 可见)。
  • Service Worker Cache:通过 caches.open(name).put() 存储,属于 Cache Storage,独立于 HTTP 缓存。
  • 其他存储
    • localStorage / sessionStorage:键值对,持久化
    • IndexedDB:客户端数据库
    • Cookie:随请求自动发送

9. 图片预加载问题

> 场景:预加载图片 A(new Image().src = A),但正式请求 A 已发出。此时预加载还有效吗?

答案:有效,且能提升性能

原理

  • 浏览器对同一 URL 的资源有全局缓存机制(包括内存缓存、磁盘缓存)。
  • 预加载时,图片开始下载并存入缓存。
  • 当正式 <img src="A"> 请求发出时:
    • 若预加载已完成 → 直接从缓存读取(毫秒级);
    • 若预加载正在进行 → 浏览器会复用同一个 TCP 连接和下载流(Chrome 等现代浏览器支持);
    • 即使正式请求先发,预加载后发,只要 URL 相同,最终都会共享缓存。

> 例外:若预加载和正式请求的 headers 不同(如 CORS、Referer),可能被视为不同资源,无法共享。

结论:预加载在绝大多数情况下有效,是常用的性能优化手段。