[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