💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
[TOC] ## 场景题 ### 1.js 实现一个带并发限制的异步调度器 题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个。完善代码中 Scheduler 类,使得以下程序能正确输出 ``` class Scheduler { add(promiseCreator) { ... } // ... } const timeout = (time) => new Promise(resolve => { setTimeout(resolve, time) }) const scheduler = new Scheduler() const addTask = (time, order) => { scheduler.add(() => timeout(time)).then(() => console.log(order)) } addTask(1000, '1') addTask(500, '2') addTask(300, '3') addTask(400, '4') // output: 2 3 1 4// 一开始,1、2两个任务进入队列// 500ms时,2完成,输出2,任务3进队// 800ms时,3完成,输出3,任务4进队// 1000ms时,1完成,输出1// 1200ms时,4完成,输出4 ``` 答案 ``` class Scheduler { this.queue = [] this.running = 0 this.MAX_RUNNING = 2 add(promiseCreator) { return new Promise(resolve => { this.queue.push({ promiseCreator, resolve }) this.schedule() }) } schedule() { while (this.queue.length !== 0 && this.running < this.MAX_RUNNING) { const currTask = queue.shift() this.running += 1 currTask.promiseCreator().then(result => { currTask.resolve(result) this.running -= 1 this.schedule() }) } } } const timeout = (time) => new Promise(resolve => { setTimeout(resolve, time) }) const scheduler = new Scheduler() const addTask = (time, order) => { scheduler.add(() => timeout(time)).then(() => console.log(order)) } ``` ### 2. js 实现版本号排序 在 JavaScript 中,版本号排序是一个常见的需求。版本号通常由多个数字和点号(`.`)组成,例如`1.2.3`或`2.10.1`。由于版本号是字符串形式,直接使用字符串排序会导致错误的结果(例如`1.10`会被排在`1.2`前面)。因此,我们需要将版本号拆分为数字部分,然后逐级比较。 ``` function compareVersions(v1, v2) { // 将版本号拆分为数字数组 const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); // 获取最大长度 const maxLength = Math.max(parts1.length, parts2.length); // 逐级比较 for (let i = 0; i < maxLength; i++) { const num1 = parts1[i] || 0; // 如果部分不存在,默认为 0 const num2 = parts2[i] || 0; if (num1 > num2) return 1; // v1 > v2 if (num1 < num2) return -1; // v1 < v2 } return 0; // 版本号相等 } function sortVersions(versions) { return versions.sort(compareVersions); } // 示例 const versions = ['1.10.0', '2.0.0', '1.2.3', '1.0.0', '2.1.0', '1.5.0']; const sortedVersions = sortVersions(versions); console.log(sortedVersions); // 输出: ['1.0.0', '1.2.3', '1.5.0', '1.10.0', '2.0.0', '2.1.0'] ``` ### 3. 模拟实现 lodash 中的 get 函数 ``` function get(obj, path, defaultValue) { // 将路径字符串转换为数组(支持 'a.b.c' 或 ['a', 'b', 'c']) const keys = Array.isArray(path) ? path : path.split('.'); // 遍历路径 let result = obj; for (const key of keys) { if (result && typeof result === 'object' && key in result) { result = result[key]; // 继续深入 } else { return defaultValue; // 路径中断,返回默认值 } } return result !== undefined ? result : defaultValue; } // 示例 const obj = { a: { b: { c: 42, }, }, }; console.log(get(obj, 'a.b.c')); // 42 console.log(get(obj, 'a.b.d', 'default')); // 'default' console.log(get(obj, ['a', 'b', 'c'])); // 42 console.log(get(obj, 'x.y.z', 'not found')); // 'not found' ``` ### 4. 模拟实现 Vue 的发布订阅,Dep、Watcher * **Dep 类**:负责收集依赖(Watcher 实例),并在数据变化时通知这些依赖。 * **Watcher 类**:负责观察数据的变化,并在数据变化时执行回调函数。 * **Vue 类**:负责初始化数据,并将数据转换为响应式。 ``` class Vue { constructor(options) { this.$data = options.data; this.observe(this.$data); } // 将数据转换为响应式 observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]); this.proxyData(key); // 将数据代理到 Vue 实例上 }); } // 定义响应式属性 defineReactive(data, key, val) { const dep = new Dep(); // 每个属性都有一个 Dep 实例 Object.defineProperty(data, key, { get() { if (Dep.target) { dep.depend(); // 收集依赖 } return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); // 通知依赖更新 } }); } // 将数据代理到 Vue 实例上 proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key]; }, set(newVal) { this.$data[key] = newVal; } }); } } class Dep { constructor() { this.subscribers = []; // 存储所有的 Watcher 实例 } // 添加依赖 depend() { if (Dep.target && !this.subscribers.includes(Dep.target)) { this.subscribers.push(Dep.target); } } // 通知所有依赖更新 notify() { this.subscribers.forEach(sub => sub.update()); } } Dep.target = null; // 全局变量,用于存储当前的 Watcher 实例 class Watcher { constructor(vm, key, cb) { this.vm = vm; // Vue 实例 this.key = key; // 监听的属性 this.cb = cb; // 回调函数 Dep.target = this; // 将当前 Watcher 实例设置为全局的 Dep.target this.value = this.vm[this.key]; // 触发 getter,收集依赖 Dep.target = null; // 重置 Dep.target } // 更新视图 update() { const newValue = this.vm[this.key]; if (newValue !== this.value) { this.value = newValue; this.cb(newValue); } } } ``` ### 5. js 实现大数相加 ~~~javaScript function addBigNumbers(num1, num2) { // 若字符串位数不一致,则短的补零 const num1_length = num1.length const num2_length = num2.length const maxLength = Math.max(num1_length, num2_length) if (num1_length < maxLength) { num1.padStart(maxLength, '0') } if (num2_length < maxLength) { num2.padStart(maxLength, '0') } // 从后往前计算;存储上一步进位结果 let curry = 0 let res = '' for (let i = maxLength - 1; i >= 0; i--) { const number_num1 = Number(num1[i]) const number_num2 = Number(num2[i]) const sum = (curry + number_num1 + number_num2) % 10 curry = Math.floor((number_num1 + number_num2) / 10) res = sum + res } // 若最后仍有进位,补充到最前面 if (curry) { res = String(curry) + res } return res } const num1 = "123456789012345678901234567890"; const num2 = "987654321098765432109876543210"; console.log(addBigNumbers(num1, num2)); // 输出: 1111111110111111111011111111100 ~~~ ### 6. js 模拟实现 LRU 缓存 LRU(Least Recently Used)缓存是一种常见的缓存淘汰策略,当缓存达到容量上限时,会优先移除最近最少使用的数据。 实现思路: 1. 使用`Map`来存储缓存项,因为`Map`可以保持插入顺序。 2. 当访问一个缓存项时,将其移到最前面(表示最近使用)。 3. 当缓存达到容量上限时,移除最久未使用的项(即`Map`中的第一项)。 ~~~ class LRUCache { constructor(capacity) { this.capacity = capacity; // 缓存容量 this.cache = new Map(); // 使用Map存储缓存 } // 获取缓存项 get(key) { if (!this.cache.has(key)) { return -1; // 如果缓存中不存在,返回-1 } const value = this.cache.get(key); this.cache.delete(key); // 删除旧的键值对 this.cache.set(key, value); // 将该项移到Map的末尾(表示最近使用) return value; } // 添加缓存项 put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); // 如果已存在,先删除 } this.cache.set(key, value); // 添加新的键值对 if (this.cache.size > this.capacity) { // 如果超出容量,移除最久未使用的项(Map中的第一个键) const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } } // 示例用法 const cache = new LRUCache(2); // 创建一个容量为2的LRU缓存 cache.put(1, 1); // 缓存为 {1=1} cache.put(2, 2); // 缓存为 {1=1, 2=2} console.log(cache.get(1)); // 返回 1,缓存为 {2=2, 1=1} cache.put(3, 3); // 移除键2,缓存为 {1=1, 3=3} console.log(cache.get(2)); // 返回 -1(未找到) cache.put(4, 4); // 移除键1,缓存为 {3=3, 4=4} console.log(cache.get(1)); // 返回 -1(未找到) console.log(cache.get(3)); // 返回 3,缓存为 {4=4, 3=3} console.log(cache.get(4)); // 返回 4,缓存为 {3=3, 4=4} ~~~ ### 7. js模拟实现请求错误超时重试并控制重试最大次数和最大超时时间 ``` /** * 带重试和超时控制的 fetch 请求 * @param {string} url - 请求的 URL * @param {Object} options - 请求选项(如 method、headers 等) * @param {number} maxRetries - 最大重试次数(默认 3 次) * @param {number} maxTimeout - 最大超时时间(默认 5000 毫秒) * @returns {Promise} - 返回一个 Promise,成功时解析为响应,失败时拒绝为错误 */ function fetchWithRetry(url, options = {}, maxRetries = 3, maxTimeout = 5000) { let retries = 0; const controller = new AbortController(); // AbortController API 用于控制是否放弃 fetch 请求 const timeoutId = setTimeout(() => { controller.abort(); throw new Error('请求超时') }, maxTimeout); // 递归函数,用于实现重试逻辑 const attemptFetch = async () => { try { const response = await fetch(url, { ...options, signal: controller.signal, // 注意这里如何传参控制的放弃请求 }); clearTimeout(timeoutId); // 清除超时计时器 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response; // 请求成功,返回响应 } catch (error) { clearTimeout(timeoutId); // 清除超时计时器 if (retries < maxRetries) { retries++; // const timeout = initialTimeout \* Math.pow(2, retries); // 可通过指数退避缓解服务器压力 console.log(`请求失败,正在重试 (${retries}/${maxRetries})...`); return attemptFetch(); // 递归重试 } else { throw new Error(`请求失败,重试次数已用完: ${error.message}`); } } }; return attemptFetch(); } // 使用示例 fetchWithRetry('https://api.example.com/data', { method: 'GET' }, 3, 5000) .then(response => response.json()) .then(data => console.log('请求成功:', data)) .catch(error => console.error('请求失败:', error.message)); ``` ### 8. 模拟实现图片懒加载 实现思路 1. **占位符**: * 使用`data-src`属性存储图片的真实 URL,而不是直接使用`src`属性。 * 初始时,`src`属性可以设置为一个占位符(如 1x1 的透明图片)。 2. **检测图片是否进入可视区域**: * 使用`IntersectionObserver`API 监听图片是否进入可视区域。 * 如果图片进入可视区域,将`data-src`的值赋给`src`,触发图片加载。 3. **兼容性**: * 对于不支持`IntersectionObserver`的浏览器,可以回退到监听`scroll`事件。 ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>图片懒加载</title> <style> img { width: 100%; height: 300px; background: #f0f0f0; display: block; margin-bottom: 20px; } </style> </head> <body> <div> <img data-src="https://picsum.photos/600/300?random=1" alt="Image 1"> <img data-src="https://picsum.photos/600/300?random=2" alt="Image 2"> <img data-src="https://picsum.photos/600/300?random=3" alt="Image 3"> <img data-src="https://picsum.photos/600/300?random=4" alt="Image 4"> <img data-src="https://picsum.photos/600/300?random=5" alt="Image 5"> </div> <script> // 获取所有需要懒加载的图片 const images = document.querySelectorAll('img[data-src]'); // 若支持 IntersectionObserver API if (window.IntersectionObserver) { // 配置 IntersectionObserver const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 如果图片进入可视区域 const img = entry.target; img.src = img.dataset.src; // 将 data-src 的值赋给 src img.removeAttribute('data-src'); // 移除 data-src 属性 observer.unobserve(img); // 停止观察该图片 } }); }, { rootMargin: '0px', // 视口的边距 threshold: 0.1, // 当图片 10% 进入视口时触发 }); // 开始观察所有图片 images.forEach(img => observer.observe(img)); } else { // 检查图片是否进入可视区域 const lazyLoad = () => { images.forEach(img => { const rect = img.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom >= 0) { img.src = img.dataset.src; // 将 data-src 的值赋给 src img.removeAttribute('data-src'); // 移除 data-src 属性 } }); }; // 初始加载可视区域内的图片 lazyLoad(); // 监听滚动事件 window.addEventListener('scroll', lazyLoad); window.addEventListener('resize', lazyLoad); } </script> </body> </html> ``` ### 9.模拟实现虚拟列表 虚拟列表(Virtual List)是一种优化长列表渲染性能的技术。它通过只渲染当前可见区域的内容,而不是渲染整个列表,从而减少 DOM 节点的数量,提升性能。以下是使用 JavaScript 模拟实现虚拟列表的详细步骤和代码示例。 ~~~javascript // 初始化数据 const itemCount = 1000; // 列表项总数 const itemHeight = 50; // 每个列表项的高度 const container = document.getElementById('container'); const content = document.getElementById('content'); // 设置内容区域的总高度 content.style.height = `${itemCount * itemHeight}px`; // 渲染可见区域的列表项 function renderVisibleItems() { const scrollTop = container.scrollTop; // 获取滚动位置 const startIndex = Math.floor(scrollTop / itemHeight); // 计算起始索引 const endIndex = Math.min(startIndex + Math.ceil(container.clientHeight / itemHeight), itemCount - 1); // 计算结束索引 // 清空当前内容 content.innerHTML = ''; // 渲染可见区域的列表项 for (let i = startIndex; i <= endIndex; i++) { const item = document.createElement('div'); item.style.position = 'absolute'; item.style.top = `${i * itemHeight}px`; item.style.height = `${itemHeight}px`; item.style.width = '100%'; item.style.backgroundColor = i % 2 === 0 ? '#f0f0f0' : '#ffffff'; item.textContent = `Item ${i + 1}`; content.appendChild(item); } } // 监听滚动事件 container.addEventListener('scroll', renderVisibleItems); // 初始化渲染 renderVisibleItems(); ~~~ ### 10. 实现一个可以控制超时时间和最大重试次数的 Promise 函数,适用于网络请求或其他异步操作(考察 promise.race) ~~~ /** * 创建支持超时和重试的异步函数 * @param {Function} fn - 需要执行的异步函数 * @param {Object} options - 配置选项 * @param {number} [options.timeout=5000] - 超时时间(ms) * @param {number} [options.maxRetries=3] - 最大重试次数 * @returns {Promise} - 返回包装后的 Promise */ function retryWithTimeout(fn, options = {}) { const { timeout = 5000, maxRetries = 3 } = options; let retries = 0; // 创建带有超时控制的 Promise function attempt() { let timeoutId; // 超时控制 Promise const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Timeout after ${timeout}ms`)); }, timeout); }); // 执行原始 Promise const executionPromise = fn(); return Promise.race([executionPromise, timeoutPromise]) .finally(() => clearTimeout(timeoutId)) .catch(error => { if (retries >= maxRetries) { error.message = `Max retries exceeded (${maxRetries}): ${error.message}`; throw error; } retries++; return attempt(); }); } return attempt(); } // 使用示例 const fakeAPI = () => new Promise((resolve, reject) => { // 模拟 50% 成功率 Math.random() > 0.5 ? setTimeout(resolve, 3000, 'Data fetched!') : setTimeout(reject, 2000, new Error('API failed')); }); // 测试调用 retryWithTimeout(fakeAPI, { timeout: 2500, maxRetries: 2 }) .then(console.log) .catch(err => console.error('Final error:', err.message)); // 可能输出: // 成功情况: "Data fetched!" // 失败情况: "Final error: Max retries exceeded (2): Timeout after 2500ms" // 或 "Final error: Max retries exceeded (2): API failed" ~~~ ## 问答题 ### 1. 简述 react fiber 原理 **1\. 问题背景** 在 React 16 之前,React 使用**栈调和算法**(Stack Reconciler)来协调虚拟 DOM 的变化。这种算法是**同步**的,一旦开始渲染,就会一直占用主线程,直到整个组件树渲染完成。对于大型应用或复杂组件树,这会导致主线程阻塞,用户交互(如动画、输入)无法及时响应,造成卡顿。 * * * **2\. Fiber 的核心思想** Fiber 通过以下方式解决上述问题: 1. **任务拆分**:将渲染任务拆分为多个小任务(Fiber 节点)。 2. **可中断**:允许 React 在执行过程中中断渲染,优先处理高优先级任务(如用户交互)。 3. **优先级调度**:根据任务的优先级动态调整执行顺序。 4. **增量渲染**:将渲染过程分成多个帧(Frame),避免长时间占用主线程。 * * * **3\. Fiber 的数据结构** Fiber 是一个**链表结构**,每个 Fiber 节点对应一个组件或 DOM 节点。Fiber 节点包含以下关键信息: * **类型信息**:组件类型(函数组件、类组件、DOM 节点等)。 * **状态信息**:组件的 props、state、hooks 等。 * **链表指针**: * `child`:指向第一个子节点。 * `sibling`:指向下一个兄弟节点。 * `return`:指向父节点。 * **工作状态**:当前节点的渲染状态(如是否已完成)。 * **优先级**:任务的优先级(如高优先级的用户交互)。 * * * **4\. Fiber 的工作流程** Fiber 的渲染过程分为两个阶段: 1. **Render Phase(渲染阶段)**: * 遍历组件树,生成 Fiber 树。 * 对比新旧 Fiber 树,标记需要更新的节点(Diff 算法)。 * 此阶段是**可中断**的,React 可以根据优先级暂停或恢复工作。 2. **Commit Phase(提交阶段)**: * 将渲染结果应用到真实 DOM。 * 此阶段是**不可中断**的,确保 DOM 更新的完整性。 * * * **5\. 优先级调度** Fiber 引入了**优先级调度机制**,React 会根据任务的优先级动态调整执行顺序。例如: * **高优先级任务**:用户交互(如点击、输入)会立即处理。 * **低优先级任务**:数据更新、渲染等可以延迟执行。 React 根据任务的类型和上下文动态确定优先级。以下是常见的任务优先级分配规则: (1)**用户交互任务** * **优先级**:`Immediate`或`UserBlocking` * **示例**: * 点击事件、输入框输入、按钮点击等。 * React 会优先处理这些任务,以确保用户体验的流畅性。 (2)**动画更新** * **优先级**:`UserBlocking` * **示例**: * CSS 动画、过渡效果等。 * React 会确保动画帧率稳定,避免卡顿。 (3)**数据更新** * **优先级**:`Normal` * **示例**: * `setState`、`useState`触发的状态更新。 * 数据获取后的 UI 渲染。 (4)**后台任务** * **优先级**:`Low`或`Idle` * **示例**: * 数据预加载、日志记录、非关键渲染等。 * 这些任务会在浏览器空闲时执行,避免阻塞高优先级任务。 ~~~ import { useState, useEffect } from 'react'; function App() { const [count, setCount] = useState(0); // 高优先级任务:用户点击 const handleClick = () => { setCount((prev) => prev + 1); }; // 低优先级任务:数据预加载 useEffect(() => { const loadData = async () => { // 模拟数据加载 await new Promise((resolve) => setTimeout(resolve, 1000)); console.log('Data loaded'); }; loadData(); }, []); return ( <div> <button onClick={handleClick}>Click me ({count})</button> </div> ); } export default App; ~~~ React 使用浏览器的`requestIdleCallback`API(或 polyfill)在空闲时间执行低优先级任务,避免阻塞主线程。 * * * **6\. 增量渲染** Fiber 将渲染任务拆分为多个小任务,并在每一帧中执行一部分任务。这样可以将渲染工作分摊到多个帧中,避免长时间占用主线程,保证动画和用户交互的流畅性。 ### 2. 简要描述ES6 module require、exports以及module.exports的区别 ES6 模块是 JavaScript 的官方模块系统,支持静态加载(在编译时确定依赖关系)。 **特点** * 静态加载:依赖关系在编译时确定。 * 支持异步加载(通过`import()`动态导入)。 * 浏览器和现代 Node.js 环境原生支持。 CommonJS 是 Node.js 的默认模块系统,支持动态加载(在运行时确定依赖关系)。 **特点** * 动态加载:依赖关系在运行时确定。 * 适用于 Node.js 环境。 * `exports`是`module.exports`的引用,直接覆盖`exports`会断开引用。 ### 3. vue 的生命周期,各个生命周期做了什么操作? 1.**`beforeCreate`** * **时机**:在实例初始化之后,数据观测(data observation)和事件/侦听器配置之前调用。 * **作用**:此时组件的`data`、`methods`、`computed`等选项还未初始化,通常用于一些与组件状态无关的初始化操作。 * * * 2.**`created`** * **时机**:在实例创建完成后调用,此时已完成数据观测、属性和方法的运算,但尚未挂载到 DOM。 * **作用**:可以访问`data`、`methods`、`computed`等,适合执行一些异步请求或初始化数据。 * * * 3.**`beforeMount`** * **时机**:在挂载开始之前调用,此时模板已编译,但尚未将组件渲染到 DOM 中。 * **作用**:适合在 DOM 挂载前执行一些操作,但很少使用。 * * * 4.**`mounted`** * **时机**:在组件挂载到 DOM 后调用,此时可以访问 DOM 元素。 * **作用**:适合执行 DOM 操作、初始化第三方库或发送异步请求。 * * * 5.**`beforeUpdate`** * **时机**:在数据变化导致 DOM 重新渲染之前调用。 * **作用**:可以在 DOM 更新前访问当前状态,适合执行一些与更新相关的逻辑。 * * * 6.**`updated`** * **时机**:在数据变化导致 DOM 重新渲染之后调用。 * **作用**:适合在 DOM 更新后执行操作,但需要注意避免在此钩子中修改状态,否则可能导致无限更新循环。 * * * 7.**`beforeUnmount`** * **时机**:在组件实例卸载之前调用(Vue 2 中为`beforeDestroy`)。 * **作用**:适合执行清理操作,如清除定时器、取消事件监听或销毁第三方库实例。 * * * 8.**`unmounted`** * **时机**:在组件实例卸载之后调用(Vue 2 中为`destroyed`)。 * **作用**:适合执行最终的清理操作,此时组件已从 DOM 中移除。 ### 4. Vue3 与 Vue2 的差别 Vue 3 在性能、开发体验和功能上都有显著提升,主要改进包括: 1. 更高效的响应式系统(基于`Proxy`)。 2. Composition API 提供更灵活的代码组织方式。(setup 语法糖替代之前的 data、methods、computed 选项)。支持了自定义 hooks,将可复用的逻辑提取到单独的函数中。 3. 更好的 TypeScript 支持。 4. 新特性如`Teleport`、`Suspense`等。`<suspense>`组件,用于处理异步组件的加载状态,提供 fallback 内容。`<teleport>`组件,可以将组件渲染到 DOM 中的任意位置,常用于模态框、弹窗等场景。 5. 更小的包体积和更快的渲染速度 ### 5. 简述 vue 的虚拟 dom 及 diff 算法的作用与实现原理 虚拟 DOM 是一个轻量级的 JavaScript 对象,用于描述真实 DOM 的结构。它的主要作用是减少直接操作真实 DOM 的开销,通过对比新旧虚拟 DOM 的差异,最小化 DOM 操作。 #### **虚拟 DOM 的结构** 虚拟 DOM 是一个树形结构,每个节点(VNode)包含以下属性: * `tag`:节点标签名(如`div`、`span`)。 * `props`:节点的属性(如`class`、`style`)。 * `children`:子节点(可以是文本、其他 VNode 或组件)。 * `key`:节点的唯一标识,用于 Diff 算法优化。 ``` const vnode = { tag: 'div', props: { class: 'container' }, children: [ { tag: 'p', props: {}, children: 'Hello World' }, { tag: 'button', props: { onClick: handleClick }, children: 'Click Me' }, ], }; ``` ### **Diff 算法** Diff 算法用于比较新旧虚拟 DOM 的差异,并计算出最小的 DOM 更新操作。Vue 的 Diff 算法基于以下策略: #### **Diff 算法的核心思想** 1. **同级比较**:只比较同一层级的节点,不跨层级比较。 2. **Key 的作用**:通过`key`标识节点的唯一性,避免不必要的节点销毁和重建。 3. **最小化操作**:尽量复用节点,只更新变化的属性或子节点。 #### **Diff 算法的具体实现** 1. **节点类型不同**: * 如果新旧节点的`tag`不同,直接销毁旧节点,创建新节点。 2. **节点类型相同**: * 如果`tag`相同,比较节点的`props`和`children`。 * 更新`props`:遍历新旧`props`,更新变化的属性。 * 更新`children`:递归比较子节点。 3. **子节点比较**: * Vue 使用**双端比较算法**(头尾指针法)优化子节点的比较: * 初始化四个指针:`oldStart`、`oldEnd`、`newStart`、`newEnd`。 * 比较`oldStart`和`newStart`、`oldEnd`和`newEnd`、`oldStart`和`newEnd`、`oldEnd`和`newStart`。 * 如果找到可复用的节点,移动指针并更新节点。 * 如果未找到可复用的节点,根据`key`查找旧节点中是否存在可复用的节点。 * 如果旧节点遍历完毕,剩余的新节点需要创建;如果新节点遍历完毕,剩余的旧节点需要销毁。 ### 6. typescript 考点 1. 常用 ts 语法 2. interface 和 type 的区别 * interface 可以被类实现(`implements`)而 type 不行。 * interface 可以被其他接口扩展(`extends`) 而 type 不行。 ``` interface Animal { name: string; } interface Dog extends Animal { bark(): void; } class Labrador implements Dog { name = 'Rex'; bark() { console.log('Woof!'); } } ``` (1) 使用`interface`的场景 * 定义对象的形状。 * 需要扩展或实现接口。 * 需要声明合并。 (2) 使用`type`的场景 * 定义联合类型、交叉类型、元组等复杂类型。 * 定义函数类型、字面量类型。 * 需要定义一次性使用的类型。 ~~~javaScript // 定义类型别名 type StringOrNumber = string | number; let value: StringOrNumber; value = "Hello"; // 合法 value = 42; // 合法 value = true; // 错误:boolean 不是 StringOrNumber 类型 // 定义联合类型 type Status = "active" | "inactive" | "pending"; let userStatus: Status; userStatus = "active"; // 合法 userStatus = "deleted"; // 错误:不在联合类型中 // 定义交叉类型 type Person = { name: string; }; type Employee = { id: number; role: string; }; type EmployeePerson = Person & Employee; const employee: EmployeePerson = { name: "Bob", id: 123, role: "Developer", }; // 定义复杂类型 type Nullable<T> = T | null; type StringOrNumberArray = (string | number)[]; type RecursiveType = { value: string; children?: RecursiveType[]; // 递归类型 }; ~~~ 3. 装饰器语法有哪些用途 **类装饰器**:修饰整个类。类装饰器接收类的构造函数作为参数,并可以返回一个新的构造函数或修改原始构造函数。 ~~~javascript function logClass(target) { console.log('Class is decorated:', target.name); // 可以在这里扩展类的功能 target.prototype.newMethod = function () { console.log('This is a new method added by decorator'); }; } @logClass class MyClass { constructor() { console.log('MyClass instance created'); } } const instance = new MyClass(); instance.newMethod(); // 输出: This is a new method added by decorator ~~~ **方法装饰器**:修饰类的方法。 方法装饰器接收三个参数:目标类的原型(如果是静态方法,则是类的构造函数),方法名称,方法的属性描述符(`descriptor`)。 ~~~javascript function logMethod(target, name, descriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args) { console.log(`Calling method ${name} with arguments:`, args); const result = originalMethod.apply(this, args); console.log(`Method ${name} returned:`, result); return result; }; return descriptor; } class MyClass { @logMethod add(a, b) { return a + b; } } const instance = new MyClass(); instance.add(2, 3); // 输出: Calling method add with arguments: [2, 3] // 输出: Method add returned: 5 ~~~ 属性装饰器 访问器装饰器 实际应用? [看这](https://jkchao.github.io/typescript-book-chinese/tips/metadata.html#controller-%E4%B8%8E-get-%E7%9A%84%E5%AE%9E%E7%8E%B0) [node框架 - ts](https://nestjs.com/) 4. 用 Pick 实现 Omit ``` type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; // 使用 type PersonWithoutAddress = MyOmit<Person, 'address'>; ``` ### 7. React 考点 **`useEffect`和`useLayoutEffect`的区别是什么?** * **回答**: * **`useEffect`**: * 在浏览器完成渲染后异步执行。 * 适合大多数副作用操作(如数据获取、订阅等)。 * **`useLayoutEffect`**: * 在浏览器完成渲染前同步执行。 * 适合需要同步更新 DOM 的场景(如测量 DOM 元素尺寸)。 * 使用不当可能导致性能问题。 **`useMemo`和`useCallback`的作用是什么?有什么区别?** **回答**:`useMemo`缓存值,`useCallback`缓存函数。 `useMemo`: * 用于缓存计算结果,避免不必要的重复计算。 * 示例: ``` const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); ``` **`useCallback`**: * 用于缓存函数,避免不必要的函数重建。 * 示例:例如用 useCallback 对 lodash debounce 的函数做处理 ``` const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); ``` **Hooks 的性能优化有哪些方法?** * **回答**: 1. 使用`React.memo`避免不必要的组件渲染。 2. 使用`useMemo`缓存计算结果。 3. 使用`useCallback`缓存函数。 4. 避免在`useEffect`中执行昂贵的操作。 5. 使用`useReducer`管理复杂状态,减少不必要的状态更新。 **React Context** React Context 本质上使用的是观察者模式,当我们在组件中调用状态管理库/React Context 暴露出的 hooks 时这个组件相当于订阅了状态管理库维护的 state 的某一变量。当 state 更新时会通知其订阅者 re-render 但是使用 React Context 目前存在一个性能问题:如下所示我们创建了一个 React Context,包含了 count1 和 count2 的数据,当我们更新 count1 的数据时发现 Count2 组件也触发了 re-render,即使 Count2 未使用到 count1 数据。这是因为 React 对比 context 前后状态是否不一致后,若不一致会沿着当前节点遍历 Fiber 树来寻找消费了当前 context 的组件,并且对其进行标记代表这个组件应该被重新渲染。这就导致渲染到这个组件时,其触发了 re-render ~~~JavaScript const context = createContext(null); const Count1 = () => { const { count1, setCount1 } = useContext(context); console.log("Count1 render"); return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>; }; const Count2 = () => { const { count2 } = useContext(context); console.log("Count2 render"); return <div>count2: {count2}</div>; }; const StateProvider = ({ children }) => { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); return ( <context.Provider value={{ count1, count2, setCount1, setCount2 }} > {children} </context.Provider> ); }; const App = () => ( <StateProvider> <Count1 /> <Count2 /> </StateProvider> ); ~~~ ### 8.Map 和对象的差别?WeakMap 和垃圾回收 **Map 和 Object 的区别** 键的类型 * **Map**: * 键可以是任意类型(包括对象、函数、基本类型等)。 * 例如:`map.set({}, 'value')`或`map.set(1, 'value')`。 * **Object**: * 键只能是字符串或 Symbol。 * 如果使用非字符串键(如对象或数字),会被自动转换为字符串。 * 例如:`obj[1]`和`obj['1']`是等价的。 键的顺序 * **Map**: * 键值对的顺序是插入顺序。 * 遍历时,键值对会按照插入的顺序返回。 * **Object**: * 在 ES6 之前,对象的键顺序是不确定的。 * 在 ES6 之后,普通对象的键顺序遵循以下规则: 1. 数字键按升序排列。 2. 字符串键按插入顺序排列。 3. Symbol 键按插入顺序排列。 WeakMap 和 Map 的区别 键的类型 * **WeakMap**: * 键必须是对象(不能是基本类型)。 * 例如:`weakMap.set({}, 'value')`。 * **Map**: * 键可以是任意类型。 弱引用 * **WeakMap**: * 对键是弱引用的。如果键对象没有被其他地方引用,它会被垃圾回收机制回收,即使它仍然存在于`WeakMap`中。 * 适合存储与对象关联的元数据,而不会阻止对象的垃圾回收。 * **Map**: * 对键是强引用的。即使键对象没有被其他地方引用,它也不会被垃圾回收。 可枚举性 * **WeakMap**: * 不可枚举。没有方法可以遍历键或值(如`keys()`、`values()`、`entries()`)。 * 没有`size`属性。 * **Map**: * 可枚举。支持遍历键、值和键值对。 * 有`size`属性。 使用场景 * **WeakMap**: * 适合存储与对象关联的私有数据或元数据。 * 例如:缓存与 DOM 元素关联的数据。 * **Map**: * 适合存储需要长期保存的键值对。 ### 9.简述 formily 表单框架原理 ① 精确渲染:setState - 每次输入某一表单字段全量渲染,formily 通过依赖收集机制可以实现精确渲染改动的表单字段 ② 领域模型:formily 内核提供 Field 组件(JSX 写法),component 属性表示字段所对应的 UI 组件和 UI 组件属性,reactions 属性可以用于监听其他表单字段的变动并添加对应的副作用操作,validator 属性可用于控制该字段的校验逻辑 ③ 路径系统:提供 form 对象作为顶层模型管理所有字段模型;Formily 提供了一些路径操作方法:简化了复杂表单状态的管理,使开发者能更灵活地操作表单数据。 * **get**: 获取路径对应的值。 * **set**: 设置路径对应的值。 * **exist**: 检查路径是否存在。 * **transform**: 对路径对应的值进行转换。 ④ 生命周期:对外暴露生命周期钩子,例如表单初始化,表单提交 ⑤ 协议驱动:如何实现配置化的表单?通过 JSON-SCHEMA 协议来渲染表单[JSON SCHEMA](https://json-schema.org/) 一个简单的 JSON Schema 示例: ~~~javaScript { "type": "object", "properties": { "name": { "type": "string", "title": "姓名", "minLength": 2, "maxLength": 10 }, "age": { "type": "number", "title": "年龄", "minimum": 0, "maximum": 120 } }, "required": ["name"] } ~~~ * **type**: 定义数据类型,如`object`、`string`、`number`、`array`等。 * **properties**: 定义对象的属性。 * **required**: 定义必填字段。 * **title**: 字段的标题(通常用于表单的标签)。 * **minLength**/**maxLength**: 字符串的最小和最大长度。 * **minimum**/**maximum**: 数字的最小和最大值。 ### 10. 如何进行页面性能打点 通过`window.performance.timing`对象访问以下关键时间点: * **navigationStart**:导航开始。 * **unloadEventStart**和**unloadEventEnd**:前一个页面卸载的开始和结束时间。 * **redirectStart**和**redirectEnd**:重定向的开始和结束时间。 * **fetchStart**:开始获取文档。 * **domainLookupStart**和**domainLookupEnd**:DNS 查询的开始和结束时间。 * **connectStart**和**connectEnd**:建立连接的开始和结束时间。 * **secureConnectionStart**:SSL 握手开始时间。 * **requestStart**和**responseStart**:请求发送和响应开始的时间。 * **responseEnd**:响应结束。 * **domLoading**:开始解析 DOM。 * **domInteractive**:DOM 解析完成,开始加载子资源。 * **domContentLoadedEventStart**和**domContentLoadedEventEnd**:DOMContentLoaded 事件的开始和结束时间。 * **domComplete**:DOM 和子资源加载完成。 * **loadEventStart**和**loadEventEnd**:load 事件的开始和结束时间。 ~~~ window.onload = function() { const timing = performance.timing; const loadTime = timing.loadEventEnd - timing.navigationStart; const domParseTime = timing.domComplete - timing.domLoading; const dnsTime = timing.domainLookupEnd - timing.domainLookupStart; const tcpTime = timing.connectEnd - timing.connectStart; const requestResponseTime = timing.responseEnd - timing.requestStart; console.log(`页面加载时间: ${loadTime}ms`); console.log(`DOM 解析时间: ${domParseTime}ms`); console.log(`DNS 查询时间: ${dnsTime}ms`); console.log(`TCP 连接时间: ${tcpTime}ms`); console.log(`请求响应时间: ${requestResponseTime}ms`); }; // 获取 FP 时间 const fp = performance.getEntriesByName('first-paint')[0].startTime; // 获取 FCP 时间 const fcp = performance.getEntriesByName('first-contentful-paint')[0].startTime; // 获取 LCP 时间 const lcpEntry = performance.getEntriesByName('largest-contentful-paint')[0]; const lcp = lcpEntry ? lcpEntry.startTime : 0; ~~~ ### 11.回流与重绘 **回流(Reflow)** * **定义**: * 当渲染树中的一部分或全部因为元素的规模、布局、隐藏等改变而需要重新构建时,浏览器会重新计算元素的位置和几何属性,这个过程称为回流。 * **触发条件**: * 添加或删除可见的 DOM 元素。 * 元素的位置、尺寸、边距、填充等几何属性发生变化。 * 页面初始化渲染。 * 浏览器窗口大小改变(resize 事件)。 * 读取某些属性(如`offsetWidth`、`offsetHeight`、`clientWidth`等),因为浏览器需要重新计算布局。 * **性能影响**: * 回流是代价较高的操作,会导致浏览器重新计算整个渲染树。 * * * **重绘(Repaint)** * **定义**: * 当元素的样式发生变化但不影响其几何属性(如颜色、背景色、可见性等)时,浏览器会重新绘制受影响的部分,这个过程称为重绘。 * **触发条件**: * 改变元素的颜色、背景色、边框颜色等样式。 * 改变元素的可见性(如`visibility`)。 * **性能影响**: * 重绘的性能开销比回流小,但频繁重绘仍会影响性能。 * * * 4.**回流与重绘的关系** * **回流一定会触发重绘**: * 当元素的几何属性发生变化时,浏览器需要重新计算布局并重新绘制。 * **重绘不一定触发回流**: * 如果只是样式变化而不影响布局,则只会触发重绘。修改字体大小或字体类型会触发回流,因为文本的布局需要重新计算: * `font-size` * `font-family` * `line-height` ### 12.Vite 构建速度为何比 webpack 快? 1.**基于原生 ES 模块的开发服务器** * **Webpack**:在开发模式下,Webpack 需要先打包整个应用,才能启动开发服务器,项目越大,启动时间越长。 * **Vite**:利用现代浏览器对原生 ES 模块的支持,Vite 直接按需提供源码,无需预先打包,启动速度极快。 2.**按需加载** * **Webpack**:即使只修改一个文件,Webpack 也可能重新打包整个应用,导致热更新较慢。 * **Vite**:通过原生 ES 模块,Vite 只编译和提供当前请求的文件,热更新时仅更新相关模块,速度更快。 3.**利用 Esbuild 进行预构建** * **Vite**:使用 Esbuild 预构建依赖项,Esbuild 是用 Go 编写的,速度远超 JavaScript 打包工具。 * **Webpack**:依赖 JavaScript 实现的打包工具,速度相对较慢。 4.**缓存机制** * **Vite**:通过强缓存减少重复构建,依赖项在初次构建后会被缓存,后续启动时直接使用缓存。 * **Webpack**:虽然也有缓存,但效果不如 Vite 显著。 5.**开发与生产环境分离** * **Vite**:开发环境利用原生 ES 模块,生产环境使用 Rollup 进行打包,兼顾开发速度和生产优化。 * **Webpack**:开发和生产环境使用相同的打包机制,无法像 Vite 那样灵活优化。 6.**现代浏览器支持** * **Vite**:面向现代浏览器,直接使用原生 ES 模块等新特性,减少兼容性处理,提升开发效率。 * **Webpack**:需要处理更多兼容性问题,增加了构建复杂性。 ### 13.什么是简单请求,什么是复杂请求?OPTIONS 预检请求何时会发送?作用? 浏览器会将请求分为**简单请求**和**复杂请求**,两者的区别主要体现在**是否需要触发预检请求(Preflight Request)** **简单请求(Simple Request)** 同时满足以下所有条件时,才是简单请求: 1. **HTTP 方法**是以下之一: * `GET` * `POST` * `HEAD` 2. **HTTP 头部**仅包含以下字段(不能有其他自定义头部): * `Accept` * `Accept-Language` * `Content-Language` * `Content-Type`(且值仅限于以下三种): * `text/plain` * `multipart/form-data` * `application/x-www-form-urlencoded` 3. 请求中不能包含自定义头部(如`X-Token`)。若使用`XMLHttpRequest`或`Fetch API`,请求不能包含事件监听器或流式操作。 **复杂请求(Complex Request)** 只要不满足简单请求的条件,就是复杂请求。对于复杂请求,浏览器会先发送一个**OPTIONS 预检请求**,确认服务器允许实际请求后,再发送真实请求。常见情况包括: 1. **使用了以下 HTTP 方法**: * `PUT` * `DELETE` * `PATCH` * `OPTIONS` 2. **设置了自定义 HTTP 头部**(如`Authorization`、`X-Custom-Header`)。 3. **`Content-Type`不是简单请求允许的值**(如`application/json`)。 4. **请求中使用了`ReadableStream`或事件监听器**。 **特点** * **会触发预检请求(Preflight)**:浏览器先发送一个`OPTIONS`请求,询问服务器是否允许实际请求。 * 服务器必须明确响应以下头部,否则实际请求会被阻止: * `Access-Control-Allow-Origin` * `Access-Control-Allow-Methods`(允许的方法,如`GET, POST, PUT`) * `Access-Control-Allow-Headers`(允许的自定义头部,如`X-Custom-Header`) * `Access-Control-Max-Age`(预检请求的缓存时间)