[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`(预检请求的缓存时间)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直线、矩形、多边形
- Part2-曲线图形
- Part3-线条操作
- Part4-文本操作
- Part5-图像操作
- Part6-变形操作
- Part7-像素操作
- Part8-渐变与阴影
- Part9-路径与状态
- Part10-物理动画
- Part11-边界检测
- Part12-碰撞检测
- Part13-用户交互
- Part14-高级动画
- CSS
- SCSS
- codePen
- 速查表
- 面试题
- 《CSS Secrets》
- SVG
- 移动端适配
- 滤镜(filter)的使用
- JS
- 基础概念
- 作用域、作用域链、闭包
- this
- 原型与继承
- 数组、字符串、Map、Set方法整理
- 垃圾回收机制
- DOM
- BOM
- 事件循环
- 严格模式
- 正则表达式
- ES6部分
- 设计模式
- AJAX
- 模块化
- 读冴羽博客笔记
- 第一部分总结-深入JS系列
- 第二部分总结-专题系列
- 第三部分总结-ES6系列
- 网络请求中的数据类型
- 事件
- 表单
- 函数式编程
- Tips
- JS-Coding
- Framework
- Vue
- 书写规范
- 基础
- vue-router & vuex
- 深入浅出 Vue
- 响应式原理及其他
- new Vue 发生了什么
- 组件化
- 编译流程
- Vue Router
- Vuex
- 前端路由的简单实现
- React
- 基础
- 书写规范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 与 Hook
- 《深入浅出React和Redux》笔记
- 前半部分
- 后半部分
- react-transition-group
- Vue 与 React 的对比
- 工程化与架构
- Hybird
- React Native
- 新手上路
- 内置组件
- 常用插件
- 问题记录
- Echarts
- 基础
- Electron
- 序言
- 配置 Electron 开发环境 & 基础概念
- React + TypeScript 仿 Antd
- TypeScript 基础
- React + ts
- 样式设计
- 组件测试
- 图标解决方案
- Storybook 的使用
- Input 组件
- 在线 mock server
- 打包与发布
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- 2025面试题收集
- Other
- markdown 书写
- Git
- LaTex 常用命令
- Bugs