數組去重的方法有哪些?
在 JavaScript 中,數組去重是一個常見的操作,有多種方法可以實現這一目標。每種方法都有其適用場景和性能特點,下面將詳細介紹幾種主要的去重方法。
使用 Set 數據結構
Set 是 ES6 引入的一種新數據結構,它類似于數組,但成員的值都是唯一的,沒有重復的值。利用這一特性,可以非常簡潔地實現數組去重。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // 輸出: [1, 2, 3, 4, 5]
這種方法的優點是代碼簡潔,性能高效,時間復雜度為 O (n)。缺點是它只能對基本數據類型(如數字、字符串、布爾值)進行去重,對于引用類型(如對象、數組)無法正確去重,因為 Set 判斷元素是否重復是基于值的比較,而引用類型比較的是引用地址。
使用 filter () 方法和 indexOf ()
利用數組的 filter () 方法結合 indexOf () 可以實現去重。filter () 方法會創建一個新數組,其中包含所有通過測試的元素。對于每個元素,如果它在數組中第一次出現的位置等于當前索引,則保留該元素。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => {return arr.indexOf(item) === index;
});
console.log(uniqueArr); // 輸出: [1, 2, 3, 4, 5]
這種方法的優點是兼容性好,在舊版本的瀏覽器中也能正常工作。缺點是時間復雜度較高,為 O (n2),因為對于每個元素都需要調用 indexOf () 方法在數組中查找,當數組較大時性能較差。
使用 reduce () 方法
reduce () 方法可以對數組中的每個元素執行一個 reducer 函數,并將結果匯總為單個值。可以利用 reduce () 方法來構建一個新數組,在構建過程中檢查元素是否已經存在。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, current) => {if (!acc.includes(current)) {acc.push(current);}return acc;
}, []);
console.log(uniqueArr); // 輸出: [1, 2, 3, 4, 5]
這種方法的優點是代碼簡潔,邏輯清晰。缺點同樣是時間復雜度較高,為 O (n2),因為在每次迭代中都需要調用 includes () 方法來檢查元素是否存在。
處理對象數組去重
當數組中的元素是對象時,上述方法無法直接去重,因為對象是引用類型。此時需要根據對象的某個屬性值來進行去重。
const arr = [{ id: 1, name: 'Alice' },{ id: 2, name: 'Bob' },{ id: 1, name: 'Alice' }
];const uniqueArr = [...new Map(arr.map(item => [item.id, item])).values()];
console.log(uniqueArr);
// 輸出: [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' }
// ]
這種方法利用 Map 的鍵唯一性,將對象的某個屬性值作為鍵,對象本身作為值,從而實現根據屬性值去重。
如何判斷一個值是數組還是對象?
在 JavaScript 中,判斷一個值是數組還是對象是一個常見的需求,因為數組和普通對象在某些操作上有不同的行為。JavaScript 提供了多種方式來進行判斷,每種方式都有其優缺點和適用場景。
使用 typeof 操作符
typeof 操作符返回一個表示數據類型的字符串。然而,對于數組和普通對象,typeof 操作符返回的都是 "object",因此無法區分它們。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };console.log(typeof arr); // 輸出: "object"
console.log(typeof obj); // 輸出: "object"
由此可見,typeof 操作符不能用于區分數組和對象。
使用 instanceof 操作符
instanceof 操作符用于檢測構造函數的 prototype 屬性是否出現在某個實例對象的原型鏈上。通過判斷一個值是否是 Array 的實例,可以確定它是否為數組。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };console.log(arr instanceof Array); // 輸出: true
console.log(obj instanceof Array); // 輸出: false
然而,instanceof 操作符有一些局限性。在不同的 iframe 或 window 環境中創建的數組,可能無法通過 instanceof 正確判斷,因為它們的原型鏈不同。
使用 Array.isArray () 方法
Array.isArray () 是 ES5.1 引入的一個靜態方法,用于判斷一個值是否為數組。這是判斷數組最直接和可靠的方法。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };console.log(Array.isArray(arr)); // 輸出: true
console.log(Array.isArray(obj)); // 輸出: false
Array.isArray () 方法的優點是簡單、直接、可靠,并且能正確處理跨 iframe 或 window 環境的數組。
使用 Object.prototype.toString.call ()
每個對象都有一個內部屬性 [[Class]],它表示對象的類型。可以通過 Object.prototype.toString.call () 方法來獲取這個內部屬性的值,從而判斷對象的類型。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };console.log(Object.prototype.toString.call(arr)); // 輸出: "[object Array]"
console.log(Object.prototype.toString.call(obj)); // 輸出: "[object Object]"
這種方法的優點是非常準確,能夠區分各種類型的對象,包括內置對象和自定義對象。缺點是代碼稍微復雜一些,需要調用 toString () 方法并通過 call () 來改變 this 指向。
使用 constructor 屬性
每個對象都有一個 constructor 屬性,它指向創建該對象的構造函數。可以通過檢查 constructor 屬性來判斷一個值是否為數組。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };console.log(arr.constructor === Array); // 輸出: true
console.log(obj.constructor === Array); // 輸出: false
然而,這種方法也有局限性。如果對象的 constructor 屬性被修改,或者在不同的環境中創建的對象,可能會導致判斷不準確。
解釋 JavaScript 事件循環機制(Event Loop)
JavaScript 事件循環機制是 JavaScript 運行時環境的核心組成部分,它負責處理異步操作,使得 JavaScript 能夠在單線程的情況下處理并發任務。理解事件循環機制對于編寫高效、無阻塞的 JavaScript 代碼至關重要。
JavaScript 的單線程特性
JavaScript 是單線程的,這意味著同一時間只能執行一個任務。這種設計主要是為了避免在瀏覽器環境中操作 DOM 時出現競態條件。如果 JavaScript 是多線程的,多個線程同時修改 DOM 可能會導致頁面狀態不一致。
然而,單線程也帶來了一個問題:如果有一個耗時的操作(如網絡請求、文件讀取),整個程序會被阻塞,用戶界面會變得卡頓甚至無響應。為了解決這個問題,JavaScript 引入了異步編程模型。
異步操作與回調函數
JavaScript 中的異步操作(如 setTimeout、Promise、fetch API 等)允許程序在執行耗時操作時不會阻塞主線程。當異步操作完成后,會通過回調函數通知主線程。
例如,使用 setTimeout 設置一個定時器:
console.log('開始');
setTimeout(() => {console.log('定時器回調執行');
}, 1000);
console.log('結束');
這段代碼的執行順序是:先打印 "開始",然后設置定時器,接著打印 "結束",最后在 1 秒后執行定時器的回調函數并打印 "定時器回調執行"。
事件循環的基本原理
JavaScript 事件循環機制的核心是由以下幾個部分組成:
-
調用棧(Call Stack):用于執行同步代碼。當執行一個函數時,會將該函數壓入調用棧;函數執行完畢后,會從調用棧中彈出。
-
任務隊列(Task Queue):也稱為回調隊列,用于存放異步操作完成后的回調函數。任務隊列分為宏任務隊列(MacroTask Queue)和微任務隊列(MicroTask Queue)。
-
事件循環(Event Loop):不斷從任務隊列中取出任務并放入調用棧執行。
宏任務和微任務
JavaScript 中的異步任務分為宏任務和微任務:
-
宏任務:包括 setTimeout、setInterval、setImmediate(Node.js 環境)、requestAnimationFrame(瀏覽器環境)、I/O 操作、UI 渲染等。
-
微任務:包括 Promise.then ()、MutationObserver、process.nextTick(Node.js 環境)等。
事件循環的執行順序是:先執行調用棧中的所有同步代碼,然后檢查微任務隊列,如果有微任務,則依次執行所有微任務,直到微任務隊列為空;接著從宏任務隊列中取出一個宏任務執行,執行完畢后,再次檢查微任務隊列,重復上述過程。
事件循環的工作流程
事件循環的工作流程可以概括為以下幾個步驟:
-
執行調用棧中的所有同步代碼。
-
當調用棧為空時,檢查微任務隊列。如果微任務隊列中有任務,則依次執行所有微任務,直到微任務隊列為空。
-
從宏任務隊列中取出一個宏任務執行。
-
宏任務執行完畢后,再次檢查微任務隊列,執行所有微任務。
-
重復步驟 3 和 4,不斷循環。
這種機制確保了異步任務能夠在合適的時機執行,同時不會阻塞主線程,從而實現了 JavaScript 的非阻塞特性。
什么是閉包?閉包的應用場景有哪些?
閉包是 JavaScript 中一個非常重要且強大的特性,它允許函數訪問并操作其外部函數作用域中的變量,即使該外部函數已經執行完畢。閉包的存在使得 JavaScript 中的函數具有了 "記憶性",能夠保留其創建時的環境。
閉包的定義與原理
閉包的形成需要滿足以下兩個條件:
-
函數嵌套:內部函數定義在外部函數內部。
-
內部函數引用外部函數作用域中的變量。
當內部函數被外部函數返回時,它會捕獲并保留外部函數的作用域鏈,即使外部函數已經執行完畢,內部函數仍然可以訪問外部函數作用域中的變量。
function outer() {const x = 10;function inner() {console.log(x); // 內部函數引用了外部函數的變量x}return inner; // 返回內部函數
}const closure = outer(); // 外部函數執行完畢
closure(); // 輸出: 10
在這個例子中,inner 函數是一個閉包,它捕獲了外部函數 outer 的變量 x。當 outer 函數執行完畢后,變量 x 并沒有被銷毀,而是被閉包 inner 保留了下來。
閉包的應用場景
閉包在 JavaScript 中有許多實際應用場景,下面介紹幾個常見的場景。
實現私有變量和方法
閉包可以用來實現私有變量和方法,這是 JavaScript 中模擬類的私有成員的一種常見方式。
function createCounter() {let count = 0; // 私有變量return {increment() {count++;return count;},decrement() {count--;return count;},getCount() {return count;}};
}const counter = createCounter();
console.log(counter.getCount()); // 輸出: 0
counter.increment();
console.log(counter.getCount()); // 輸出: 1
counter.decrement();
console.log(counter.getCount()); // 輸出: 0
在這個例子中,count 變量是私有的,外部無法直接訪問或修改它,只能通過返回的對象中的方法來操作。
函數柯里化
函數柯里化是指將一個多參數函數轉換為一系列單參數函數的過程。閉包可以用來實現函數柯里化。
function add(a, b) {return a + b;
}// 柯里化后的add函數
function curriedAdd(a) {return function(b) {return a + b;};
}const add5 = curriedAdd(5);
console.log(add5(3)); // 輸出: 8
console.log(add5(7)); // 輸出: 12
在這個例子中,curriedAdd 函數返回了一個閉包,該閉包捕獲了參數 a 的值。每次調用閉包時,都會使用捕獲的 a 值和傳入的參數 b 進行計算。
事件處理與回調函數
閉包在事件處理和回調函數中也非常有用,可以用來保存事件處理函數的狀態。
function createButton() {const button = document.createElement('button');button.textContent = '點擊';let count = 0;button.addEventListener('click', function() {count++;button.textContent = `點擊了 ${count} 次`;});return button;
}document.body.appendChild(createButton());
在這個例子中,事件處理函數是一個閉包,它捕獲了 count 變量。每次點擊按鈕時,count 的值都會增加,并更新按鈕的文本內容。
模塊化設計
閉包可以用來實現模塊化設計,將相關的變量和函數封裝在一個閉包中,避免全局變量污染。
const MyModule = (function() {// 私有變量和函數const privateVariable = '私有變量';function privateFunction() {return '私有函數';}// 公開的接口return {publicMethod() {return `訪問 ${privateVariable} 和 ${privateFunction()}`;}};
})();console.log(MyModule.publicMethod()); // 輸出: "訪問 私有變量 和 私有函數"
在這個例子中,立即執行函數表達式(IIFE)返回了一個對象,該對象包含了公開的方法。這些方法可以訪問閉包中的私有變量和函數,而外部無法直接訪問這些私有成員。
手寫深拷貝函數,需考慮數組、對象、循環引用等場景。
深拷貝是 JavaScript 中一個重要的概念,它指的是創建一個新對象或數組,其內容與原對象或數組完全相同,但在內存中占據不同的空間。這意味著修改新對象不會影響原對象,反之亦然。實現一個完整的深拷貝函數需要考慮多種場景,包括基本數據類型、對象、數組、循環引用等。
基礎版本的深拷貝函數
下面是一個基礎版本的深拷貝函數,它可以處理基本數據類型、普通對象和數組:
function deepClone(target) {// 處理基本數據類型(包括null)if (typeof target !== 'object' || target === null) {return target;}// 處理數組if (Array.isArray(target)) {const clone = [];for (let i = 0; i < target.length; i++) {clone[i] = deepClone(target[i]);}return clone;}// 處理普通對象const clone = {};for (const key in target) {if (target.hasOwnProperty(key)) {clone[key] = deepClone(target[key]);}}return clone;
}
這個函數的工作原理是:首先檢查目標是否為基本數據類型,如果是則直接返回;然后檢查是否為數組,如果是則創建一個新數組,并遞歸地對每個元素進行深拷貝;最后處理普通對象,創建一個新對象,并遞歸地對每個屬性值進行深拷貝。
處理循環引用
基礎版本的深拷貝函數存在一個問題,就是無法處理循環引用。循環引用指的是對象之間相互引用,形成一個閉環。如果不處理循環引用,深拷貝函數會陷入無限遞歸,最終導致棧溢出錯誤。
為了處理循環引用,可以使用一個 WeakMap 來記錄已經拷貝過的對象。在遞歸拷貝之前,先檢查對象是否已經被拷貝過,如果是則直接返回之前拷貝的對象,避免無限遞歸。
function deepClone(target, map = new WeakMap()) {// 處理基本數據類型(包括null)if (typeof target !== 'object' || target === null) {return target;}// 檢查是否已經拷貝過if (map.has(target)) {return map.get(target);}// 處理數組if (Array.isArray(target)) {const clone = [];map.set(target, clone); // 記錄已經拷貝的數組for (let i = 0; i < target.length; i++) {clone[i] = deepClone(target[i], map);}return clone;}// 處理普通對象const clone = {};map.set(target, clone); // 記錄已經拷貝的對象for (const key in target) {if (target.hasOwnProperty(key)) {clone[key] = deepClone(target[key], map);}}return clone;
}
在這個改進版本中,我們添加了一個 WeakMap 參數map
,用于記錄已經拷貝過的對象。在處理對象或數組之前,先檢查map
中是否已經存在該對象,如果存在則直接返回對應的拷貝對象。這樣就避免了循環引用導致的無限遞歸問題。
處理特殊對象類型
除了普通對象和數組,JavaScript 中還有許多特殊類型的對象,如 Date、RegExp、Set、Map 等。完整的深拷貝函數還需要處理這些特殊類型的對象。
function deepClone(target, map = new WeakMap()) {// 處理基本數據類型(包括null)if (typeof target !== 'object' || target === null) {return target;}// 處理特殊對象類型if (target instanceof Date) {return new Date(target.getTime());}if (target instanceof RegExp) {return new RegExp(target);}// 處理Setif (target instanceof Set) {const clone = new Set();target.forEach(value => {clone.add(deepClone(value, map));});return clone;}// 處理Mapif (target instanceof Map) {const clone = new Map();target.forEach((value, key) => {clone.set(key, deepClone(value, map));});return clone;}// 檢查是否已經拷貝過if (map.has(target)) {return map.get(target);}// 處理數組if (Array.isArray(target)) {const clone = [];map.set(target, clone);for (let i = 0; i < target.length; i++) {clone[i] = deepClone(target[i], map);}return clone;}// 處理普通對象const clone = {};map.set(target, clone);for (const key in target) {if (target.hasOwnProperty(key)) {clone[key] = deepClone(target[key], map);}}return clone;
}
在這個最終版本中,我們添加了對 Date、RegExp、Set 和 Map 等特殊對象類型的處理。對于每種特殊對象類型,我們都使用相應的構造函數創建一個新對象,并復制原對象的內容。同時,仍然保留了對循環引用的處理,確保函數在面對復雜對象結構時也能正常工作。
通過這個深拷貝函數,我們可以安全地復制包含各種數據類型和復雜結構的對象,而不用擔心修改新對象會影響原對象。
箭頭函數和普通函數的區別有哪些?箭頭函數能否使用 new 操作符?為什么?
箭頭函數與普通函數在語法和行為上存在多方面差異,這些差異源于它們不同的設計目標和適用場景。
首先是語法簡潔性。箭頭函數采用更緊湊的語法格式,省略了function
關鍵字,通過=>
定義函數體。當參數僅有一個時,括號可以省略;若函數體為表達式,還能省略大括號并隱式返回結果。例如:
// 普通函數
function add(a, b) { return a + b; }
// 箭頭函數
const add = (a, b) => a + b;
而普通函數需完整書寫function
關鍵字、參數列表和大括號,語法結構更為傳統。
其次是 this 指向的差異。普通函數的this
在調用時動態綁定,取決于函數的調用上下文 —— 作為對象方法調用時指向對象,普通函數調用時指向全局對象(瀏覽器中為window
,嚴格模式下為undefined
),通過call
/apply
/bind
方法可顯式改變其指向。箭頭函數則不具備自身的this
,其內部的this
繼承自外層作用域,在定義時就已確定,無法通過call
/apply
/bind
修改。這一特性使其在處理回調函數或嵌套函數時能更便捷地保留外層作用域的this
,避免傳統函數中常見的this
指向混亂問題。
關于參數處理,普通函數支持arguments
對象,可獲取所有傳入參數,還能通過rest參數(...args)
收集剩余參數。箭頭函數不支持arguments
對象,但可使用rest參數
,其內部的arguments
會引用外層作用域的arguments
。例如:
function普通函數() { console.log(arguments); }
const箭頭函數 = () => { console.log(arguments); };
// 箭頭函數內的arguments指向外層作用域(若有),否則報錯
在構造函數方面,箭頭函數不能作為構造函數使用,無法通過new
操作符創建實例。這是因為箭頭函數沒有prototype
屬性,也不存在constructor
方法。當對箭頭函數使用new
時會拋出錯誤,而普通函數作為構造函數時,this
會指向新創建的實例,且可通過prototype
添加原型方法。
此外,箭頭函數不支持super
和new.target
。在繼承場景中,普通函數可通過super
調用父類方法,new.target
可用于檢測函數是否作為構造函數被調用;箭頭函數由于沒有自身的this
和構造函數行為,無法使用這些特性。
總結來看,箭頭函數的設計初衷是為了簡化簡單函數的書寫,尤其適用于需要保持this
指向一致的回調函數、對象方法以外的場景。而普通函數在需要動態this
綁定、作為構造函數或使用arguments
對象時更為合適。
列舉 ES6 的新特性(如 let/const、箭頭函數、Promise、Proxy、Map/Set、解構賦值等)
ES6(ECMAScript 2015)帶來了眾多重要特性,顯著提升了 JavaScript 的語法表現力和開發效率,以下是核心特性的詳細說明:
1. 塊級作用域聲明:let 和 const
let
和const
是 ES6 引入的新變量聲明方式,用于替代傳統的var
。let
聲明的變量具有塊級作用域(如if
語句、for
循環體),避免了var
的函數作用域導致的變量提升和意外覆蓋問題。const
用于聲明常量,一旦賦值便不可更改,其作用域同樣為塊級,且要求聲明時必須初始化。例如:
{ let a = 1; const b = 2; a = 3; // 允許 b = 4; // 報錯,常量不可重新賦值
}
2. 箭頭函數(Arrow Functions)
箭頭函數提供了更簡潔的函數定義語法,省略了function
關鍵字,通過=>
符號連接參數和函數體。其核心特點包括:沒有自身的this
,this
繼承自外層作用域;不支持arguments
對象,可使用rest參數(...args)
;不能作為構造函數,無法通過new
創建實例。箭頭函數適用于需要固定this
指向的回調函數,如數組的map
、filter
方法回調。
3. 解構賦值(Destructuring Assignment)
解構賦值允許從數組或對象中提取值,并將其賦值給變量。數組解構按位置匹配,對象解構按鍵名匹配,可嵌套使用并設置默認值。例如:
// 數組解構
const [a, b, c = 3] = [1, 2]; // a=1, b=2, c=3
// 對象解構
const { name, age = 18 } = { name: "Alice" }; // name="Alice", age=18
4. Promise 對象
Promise 是處理異步操作的標準化解決方案,用于替代傳統的回調函數,避免 “回調地獄”。它代表一個異步操作的最終狀態(成功fulfilled
或失敗rejected
),通過then()
方法處理成功結果,catch()
方法處理錯誤。Promise 支持鏈式調用,可組合多個異步操作。例如:
fetch("https://api.example.com/data") .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error("Error:", error));
5. 類(Class)語法
ES6 引入基于class
關鍵字的面向對象編程語法,簡化了原型鏈繼承的寫法。class
包含構造方法constructor
和原型方法,通過extends
關鍵字實現繼承,通過super
調用父類方法。例如:
class Animal { constructor(name) { this.name = name; } speak() { return "Sound"; }
}
class Dog extends Animal { speak() { return `${super.speak()} Woof!`; }
}
6. 模塊系統(Modules)
ES6 正式支持原生模塊系統,通過export
導出模塊內容,import
導入其他模塊。模塊具有獨立的作用域,避免全局變量污染。例如:
// module.js
export const name = "Module";
export function greet() { return "Hello"; }
// app.js
import { name, greet } from "./module.js";
console.log(greet(), name); // 輸出 "Hello Module"
7. 迭代器(Iterator)和可迭代對象(Iterable)
迭代器是一種接口,用于遍歷數據結構中的元素,通過Symbol.iterator
方法實現。數組、字符串、Set、Map 等內置類型均為可迭代對象,可通過for...of
循環遍歷。自定義對象也可實現迭代器接口,使其支持for...of
遍歷。
8. Proxy 和 Reflect
Proxy
用于創建對象的代理,可攔截對象的各種操作(如屬性讀取、賦值、函數調用等),實現數據響應式、權限控制等功能。Reflect
是一個內置對象,提供了與Proxy
攔截方法對應的默認行為,用于替代某些操作的默認語法(如delete
、apply
等)。
9. Map 和 Set 數據結構
- Map:鍵值對集合,鍵可以是任意類型(包括對象),通過
get
、set
、has
等方法操作,適合需要靈活鍵類型的場景。 - Set:唯一值集合,自動去重,適合存儲不重復的數據,可通過
add
、delete
、has
等方法操作。
10. 模板字符串(Template Literals)
模板字符串使用反引號(`
)包裹,支持變量插值(${變量}
)和多行文本,替代了傳統字符串拼接的繁瑣方式。例如:
const name = "Bob";
const age = 25;
const message = `Name: ${name}, Age: ${age}`; // 輸出 "Name: Bob, Age: 25"
11. Rest 參數和擴展運算符
- Rest 參數(...args):用于函數參數中,將多余的參數收集為數組,替代傳統的
arguments
對象。function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
- 擴展運算符(...):用于展開數組或對象,例如合并數組、函數調用時傳遞參數等。
const arr1 = [1, 2], arr2 = [3, 4]; const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
12. Symbol 原始數據類型
Symbol 是一種唯一的標識符,用于創建對象的唯一屬性鍵,避免屬性名沖突。通過Symbol()
創建,例如:
const key = Symbol("unique");
const obj = { [key]: "value" };
這些特性共同構成了 ES6 的核心內容,推動 JavaScript 向更現代、更高效的編程語言演進,廣泛應用于前端開發的各個領域。
Proxy 對象的作用是什么?除了在 Vue3 中使用,還有哪些實際應用場景?
Proxy 是 ES6 引入的強大特性,用于創建一個對象的代理,允許通過自定義 “陷阱”(trap)攔截并自定義對象的基本操作(如屬性訪問、賦值、枚舉、函數調用等)。其核心作用是在不直接修改原對象的前提下,對對象的行為進行一層抽象和控制,實現數據劫持、權限控制、日志記錄等功能。
Proxy 的主要作用包括:
-
數據響應式(Reactivity)
這是 Proxy 最典型的應用場景,Vue3 正是基于 Proxy 實現了響應式系統。通過攔截對象的屬性讀取(get
陷阱)和賦值(set
陷阱),可以自動追蹤依賴并觸發更新,相比 Vue2 使用的Object.defineProperty
,Proxy 支持對數組和對象的深層次響應式處理,且無需預先知道所有屬性。 -
訪問控制與權限管理
通過攔截get
、set
、deleteProperty
等陷阱,可以限制對對象屬性的訪問或修改。例如,創建一個只讀代理,禁止修改某些屬性:const data = { name: "Alice", age: 25 }; const handler = { set(target, key, value) { if (key === "age" && value < 0) { throw new Error("Age cannot be negative"); } return Reflect.set(target, key, value); } }; const proxy = new Proxy(data, handler); proxy.age = -5; // 拋出錯誤
-
日志記錄與調試
攔截對象操作并添加日志,可用于追蹤對象的使用情況,輔助調試。例如,記錄每次屬性訪問和修改的時間、值等信息:const handler = { get(target, key) { console.log(`Accessing property: ${key}`); return Reflect.get(target, key); }, set(target, key, value) { console.log(`Updating ${key} from ${target[key]} to ${value}`); return Reflect.set(target, key, value); } }; const proxy = new Proxy(obj, handler); proxy.name = "Bob"; // 輸出更新日志
-
惰性加載(Lazy Loading)
在訪問對象屬性時動態加載數據,避免提前請求或計算資源。例如,當訪問某個屬性時才從服務器獲取數據:const resource = { data: null, loadData() { this.data = fetch("https://api.example.com/data").then(res => res.json()); } }; const handler = { get(target, key) { if (key === "data" && target.data === null) { target.loadData(); } return Reflect.get(target, key); } }; const proxy = new Proxy(resource, handler); // 首次訪問data時觸發加載 proxy.data.then(data => console.log(data));
-
函數參數校驗
攔截函數調用(apply
陷阱),對傳入的參數進行校驗,確保符合預期格式。例如,限制函數參數必須為數字:const add = (a, b) => a + b; const handler = { apply(target, thisArg, args) { if (!args.every(arg => typeof arg === "number")) { throw new Error("Parameters must be numbers"); } return target.apply(thisArg, args); } }; const proxy = new Proxy(add, handler); proxy("1", 2); // 拋出錯誤
-
對象序列化與轉換
在讀取對象屬性時自動進行格式轉換,例如將日期對象轉換為字符串:const data = { birthDate: new Date(2000, 0, 1) }; const handler = { get(target, key) { const value = Reflect.get(target, key); if (value instanceof Date) { return value.toISOString(); } return value; } }; const proxy = new Proxy(data, handler); console.log(proxy.birthDate); // 輸出日期字符串 "2000-01-01T00:00:00.000Z"
-
代理模式實現
通過 Proxy 創建代理對象,實現設計模式中的代理模式,例如虛擬代理(延遲創建昂貴對象)、緩存代理(緩存函數調用結果)等。
其他實際應用場景舉例:
- 數組操作攔截:監控數組的
push
、pop
等方法調用,統計數組變化次數或限制數組長度。 - 只讀代理:創建不可修改的對象副本,防止數據被意外篡改。
- 自定義迭代器:通過攔截
Symbol.iterator
陷阱,為非可迭代對象添加迭代器支持。 - 性能監控:在對象方法調用前后添加性能統計代碼,分析方法執行耗時。
Proxy 的靈活性使其在需要動態控制對象行為的場景中具有廣泛應用。需要注意的是,Proxy 的兼容性需結合實際項目需求,且過度使用可能帶來一定的性能開銷,需根據場景權衡選擇。
Map 和 Object 的區別是什么?各自的適用場景有哪些?
Map 和 Object 都是用于存儲鍵值對的數據結構,但在設計理念、功能特性和適用場景上存在顯著差異。以下從多個維度對比分析兩者的區別,并說明各自的適用場景。
一、鍵的類型與限制
-
Object:
- 鍵只能是字符串或 Symbol 類型(非字符串類型會被自動轉換為字符串)。例如,使用數字作為鍵時會被轉為字符串
"1"
,使用對象作為鍵時會被轉為"[object Object]"
。 - 示例:
const obj = { }; obj[1] = "one"; // 鍵為字符串"1" obj[{ a: 1 }] = "value"; // 鍵為字符串"[object Object]"
- 鍵只能是字符串或 Symbol 類型(非字符串類型會被自動轉換為字符串)。例如,使用數字作為鍵時會被轉為字符串
-
Map:
- 鍵可以是任意類型(包括對象、函數、原始值等),且鍵的相等性基于
SameValueZero
算法(與===
類似,但NaN
等于自身)。 - 示例:
const map = new Map(); map.set(1, "one"); // 鍵為數字1 const keyObj = { a: 1 }; map.set(keyObj, "value"); // 鍵為對象引用
- 鍵可以是任意類型(包括對象、函數、原始值等),且鍵的相等性基于
二、數據初始化與遍歷順序
-
Object:
- 屬性沒有內置的順序(ES6 之后規范要求對象屬性按定義順序遍歷,但早期環境可能不保證)。
- 初始化屬性需逐個添加,或通過對象字面量一次性定義。
- 遍歷方式包括
for...in
(遍歷自身及原型鏈屬性,需配合hasOwnProperty
過濾)、Object.keys()
(自身可枚舉字符串鍵)、Object.getOwnPropertySymbols()
(自身 Symbol 鍵)、Object.values()
/Object.entries()
等。
-
Map:
- 鍵值對按插入順序排列,遍歷時遵循插入順序。
- 可通過構造函數接收一個包含鍵值對的數組(如
[[key1, val1], [key2, val2]]
)進行初始化。 - 遍歷方式包括
for...of
(直接遍歷鍵值對)、map.keys()
(鍵)、map.values()
(值)、map.entries()
(鍵值對),且默認迭代器為entries()
。
三、屬性與方法
-
Object:
- 自身擁有原型屬性(如
toString
、hasOwnProperty
等),可能與用戶定義的鍵沖突。 - 操作方法需通過
Object
對象的靜態方法實現,例如:Object.hasOwn(obj, key)
:判斷是否包含指定鍵(ES2022 新增,替代hasOwnProperty
)。delete obj.key
:刪除屬性。obj.key = value
:添加 / 修改屬性。
- 自身擁有原型屬性(如
-
Map:
- 自身方法集中在實例上,包括:
set(key, value)
:設置鍵值對,返回 Map 實例,支持鏈式調用。get(key)
:獲取對應值,鍵不存在時返回undefined
。has(key)
:判斷是否存在鍵。delete(key)
:刪除鍵值對,返回布爾值表示是否成功。clear()
:清空所有鍵值對。
- 沒有原型屬性干擾,鍵名更安全。
- 自身方法集中在實例上,包括:
四、內存與性能
-
Object:
- 適合存儲少量鍵值對,尤其是鍵為字符串且需與 JSON 兼容的場景(JSON 僅支持字符串鍵)。
- 對于大量動態鍵,可能因屬性擴散(dictionary mode)導致性能下降。
-
Map:
- 更適合存儲大量動態鍵(如對象鍵),內部實現為哈希表,插入、刪除、查詢的平均時間復雜度為 O (1)。
- 在頻繁增刪操作的場景中性能優于 Object,尤其當鍵為對象時無需轉換為字符串,減少了隱式類型轉換的開銷。
五、適用場景對比
場景 | Object?適用 | Map?適用 |
---|---|---|
鍵為字符串 / Symbol | 推薦使用,符合傳統對象用法,且可直接通過字面量初始化。 | 也可使用,但初始化更繁瑣,適合需要按順序遍歷的場景。 |
鍵為任意類型(如對象) | 不推薦,鍵會被轉為字符串,無法通過對象引用精確匹配。 | 強烈推薦,支持對象作為鍵,按引用相等性匹配。 |
需要保持插入順序 | ES6 + 環境下可行,但遍歷需注意順序保證(部分舊環境不支持)。 | 推薦,內置順序保證,遍歷順序與插入一致。 |
頻繁增刪操作 | 性能可能較低,尤其當鍵為動態字符串時。 | 性能更優,內部哈希表優化了增刪操作。 |
與 JSON 交互 | 必須使用,因為 JSON 僅支持字符串鍵和基本類型值。 | 不適用,需先轉換為 Object(如Object.fromEntries(map) )。 |
簡單配置項存儲 | 推薦,通過字面量簡潔明了。 | 可選,但可能過度設計。 |
復雜數據結構(如緩存) | 若鍵為字符串且需順序無關,可使用;若鍵為對象或需順序,不適用。 | 推薦,尤其適合以對象為鍵的緩存場景(如 DOM 節點作為鍵關聯數據)。 |
Map 和 Set 的常用方法及使用場景
Map 和 Set 是 ES6 引入的兩種集合類型,分別用于鍵值對存儲和唯一值集合。它們提供了高效的數據操作方法,適用于不同的業務場景。以下詳細介紹兩者的常用方法及典型應用場景。
一、Map 的常用方法及使用場景
Map 是鍵值對的有序集合,鍵可以是任意類型,支持按插入順序遍歷。
常用方法
-
new Map([iterable])
- 作用:創建 Map 實例,可選參數為包含鍵值對的可迭代對象(如數組數組)。
- 示例:
const map = new Map([["a", 1], ["b", 2]]); // 初始化為{a:1, b:2}
-
set(key, value)
- 作用:設置鍵值對,返回 Map 實例,支持鏈式調用。
- 示例:
map.set("c", 3).set("d", 4); // 鏈式添加多個鍵值對
-
get(key)
- 作用:獲取指定鍵的值,若鍵不存在則返回
undefined
。 - 示例:
console.log(map.get("a")); // 輸出1 console.log(map.get("e")); // 輸出undefined
- 作用:獲取指定鍵的值,若鍵不存在則返回
-
has(key)
- 作用:判斷是否存在指定鍵,返回布爾值。
- 示例:
console.log(map.has("b")); // 輸出true
-
delete(key)
- 作用:刪除指定鍵值對,返回布爾值表示是否成功。
- 示例:
map.delete("c"); // 刪除鍵"c"
-
clear()
- 作用:清空 Map 中的所有鍵值對。
- 示例:
map.clear(); // 清空所有數據
-
size
- 作用:獲取 Map 中鍵值對的數量(只讀屬性)。
- 示例:
console.log(map.size); // 輸出當前鍵值對個數
-
遍歷方法
keys()
:返回鍵的迭代器。values()
:返回值的迭代器。entries()
:返回鍵值對的迭代器(默認迭代器)。for...of
遍歷:for (const [key, value] of map) { console.log(key, value); // 按插入順序輸出鍵值對 }
使用場景
-
鍵為對象的場景
需要以對象(如 DOM 元素、函數、其他對象)作為鍵時,Map 是最佳選擇,因為 Object 會將非字符串鍵轉為字符串,無法精確匹配對象引用。
示例:為每個 DOM 元素關聯自定義數據:const button = document.querySelector("button"); const dataMap = new Map(); dataMap.set(button, { count: 0 }); button.addEventListener("click", () => { const data = dataMap.get(button); data.count++; });
-
需要保持插入順序的鍵值對存儲
當數據順序至關重要時(如撤銷 / 重做歷史記錄),Map 的有序性可確保遍歷順序與操作順序一致。 -
頻繁增刪的大規模數據
Map 內部基于哈希表實現,增刪操作的平均時間復雜度為 O (1),性能優于 Object,適合處理大量動態數據。 -
鏈式操作場景
set
方法返回 Map 實例,支持鏈式調用,可簡化代碼結構:map.set("a", 1).set("b", 2).set("c", 3);
二、Set 的常用方法及使用場景
Set 是唯一值的集合,自動去重,值可以是任意類型。
常用方法
-
new Set([iterable])
- 作用:創建 Set 實例,可選參數為可迭代對象,自動去重。
- 示例:
const set = new Set([1, 2, 2, 3]); // 初始化為{1, 2, 3}
-
add(value)
- 作用:添加值到 Set,返回 Set 實例,支持鏈式調用。
- 示例:
set.add(4).add(5); // 鏈式添加多個值
-
has(value)
- 作用:判斷是否存在指定值,返回布爾值。
- 示例:
console.log(set.has(3)); // 輸出true
-
delete(value)
- 作用:刪除指定值,返回布爾值表示是否成功。
- 示例:
set.delete(2); // 刪除值2
-
clear()
- 作用:清空 Set 中的所有值。
- 示例:
set.clear(); // 清空所有數據
-
size
- 作用:獲取 Set 中值的數量(只讀屬性)。
- 示例:
console.log(set.size); // 輸出當前值的個數
-
遍歷方法
keys()
/values()
:返回值的迭代器(兩者行為一致,因為 Set 中值即鍵)。entries()
:返回包含值和自身的鍵值對迭代器(如[value, value]
)。for...of
遍歷:for (const value of set) { console.log(value); // 按插入順序輸出值 }
使用場景
-
數組去重
利用 Set 自動去重的特性,快速實現數組去重,尤其適合處理復雜類型(如對象需注意:Set 判斷對象相等基于引用,而非內容)。
示例:const arr = [1, 2, 2, 3, { a: 1 }, { a: 1 }]; const uniqueArr = [...new Set(arr)]; // 結果:[1, 2, 3, {a:1}, {a:1}](對象因引用不同未去重)
-
快速判斷成員是否存在
需要頻繁檢查某個值是否在集合中時,Set 的has
方法性能優于數組的includes
(時間復雜度為 O (1) vs O (n))。
示例:用戶權限校驗:const allowedUsers = new Set(["admin", "editor"]); function checkPermission(user) { return allowedUsers.has(user); }
-
交集、并集、差集運算
通過遍歷 Set 實現集合運算,適用于數據篩選場景。- 交集:
const setA = new Set([1, 2, 3]); const setB = new Set([3, 4, 5]); const intersection = new Set([...setA].filter(x => setB.has(x))); // {3}
- 并集:
const union = new Set([...setA, ...setB]); // {1,2,3,4,5}
- 交集:
-
存儲臨時狀態或標記
需要記錄已訪問的節點、已處理的任務等場景,利用 Set 的唯一性避免重復操作。
示例:DFS 遍歷圖結構時標記已訪問節點:const visited = new Set(); function dfs(node) { if (visited.has(node)) return; visited.add(node); // 處理節點... node.children.forEach(child => dfs(child)); }
-
類數組轉換為集合
將參數對象、DOM 節點列表等類數組轉換為 Set,方便操作:const args = new Set(arguments); // 將函數參數轉為Set const elements = new Set(document.querySelectorAll("div")); // DOM節點集合
總結對比
類型 | 核心特性 | 典型場景 | 關鍵方法 |
---|---|---|---|
Map | 鍵值對,鍵為任意類型,有序 | 對象鍵存儲、有序數據、頻繁增刪、鏈式操作 | set /get /has /for...of |
Set | 唯一值集合,自動去重 | 數組去重、成員校驗、集合運算、臨時標記 | add /has /delete /size |
在實際開發中,若需要存儲鍵值對且鍵為復雜類型,選擇 Map;若只需存儲唯一值或進行集合操作,選擇 Set。兩者結合使用可實現更復雜的數據管理,例如用 Map 的鍵為 Set 來存儲多個關聯的唯一值集合。
如何判斷一個對象是否為空對象(需考慮原型鏈屬性)?
判斷一個對象是否為空對象需要綜合考慮自身屬性和原型鏈屬性的情況,同時要明確 “空” 的定義:通常指對象既沒有自身可枚舉屬性,也不繼承原型鏈上的可枚舉屬性(若有特殊需求,可能僅需判斷自身屬性)。以下是具體方法及適用場景:
1.?僅判斷自身可枚舉屬性(不考慮原型鏈)
最常用的方式是通過Object.keys()
獲取對象自身的可枚舉屬性數組,若長度為 0 則為空。該方法不會遍歷原型鏈,適用于僅需檢查對象自身是否有屬性的場景。
function isEmptyObject(obj) {return Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).length === 0;
}
注意:需先通過toString
判斷是否為對象(排除null
、數組等),否則Object.keys(null)
會報錯。
2.?判斷自身所有屬性(包括不可枚舉屬性)
若需檢測對象自身的所有屬性(包括Object.defineProperty
定義的不可枚舉屬性),可使用Object.getOwnPropertyNames()
或Object.getOwnPropertySymbols()
,前者獲取字符串鍵名,后者獲取 Symbol 鍵名。
function isEmptyObjectDeep(obj) {if (Object.prototype.toString.call(obj) !== '[object Object]') return true;const ownProps = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));return ownProps.length === 0;
}
場景:適用于需要嚴格檢查對象自身是否存在任何屬性(如框架內部對數據模型的初始化校驗)。
3.?排除原型鏈可枚舉屬性
若需確保對象既無自身屬性,也不繼承原型鏈上的可枚舉屬性(例如自定義原型對象的場景),需手動遍歷原型鏈。
function isEmptyObjectStrict(obj) {if (Object.prototype.toString.call(obj) !== '[object Object]') return true;let current = obj;while (current !== null) {if (Object.keys(current).length > 0) return false; // 自身有屬性則非空current = Object.getPrototypeOf(current); // 遍歷原型鏈}return true;
}
原理:通過Object.getPrototypeOf
逐層向上查找原型對象,若任意一層存在可枚舉屬性,則返回false
。該方法適用于嚴格隔離原型鏈影響的場景(如模擬 “純凈” 對象)。
4.?區分數組和對象
需注意:數組[]
通過Object.keys
判斷時長度為 0,但數組本身有原型鏈屬性(如push
、pop
)。若需求中 “空對象” 需排除數組,可在判斷前增加類型校驗:
function isEmptyObjectExcludingArray(obj) {return Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).length === 0;
}
調用isEmptyObjectExcludingArray([])
會返回false
,因數組類型不滿足[object Object]
。
5.?特殊場景:原型鏈污染攻擊檢測
在安全場景中,可能需要檢測對象是否被原型鏈污染(即原型鏈上新增了可枚舉屬性)。此時可通過hasOwnProperty
結合for...in
遍歷:
function hasPrototypePollution(obj) {for (const key in obj) {if (!obj.hasOwnProperty(key)) { // key來自原型鏈return true;}}return false;
}
該方法可檢測原型鏈是否存在可枚舉屬性,但無法區分屬性是否為對象自身所有,需結合具體需求使用。
解釋 Promise 的作用及常用方法(如 then、catch、all、race 等)?
Promise 的作用在于解決 JavaScript 異步編程中的 “回調地獄” 問題,通過鏈式調用和統一的錯誤處理機制,讓異步代碼更易讀、可維護。它表示一個異步操作的最終狀態(成功 / 失敗),并允許在狀態變更時執行相應的回調函數。以下從核心概念、常用方法及應用場景展開說明:
一、Promise 的核心概念
-
三種狀態
- pending(進行中):初始狀態,異步操作未完成或未失敗。
- fulfilled(已成功):異步操作完成,結果可用(通過
resolve
觸發)。 - rejected(已失敗):異步操作失敗,原因可捕獲(通過
reject
觸發)。
狀態一旦變更(pending
→fulfilled
或pending
→rejected
),便不可逆轉,確保回調執行的確定性。
-
鏈式調用的本質
then
/catch
方法返回一個新的 Promise,允許連續調用(如promise.then().then()
),避免層層嵌套回調。每個then
可接收兩個參數:成功回調(onFulfilled
)和失敗回調(onRejected
),后者可省略,失敗會沿鏈向上傳遞。
二、常用方法及實戰示例
1.?then(onFulfilled, onRejected)
作用:注冊成功或失敗的回調函數,返回新 Promise。
- 成功場景:獲取異步操作結果并處理。
fetch('https://api.example.com/data').then(response => response.json()) // 解析JSON.then(data => console.log('數據:', data)); // 處理數據
- 失敗處理:第二個參數或后續
catch
捕獲錯誤。fetch('https://api.example.com/invalid').then(response => response.json()).then(data => console.log(data)).catch(error => console.error('請求失敗:', error)); // 統一處理錯誤
2.?catch(onRejected)
作用:等價于then(undefined, onRejected)
,專門處理鏈中的錯誤,提升代碼可讀性。
asyncFunction().then(result => process(result)).catch(error => {logError(error); // 記錄錯誤日志return defaultResult; // 可返回新值讓后續then接收});
注意:catch
會捕獲前面所有未處理的錯誤,包括同步代碼中的錯誤(如then
回調內的JSON.parse
拋錯)。
3.?Promise.all(iterable)
作用:接收一個 Promise 數組,所有 Promise 都成功時,返回包含所有結果的數組;任意一個失敗時,立即返回失敗原因(第一個拒絕的 Promise 的理由)。
const promise1 = fetch('https://api1.com');
const promise2 = fetch('https://api2.com');
Promise.all([promise1, promise2]).then(responses => responses.map(res => res.json())).then(datas => console.log('全部數據:', datas)).catch(error => console.error('任一請求失敗:', error));
場景:并行獲取多個不相關的異步數據,需等待全部完成后再處理(如頁面初始化時加載多個模塊的數據)。
4.?Promise.race(iterable)
作用:接收一個 Promise 數組,第一個狀態變更的 Promise 的結果(成功或失敗)會成為最終結果。
const timeout = new Promise((_, reject) => {setTimeout(() => reject('請求超時'), 5000);
});
const fetchData = fetch('https://api.example.com');
Promise.race([fetchData, timeout]).then(response => response.json()).catch(error => console.error('超時或失敗:', error));
場景:設置請求超時機制,或在多個并行請求中取最快返回的結果(如負載均衡)。
5.?Promise.allSettled(iterable)
(ES2020 新增)
作用:等待所有 Promise 完成(無論成功或失敗),返回一個數組,每個元素包含status
(fulfilled
/rejected
)和value
/reason
。
const promises = [Promise.resolve(1), Promise.reject('出錯')];
Promise.allSettled(promises).then(results => {results.forEach(result => {if (result.status === 'fulfilled') {console.log('成功值:', result.value);} else {console.error('失敗原因:', result.reason);}});});
場景:需要確保所有異步操作執行完畢(如批量上傳文件,無論成功與否都記錄狀態)。
6.?Promise.resolve(value)
?&?Promise.reject(reason)
Promise.resolve
:將現有值 / Promise 轉換為已成功的 Promise,用于統一異步接口。function getValue() {return Promise.resolve(42); // 等價于 new Promise(resolve => resolve(42)) }
Promise.reject
:創建一個已拒絕的 Promise,用于快速拋出錯誤。
三、與事件循環的關系
Promise 的回調(then/catch
)屬于微任務(Microtask),會在當前宏任務(如同步代碼、定時器回調)執行完畢后,下一個宏任務之前立即執行。這確保了異步操作的結果能以可預測的順序處理,避免阻塞主線程。
四、最佳實踐建議
- 統一錯誤處理:在 Promise 鏈末尾添加
catch
,避免未捕獲的錯誤導致程序崩潰。 - 避免內存泄漏:若
then
中返回 Promise,需確保后續鏈有catch
處理,否則錯誤會被忽略。 - 合理使用并行操作:
all
適用于強依賴所有結果的場景,race
用于取最快結果,allSettled
用于容忍部分失敗的批量操作。
通過 Promise,開發者能以同步代碼的思維組織異步邏輯,結合 async/await 語法糖(本質是 Promise 的語法封裝),進一步提升異步代碼的可讀性和維護性,成為現代前端開發中處理異步操作的核心工具。
一道 Promise 代碼的輸出結果分析題(需結合事件循環機制)?
以下通過具體代碼示例分析 Promise 與事件循環(Event Loop)的交互邏輯,重點理解 ** 宏任務(Macrotask)和微任務(Microtask)** 的執行順序。
示例代碼
console.log('start');setTimeout(() => {console.log('timer1');Promise.resolve().then(() => {console.log('promise1 in timer1');});
}, 0);Promise.resolve().then(() => {console.log('promise1');setTimeout(() => {console.log('timer2 in promise1');}, 0);
});setTimeout(() => {console.log('timer2');Promise.resolve().then(() => {console.log('promise2 in timer2');});
}, 0);console.log('end');
輸出結果分析
按事件循環的執行規則,代碼執行流程可分為以下階段:
1.?同步代碼執行(宏任務初始階段)
console.log('start')
?→ 輸出:start
console.log('end')
?→ 輸出:end
- 定時器回調:兩個
setTimeout
(延遲 0ms)將回調函數加入宏任務隊列。 - Promise 微任務:第一個
Promise.resolve().then
回調加入微任務隊列。
當前狀態:
- 宏任務隊列:
[timer1回調, timer2回調]
- 微任務隊列:
[promise1回調]
2.?處理微任務隊列
同步代碼執行完畢后,事件循環會先清空微任務隊列中的所有任務:
- 執行
promise1回調
:console.log('promise1')
?→ 輸出:promise1
- 回調中又創建一個
setTimeout
,其回調timer2 in promise1
加入宏任務隊列末尾。
- 此時微任務隊列已空,事件循環進入下一個宏任務。
當前狀態:
- 宏任務隊列:
[timer1回調, timer2回調, timer2 in promise1回調]
- 微任務隊列:
[]
3.?執行第一個宏任務(timer1 回調)
- 執行
timer1回調
:console.log('timer1')
?→ 輸出:timer1
- 回調中創建
Promise.resolve().then
,其回調promise1 in timer1
加入微任務隊列。
- 宏任務執行完畢后,事件循環再次處理微任務隊列:
- 執行
promise1 in timer1回調
?→ 輸出:promise1 in timer1
- 執行
- 微任務隊列再次清空。
當前狀態:
- 宏任務隊列:
[timer2回調, timer2 in promise1回調]
- 微任務隊列:
[]
4.?執行第二個宏任務(timer2 回調)
- 執行
timer2回調
:console.log('timer2')
?→ 輸出:timer2
- 回調中創建
Promise.resolve().then
,其回調promise2 in timer2
加入微任務隊列。
- 宏任務執行完畢后,處理微任務隊列:
- 執行
promise2 in timer2回調
?→ 輸出:promise2 in timer2
- 執行
- 微任務隊列再次清空。
當前狀態:
- 宏任務隊列:
[timer2 in promise1回調]
- 微任務隊列:
[]
5.?執行第三個宏任務(timer2 in promise1 回調)
- 執行
timer2 in promise1回調
:console.log('timer2 in promise1')
?→ 輸出:timer2 in promise1
- 該回調無其他異步操作,宏任務隊列清空。
最終輸出順序
start
end
promise1
timer1
promise1 in timer1
timer2
promise2 in timer2
timer2 in promise1
關鍵規則總結
-
宏任務與微任務的優先級:
- 宏任務包括:
setTimeout
、setInterval
、setImmediate
(Node.js)、I/O 操作、UI 渲染等。 - 微任務包括:
Promise.then/catch/finally
、MutationObserver
、process.nextTick
(Node.js)。 - 每完成一個宏任務,會立即處理所有微任務,再取下一個宏任務。
- 宏任務包括:
-
同一類型任務的執行順序:
- 微任務按加入隊列的順序執行(如示例中先加入
promise1
回調,后加入timer1
中的微任務)。 - 宏任務同樣按隊列順序執行(如示例中兩個
setTimeout
按代碼順序加入隊列,先執行timer1
再執行timer2
)。
- 微任務按加入隊列的順序執行(如示例中先加入
-
嵌套異步操作的影響:
- 在微任務(如
Promise.then
)中創建的宏任務(如setTimeout
),會被加入宏任務隊列末尾,等待當前微任務隊列處理完畢后,由后續宏任務處理。
- 在微任務(如
常見誤區辨析
- 誤區 1:認為
setTimeout(0)
會立即執行。
實際上,0ms
是操作系統允許的最小延遲,回調仍需等待當前宏任務和微任務處理完畢才會執行。 - 誤區 2:認為 Promise 回調會在
setTimeout
之前立即執行。
正確邏輯是:同步代碼→微任務隊列→宏任務隊列,因此 Promise 回調(微任務)會在當前宏任務(同步代碼)結束后優先于setTimeout
(宏任務)執行。
通過此類示例可深入理解事件循環機制,在實際開發中合理安排異步操作順序,避免因任務優先級導致的邏輯錯誤或性能問題(如微任務過多阻塞 UI 渲染)。
對函數式編程的理解(如純函數、數據不可變、高階函數等)?
函數式編程(Functional Programming,簡稱 FP)是一種以函數為核心的編程范式,強調通過函數的組合和變換處理數據,而非依賴對象狀態或命令式操作。其核心概念包括純函數、數據不可變、高階函數、函數組合等,以下從核心思想、關鍵概念及實際應用展開說明:
一、核心思想:聲明式編程與數據抽象
函數式編程倡導聲明式編程風格,即描述 “做什么” 而非 “如何做”,通過函數的組合簡化邏輯復雜度。例如,相較于命令式的循環遍歷數組,FP 更傾向于使用map
、filter
等高階函數處理數據:
// 命令式:關注過程
const numbers = [1, 2, 3];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {doubled.push(numbers[i] * 2);
}// 函數式:關注結果
const doubled = numbers.map(n => n * 2);
這種方式將具體實現封裝在函數中,代碼更簡潔且易于推理。
二、關鍵概念解析
1.?純函數(Pure Function)
定義:滿足以下條件的函數:
- 相同輸入必有相同輸出(無副作用);
- 不修改外部狀態(輸入參數不可變)。
示例:
// 純函數:輸入輸出確定,無副作用
function add(a, b) {return a + b;
}// 非純函數:依賴外部變量,輸出不確定
let count = 0;
function impureAdd(b) {return count++ + b; // 修改外部狀態,副作用
}
作用:
- 可預測性:便于測試和調試;
- 可組合性:純函數可作為 “積木” 自由組合(如
compose
、pipe
); - 并行安全:無共享狀態,適合多線程或異步場景。
2.?數據不可變(Immutable Data)
禁止直接修改原始數據,如需變更需返回新數據。常見實現方式包括:
- 使用
展開運算符
(...
)創建新數組 / 對象:const arr = [1, 2, 3]; const newArr = [...arr, 4]; // 新建數組,原數組不變
- 使用 Immutable.js 等庫實現持久化數據結構。
優勢: - 避免副作用:防止意外修改導致的邏輯錯誤;
- 簡化狀態管理:如 Redux 通過不可變數據確保狀態可追蹤。
3.?高階函數(Higher-Order Function)
接收函數作為參數或返回函數的函數,是 FP 的核心工具。常見類型包括:
- 函數作為參數:
map
、filter
、reduce
等數組方法;const numbers = [1, 2, 3]; const evenNumbers = numbers.filter(n => n % 2 === 0); // filter接收函數參數
- 函數作為返回值:柯里化(Currying)函數;
function add(a) {return function(b) {return a + b;}; } const add5 = add(5); // 返回函數,可延遲調用 add5(3); // 8
作用:
- 抽象通用邏輯:如防抖函數
debounce(fn)
接收函數并返回包裝后的函數; - 實現函數組合:通過
compose
將多個函數串聯執行(如compose(f, g, h)(x) = f(g(h(x)))
)。
4.?函數組合(Function Composition)
將多個函數組合成一個新函數,前一個函數的輸出作為后一個函數的輸入,分為compose
(從右向左)和pipe
(從左向右):
// 從右向左組合:compose(f, g, h)(x) = f(g(h(x)))
function compose(...fns) {return x => fns.reverse().reduce((acc, fn) => fn(acc), x);
}// 示例:先轉大寫,再添加前綴,最后輸出
const process = compose(str => `結果:${str}`,str => str.toUpperCase(),str => str.trim()
);
process(' hello '); // "結果:HELLO"
優勢:將復雜邏輯拆解為簡單函數,提高代碼復用性和可維護性。
三、與面向對象編程(OOP)的對比
特性 | 函數式編程 | 面向對象編程 |
---|---|---|
核心組織單元 | 函數(無狀態) | 對象(包含狀態和方法) |
數據操作 | 不可變,返回新數據 | 可變,修改對象狀態 |
副作用處理 | 顯式控制,盡量避免 | 隱式存在,依賴對象生命周期 |
并發性支持 | 天然安全(無共享狀態) | 需通過鎖等機制保證安全 |
四、實際應用場景
- 狀態管理:如 Redux 遵循 FP 原則,通過純函數
reducer
處理狀態變更,確保可預測性; - 異步處理:通過
Promise
組合、async/await
(本質是函數組合)處理異步邏輯; - 數組 / 集合操作:使用
map
、filter
、reduce
等高階函數替代命令式循環; - 工具庫開發:Lodash、Ramda 等庫提供大量 FP 工具函數,簡化數據處理;
- 性能優化:不可變數據配合淺比較(如 React 的
useState
依賴項檢查),減少不必要的重新渲染。
五、優缺點分析
- 優點:
- 代碼更簡潔、可測試性強;
- 易于并行處理和推理邏輯;
- 避免共享狀態帶來的競態條件。
- 缺點:
- 學習曲線較陡(需理解柯里化、函子等概念);
- 頻繁創建新數據可能帶來內存開銷(現代 JS 引擎已優化);
- 復雜業務場景下可能過度嵌套函數組合。
六、如何在 JavaScript 中實踐 FP
- 優先使用純函數:避免修改參數,用返回值替代賦值;
- 擁抱不可變數據:使用
const
聲明變量,避免push
、splice
等 mutable 方法; - 利用高階函數:用
map/filter/reduce
替代循環,用柯里化封裝通用邏輯; - 使用 FP 工具庫:如 Ramda 提供
pipe
、compose
、curry
等函數,降低手動實現成本。
函數式編程并非完全替代面向對象編程,而是作為一種編程思維,幫助開發者以更抽象、更可靠的方式組織代碼,尤其在處理復雜數據變換和異步流程時優勢顯著。
防抖(Debounce)和節流(Throttle)的區別是什么?各自的使用場景有哪些?
防抖和節流是前端性能優化中處理高頻事件(如窗口 Resize、輸入框輸入、按鈕快速點擊)的核心技術,二者通過延遲執行函數減少回調次數,但實現邏輯和應用場景有顯著差異。
一、核心原理對比
特性 | 防抖(Debounce) | 節流(Throttle) |
---|---|---|
觸發時機 | 在事件觸發后,等待指定時間內無后續事件才執行 | 按固定時間間隔周期性執行事件處理函數 |
核心邏輯 | 使用定時器,若期間再次觸發則重置定時器 | 使用定時器或時間戳,控制函數執行頻率 |
執行次數 | 事件停止觸發后執行一次 | 事件持續觸發時按間隔執行多次 |
典型場景 | 輸入框實時搜索、按鈕防重復點擊 | 窗口 Resize、滾動事件、canvas 畫筆實時繪制 |
二、防抖(Debounce)的實現與場景
1. 基本實現(定時器版)
function debounce(fn, delay = 300) {let timer = null;return function(...args) {if (timer) clearTimeout(timer); // 清除前一個定時器timer = setTimeout(() => {fn.apply(this, args); // 延遲delay ms后執行timer = null; // 重置定時器}, delay);};
}
關鍵點:每次觸發事件都重置定時器,確保只有最后一次觸發且間隔超過delay
時才執行函數。
2. 立即執行版(leading edge)
function debounce(fn, delay = 300, immediate = false) {let timer = null;return function(...args) {const context = this;if (immediate && !timer) { // 首次觸發時立即執行fn.apply(context, args);timer = setTimeout(() => {timer = null; // 清空定時器,允許下次立即執行}, delay);} else if (!immediate) { // 非立即執行模式,等待延遲clearTimeout(timer);timer = setTimeout(() => {fn.apply(context, args);timer = null;}, delay);}};
}
區別:通過immediate
參數控制是否在首次觸發時立即執行,適用于需要 “先執行、再防抖” 的場景(如搜索按鈕點擊)。
3. 典型應用場景
- 輸入框實時搜索:用戶輸入時實時請求接口,但避免每秒發起數十次請求。例如,用戶停止輸入 300ms 后再觸發搜索:
const searchInput = document.getElementById('search'); searchInput.addEventListener('input', debounce((e) => {fetch(`/api/search?q=${e.target.value}`); }, 300));
- 按鈕防重復點擊:防止用戶快速點擊提交按鈕導致多次請求,確保點擊間隔超過指定時間:
submitButton.addEventListener('click', debounce(() => {// 提交表單邏輯 }, 1000, true)); // immediate=true,點擊立即執行,1秒內無法再次觸發
- 窗口 Resize 后的布局調整:避免 Resize 過程中頻繁計算布局,僅在 Resize 停止后執行一次:
window.addEventListener('resize', debounce(() => {calculateLayout(); }, 200));
三、節流(Throttle)的實現與場景
1. 時間戳版(leading edge)
function throttle(fn, limit = 300) {let lastCallTime = 0;return function(...args) {const now = Date.now();if (now - lastCallTime >= limit) { // 超過間隔時間則執行fn.apply(this, args);lastCallTime = now; // 更新最后執行時間}};
}
特點:首次觸發時立即執行,之后按limit
間隔執行,事件結束后若剩余時間不足間隔則不再執行。
2. 定時器版(trailing edge)
function throttle(fn, limit = 300) {let timer = null;return function(...args) {const context = this;if (!timer) { // 首次觸發時設置定時器timer = setTimeout(() => {fn.apply(context, args);timer = null; // 執行后清空定時器,允許下次觸發}, limit);}};
}
特點:首次觸發不立即執行,等待limit
時間后執行,事件持續觸發時按間隔執行,結束后會執行最后一次觸發的回調(與時間戳版互補)。
3. 混合版(同時支持 leading 和 trailing)
function throttle(fn, limit = 300) {let lastCallTime = 0;let timer = null;return function(...args) {const now = Date.now();const context = this;if (now - lastCallTime >= limit) { // 處理leading edgefn.apply(context, args);lastCallTime = now;} else if (!timer) { // 處理trailing edgetimer = setTimeout(() => {fn.apply(context, args);lastCallTime = Date.now();timer = null;}, limit - (now - lastCallTime)); // 計算剩余時間}};
}
優勢:兼顧立即執行和結束后執行,確保事件開始和結束時都能觸發回調。
4. 典型應用場景
- 滾動事件處理:如無限滾動加載數據時,避免每秒觸發數十次
scroll
事件:window.addEventListener('scroll', throttle(() => {if (isBottomReached()) loadMoreData(); }, 200));
- canvas 畫筆實時繪制:用戶拖動鼠標時實時繪制路徑,通過節流控制繪制頻率,避免幀率下降:
canvas.addEventListener('mousemove', throttle((e) => {drawPoint(e.clientX, e.clientY); }, 50)); // 每秒20次繪制
- 高頻點擊統計:記錄用戶點擊次數時,按固定間隔上報數據(如每秒一次),而非每次點擊都上報:
button.addEventListener('click', throttle(() => {sendClickStatistics(); }, 1000));
四、選擇策略與注意事項
- 根據業務需求選擇:
- 若需要 “等待操作停止后執行”(如輸入完成后搜索),用防抖;
- 若需要 “控制操作頻率”(如持續拖動時實時反饋),用節流。
- 參數調優:
- 防抖的
delay
需平衡用戶體驗與性能(如搜索場景建議 300-500ms); - 節流的
limit
需根據操作頻率和計算復雜度調整(如動畫相關操作建議 16ms/60fps)。
- 防抖的
- 內存泄漏風險:
- 防抖函數中若引用 DOM 元素,需在組件卸載時手動清除定時器(如 React 中使用
useEffect
的清理函數)。
- 防抖函數中若引用 DOM 元素,需在組件卸載時手動清除定時器(如 React 中使用
手寫防抖和節流函數,并舉例說明調用后的執行結果
防抖(Debounce)和節流(Throttle)是前端性能優化的重要技術,用于處理高頻觸發的事件(如滾動、輸入、點擊)。以下通過代碼實現并結合示例說明其執行邏輯。
防抖函數實現
防抖的核心是合并多次觸發為一次,通過定時器延遲執行,期間若再次觸發則重置定時器。
function debounce(fn, delay = 300) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); };
} // 示例:模擬搜索輸入
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => { console.log(`搜索: ${e.target.value}`);
}, 500));
執行結果:
- 當用戶連續輸入
hello
(每 100ms 輸入一個字符),期間不會觸發搜索。 - 輸入完成后停止 500ms,才會執行一次搜索操作,輸出
搜索: hello
。
節流函數實現
節流的核心是控制執行頻率,按固定間隔執行函數,通過時間戳或定時器實現。
// 時間戳版(立即執行)
function throttle(fn, limit = 300) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= limit) { fn.apply(this, args); lastTime = now; } };
} // 示例:窗口滾動事件
window.addEventListener('scroll', throttle(() => { console.log('滾動位置:', window.scrollY);
}, 200));
執行結果:
- 當用戶持續滾動頁面時,無論滾動速度多快,函數每 200ms 執行一次。
- 若滾動持續 1 秒,將觸發約 5 次輸出(1000ms / 200ms = 5)。
對比與選擇策略
場景 | 防抖(Debounce) | 節流(Throttle) |
---|---|---|
搜索輸入實時聯想 | ? 停止輸入后觸發一次 | ? 按間隔觸發可能顯示不全 |
按鈕防重復點擊 | ? 避免短時間內多次觸發 | ? 仍會按間隔執行多次 |
窗口 Resize 事件 | ? 只在調整完成后計算布局 | ? 按間隔更新布局(需權衡) |
滾動加載更多數據 | ? 可能錯過臨界點 | ? 持續滾動時按頻率加載 |
在實際開發中,若需合并高頻操作為一次有效執行,選擇防抖;若需控制操作的最大頻率,選擇節流。兩者結合使用(如先節流收集數據,再防抖批量提交)可應對更復雜的場景。
元素垂直居中對齊的方式有哪些?
在 CSS 中實現元素垂直居中是常見需求,可通過多種布局模型實現。以下介紹主流方案及適用場景,重點解析 Flex 布局中justify-content
與align-items
的區別。
一、行內元素垂直居中
適用于文本、圖片等行內元素(inline/inline-block):
- 單行文本:通過
line-height
等于容器高度實現。.container { height: 100px; line-height: 100px; /* 文本垂直居中 */ }
- 多行文本:結合
display: table-cell
與vertical-align: middle
。.container { display: table-cell; height: 100px; vertical-align: middle; /* 多行文本垂直居中 */ }
二、Flex 布局(現代方案)
通過display: flex
或display: inline-flex
實現,需區分主軸與交叉軸:
.container { display: flex; justify-content: center; /* 主軸(水平)居中 */ align-items: center; /* 交叉軸(垂直)居中 */
}
關鍵點:
justify-content
:控制主軸方向的對齊方式(默認水平方向),可選值包括flex-start
、center
、flex-end
、space-between
等。align-items
:控制交叉軸方向的對齊方式(默認垂直方向),可選值包括flex-start
、center
、flex-end
、stretch
等。
三、絕對定位方案
適用于固定寬高的元素:
.container { position: relative;
}
.element { position: absolute; top: 50%; left: 50%; width: 100px; height: 100px; margin-top: -50px; /* 向上偏移自身高度的一半 */ margin-left: -50px; /* 向左偏移自身寬度的一半 */
}
優化版:使用transform: translate(-50%, -50%)
,無需提前知道元素尺寸。
.element { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* 自動計算偏移 */
}
四、Grid 布局(二維居中)
通過place-items
簡寫屬性同時控制水平和垂直方向:
.container { display: grid; place-items: center; /* 等價于 justify-items: center + align-items: center */
}
五、對比與選擇策略
方案 | 優點 | 缺點 |
---|---|---|
Flex 布局 | 簡潔、響應式、兼容性好 | 需考慮主軸方向 |
絕對定位 + transform | 無需提前知道元素尺寸 | 依賴父元素定位 |
Grid 布局 | 二維居中能力更強 | 兼容性略差(IE 不支持) |
行內元素法 | 簡單直接 | 僅適用于行內元素 |
在實際開發中,優先使用 Flex 布局(如導航欄、卡片組件),若需精確控制二維空間(如彈窗居中)則選擇 Grid。絕對定位方案適用于需要脫離文檔流的場景,但需注意父元素的定位屬性。
簡述 position 屬性的取值及默認值
CSS 的position
屬性用于控制元素的定位方式,其取值決定了元素在文檔流中的表現形式。以下是各值的詳細說明及典型場景:
1.?static
(默認值)
元素按正常文檔流布局,top
、left
、right
、bottom
屬性無效。
.element { position: static; /* 默認值,無需顯式聲明 */
}
特點:元素的位置由 HTML 結構決定,無法通過定位屬性調整。
2.?relative
(相對定位)
元素相對于其正常位置進行定位,仍占據原文檔流空間。
.element { position: relative; top: 20px; /* 向下偏移20px */ left: 10px; /* 向右偏移10px */
}
典型場景:
- 作為
absolute
子元素的定位容器。 - 微調元素位置(如圖標與文本的對齊)。
3.?absolute
(絕對定位)
元素相對于最近的已定位祖先元素(即position
值不為static
的元素)定位,脫離正常文檔流。
.container { position: relative; /* 作為參考容器 */
}
.element { position: absolute; top: 0; right: 0; /* 右上角定位 */
}
特點:
- 若沒有已定位的祖先元素,則相對于初始包含塊(通常是瀏覽器窗口)。
- 寬度默認由內容決定,除非顯式設置。
4.?fixed
(固定定位)
元素相對于瀏覽器視口定位,滾動時位置保持不變。
.element { position: fixed; bottom: 20px; right: 20px; /* 右下角固定位置 */
}
典型場景:
- 懸浮按鈕(如返回頂部)。
- 固定導航欄。
5.?sticky
(粘性定位)
元素在滾動時初始按正常文檔流布局,到達指定位置后變為fixed
定位。
.header { position: sticky; top: 0; /* 滾動到頂部時固定 */
}
特點:
- 需指定
top
、left
、right
、bottom
中的至少一個值。 - 兼容性略差(IE/Edge 15 及以下不支持)。
6. 對比與應用場景
值 | 參考對象 | 是否脫離文檔流 | 滾動時表現 |
---|---|---|---|
static | 正常文檔流 | 否 | 隨文檔流滾動 |
relative | 自身正常位置 | 否 | 隨文檔流滾動 |
absolute | 最近已定位祖先 | 是 | 隨祖先元素滾動 |
fixed | 瀏覽器視口 | 是 | 固定不動 |
sticky | 正常文檔流 | 否(滾動到臨界點后是) | 臨界點前隨流滾動,之后固定 |
在實際開發中,relative
常作為定位容器,absolute
用于精確定位元素(如下拉菜單),fixed
用于全局交互元素,sticky
則適合需要臨時固定的內容(如表格表頭)。合理使用定位屬性可構建復雜的頁面布局,同時避免脫離文檔流導致的布局塌陷問題。
relative 和 absolute 定位分別相對于誰進行定位?
在 CSS 中,position: relative
和position: absolute
是兩種常用的定位方式,其定位基準和行為差異直接影響頁面布局。以下通過對比解析兩者的核心區別及應用場景。
relative(相對定位)的定位基準
relative
定位的元素相對于其正常文檔流中的位置進行偏移,仍占據原空間。
.box { position: relative; top: 20px; left: 30px; /* 相對于原位置向右下偏移 */
}
關鍵點:
- 無論是否設置偏移值,元素始終保留在文檔流中,不會影響其他元素的布局。
- 偏移方向由
top
、left
、right
、bottom
控制(正值表示向相反方向偏移)。
典型場景:
- 微調元素位置(如圖標與文本的對齊)。
- 作為
absolute
子元素的定位容器(自身無需偏移)。
absolute(絕對定位)的定位基準
absolute
定位的元素相對于最近的已定位祖先元素(即position
值不為static
的元素)進行定位,完全脫離文檔流。
<div class="container"> <!-- 需設置position: relative/absolute/fixed/sticky --> <div class="box"> <!-- 絕對定位元素 -->
</div>
.container { position: relative; /* 作為參考容器 */
}
.box { position: absolute; top: 10px; right: 10px; /* 相對于.container右上角偏移 */
}
關鍵點:
- 若沒有已定位的祖先元素,則相對于初始包含塊(通常是瀏覽器窗口或
<html>
元素)。 - 寬度默認由內容決定,除非顯式設置(如
width: 100%
)。
典型場景:
- 懸浮層(如下拉菜單、提示框)。
- 絕對定位的廣告組件。
- 響應式布局中的元素重疊效果。
對比與注意事項
特性 | relative | absolute |
---|---|---|
參考對象 | 自身正常位置 | 最近的已定位祖先元素 |
文檔流占用 | 保留 | 脫離 |
對兄弟元素的影響 | 無 | 可能導致布局塌陷 |
默認寬度 | 由內容或容器決定 | 由內容決定 |
常見用途 | 微調位置、作為定位容器 | 創建浮動元素、覆蓋效果 |
實戰案例:相對定位容器 + 絕對定位子元素
<div class="card"> <img src="image.jpg" alt="Product"> <div class="badge">NEW</div> <!-- 絕對定位的標簽 -->
</div>
.card { position: relative; /* 容器需相對定位 */ width: 200px; height: 200px;
}
.badge { position: absolute; top: 10px; right: 10px; /* 相對于.card右上角定位 */ background: red;
}
常見誤區
- 未設置參考容器:若直接對元素使用
absolute
而不指定已定位的祖先元素,元素會相對于視口定位,導致布局錯亂。 - 過度使用絕對定位:脫離文檔流可能導致父元素高度塌陷,需謹慎處理。
- 混淆偏移方向:
top: 10px
表示向下偏移,而非向上(與直覺相反)。
理解relative
和absolute
的定位基準是構建復雜布局的基礎,合理組合兩者可實現如模態框、導航菜單、卡片徽章等常見 UI 組件,同時避免布局失控。
實現兩欄布局(左側寬度不固定,右側自適應)的方法有哪些?
兩欄布局是前端常見需求,其中左側寬度由內容決定(不固定),右側自適應剩余空間。以下介紹主流實現方案及適用場景,結合代碼示例說明。
一、浮動(Float)布局
利用float
使左側元素脫離文檔流,右側通過margin-left
讓出空間。
<div class="container"> <div class="left">左側內容</div> <div class="right">右側自適應內容</div>
</div>
.left { float: left;
}
.right { margin-left: 100px; /* 需與左側寬度匹配 */
}
局限性:需預先知道左側寬度,否則右側會換行。
二、Flex 布局(推薦)
通過flex
實現自動分配空間,右側使用flex: 1
占滿剩余寬度。
.container { display: flex;
}
.left { /* 寬度由內容決定 */
}
.right { flex: 1; /* 自適應剩余空間 */
}
優勢:
- 簡潔高效,無需關心左側實際寬度。
- 支持響應式設計(如通過
media query
調整為垂直布局)。
三、Grid 布局
通過grid-template-columns
定義列寬,右側使用1fr
表示剩余空間。
.container { display: grid; grid-template-columns: auto 1fr; /* 左側自動,右側占滿剩余 */
}
優勢:
- 二維布局能力更強,支持多行列控制。
- 代碼更簡潔,無需額外設置。
四、絕對定位 + 負邊距
左側絕對定位,右側通過負邊距抵消左側寬度。
.container { position: relative;
}
.left { position: absolute; left: 0; top: 0;
}
.right { margin-left: 100px; /* 需與左側寬度匹配 */
}
局限性:需預先知道左側寬度,且父容器需設置高度。
五、表格布局
通過display: table-cell
實現類似表格的布局。
.container { display: table; width: 100%;
}
.left, .right { display: table-cell;
}
.right { width: 100%; /* 強制右側占滿剩余空間 */
}
兼容性:IE8 + 支持,但語義不夠現代。
六、對比與選擇策略
方案 | 優點 | 缺點 | 兼容性 |
---|---|---|---|
Flex 布局 | 代碼簡潔,自動適應寬度 | 需考慮瀏覽器兼容性 | IE10+ |
Grid 布局 | 二維控制能力更強 | 兼容性略差(IE 不支持) | 現代瀏覽器 |
浮動布局 | 兼容性好 | 需固定左側寬度 | 全兼容 |
表格布局 | 無需固定寬度 | 語義不明確 | IE8+ |
七、響應式優化
在小屏幕設備上,可通過媒體查詢將兩欄轉為垂直布局:
@media (max-width: 768px) { .container { flex-direction: column; /* Flex布局轉為垂直 */ } .left, .right { width: 100%; /* 寬度恢復為100% */ }
}
八、實戰案例:導航欄 + 內容區
<div class="app"> <nav class="sidebar"> <!-- 導航菜單,寬度由內容決定 --> </nav> <main class="content"> <!-- 主要內容,自適應寬度 --> </main>
</div>
.app { display: flex; height: 100vh;
}
.sidebar { width: 200px; /* 或由內容決定 */
}
.content { flex: 1; /* 占滿剩余空間 */ overflow: auto; /* 內容過多時顯示滾動條 */
}
在實際開發中,優先使用 Flex 布局(兼容性好且簡潔),若需更復雜的二維控制則選擇 Grid。浮動和表格布局適用于兼容性要求高的場景,但需注意其局限性。響應式設計應作為必備優化,確保在不同設備上均有良好表現。
如何讓一行內的三個元素等間隔排列?
實現一行內三個元素等間隔排列的核心是利用 CSS 布局屬性控制元素間距和對齊方式,常見方法包括Flex 布局、Grid 布局和傳統盒模型 + text-align等,不同場景下需結合兼容性和需求選擇。
1.?Flex 布局(推薦)
Flex 布局是最靈活的方案,通過justify-content
屬性控制主軸方向的間隔,配合space-between
或space-around
實現不同效果:
justify-content: space-between
:元素兩端對齊,中間間隔相等(兩側無間隔)。justify-content: space-around
:元素周圍間隔相等(兩側間隔為中間的一半)。justify-content: space-evenly
(CSS3 新增):所有間隔完全相等(包括兩側)。
示例代碼:
.container { display: flex; justify-content: space-between; /* 或 space-around/space-evenly */ width: 80%; /* 容器寬度,避免撐滿整行 */ margin: 0 auto; /* 居中容器 */
}
.item { padding: 10px 20px; background: #f0f0f0;
}
效果說明:三個.item
元素在容器內水平等間隔排列,space-between
適用于兩側需要貼邊的場景,space-around
則適合元素周圍均勻留白的需求。
2.?Grid 布局(適用于二維場景)
Grid 布局通過網格軌道分配空間,利用justify-items
控制單元格水平對齊,配合grid-template-columns
定義列寬:
.container { display: grid; grid-template-columns: repeat(3, 1fr); /* 三列等寬 */ gap: 20px; /* 列間距 */ width: 80%; margin: 0 auto;
}
關鍵點:1fr
表示等比例分配剩余空間,gap
屬性設置列與列之間的間隔,實現視覺上的等間隔效果。此方法適合需要同時控制行列間隔的復雜布局。
3.?傳統盒模型 + text-align(兼容 IE)
通過給父元素設置text-align: justify
(兩端對齊),并在最后一個子元素后添加偽元素模擬換行,使前三個元素均勻分布:
.container { width: 80%; margin: 0 auto; text-align: justify; /* 兼容IE的hack */ font-size: 0; /* 消除行內元素間的空格 */
}
.item { display: inline-block; padding: 10px 20px; background: #f0f0f0; font-size: 16px; /* 恢復字體大小 */
}
.container::after { content: ''; display: inline-block; width: 100%;
}
原理:text-align: justify
會使行內元素兩端對齊,但僅當內容超過一行時生效,因此需要偽元素撐開容器強制對齊。此方法兼容性好,但代碼復雜度較高。
4.?絕對定位 + 計算(適用于固定寬度元素)
若元素寬度固定,可通過絕對定位計算每個元素的位置:
.container { position: relative; width: 80%; height: 100px; margin: 0 auto;
}
.item { position: absolute; width: 100px; height: 100px; background: #f0f0f0;
}
.item:nth-child(1) { left: 0; }
.item:nth-child(2) { left: 50%; transform: translateX(-50%); } /* 居中 */
.item:nth-child(3) { right: 0; }
限制:元素寬度需已知,且無法自適應內容變化,適合固定尺寸的場景。
關鍵對比
方法 | 兼容性 | 靈活性 | 適用場景 |
---|---|---|---|
Flex 布局 | 現代瀏覽器 | 高(自適應) | 大多數場景,推薦使用 |
Grid 布局 | 現代瀏覽器 | 高(二維) | 需要行列對齊的復雜布局 |
傳統盒模型 | IE8+ | 中 | 兼容舊版瀏覽器 |
絕對定位 | 全兼容 | 低(固定寬) | 已知元素尺寸的簡單布局 |
注意事項
- 空白節點問題:行內元素(如
inline-block
)之間的空格會被解析為字符間距,可通過父元素font-size: 0
消除,子元素再恢復字體大小。 - 垂直對齊:若需元素垂直居中,可配合
align-items: center
(Flex 布局)或vertical-align: middle
(行內元素)使用。 - 響應式設計:結合媒體查詢調整容器寬度或布局方式,使排列在不同屏幕尺寸下保持合理。
通過上述方法,可根據具體需求選擇最適合的等間隔排列方案,其中 Flex 布局因簡潔性和強大功能成為主流選擇。
實現三列布局,元素一行顯示,多余元素自動折行,且折行后保持相同縱列對齊。
實現三列布局并保證折行后縱列對齊,需借助 CSS 的彈性布局(Flex)或網格布局(Grid),結合換行屬性和軌道對齊功能。以下是具體方案及原理分析:
方案一:Flex 布局(推薦)
Flex 布局通過flex-wrap
允許元素換行,并利用flex-basis
控制列寬基準,配合align-content
調整換行后縱列對齊。
核心思路:
- 設置父容器為 Flex 容器,啟用換行(
flex-wrap: wrap
)。 - 定義子元素的基準寬度(如
flex-basis: 33.33%
),確保每行最多三列。 - 通過
justify-content
控制行內水平對齊,align-content
控制換行后縱列垂直對齊(如space-between
避免列間距變化)。
代碼示例:
.container { display: flex; flex-wrap: wrap; justify-content: flex-start; /* 行內左對齊 */ align-content: flex-start; /* 換行后縱列頂部對齊 */ width: 100%; gap: 10px; /* 元素間距 */
}
.item { flex-basis: calc(33.33% - 20px); /* 減去兩倍間距,避免超出容器 */ max-width: calc(33.33% - 20px); /* 限制最大寬度 */ padding: 20px; background: #f0f0f0; box-sizing: border-box; /* 包含padding計算寬度 */
}
效果解析:
- 當容器寬度足夠時,每行顯示三列,元素寬度由
flex-basis
計算得出(33.33%
減去左右間距)。 - 容器寬度不足時,多余元素自動換行,新行與前一行保持相同列數和寬度,實現縱列對齊。
gap
屬性統一元素間距,避免換行后間距錯位。
進階優化:等間距對齊
若需每行元素左右兩端對齊(類似 justify-content: space-between),可結合偽元素模擬彈性空間:
.container { justify-content: space-between; /* 行內兩端對齊 */
}
.container::after { content: ''; flex-grow: 1; /* 占據剩余空間,確保最后一行不足三列時仍對齊 */
}
此技巧可使最后一行不足三列的元素仍保持兩端對齊,避免左對齊導致的縱列錯位。
方案二:Grid 布局(二維對齊更精準)
Grid 布局通過grid-template-columns
定義固定列數,grid-auto-rows
控制行高,自動換行時列軌道會嚴格對齊。
核心思路:
- 父容器設為 Grid 布局,定義三列等寬軌道(
grid-template-columns: repeat(3, 1fr)
)。 - 啟用自動換行(隱式網格),通過
gap
設置行列間距。 - 利用
justify-items
和align-items
控制單元格內容對齊。
代碼示例:
.container { display: grid; grid-template-columns: repeat(3, 1fr); /* 三列等寬 */ gap: 10px; /* 行列間距 */ width: 100%;
}
.item { padding: 20px; background: #f0f0f0;
}
優勢:
- Grid 布局天然支持二維對齊,換行后列軌道會嚴格繼承第一行的寬度,無需額外處理。
1fr
單位會自動分配剩余空間,適應不同內容高度的元素。- 缺點是 IE 不支持,需配合 Polyfill 或放棄舊版瀏覽器兼容。
方案三:浮動布局(兼容舊版瀏覽器)
利用浮動和清除機制實現三列布局,但需手動計算寬度并處理高度塌陷問題,適合需要兼容 IE8 + 的場景。
核心思路:
- 子元素設置浮動(
float: left
),寬度設為33.33%
并減去間距。 - 父容器添加
overflow: hidden
清除浮動影響。 - 通過
margin
或padding
控制元素間距。
代碼示例:
.container { width: 100%; overflow: hidden; /* 清除浮動 */
}
.item { float: left; width: calc(33.33% - 20px); margin: 0 10px 20px; /* 左右間距10px,底部間距20px */ padding: 20px; background: #f0f0f0;
}
局限性:
- 浮動布局無法自動感知容器寬度變化,需手動計算寬度與間距的關系。
- 換行后元素可能因高度不同導致縱列錯位,需額外處理(如等高布局
flex-shrink: 0
)。
關鍵對比與適用場景
布局方式 | 兼容性 | 縱列對齊精度 | 代碼復雜度 | 動態內容適應性 |
---|---|---|---|---|
Flex 布局 | 現代瀏覽器 | 高(需輔助技巧) | 中 | 優(自適應寬度) |
Grid 布局 | 現代瀏覽器 | 極高 | 低 | 優(二維對齊) |
浮動布局 | IE8+ | 中(需手動調整) | 高 | 差(固定寬度) |
注意事項
- 盒模型處理:使用
box-sizing: border-box
確保padding
和border
不影響元素計算寬度。 - 間距一致性:通過
gap
(Flex/Grid)或統一margin
值避免間距錯亂。 - 響應式設計:結合媒體查詢,在小屏幕下調整為兩列或單列布局,提升移動端體驗:
@media (max-width: 768px) { .item { flex-basis: calc(50% - 20px); } /* 兩列布局 */ }
通過 Flex 或 Grid 布局,可高效實現三列自動換行且縱列對齊的效果,其中 Grid 布局因二維特性更適合復雜場景,而 Flex 布局在兼容性和靈活性之間取得平衡,是現代項目的首選方案。
用 CSS 實現九宮格布局,要求鼠標懸停時邊框變為 1px 紅色實線。
實現九宮格布局并添加懸停邊框效果,可結合Grid 布局或Flex 布局快速生成網格結構,再通過偽類選擇器:hover
動態修改邊框樣式。以下是具體實現步驟和代碼示例:
方案一:Grid 布局(推薦)
Grid 布局是最適合二維網格的方案,通過grid-template-columns
和grid-template-rows
定義三行三列結構,配合gap
設置單元格間距。
核心步驟:
- 創建父容器并設為 Grid 布局,定義 3×3 網格軌道。
- 子元素默認填充網格單元格,通過
padding
或background
區分格子。 - 使用
:hover
選擇器為子元素添加紅色邊框,注意邊框會覆蓋原有間距,需通過box-sizing
或調整gap
避免布局偏移。
代碼示例:
/* 基礎布局 */
.grid-container { display: grid; grid-template-columns: repeat(3, 1fr); /* 三列等寬 */ grid-template-rows: repeat(3, 100px); /* 三行等高,可改為auto適應內容 */ gap: 5px; /* 單元格間距,需與邊框寬度協調 */ width: 320px; /* 總寬度 = 3×100px + 2×5px×2(左右間距) */ margin: 20px auto; background: #f0f0f0; /* 父容器背景,襯托間距 */
}
.grid-item { background: #fff; display: flex; /* 方便內容居中 */ align-items: center; justify-content: center; font-size: 1.2em; transition: border 0.3s ease; /* 添加過渡效果 */ box-sizing: border-box; /* 使邊框不影響尺寸 */ border: 1px solid #e0e0e0; /* 默認邊框 */
}
/* 懸停效果 */
.grid-item:hover { border-color: #ff0000; /* 紅色邊框 */ border-width: 1px; border-style: solid; /* 可選:提升層級避免遮擋 */ z-index: 1;
}
效果解析:
repeat(3, 1fr)
生成三列等寬軌道,gap: 5px
設置單元格之間的間距,父容器背景色顯示間距效果。- 子元素默認邊框為淺灰色,懸停時變為紅色,
transition
屬性使顏色變化平滑。 box-sizing: border-box
確保邊框寬度包含在元素尺寸內,避免懸停時布局跳動。
優化點:若希望懸停時邊框覆蓋間距(即相鄰格子邊框合并),可移除gap
并通過父容器border-collapse
模擬,但會增加復雜度,建議保留間距以清晰區分單元格。
方案二:Flex 布局 + 嵌套容器
通過 Flex 布局實現行級排列,每行包含三個子元素,配合flex-wrap: wrap
換行生成九宮格。
代碼示例:
.flex-container { display: flex; flex-wrap: wrap; width: 320px; margin: 20px auto;
}
.flex-item { width: 100px; height: 100px; margin: 5px; /* 模擬間距 */ background: #fff; display: flex; align-items: center; justify-content: center; border: 1px solid #e0e0e0; transition: border 0.3s ease; box-sizing: border-box;
}
.flex-item:hover { border-color: #ff0000;
}
局限性:
- 間距通過
margin
實現,懸停時邊框會覆蓋margin
,導致相鄰元素間距縮小(視覺上邊框疊加)。 - 需手動計算寬度和間距(如
width: 100px + 2×5px margin
),靈活性低于 Grid 布局。
方案三:傳統定位 + 浮動(兼容舊版瀏覽器)
利用浮動和清除浮動實現九宮格,適合需要兼容 IE8 + 的場景,但代碼復雜度較高:
.float-container { width: 320px; margin: 20px auto;
}
.float-item { float: left; width: 100px; height: 100px; margin: 5px; background: #fff; border: 1px solid #e0e0e0; transition: border 0.3s ease; box-sizing: border-box;
}
.float-container::after { content: ''; display: block; clear: both;
}
.float-item:hover { border-color: red; }
注意事項:
- 需手動清除浮動(
::after
偽元素),避免父容器高度塌陷。 - 浮動布局下元素換行可能因高度不一致導致對齊問題,本例中固定高度可避免此問題。
關鍵細節處理
- 邊框過渡效果:通過
transition
屬性為border-color
添加漸變動畫,提升交互體驗:transition: border-color 0.2s, border-width 0.2s; /* 若改變邊框寬度 */
- 層級控制:懸停時通過
z-index
提升元素層級,避免被相鄰元素遮擋(尤其在邊框變粗時)。 - 響應式適配:可通過媒體查詢調整網格尺寸,例如在小屏幕下改為兩列布局:
@media (max-width: 480px) { .grid-container { grid-template-columns: repeat(2, 1fr); } }
行內元素(inline)和行內塊元素(inline-block)的區別是什么?行內元素能否設置內邊距(padding)?如何實現行內元素的垂直對齊?
行內元素與行內塊元素的區別源于 CSS 盒模型對元素的渲染規則,兩者在布局特性、樣式支持和對齊方式上有顯著差異。以下是詳細對比及實踐方法:
一、行內元素(inline)與行內塊元素(inline-block)的核心區別
特性 | 行內元素(inline) | 行內塊元素(inline-block) |
---|---|---|
布局表現 | 多個元素在同一行排列,不會自動換行(需br 標簽) | 同樣支持一行排列,但可設置寬高,多余元素自動換行 |
寬高屬性 | 無法通過width /height 設置尺寸,由內容撐開 | 支持width /height ,也可由內容撐開 |
盒模型支持 | 左右margin /padding 有效,上下方向無效(不影響行高) | 上下左右margin /padding 均有效,會影響布局 |
邊框與背景 | 背景和邊框會應用,但上下邊框不影響行間距 | 邊框和背景完全包裹元素,占據物理空間 |
子元素兼容性 | 通常只能包含文本或行內元素,嵌套塊級元素可能導致渲染異常 | 可包含塊級元素,渲染規則與塊級元素一致 |
典型元素:
- 行內元素:
span
、a
、strong
、em
等。 - 行內塊元素:
img
、input
、button
、textarea
等(默認樣式為inline-block
)。
二、行內元素能否設置內邊距(padding)?
行內元素的padding
屬性是有效的,但表現與塊級元素不同:
- 左右方向:
padding-left
/padding-right
會增加元素水平空間,導致相鄰元素間距擴大,且背景色會填充左右內邊距區域。 - 上下方向:
padding-top
/padding-bottom
會增加元素上下內邊距,但不會影響行高或布局,即相鄰行的行內元素不會因上下內邊距而改變位置,背景色會覆蓋行高范圍(可能與相鄰行重疊)。
示例代碼:
.inline-element { display: inline; padding: 20px 40px; /* 上下20px,左右40px */ background: #f0f0f0; border: 1px solid #333;
}
效果說明:
- 左右內邊距使元素水平區域擴大,相鄰行內元素會被推開。
- 上下內邊距雖然增加了元素視覺高度,但不會改變行高(行高由
line-height
或字體大小決定),可能導致背景色超出文本行范圍。
三、如何實現行內元素的垂直對齊?
行內元素的垂直對齊基于基線對齊(baseline),基線是字體底部的假想線(如字母x
的底部)。可通過以下屬性調整對齊方式:
1.?vertical-align
屬性(核心方法)
該屬性控制行內元素相對于父元素基線或行內其他元素的垂直位置,常見取值:
baseline
(默認值):元素基線與父元素基線對齊(如文本與圖片底部不對齊的問題即源于此)。top
:元素頂部與行內最高元素的頂部對齊。middle
:元素中點與父元素基線向上偏移0.5em
的位置對齊(近似垂直居中)。bottom
:元素底部與行內最低元素的底部對齊。sub
/super
:下標 / 上標對齊,用于文本排版。px/em
等數值:相對于基線偏移指定距離(如vertical-align: 2px
使元素上移 2px)。
示例:圖片與文本垂直居中對齊
<span>文本</span>
<img src="example.jpg" style="vertical-align: middle;">
問題場景:當行內元素包含不同高度的子元素(如圖片和文本)時,默認baseline
對齊會導致圖片底部與文本基線對齊,視覺上圖片偏下,此時vertical-align: middle
可改善對齊效果。
2.?父元素設置line-height
通過調整父元素的line-height
使其等于自身高度,可使單行內的行內元素垂直居中(僅適用于單行文本):
.parent { height: 80px; line-height: 80px;
}
原理:行高等于容器高度時,文本行的基線會垂直居中,行內元素若為文本則自動居中;若為非文本元素(如span
包裹的塊級元素),需配合vertical-align: middle
。
3.?轉換為行內塊元素并使用 Flex 布局
將行內元素轉為inline-block
,并對父元素使用 Flex 布局實現垂直居中,此方法適用于多行或復雜內容:
.parent { display: inline-flex; /* 行內Flex容器 */ align-items: center; /* 垂直居中 */ height: 80px;
}
.child { display: inline-block;
}
優勢:無需依賴基線對齊,直接通過 Flex 的align-items
實現視覺居中,兼容性好且靈活。
四、常見問題與解決方案
-
行內塊元素間的空格問題
多個inline-block
元素的 HTML 代碼間若存在空格(換行 / 制表符),會被解析為字符間距,導致元素間出現縫隙。
解決方法:- 父元素設置
font-size: 0
,子元素恢復字體大小:.parent { font-size: 0; } .child { font-size: 16px; }
- 移除 HTML 代碼中的空格:
預覽
<span class="child"></span><span class="child"></span>
- 父元素設置
-
行內元素背景溢出問題
行內元素的上下padding
會使背景色超出文本行范圍,可能與相鄰行重疊。
解決方法:- 避免對行內元素設置過大的上下
padding
,或改用line-height
調整行高。 - 轉換為
inline-block
元素,此時上下padding
會占據物理空間,避免重疊。
- 避免對行內元素設置過大的上下
-
基線對齊與邊框的沖突
行內元素若包含邊框(如img
標簽默認有邊框),基線對齊可能導致元素底部與邊框底邊不一致。
解決方法:明確設置vertical-align: bottom
使元素底部與邊框底邊對齊。
Vue2 和 Vue3 的主要區別有哪些?
Vue3(Vue 3.x)是 Vue.js 的重大升級版本,相比 Vue2(2.x)在性能、語法、生態和底層實現上有顯著改進。以下是核心區別的詳細對比:
一、響應式系統:Proxy 替代 Object.defineProperty
-
Vue2:
使用Object.defineProperty
劫持對象的get
和set
方法實現響應式,但存在以下局限:- 無法監聽數組索引修改(如
arr[0] = newVal
)和對象新增 / 刪除屬性(需手動調用Vue.set
或this.$set
)。 - 深度響應式需遞歸遍歷嵌套對象,性能開銷較大,尤其在處理大型數據時。
- 無法監聽數組索引修改(如
-
Vue3:
基于 ES6 的Proxy
重構響應式系統,優勢包括:- 原生支持數組索引和對象屬性的增刪,無需額外 API(如直接修改
arr[0]
或新增obj.newProp
即可觸發更新)。 - 懶響應式:僅在訪問屬性時建立響應式聯系,避免無效代理,提升初始化性能。
- 更好的內存管理:通過
ReactiveMap
和WeakMap
實現響應式對象的緩存與釋放,減少內存泄漏。
- 原生支持數組索引和對象屬性的增刪,無需額外 API(如直接修改
代碼對比:
// Vue2:需手動處理數組索引更新
this.items.splice(0, 1, newItem); // 替代 arr[0] = newItem // Vue3:直接修改索引即可響應
this.items[0] = newItem; // 自動觸發視圖更新
二、組件 API:組合式 API(Composition API)的引入
-
Vue2:
采用選項式 API(Options API),邏輯按功能模塊(如data
、methods
、watch
)分塊,適合中小型項目,但在處理復雜邏輯時會導致代碼碎片化(如多個watch
和computed
分散在不同選項中)。 -
Vue3:
新增組合式 API(Composition API),通過函數(如setup
、ref
、reactive
)將相關邏輯組合在一起,優勢包括:- 邏輯復用更靈活:通過自定義 Hook 函數(如
useMousePosition
)復用邏輯,避免 Vue2 中 Mixin 的命名沖突和性能問題。 - 更好的類型推導:TypeScript 支持更友好,函數參數和返回值類型可直接推斷。
- 減少模板依賴:部分邏輯可在
setup
中直接處理,降低模板復雜度。
- 邏輯復用更靈活:通過自定義 Hook 函數(如
示例:組合式 API 實現計數器
// Vue3
import { ref, computed } from 'vue';
export default { setup() { const count = ref(0); const double = computed(() => count.value * 2); const increment = () => count.value++; return { count, double, increment }; }
};
對比 Vue2 選項式 API:
// Vue2
export default { data() { return { count: 0 }; }, computed: { double() { return this.count * 2; } }, methods: { increment() { this.count++; } }
};
三、組件生命周期鉤子:setup 替代 beforeCreate 和 created
-
Vue2:
生命周期鉤子包括beforeCreate
、created
、beforeMount
、mounted
等,在選項中直接定義。 -
Vue3:
setup
函數替代beforeCreate
和created
,作為組件邏輯的入口,在響應式數據初始化前執行。- 其他生命周期鉤子需在
setup
中通過onXX
函數注冊(如onMounted
、onUnmounted
),更貼近函數式編程風格:import { onMounted } from 'vue'; setup() { onMounted(() => { console.log('組件掛載完成'); }); }
四、性能優化:編譯優化與 Tree-shaking
-
靜態提升(Static Hoisting)
Vue3 編譯器會自動將模板中的靜態節點(如無響應式數據的 HTML)提升為常量,避免重復渲染,減少運行時開銷。預覽
<!-- Vue3編譯后會將靜態文本提升,僅動態節點保留響應式 --> <div> <span>靜態文本</span> <span>{{ dynamicText }}</span> </div>
-
片段(Fragments)與 Teleport
- Vue2 組件模板必須有一個根節點,Vue3 支持多根節點(片段),無需額外包裹
div
:預覽
<!-- Vue3合法模板 --> <header></header> <main></main> <footer></footer>
Teleport
組件允許將子節點渲染到 DOM 樹的其他位置(如模態框掛載到body
下),解決層級嵌套問題。
- Vue2 組件模板必須有一個根節點,Vue3 支持多根節點(片段),無需額外包裹
-
Tree-shaking 支持
Vue3 的 API 采用 ES 模塊導出(如import { ref } from 'vue'
),打包工具可直接移除未使用的代碼(如僅使用ref
時,未使用的reactive
會被搖樹優化掉),減小打包體積。
五、其他重要變化
特性 | Vue2 | Vue3 |
---|---|---|
全局 API | 通過Vue.xxx 調用(如Vue.component ) | 改為應用實例方法(如app.component ) |
自定義指令鉤子 | bind 、inserted 等 | 統一為beforeMount 、mounted 等,與組件生命周期對齊 |
異步組件 | 通過() => import('Component.vue') 定義 | 支持defineAsyncComponent 函數,參數更靈活 |
過渡動畫 | vue-transition 組件 | 內置Transition 和TransitionGroup ,支持更復雜的狀態過渡 |
TypeScript 支持 | 需額外配置聲明文件 | 內置類型定義,組合式 API 天然適配 TS |
六、生態與工具鏈
-
Vue Router 4.x:
適配 Vue3,支持組合式 API,路由守衛改為在setup
中通過useRoute
、useRouter
鉤子調用。 -
Vuex 4.x:
兼容 Vue3,同時推出輕量級狀態管理方案Pinia
,基于組合式 API 設計,逐步替代 Vuex。 -
開發工具:
Vue3 支持 Vite 快速構建(原生 ES 模塊,冷啟動速度比 Webpack 快數倍),搭配 HMR(熱更新)提升開發體驗。
升級建議與兼容性
- 新項目:優先使用 Vue3,充分利用組合式 API 和性能優化特性。
- 舊項目遷移:可通過
vue-compat
插件在 Vue3 中兼容 Vue2 的選項式 API,逐步重構為組合式邏輯。 - 注意點:Vue3 移除了
$attrs
的繼承行為(需顯式通過inheritAttrs: false
關閉),且v-model
在組件上的用法改為modelValue
和update:modelValue
(多模型支持)。
Vue3 通過響應式系統升級、組合式 API 和編譯優化,顯著提升了開發效率和應用性能,是現代前端項目的理想選擇。
常用的 Vue 父子組件通信方式有哪些?
在 Vue 中,父子組件通信是組件化開發的核心場景之一,不同的通信需求可選擇不同的方式實現。以下是常用的通信方法及特點:
1.?props 與 $emit:基礎單向數據流
- props:父組件通過標簽屬性向子組件傳遞數據,子組件通過?
props
?選項聲明接收的字段。這是單向綁定,父組件數據更新會觸發子組件重新渲染,但子組件無法直接修改 props,需通過事件通知父組件。<!-- 父組件 --> <ChildComponent :msg="parentMsg" /> <!-- 子組件 --> export default { props: { msg: String } }
- **:子組件通過emit (' 事件名 ', 數據)
觸發自定義事件,父組件在子組件標簽上用
@事件名 ="處理函數"` 監聽。<!-- 子組件 --> this.$emit('update', newVal) <!-- 父組件 --> <ChildComponent @update="handleUpdate" />
2.?自定義事件與 v-model:雙向綁定簡化
- 通過?
props
?和?$emit
?結合實現雙向綁定,Vue 提供?v-model
?語法糖。子組件需觸發?input
?事件并傳遞新值,父組件自動更新綁定的變量。<!-- 父組件 --> <ChildComponent v-model="inputValue" /> <!-- 子組件 --> export default { props: { modelValue: String }, methods: { changeValue(e) { this.$emit('input', e.target.value) } } }
3.?refs 與?parent/children:直接訪問實例
- refs:父組件通過?
ref
?為子組件添加引用(如?<ChildComponent ref="child" />
),然后通過?this.$refs.child
?直接訪問子組件實例,調用其方法或屬性。需注意這打破了單向數據流原則,僅適用于特殊場景。 - parent/children:子組件通過?
this.$parent
?訪問父組件實例,父組件通過?this.$children
?訪問所有子組件實例。但?$children
?是數組且順序不固定,需謹慎使用。
4.?Provide 與 Inject:跨層級通信(祖先與后代)
- 適用于深層嵌套組件間通信,避免逐層傳遞 props。祖先組件通過?
provide
?選項暴露數據或方法,后代組件通過?inject
?選項直接注入使用,無需在中間層級逐層傳遞。<!-- 祖先組件 --> export default { provide() { return { theme: this.theme, changeTheme: () => { /* ... */ } } } } <!-- 后代組件 --> export default { inject: ['theme', 'changeTheme'] }
注意:provide/inject
?是響應式的,若要保持響應性,需暴露?ref
?或?reactive
?對象。
5.?事件總線(Event Bus):非父子組件通信
- 創建一個全局的事件中心(如?
eventBus
?實例),組件通過?eventBus.$on('事件名', 回調)
?監聽事件,通過?eventBus.$emit('事件名', 數據)
?觸發事件。適用于非父子關系的組件通信,但在 Vue3 中更推薦使用 Composition API 配合全局狀態管理(如 Pinia)。// 全局事件總線 const eventBus = new Vue() // 組件 A 觸發事件 eventBus.$emit('data-change', newData) // 組件 B 監聽事件 eventBus.$on('data-change', (data) => { /* ... */ })
6.?全局狀態管理(Vuex/Pinia):復雜狀態共享
- 當多個組件需要共享狀態時,使用 Vuex(Vue2)或 Pinia(Vue3)進行集中管理。狀態存儲在全局 store 中,組件通過?
mapState
?映射狀態,通過 mutations/actions 修改狀態。適用于中大型項目,避免組件間多層級通信的復雜性。
選擇建議
- 簡單單向數據傳遞:優先使用 props/$emit 或 v-model。
- 跨層級通信:祖先與后代組件用 provide/inject,非父子組件用事件總線或全局狀態管理。
- 復雜狀態邏輯:使用 Vuex/Pinia 實現集中式管理。
- 避免濫用 refs/$parent:直接操作組件實例會降低代碼可維護性,僅在必要時使用(如操作 DOM 元素)。
通過合理選擇通信方式,可確保組件間數據流動清晰,提升代碼的可維護性和可測試性。
簡述 Vue3 的生命周期鉤子函數(如 setup、onMounted、onUnmounted 等)
Vue3 基于 Composition API 重構了生命周期機制,鉤子函數的使用方式與 Vue2 的選項式 API 有所不同。以下是 Vue3 中主要生命周期鉤子的作用、觸發時機及使用場景:
1.?setup:組合式 API 的入口
- 觸發時機:在組件創建之前,beforeCreate 和 created 鉤子之前執行,是 Composition API 的起點。
- 作用:
- 初始化響應式狀態(通過?
ref
?或?reactive
)。 - 注冊生命周期鉤子、事件監聽等。
- 返回對象或函數,供模板或其他鉤子使用。
- 初始化響應式狀態(通過?
- 注意:
- 無法訪問?
this
(組件實例未創建),需通過參數獲取 props 和 context。 - 若使用?
setup
,則不再需要選項式的?data
、methods
?等選項。
export default { setup(props, context) { // 響應式狀態 const count = ref(0) // 生命周期鉤子需在 setup 內調用 onMounted(() => { console.log('組件掛載完成') }) return { count } } }
- 無法訪問?
2.?掛載階段鉤子
- onBeforeMount:組件即將掛載到 DOM 前觸發,對應 Vue2 的?
beforeMount
。 - onMounted:組件掛載到 DOM 后觸發,可在此訪問真實 DOM,發送異步請求等。
3.?更新階段鉤子
- onBeforeUpdate:數據更新導致組件重新渲染前觸發,對應 Vue2 的?
beforeUpdate
。 - onUpdated:組件重新渲染完成后觸發,此時 DOM 已更新,可進行 DOM 操作(注意避免在此期間修改響應式數據,可能引發無限循環)。
4.?卸載階段鉤子
- onBeforeUnmount:組件即將卸載前觸發,對應 Vue2 的?
beforeDestroy
。 - onUnmounted:組件卸載后觸發,用于清理副作用(如清除定時器、解綁事件監聽等)。
5.?錯誤處理鉤子
- onErrorCaptured:捕獲組件內的錯誤(包括子組件錯誤),可用于錯誤日志上報。
setup() { onErrorCaptured((error, instance, info) => { console.error('捕獲到錯誤:', error, info) return true // 阻止錯誤繼續向上傳播 }) }
6.?與 Vue2 鉤子的對應關系
Vue2 選項式鉤子 | Vue3 組合式鉤子(需在 setup 中調用) |
---|---|
beforeCreate | 無(setup 替代其初始化邏輯) |
created | 無(setup 替代其初始化邏輯) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
使用注意事項
- 組合式 API 的靈活性:在?
setup
?中可按需引入生命周期鉤子,邏輯按功能分組,而非按生命周期階段排列,提升代碼可讀性。 - 副作用清理:在?
onUnmounted
?中清除?setup
?中創建的定時器、事件監聽器等,避免內存泄漏。例如:setup() { const timer = setInterval(() => { /* ... */ }, 1000) onUnmounted(() => clearInterval(timer)) }
- 避免重復調用:生命周期鉤子需在?
setup
?內直接調用,不要在函數或條件語句中嵌套調用,確保每個鉤子只注冊一次。
Vue3 的生命周期機制更貼合組合式開發模式,通過將邏輯拆分為獨立的鉤子函數,使代碼結構更清晰,尤其適合復雜組件的邏輯復用和測試。
描述父組件和子組件的完整生命周期執行流程
在 Vue 中,組件的生命周期執行順序與組件嵌套關系密切相關。父組件與子組件的生命周期鉤子會按照特定順序觸發,理解這一流程有助于調試和處理組件間的交互邏輯。以下是完整的執行流程分析(以 Vue3 為例,結合選項式和組合式 API):
一、組件初始化階段(掛載前)
-
父組件創建
- 觸發?beforeCreate(選項式 API,僅 Vue2 存在)或進入?
setup
(組合式 API,Vue2/3 均適用)。 - 在?
setup
?中初始化響應式狀態、注冊生命周期鉤子(如?onBeforeMount
)。
- 觸發?beforeCreate(選項式 API,僅 Vue2 存在)或進入?
-
子組件創建
- 父組件在模板中調用子組件時,子組件進入創建階段:
- 觸發子組件的?beforeCreate?或?
setup
。 - 子組件在?
setup
?中完成狀態初始化和鉤子注冊。
- 觸發子組件的?beforeCreate?或?
- 父組件在模板中調用子組件時,子組件進入創建階段:
-
父組件 props 傳遞
- 父組件將 props 數據傳遞給子組件,子組件接收并校驗 props。
-
父組件 beforeMount
- 觸發父組件的?onBeforeMount(組合式)或?
beforeMount
(選項式),此時組件尚未掛載到 DOM。
- 觸發父組件的?onBeforeMount(組合式)或?
-
子組件 beforeMount
- 觸發子組件的?onBeforeMount?或?
beforeMount
,父子組件均處于 “即將掛載” 狀態。
- 觸發子組件的?onBeforeMount?或?
二、組件掛載階段(插入 DOM)
-
子組件先掛載
- 子組件的虛擬 DOM 先渲染為真實 DOM,觸發?onMounted(組合式)或?
mounted
(選項式)。 - 此時子組件已可訪問真實 DOM,例如:
// 子組件 setup() { onMounted(() => { console.log('子組件 mounted') // 先輸出 }) }
- 子組件的虛擬 DOM 先渲染為真實 DOM,觸發?onMounted(組合式)或?
-
父組件后掛載
- 父組件的虛擬 DOM 渲染完成,包含子組件的 DOM 結構,觸發父組件的?onMounted?或?
mounted
。 - 父組件此時可訪問包含子組件的完整 DOM 樹:
// 父組件 setup() { onMounted(() => { console.log('父組件 mounted') // 后輸出 }) }
總結順序:
父 beforeCreate → 父 setup → 子 beforeCreate → 子 setup → 父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted
- 父組件的虛擬 DOM 渲染完成,包含子組件的 DOM 結構,觸發父組件的?onMounted?或?
三、組件更新階段(數據變化時)
當父組件或子組件的響應式數據變化時,會觸發重新渲染,流程如下:
-
父組件數據更新
- 父組件狀態變更,觸發?onBeforeUpdate(組合式)或?
beforeUpdate
(選項式)。
- 父組件狀態變更,觸發?onBeforeUpdate(組合式)或?
-
子組件接收更新后的 props
- 父組件傳遞的 props 變化,子組件觸發?onBeforeUpdate?或?
beforeUpdate
。
- 父組件傳遞的 props 變化,子組件觸發?onBeforeUpdate?或?
-
子組件先更新
- 子組件虛擬 DOM 重新渲染,觸發?onUpdated?或?
updated
。
- 子組件虛擬 DOM 重新渲染,觸發?onUpdated?或?
-
父組件后更新
- 父組件虛擬 DOM 重新渲染完成,觸發?onUpdated?或?
updated
。
總結順序:
父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
- 父組件虛擬 DOM 重新渲染完成,觸發?onUpdated?或?
四、組件卸載階段(移除 DOM)
當父組件或子組件被卸載(如通過?v-if
?控制顯示 / 隱藏)時:
-
父組件觸發卸載
- 父組件進入卸載流程,觸發?onBeforeUnmount?或?
beforeUnmount
。
- 父組件進入卸載流程,觸發?onBeforeUnmount?或?
-
子組件先卸載
- 子組件的所有生命周期鉤子優先執行清理邏輯,觸發?onUnmounted?或?
destroyed
。 - 例如,子組件中清除定時器:
setup() { const timer = setInterval(() => { /* ... */ }, 1000) onUnmounted(() => clearInterval(timer)) // 子組件先執行 }
- 子組件的所有生命周期鉤子優先執行清理邏輯,觸發?onUnmounted?或?
-
父組件后卸載
- 父組件完成卸載,觸發?onUnmounted?或?
destroyed
,此時父子組件的 DOM 已被移除。
總結順序:
父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted
- 父組件完成卸載,觸發?onUnmounted?或?
五、特殊場景:子組件內異步組件或動態組件
- 若子組件是異步組件(通過?
defineAsyncComponent
?創建)或動態組件(使用?<component :is="currentComponent"
),其生命周期鉤子會在組件實際加載或切換時觸發,順序遵循上述規則。 - 例如,異步子組件的?
mounted
?會在其加載完成并插入 DOM 后觸發,晚于父組件的?mounted
。
關鍵記憶點
- 掛載 / 卸載順序:子組件先于父組件完成掛載和卸載,即 “先子后父”。
- 更新順序:父組件先觸發更新鉤子,子組件后執行,但子組件實際更新完成早于父組件(虛擬 DOM diff 從子節點開始)。
- 組合式 API 與選項式 API 共存:在 Vue3 中,若同時使用選項式和組合式鉤子,組合式鉤子(如?
onMounted
)會在選項式鉤子(如?mounted
)之前觸發。
理解這些流程有助于在開發中合理安排邏輯,例如在父組件?mounted
?中訪問子組件狀態時,需確保子組件已掛載完成,或通過?ref
?引用配合?onMounted
?回調實現安全訪問。
watch 和 computed 在數據變化時的區別是什么?
在 Vue 中,watch
?和?computed
?是響應式系統的重要工具,用于處理數據變化后的邏輯,但兩者的設計理念和使用場景有顯著差異。以下從多個維度對比分析:
一、本質區別:被動監聽 vs 主動計算
-
computed:
- 基于依賴的緩存計算:內部維護一個緩存,只有當依賴的響應式數據(如?
data
、props
?中的屬性)發生變化時,才會重新計算結果。 - 自動更新視圖:計算結果會被模板或其他響應式函數依賴,當結果變化時自動觸發視圖更新。
- 適用于復雜邏輯的實時計算,例如表單聯動、數據格式化等。
- 基于依賴的緩存計算:內部維護一個緩存,只有當依賴的響應式數據(如?
-
watch:
- 被動監聽數據變化:監聽特定數據(如?
data
、props
、computed
?的值)的變化,當數據變化時執行回調函數。 - 可執行異步操作或副作用:如發送網絡請求、修改非響應式數據、操作 DOM 等。
- 適用于需要 “觀察” 數據變化并執行特定邏輯的場景,例如搜索框防抖、狀態變更通知等。
- 被動監聽數據變化:監聽特定數據(如?
二、觸發時機與執行方式
特性 | computed | watch |
---|---|---|
觸發條件 | 依賴數據變化時自動重新計算 | 監聽的數據變化時觸發回調 |
首次執行 | 首次渲染時觸發(需被模板引用) | 需配置?immediate: true ?才會在初始化時執行 |
執行次數 | 依賴不變則復用緩存結果,僅執行一次 | 每次監聽數據變化均執行 |
返回值 | 必須返回計算結果(用于響應式依賴) | 無強制返回值(可執行任意邏輯) |
異步支持 | 不支持(同步計算) | 支持(回調中可使用 async/await) |
示例對比:
<!-- computed 示例:實時計算總價 -->
<template> <div> 單價:{{ price }} 元<br> 數量:{{ count }} 件<br> 總價:{{ totalPrice }} 元 </div>
</template>
<script>
export default { data() { return { price: 100, count: 2 } }, computed: { totalPrice() { console.log('計算總價') // 僅在 price 或 count 變化時觸發 return this.price * this.count } }
}
</script>
<!-- watch 示例:監聽搜索詞并發起請求 -->
<template> <input v-model="searchKey" />
</template>
<script>
export default { data() { return { searchKey: '' } }, watch: { searchKey(newVal, oldVal) { if (newVal.trim()) { this.fetchData(newVal) // 異步請求 } }, immediate: true // 初始化時執行一次 }, methods: { async fetchData(key) { const result = await axios.get(`/api/search?key=${key}`) this.results = result.data } }
}
</script>
三、依賴管理與性能影響
-
computed 的依賴收集:
在計算函數中訪問的響應式數據會被自動收集為依賴,例如?totalPrice
?依賴?price
?和?count
。當且僅當這兩個值變化時,才會重新計算?totalPrice
,避免無效渲染,提升性能。 -
watch 的依賴顯式聲明:
監聽的數據源可以是單個屬性(如?searchKey
),也可以是復雜表達式(如?'obj.a + obj.b'
),但需注意:- 監聽對象屬性時,需使用深度監聽(
deep: true
)才能捕獲對象內部變化。 - 監聽數組時,默認只能捕獲數組引用的變化,無法檢測元素新增 / 刪除(需通過?
deep
?或特定方法觸發)。
- 監聽對象屬性時,需使用深度監聽(
深度監聽示例:
watch: { obj: { handler(newVal, oldVal) { // 監聽對象內部屬性變化 }, deep: true // 開啟深度監聽 }
}
四、適用場景總結
場景描述 | 推薦使用 | 原因 |
---|---|---|
復雜數據的實時計算(如過濾、求和) | computed | 自動緩存,避免重復計算,響應式依賴清晰 |
數據變化時觸發異步操作(如請求) | watch | 支持異步邏輯,可訪問新舊值 |
數據變化時執行多步操作或副作用 | watch | 可執行任意邏輯,如修改非響應式變量、操作 DOM |
初始化時執行一次邏輯 | watch(配 immediate) | computed ?需被引用才會執行,watch ?可通過配置直接初始化執行 |
監聽對象 / 數組的深層變化 | watch(配 deep) | computed ?無法直接處理深層依賴,需手動拆解為多個屬性 |
五、注意事項
- 避免濫用 computed:若計算邏輯中包含異步操作或副作用(如修改 DOM),會導致邏輯混亂,此時應使用?
watch
。 - watch 的性能優化:
- 監聽復雜對象時,優先拆解為多個簡單屬性監聽,避免?
deep: true
?帶來的性能開銷。 - 使用?
debounce
?或?throttle
?優化高頻觸發的監聽(如搜索框輸入)。
- 監聽復雜對象時,優先拆解為多個簡單屬性監聽,避免?
- 組合使用場景:
有時需要結合兩者,例如先用?computed
?處理數據轉換,再用?watch
?監聽轉換后的結果并執行副作用:computed: { formattedValue() { return this.rawValue.toUpperCase() } }, watch: { formattedValue(newVal) { this.logToConsole(newVal) } }
什么是 Vue3 的組合式 API(Composition API)?列舉常用的組合式 API(如 ref、reactive、watchEffect 等)
Vue3 的 ** 組合式 API(Composition API)** 是一種基于函數的組件開發模式,允許開發者通過組合不同的邏輯函數來組織組件代碼,而非依賴傳統的選項式 API(如?data
、methods
、computed
?等選項)。它解決了選項式 API 中邏輯復用困難、組件代碼碎片化等問題,使代碼更易維護、測試和復用。
一、核心思想:邏輯組合與復用
- 按功能分組:將相關邏輯(如數據獲取、DOM 操作、狀態管理)封裝為獨立的函數(稱為 “組合函數”),可在不同組件中重復使用,避免選項式 API 中同一功能邏輯分散在多個選項中的問題。
- 響應式系統底層統一:通過?
ref
?和?reactive
?創建響應式數據,watch
?和?watchEffect
?實現依賴監聽,使邏輯更貼近原生 JavaScript。 - 無 this 上下文依賴:組合函數中無需依賴組件實例?
this
,降低了代碼的隱性依賴,提升可讀性。
二、常用組合式 API 及功能
以下是 Vue3 中最常用的組合式 API,按功能分類說明:
1. 響應式數據創建
-
ref
:創建響應式引用- 用于創建單個響應式變量,可存儲任意類型(包括基本類型和對象)。
- 基本類型需通過?
.value
?訪問,對象類型會被自動轉為?reactive
?代理。
import { ref } from 'vue' const count = ref(0) count.value++ // 修改值
-
reactive
:創建響應式對象- 用于將普通對象轉為響應式代理,適用于復雜數據結構(如對象、數組)。
- 內部基于 ES6 Proxy 實現,可監聽對象屬性的新增、刪除和修改。
import { reactive } from 'vue' const state = reactive({ user: { name: 'Alice' }, list: [1, 2, 3] }) state.user.name = 'Bob' // 響應式更新
-
readonly
:創建只讀響應式數據- 接收?
ref
?或?reactive
?對象,返回一個只讀的代理,禁止修改原始數據。
const original = ref(10) const readonlyVal = readonly(original) // readonlyVal.value = 20 // 報錯:無法修改只讀屬性
- 接收?
2. 依賴監聽與副作用
-
watch
:顯式監聽響應式數據- 監聽單個或多個響應式數據源(
ref
、reactive
?屬性、計算值等),支持深度監聽對象 / 數組。 - 回調函數接收新值和舊值,可用于執行異步操作或副作用。
import { watch, ref } from 'vue' const searchKey = ref('') watch(searchKey, (newVal, oldVal) => { if (newVal.trim()) { fetchData(newVal) // 異步請求 } }, { immediate: true }) // 初始化時執行
- 監聽單個或多個響應式數據源(
-
watchEffect
:自動追蹤依賴的副作用- 無需顯式聲明監聽源,回調函數中使用的響應式數據會被自動收集依賴,依賴變化時重新執行回調。
- 適用于執行與響應式數據相關的副作用(如 DOM 更新、定時器、日志輸出)。
import { watchEffect, ref } from 'vue' const count = ref(0) watchEffect(() => { console.log('Count is:', count.value) // 依賴 count.value,自動追蹤 }) count.value = 1 // 觸發回調
-
watchPostEffect
:延遲執行副作用- 與?
watchEffect
?類似,但回調會在組件更新完成后執行,確保能訪問最新 DOM。
watchPostEffect(() => { // 這里可以安全地操作 DOM })
- 與?
3. 計算屬性與依賴收集
computed
:創建響應式計算屬性- 與選項式 API 中的?
computed
?功能一致,返回一個只讀的?ref
?對象,依賴變化時自動重新計算。
import { ref, computed } from 'vue' const a = ref(1) const b = ref(2) const sum = computed(() => a.value + b.value) // 依賴 a 和 b
- 與選項式 API 中的?
4. 生命周期鉤子
在組合式 API 中,生命周期鉤子通過獨立函數引入,需在?setup
?或組合函數中調用:
onBeforeMount
:組件掛載前觸發。onMounted
:組件掛載后觸發(可訪問 DOM)。onBeforeUpdate
:組件更新前觸發。onUpdated
:組件更新后觸發(DOM 已更新)。onBeforeUnmount
:組件卸載前觸發。onUnmounted
:組件卸載后觸發(清理副作用)。
import { onMounted, ref } from 'vue'
setup() { const timer = setInterval(() => { /* ... */ }, 1000) onUnmounted(() => clearInterval(timer)) // 卸載時清理定時器
}
5. 依賴注入與上下文
-
provide
?和?inject
:跨層級組件通信,替代選項式 API 中的?provide/inject
。// 父組件 import { provide, ref } from 'vue' provide('theme', ref('light')) // 子組件 import { inject } from 'vue' const theme = inject('theme')
-
useContext
:獲取當前組件的上下文(如?attrs
、slots
、emit
),替代選項式 API 中的?this.$attrs
?等。import { useContext } from 'vue' const { emit, slots } = useContext()
6. 其他實用工具
-
toRef
:為響應式對象屬性創建獨立 ref
從?reactive
?對象中提取屬性并轉為?ref
,保持與原始對象的響應式關聯:const state = reactive({ name: 'Alice' }) const nameRef = toRef(state, 'name') nameRef.value = 'Bob' // 會同步修改 state.name
-
toRefs
:批量轉換對象屬性為 refs
將?reactive
?對象的所有屬性轉為獨立的?ref
,方便解構賦值:const state = reactive({ x: 0, y: 0 }) const { x, y } = toRefs(state) // x 和 y 都是 ref 對象
-
shallowRef
/shallowReactive
:淺響應式
僅對第一層屬性進行響應式代理,適用于性能優化或無需深層監聽的場景:const shallowState = shallowReactive({ obj: { a: 1 } }) shallowState.obj.a = 2 // 不會觸發響應式更新
三、組合式 API 的優勢
- 邏輯復用更靈活:通過組合函數(如?
useFetch
、useTimer
)封裝可復用邏輯,避免選項式 API 中 Mixin 的命名沖突和隱性依賴問題。 - 代碼組織更清晰:按功能(如數據獲取、表單驗證)分組代碼,而非按生命周期階段排列,提升可讀性。
- 更好的 Tree-shaking:按需引入 API(如僅使用?
ref
?和?watch
),減少打包體積。 - 支持 TypeScript:類型推斷更友好,組合函數可顯式聲明參數和返回值類型。
四、與選項式 API 的對比
特性 | 組合式 API | 選項式 API |
---|---|---|
代碼組織方式 | 函數式,按功能分組 | 對象式,按選項分組 |
邏輯復用 | 組合函數(可直接導入) | Mixin / 組件繼承(易沖突) |
this 依賴 | 無(避免隱性上下文問題) | 依賴組件實例?this |
類型支持 | 優秀(TS 友好) | 較弱(需額外聲明) |
大型組件維護 | 更易拆分和測試 | 邏輯分散在多個選項中 |
Vue3 同時支持組合式 API 和選項式 API,但組合式 API 是官方推薦的先進模式,尤其適合中大型項目和邏輯復雜的組件開發。通過合理使用組合式 API,可顯著提升開發效率和代碼質量。
在 Vue3 中如何自定義一個 v-model 指令?
在 Vue3 中自定義?v-model
?指令需結合組件的?modelValue
?props 和?update:modelValue
?事件實現雙向綁定。原生?v-model
?本質是語法糖,會展開為接收?modelValue
?并監聽?update:modelValue
?事件的形式,因此自定義指令需遵循這一模式。
核心步驟如下:
- 定義子組件:聲明?
modelValue
?作為 props 接收父組件值,并通過?defineEmits
?定義?update:modelValue
?事件用于更新父組件數據。 - 綁定交互元素:在子組件模板中,將?
modelValue
?綁定到表單元素(如?input
)的?value
?屬性,并在元素事件(如?input
?事件)中觸發?update:modelValue
?事件,傳遞新值。 - 父組件使用:通過?
v-model
?指令綁定子組件,此時 Vue 會自動將指令的值作為?modelValue
?傳入子組件,并監聽更新事件。
示例代碼:
子組件(CustomInput.vue)
<template> <input :value="modelValue" @input="handleInput" />
</template> <script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue']) const handleInput = (e) => { emit('update:modelValue', e.target.value)
}
</script>
父組件使用
<template> <CustomInput v-model="parentValue" /> <p>父組件值:{{ parentValue }}</p>
</template> <script setup>
import { ref } from 'vue'
const parentValue = ref('初始值')
</script>
進階場景:多個 v-model 綁定
若需在組件中定義多個?v-model
?綁定,可通過?model
?選項指定不同的 prop 和事件名。例如:
<template> <input :value="name" @input="updateName" /> <input :value="age" @input="updateAge" />
</template> <script setup>
const props = defineProps({ name: String, age: Number
})
const emit = defineEmits(['update:name', 'update:age']) const updateName = (e) => { emit('update:name', e.target.value)
}
const updateAge = (e) => { emit('update:age', e.target.value)
}
</script>
父組件中可分別綁定:
<MultiModelComponent v-model:name="userName" v-model:age="userAge"
/>
注意事項:
- 自定義?
v-model
?本質是通過 props 和事件實現雙向通信,需確保數據流動單向(父 → 子通過 props,子 → 父通過事件)。 - 若子組件需要處理非原始值(如對象),需注意引用類型的響應式問題,可結合?
watch
?或?toRefs
?保持響應性。 - 避免在子組件中直接修改?
modelValue
?原值,應通過事件觸發更新,以遵循 Vue 的單向數據流原則。
通過這種方式,自定義?v-model
?指令可靈活適配各種組件交互場景,如自定義表單組件、數據展示組件等,提升代碼復用性和組件封裝能力。
項目中如何封裝 Axios?請說明封裝的思路和步驟。
在前端項目中封裝 Axios 的核心目標是統一處理請求 / 響應邏輯、簡化重復代碼、增強可維護性和擴展性。以下是具體的封裝思路和步驟:
一、基礎封裝:設置全局配置與攔截器
-
創建 Axios 實例
通過?axios.create()
?創建獨立實例,避免污染全局配置。例如:import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_APP_API_BASE, // 環境變量配置接口前綴 timeout: 5000, // 請求超時時間 withCredentials: true // 跨域時攜帶 cookie })
-
請求攔截器:統一處理請求頭
在請求發送前添加公共參數、Token 等,例如:service.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 添加 Token 到請求頭 } // 統一添加時間戳防止緩存(可選) if (config.method === 'get') { config.params = { ...config.params, timestamp: Date.now() } } return config }, (error) => { return Promise.reject(error) // 處理請求錯誤 } )
-
響應攔截器:統一處理響應結果
解析響應數據、處理錯誤狀態碼(如 401 未認證、500 服務器錯誤):service.interceptors.response.use( (response) => { const { data } = response // 假設后端返回格式:{ code: number, data: any, message: string } if (data.code === 200) { return data.data // 正常響應返回業務數據 } else { // 處理業務錯誤(如提示用戶) console.error('業務錯誤:', data.message) return Promise.reject(new Error(data.message)) } }, (error) => { // 處理網絡錯誤或狀態碼非 2xx 的情況 const status = error.response?.status switch (status) { case 401: // 跳轉到登錄頁 router.push('/login') break case 404: console.error('接口不存在') break default: console.error('網絡請求失敗:', error.message) } return Promise.reject(error) } )
二、模塊化封裝:按業務拆分接口
將不同業務的接口分文件管理,例如在?src/api
?目錄下創建?user.js
、order.js
?等文件:
// src/api/user.js
import service from '@/utils/axios' // 登錄接口
export const login = (data) => { return service.post('/user/login', data)
} // 獲取用戶信息
export const getUserInfo = () => { return service.get('/user/info')
}
優勢:
- 接口按功能分組,方便查找和維護;
- 可單獨 mock 某模塊接口,便于單元測試;
- 避免單個文件過于臃腫。
三、高級封裝:支持請求配置擴展
為滿足復雜場景(如上傳文件、取消請求等),可在基礎封裝上添加參數擴展:
-
支持自定義配置
在請求函數中接收第三個參數?config
,合并到 Axios 配置中:export const uploadFile = (data, config = {}) => { return service.post('/file/upload', data, { headers: { 'Content-Type': 'multipart/form-data' }, ...config // 允許傳入自定義配置覆蓋默認值 }) }
-
請求取消(AbortController)
使用瀏覽器原生?AbortController
?取消未完成的請求,避免組件卸載后請求回調導致的內存泄漏:export const fetchDataWithCancel = () => { const controller = new AbortController() service.get('/data', { signal: controller.signal }) .then((res) => { /* 處理結果 */ }) return { controller } // 返回取消控制器供外部調用 } // 使用時 const { controller } = fetchDataWithCancel() // 取消請求 controller.abort()
四、集成到 Vue 項目(以 Vue3 為例)
-
全局掛載
在?main.js
?中掛載封裝好的?service
?到?app.config.globalProperties
,方便組件中直接使用:import { createApp } from 'vue' import App from './App.vue' import service from './utils/axios' const app = createApp(App) app.config.globalProperties.$http = service app.mount('#app')
-
組件中使用
<script setup> import { getCurrentInstance } from 'vue' const { appContext } = getCurrentInstance() const $http = appContext.config.globalProperties.$http // 調用接口 const fetchUser = async () => { try { const data = await $http.get('/user/info') console.log('用戶信息:', data) } catch (error) { console.error('請求失敗:', error) } } </script>
封裝的核心優勢
- 統一錯誤處理:避免每個接口重復編寫錯誤處理邏輯,提升代碼一致性;
- 便捷的環境切換:通過?
baseURL
?配置不同環境(開發、測試、生產)的接口地址,配合?.env
?文件管理; - 可擴展性強:通過攔截器輕松添加日志記錄、請求加密、參數校驗等功能;
- 組件解耦:接口調用邏輯與組件分離,便于復用和維護。
通過以上步驟,可構建一個健壯、靈活的 Axios 封裝方案,適應大型項目的復雜需求。
React 和 Vue 的主要區別有哪些?(可結合數據驅動、組件模型、生態等方面)
React 與 Vue 作為主流前端框架,在設計理念和實現方式上存在顯著差異,以下從核心機制、組件模型、生態等維度對比分析:
一、數據驅動:響應式原理的差異
-
React:基于?狀態(State)驅動視圖,采用?不可變數據(Immutable Data)?和?虛擬 DOM(Virtual DOM)?機制。
- 狀態更新需通過?
setState
?或 Hooks(如?useState
)觸發,且每次更新會生成新的虛擬 DOM 樹,通過?diff 算法?對比差異后批量更新真實 DOM。 - 優點:數據流動清晰(單向數據流),便于追蹤狀態變化;虛擬 DOM 抽象層使跨平臺(如 React Native)成為可能。
- 缺點:頻繁狀態更新可能導致不必要的重新渲染,需通過?
useMemo
、useCallback
?等優化性能。
- 狀態更新需通過?
-
Vue:基于?響應式系統(Reactive System),通過?Proxy(Vue3)或 Object.defineProperty(Vue2)?劫持數據變化,精準更新依賴組件。
- 狀態直接修改響應式數據(如?
this.count = 1
)即可觸發視圖更新,無需手動調用更新函數。 - 優點:細粒度響應式更新,性能優化更精準;模板語法直觀,接近原生 HTML。
- 缺點:響應式依賴收集在復雜場景下可能存在邊界問題(如動態添加對象屬性需手動處理)。
- 狀態直接修改響應式數據(如?
二、組件模型:聲明方式與邏輯組織
-
React:
- 組件聲明:以?函數組件(Functional Component)?為主(推薦使用 Hooks),類組件(Class Component)逐漸被淘汰。
- 邏輯復用:通過?自定義 Hooks(如?
useFetch
、useForm
)實現邏輯共享,靈活性高但學習成本較高。 - 模板語法:使用?JSX(JavaScript XML),將 HTML 與 JavaScript 深度融合,可直接在模板中編寫表達式和邏輯。
-
Vue:
- 組件聲明:采用?單文件組件(.vue)?格式,包含?
<template>
(模板)、<script>
(邏輯)、<style>
(樣式),結構清晰易上手。 - 邏輯復用:通過?Mixin(Vue2)或?組合式 API(Composition API,Vue3)?實現邏輯抽離,組合式 API 更貼合函數式編程風格。
- 模板語法:使用?模板表達式(Mustache)?和?指令(Directives)(如?
v-if
、v-for
),語法簡潔,對 HTML 開發者更友好。
- 組件聲明:采用?單文件組件(.vue)?格式,包含?
三、生態系統:工具鏈與社區支持
-
React:
- 核心生態:依賴?npm 生態,主力庫包括?
react-router
(路由)、redux
/zustand
(狀態管理)、react-dom
(DOM 操作)。 - 工具鏈:官方推薦?Create React App(CRA)?初始化項目,搭配?
Webpack
/Vite
?構建,調試工具有?React DevTools。 - 社區與就業:生態成熟,適合大型團隊開發復雜應用(如企業級后臺、電商平臺),但需學習較多周邊庫(如狀態管理、路由)。
- 核心生態:依賴?npm 生態,主力庫包括?
-
Vue:
- 核心生態:官方提供?Vue CLI?和?Vite?快速搭建項目,配套庫包括?
vue-router
(路由)、pinia
(狀態管理)、vuex
(Vue2 主流)。 - 工具鏈:單文件組件支持熱更新和樣式作用域(
scoped
),生態更輕量,入門門檻低。 - 社區與就業:國內社區活躍,適合中小項目快速開發,也可通過?Nuxt.js?構建 SSR 應用(如博客、營銷網站)。
- 核心生態:官方提供?Vue CLI?和?Vite?快速搭建項目,配套庫包括?
四、性能與優化
-
React:
- 虛擬 DOM 的 diff 算法在復雜列表渲染時可能存在性能瓶頸,需通過?
key
?屬性優化列表 diff,或使用?react-window
?等庫實現虛擬列表。 - 函數組件的 memoization(記憶化)依賴?
useMemo
?和?useCallback
,需手動優化避免不必要的重新渲染。
- 虛擬 DOM 的 diff 算法在復雜列表渲染時可能存在性能瓶頸,需通過?
-
Vue:
- 響應式系統自動追蹤依賴,組件更新粒度更細,在數據驅動的簡單場景下性能更優。
- Vue3 引入?Proxy 響應式?和?Composition API,進一步提升了大型應用的可維護性和性能表現。
五、適用場景
-
選擇 React 的場景:
- 構建大型單頁應用(SPA)或需要復雜狀態管理的項目(如社交平臺、儀表盤);
- 跨平臺開發(如同時開發 Web 和移動端應用);
- 團隊熟悉函數式編程,或需要使用前沿技術(如 Server Components)。
-
選擇 Vue 的場景:
- 快速開發中小型項目(如企業官網、內部管理系統);
- 團隊中有較多 HTML/CSS 開發者,希望降低學習成本;
- 需要與現有 jQuery 或舊項目集成,或使用 SSR(如 Nuxt.js 構建 SEO 友好型網站)。
簡述 React 的 Hooks 機制(如 useState、useEffect、useContext 等)。
React 的 Hooks 是 React 16.8 引入的新特性,允許在函數組件中使用狀態和副作用等功能,徹底改變了函數組件的開發模式。以下從核心 Hooks 原理、使用規則及實踐場景展開說明:
一、核心 Hooks 解析
1. ?useState:狀態管理的核心?
-
作用:為函數組件添加狀態,替代類組件的?
this.state
?和?setState
。 -
語法:
const [state, setState] = useState(initialState)
state
?是當前狀態值,setState
?是更新狀態的函數。initialState
?可以是任意類型(如數值、對象、數組),首次渲染后不可修改(僅在初始化時生效)。
-
更新機制:
- 調用?
setState
?會觸發組件重新渲染,并將新狀態值傳入下一次渲染的組件函數中。 - 若新狀態與舊狀態 ** 淺比較(shallow comparison)** 相等,React 會跳過渲染以優化性能。
- 調用?
-
示例:
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>計數:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ) }
2. useEffect:副作用操作的入口
-
作用:處理組件渲染后的副作用(如數據請求、DOM 操作、訂閱 / 取消訂閱),替代類組件的?
componentDidMount
、componentDidUpdate
?和?componentWillUnmount
。 -
語法:
useEffect(effect, dependencies?)
effect
?是副作用函數,可返回一個清理函數(用于清除副作用,如取消訂閱、清除定時器)。dependencies
?是依賴數組,決定?effect
?的觸發時機:- 空數組(
[]
):僅在組件掛載時執行一次(類似?componentDidMount
); - 包含狀態或 props(如?
[count]
):當依賴項變化時執行(類似?componentDidUpdate
); - 不傳入依賴數組(不推薦):每次組件渲染后都會執行(性能隱患)。
- 空數組(
-
執行流程:
- 組件首次掛載后,執行?
effect
; - 依賴項變化時,先執行上一次?
effect
?的清理函數(若有),再執行新的?effect
; - 組件卸載時,執行清理函數(若有)。
- 組件首次掛載后,執行?
-
示例:模擬數據請求
function UserInfo({ userId }) { const [user, setUser] = useState(null) useEffect(() => { // 掛載時發送請求 fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => setUser(data)) // 清理函數:組件卸載時取消未完成的請求(需配合 AbortController) return () => { // 此處可添加取消請求的邏輯 } }, [userId]) // 僅當 userId 變化時重新請求 return user ? <div>用戶:{user.name}</div> : <p>加載中...</p> }
3. ?useContext:跨組件層級的數據傳遞
-
作用:替代類組件的?
contextType
?或?Consumer
?組件,實現跨層級組件的數據共享(無需逐層傳遞 props)。 -
使用步驟:
- 創建 Context 對象:
const ThemeContext = createContext(defaultValue)
- 在頂層組件提供數據:
<ThemeContext.Provider value={theme}> <App /> </ThemeContext.Provider>
- 在任意子組件中消費數據:
function Button() { const theme = useContext(ThemeContext) return <button style={{ color: theme.color }}>按鈕</button> }
- 創建 Context 對象:
-
注意事項:
useContext
?的參數是?createContext
?返回的對象,消費時會訂閱該 Context 的變化;- 當?
Provider
?的?value
?引用變化時,所有消費該 Context 的組件都會重新渲染,需通過?useMemo
?優化?value
?的穩定性。
二、Hooks 的規則(必須嚴格遵守)
-
只能在函數組件或自定義 Hooks 中調用
- 禁止在循環、條件判斷或普通函數中調用 Hooks,確保 Hooks 的調用順序在每次渲染時一致。
-
優先使用數組進行依賴管理
useEffect
、useMemo
?等 Hooks 的依賴數組需包含所有在副作用中使用的 props 或 state,避免出現 “過時閉包” 問題(即副作用中訪問的狀態不是最新值)。
-
自定義 Hooks 需以 use 開頭
- 遵循命名約定(如?
useFetch
、useForm
),便于 React 識別和校驗 Hooks 的調用規則。
- 遵循命名約定(如?
三、常見自定義 Hooks 場景
-
數據請求封裝
function useFetch(url) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data) setLoading(false) }) }, [url]) return { data, loading } }
-
表單處理
function useForm(initialState) { const [form, setForm] = useState(initialState) const handleChange = (e) => { const { name, value } = e.target setForm(prev => ({ ...prev, [name]: value })) } return { form, handleChange } }
-
窗口尺寸監聽
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }) useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }) } window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) // 僅在掛載時添加監聽 return size }
四、Hooks 的優勢與挑戰
-
優勢:
- 邏輯復用更靈活:通過自定義 Hooks 抽離重復邏輯,避免 Mixin 的命名沖突問題;
- 組件更簡潔:函數組件無類組件的生命周期鉤子分散問題,邏輯按副作用類型組織;
- 性能優化更精準:依賴數組明確控制副作用觸發時機,減少不必要的重新渲染。
-
挑戰:
- 閉包陷阱:Hooks 的函數作用域可能導致回調函數中捕獲的狀態不是最新值,需正確管理依賴數組;
- 調試難度:復雜自定義 Hooks 的邏輯嵌套可能增加調試成本,需借助 React DevTools 的 Hooks 面板追蹤狀態變化。
Hooks 機制徹底釋放了函數組件的潛力,使其成為 React 開發的主流模式。理解 Hooks 的原理和規則,是掌握現代 React 開發的核心關鍵。
React 函數組件和類組件(Class Component)的區別是什么?函數組件有哪些改進?
在 React 中,函數組件和類組件是兩種不同的組件聲明方式,隨著版本迭代,函數組件逐漸成為主流。以下從核心特性、開發模式、性能等方面對比兩者差異,并分析函數組件的改進點:
一、本質區別:組件的定義方式
-
類組件(Class Component)
- 基于 ES6?
class
?語法定義,繼承自?React.Component
。 - 需要通過?
this.state
?管理狀態,通過?this.setState()
?更新狀態。 - 包含生命周期鉤子函數(如?
componentDidMount
、shouldComponentUpdate
)。 - 示例:
class Counter extends React.Component { state = { count: 0 } handleClick = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <p>計數:{this.state.count}</p> <button onClick={this.handleClick}>+1</button> </div> ) } }
- 基于 ES6?
-
函數組件(Functional Component)
- 本質是 JavaScript 函數,接收?
props
?并返回 JSX。 - 早期(React 16.8 前)僅用于無狀態組件(Stateless Component),無法管理狀態或副作用。
- React 16.8 引入 Hooks 后,函數組件可通過?
useState
、useEffect
?等 Hooks 實現類組件的所有功能。 - 示例(使用 Hooks):
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>計數:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ) }
- 本質是 JavaScript 函數,接收?
二、核心特性對比
特性 | 類組件 | 函數組件(含 Hooks) |
---|---|---|
狀態管理 | 通過?this.state ?和?setState ?管理 | 通過?useState ?鉤子函數管理 |
生命周期 | 包含?mount 、update 、unmount ?鉤子 | 通過?useEffect ?統一處理副作用 |
邏輯復用 | 通過 Mixin、HOC(高階組件)實現 | 通過自定義 Hooks 實現,更靈活輕量 |
性能優化 | shouldComponentUpdate 、PureComponent | React.memo 、useMemo 、useCallback |
代碼可讀性 | 邏輯按生命周期鉤子拆分,可能較分散 | 邏輯按副作用類型組織,更集中 |
this 指向問題 | 需手動綁定?this (如箭頭函數) | 無?this ?上下文,函數作用域更清晰 |
跨平臺支持 | 依賴?react-dom | 更易適配 React Native 等非 DOM 環境 |
三、函數組件的主要改進
1.?拋棄?this
,避免上下文混亂
類組件中?this
?指向容易因綁定問題引發 bug(如事件處理函數中?this
?丟失),而函數組件完全不存在?this
?上下文,所有邏輯均在函數作用域內處理,代碼更簡潔可靠。
2.?邏輯復用更高效:自定義 Hooks
類組件通過 Mixin 或 HOC 復用邏輯時,可能導致組件層級嵌套過深、命名沖突等問題(如 “嵌套地獄”)。函數組件通過自定義 Hooks(如?useForm
、useFetch
)可輕松抽離邏輯,且邏輯復用更靈活,無組件包裹的層級負擔。
示例:Hooks 復用表單邏輯
// 自定義 Hook
function useForm(initialState) { const [form, setForm] = useState(initialState) const handleChange = (e) => { const { name, value } = e.target setForm(prev => ({ ...prev, [name]: value })) } return { form, handleChange }
} // 組件中使用
function UserForm() { const { form, handleChange } = useForm({ name: '', email: '' }) return ( <form> <input name="name" value={form.name} onChange={handleChange} /> <input name="email" value={form.email} onChange={handleChange} /> </form> )
}
3.?更細粒度的性能優化
- 類組件依賴?
shouldComponentUpdate
?或?PureComponent
?進行淺比較優化,需手動實現。 - 函數組件通過?
React.memo
?包裹組件,結合?useMemo
、useCallback
?緩存計算結果或回調函數,避免子組件不必要的重新渲染,優化更精準。
示例:使用?useMemo
?緩存計算
function List({ items }) { const expensiveValue = useMemo(() => { // 復雜計算邏輯 return items.reduce((acc, cur) => acc + cur.value, 0) }, [items]) // 僅當 items 變化時重新計算 return <div>計算結果:{expensiveValue}</div>
}
4.?簡化生命周期管理
類組件的生命周期鉤子(如?componentDidMount
、componentDidUpdate
)需分不同鉤子處理邏輯,而函數組件通過?useEffect
?統一處理副作用,并通過依賴數組靈活控制執行時機(掛載時、更新時、卸載時),邏輯更集中。
示例:替代類組件生命周期
類組件鉤子 | 函數組件實現 |
---|---|
componentDidMount | useEffect(effect, []) |
componentDidUpdate | useEffect(effect, [deps...]) |
componentWillUnmount | useEffect(() => () => cleanup) |
5.?更好的 TypeScript 支持
函數組件的 props 和狀態類型聲明更簡潔,無需像類組件那樣綁定?this
?的類型(如?this.setState
?的類型推斷),結合 TypeScript 可提供更友好的類型提示。
6.?體積更小,性能更優
函數組件無需創建類實例,減少了內存開銷;Hooks 的實現機制(如?useState
?基于數組索引)比類組件的?setState
?更高效,尤其在大型應用中性能優勢更明顯。
四、類組件的現狀與遷移建議
-
現狀:React 官方仍支持類組件,但不再推薦新功能開發使用,社區逐漸向函數組件遷移。
-
遷移場景:
- 復雜狀態邏輯:可通過?
useReducer
?鉤子替代類組件的?this.state
?和自定義更新邏輯; - 生命周期鉤子:通過?
useEffect
?組合實現; - 代碼遷移工具:使用?
react-codemod
?自動將類組件轉換為函數組件(需手動處理?this
?相關邏輯)。
- 復雜狀態邏輯:可通過?
-
何時保留類組件:
- 維護舊項目中的類組件代碼,無重構需求;
- 使用僅類組件支持的特性(如?
getChildContext
,已廢棄)。
解釋 BFC(塊級格式化上下文)的概念及應用場景
BFC(Block Formatting Context,塊級格式化上下文)是 CSS 中一個獨立的渲染區域,規定了內部元素如何布局,以及與外部元素的相互作用。它具有以下特性:內部盒子會在垂直方向排列,盒子垂直方向的間距由 margin 決定(同一 BFC 中相鄰塊級元素的垂直 margin 會重疊),BFC 的區域不會與浮動元素的盒子重疊,計算 BFC 高度時會包含浮動元素,BFC 是頁面上的一個隔離容器,容器內部元素不會影響外部元素。
創建 BFC 的方式包括:將元素的?display
?設置為?table-cell
、table-caption
、inline-block
?或?flex
;設置?overflow
?為?auto
、scroll
?或?hidden
(非?visible
);設置?float
?為非?none
?的值;設置?position
?為?absolute
?或?fixed
。
應用場景主要有以下幾個方面:
- 解決 margin 重疊問題:當兩個相鄰塊級元素處于同一 BFC 時,它們的垂直 margin 會重疊。通過為其中一個元素創建新的 BFC,可避免這種重疊。例如,兩個段落元素上下排列,若都在同一個 BFC 中,它們的 margin 會合并,給其中一個段落包裹一個創建了 BFC 的容器,就能使 margin 正常顯示。
- 清除浮動影響:傳統清除浮動需使用額外標簽并設置?
clear: both
,而利用 BFC 的特性,給父元素創建 BFC(如設置?overflow: auto
),父元素就能包含浮動子元素,從而計算高度,解決高度塌陷問題。 - 避免與浮動元素重疊:當一個元素設置浮動后,相鄰的塊級元素會圍繞它排列。若希望相鄰元素不與浮動元素重疊,可將其放入新的 BFC 中,這樣該元素的內容就不會進入浮動元素的區域。比如側邊欄浮動,主內容區域創建 BFC 后,主內容的文本就不會緊貼側邊欄,而是正常顯示在右側。
- 實現多列布局:利用 BFC 不與浮動元素重疊的特性,可實現簡單的多列布局。例如,左側元素浮動,右側元素創建 BFC,兩者可并列顯示,互不影響。
圖片性能優化的方法有哪些?(如壓縮、WebP 格式、雪碧圖、懶加載等)
圖片性能優化是前端性能優化的重要環節,可從圖片體積、加載方式、格式選擇等多方面入手,以下是常見的優化方法:
1. 圖片壓縮
通過工具減少圖片文件大小,同時盡量保持視覺質量。分為有損壓縮和無損壓縮:
- 有損壓縮:刪除圖像中冗余數據,會損失部分細節,適用于照片等對細節要求不高的場景。常用工具有 Photoshop、TinyPNG、Squoosh 等。例如,將一張 2MB 的 JPEG 照片通過 TinyPNG 壓縮后,可能降至 500KB 左右,肉眼難以察覺畫質明顯下降。
- 無損壓縮:通過算法優化文件編碼,不損失像素數據,適合圖標、線條圖等對細節敏感的圖片。工具如 ImageOptim、PNGGauntlet。
2. 選擇合適的圖片格式
不同格式適用于不同場景,合理選擇可平衡畫質與體積:
- JPEG(JPG):適合色彩豐富的照片,支持高壓縮比,但不支持透明背景。
- PNG:支持透明背景,適合圖標、圖形,但文件體積通常大于 JPEG。其中 PNG-8 適合簡單圖形,PNG-24 適合復雜透明圖像。
- WebP:谷歌開發的現代格式,同等畫質下體積比 JPEG/PNG 小 30% 以上,支持有損和無損壓縮、透明背景及動畫。但需注意兼容性,可通過漸進式增強(先提供 JPEG/PNG,再檢測支持 WebP 后替換)解決。
- AVIF:新一代圖片格式,壓縮效率優于 WebP,但目前瀏覽器支持度較低,可作為未來優化方向。
- SVG:矢量圖,無限縮放不失真,體積小,適合圖標、圖表。可通過圖標字體(如 Font Awesome)或直接嵌入 HTML 使用。
3. 雪碧圖(CSS Sprites)
將多張小型圖標合并為一張大圖,通過 CSS 背景定位顯示具體圖標。減少 HTTP 請求次數,提升頁面加載速度,尤其適合移動端項目。例如,將導航欄的多個圖標合并為雪碧圖,瀏覽器只需加載一次圖片,通過?background-position
?屬性顯示不同圖標。但需注意,雪碧圖維護成本較高,新增或修改圖標需重新制作圖片。
4. 懶加載(延遲加載)
非關鍵圖片(如頁面底部的圖片)在用戶滾動到可視區域時再加載,減少初始加載時的請求數量,加快首屏渲染。實現方式包括:
- 原生屬性:給圖片標簽添加?
loading="lazy"
(瀏覽器支持有限)。 - JavaScript 監聽滾動事件:通過計算元素與視口的位置關系,判斷是否加載圖片。例如,使用?
getBoundingClientRect()
?方法獲取元素位置,當進入視口附近時,將?img
?標簽的?data-src
?屬性值賦給?src
。 - Intersection Observer API:更高效的方式,自動監聽元素是否進入可視區域,觸發回調函數加載圖片,避免頻繁滾動事件監聽帶來的性能開銷。
5. 響應式圖片(自適應圖片)
根據設備屏幕尺寸和分辨率加載不同尺寸的圖片,避免大尺寸圖片在小屏幕設備上浪費流量。通過?srcset
?和?sizes
?屬性實現,例如:
<img src="small.jpg" srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1024w" sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1024px" alt="響應式圖片"
>
瀏覽器會根據當前視口寬度和設備像素比,選擇最合適的圖片加載。
6. 優化圖片分辨率
避免使用分辨率遠高于顯示區域的圖片。例如,在設計稿中某圖片顯示尺寸為 300px×200px,就無需上傳 1200px×800px 的原圖,可提前縮放至合適尺寸再上傳。
7. 使用 CDN 加速
將圖片存儲在 CDN(內容分發網絡)上,利用全球節點加速圖片加載,減少服務器壓力,尤其適合國際化項目。
8. 字體圖標替代簡單圖形
對于純色圖標,使用字體圖標(如 Iconfont)替代位圖,字體文件體積小且可通過 CSS 靈活控制顏色、大小等樣式,減少圖片請求。
懶加載的實現方式有哪些?(如計算元素位置、IntersectionObserver API)
懶加載(Lazy Loading)是一種延遲加載非關鍵資源的技術,通過在需要時(如用戶滾動到元素可視區域)再加載資源,減少初始加載時間,提升頁面性能。以下是常見的實現方式及其原理和特點:
1. 通過滾動事件監聽計算元素位置
原理:利用 JavaScript 監聽瀏覽器的?scroll
?事件,在滾動過程中計算目標元素是否進入視口(viewport),若進入則觸發加載操作。
關鍵步驟:
- 獲取目標元素的位置:通過?
getBoundingClientRect()
?方法獲取元素相對于視口的位置信息(包含?top
、bottom
、left
、right
?等屬性)。 - 判斷元素是否可見:設置一個閾值(如距離視口頂部或底部一定距離時提前加載),當元素的?
top
?值小于視口高度加上閾值,且?bottom
?值大于 0 時,認為元素進入可視區域。 - 替換圖片地址:將?
img
?標簽的?data-src
(或自定義屬性)值賦給?src
,觸發圖片加載。
代碼示例:
<img src="placeholder.jpg" data-src="real-image.jpg" class="lazy">
const lazyImages = document.querySelectorAll('img.lazy');
const threshold = 200; // 提前 200 像素加載 function loadLazyImage(img) { const rect = img.getBoundingClientRect(); if (rect.bottom >= -threshold && rect.top <= window.innerHeight + threshold) { if (img.src === 'placeholder.jpg') { // 避免重復加載 img.src = img.dataset.src; img.classList.remove('lazy'); // 移除懶加載類,防止再次觸發 } }
} function handleScroll() { lazyImages.forEach(img => loadLazyImage(img));
} // 監聽滾動事件
window.addEventListener('scroll', handleScroll);
// 初始化時檢查(處理頁面初始可見的元素)
handleScroll();
優缺點:
- 優點:兼容性好,可支持低版本瀏覽器。
- 缺點:頻繁觸發?
scroll
?事件可能導致性能問題(需通過?debounce
?優化);手動計算位置邏輯較繁瑣,且可能因頁面動態變化(如元素位置改變)導致判斷不準確。
2. 使用 Intersection Observer API
原理:Intersection Observer 是瀏覽器原生提供的 API,用于異步觀察目標元素與祖先元素(或視口)的相交情況。通過創建觀察者實例,監聽元素的可見性變化,當元素進入或離開可視區域時觸發回調函數,無需手動計算位置或監聽滾動事件。
關鍵步驟:
- 創建觀察者實例:通過?
new IntersectionObserver(callback, options)
?初始化,callback
?為元素可見性變化時的回調函數,options
?可配置閾值、根元素等。 - 注冊目標元素:使用?
observer.observe(element)
?將目標元素加入觀察列表。 - 在回調函數中處理加載邏輯:當元素可見性滿足條件(如進入視口)時,加載資源并停止觀察(避免重復觸發)。
代碼示例:
<img src="placeholder.jpg" data-src="real-image.jpg" class="lazy">
const lazyImages = document.querySelectorAll('img.lazy'); const observerOptions = { rootMargin: '0px 0px 200px 0px', // 根元素(視口)外擴展 200px 作為觸發區域 threshold: 0.1 // 元素可見區域占比超過 10% 時觸發
}; const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 元素進入可視區域 const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); observer.unobserve(img); // 停止觀察,避免重復加載 } });
}, observerOptions); // 注冊所有懶加載圖片
lazyImages.forEach(img => imageObserver.observe(img));
優缺點:
- 優點:性能更優,異步非阻塞,瀏覽器自動優化回調頻率,無需手動處理滾動事件;配置靈活,可通過?
rootMargin
?和?threshold
?精準控制觸發條件。 - 缺點:兼容性有限(需瀏覽器支持 Intersection Observer,可通過 polyfill 解決)。
3. 原生?loading="lazy"
?屬性
原理:HTML5 新增的?loading
?屬性,為圖片和 iframe 提供原生懶加載支持。將?loading
?設置為?lazy
?時,瀏覽器會在元素接近視口時再加載,減少初始請求。
使用方式:
<img src="image.jpg" loading="lazy" alt="懶加載圖片" />
優缺點:
- 優點:代碼極簡,無需額外 JavaScript 邏輯。
- 缺點:兼容性較差(僅部分現代瀏覽器支持,如 Chrome 77+),且控制粒度較粗,無法自定義閾值或加載邏輯。
4. 框架 / 庫的封裝方案
在 Vue、React 等框架中,可使用現成的庫或插件實現懶加載,簡化開發流程:
- Vue 中使用?
vue-lazyload
:通過指令?v-lazy
?綁定圖片地址,自動處理加載邏輯。預覽
<img v-lazy="imageUrl" placeholder="placeholder.jpg" />
- React 中使用?
react-lazyload
:包裹圖片組件,監聽滾動事件并觸發加載。import LazyLoad from 'react-lazyload'; <LazyLoad> <img src="placeholder.jpg" data-src="real-image.jpg" /> </LazyLoad>
選擇建議
- 現代項目優先使用 Intersection Observer API:性能最佳,配置靈活,配合 polyfill 可兼容低版本瀏覽器。
- 兼容性要求高的項目:使用滾動事件監聽 + debounce 優化,或引入成熟的懶加載庫(如?
lozad.js
)。 - 簡單場景:若瀏覽器兼容性允許,可直接使用原生?
loading="lazy"
?屬性。
懶加載的核心是按需加載,需根據項目需求和瀏覽器兼容性選擇合適的方案,同時注意處理加載失敗、占位圖樣式等細節,提升用戶體驗。
路由懶加載的實現方式(如使用 import () 動態導入)
路由懶加載(也稱為代碼拆分)是前端路由優化的重要手段,通過將路由對應的組件代碼延遲加載,避免初始加載時一次性加載所有組件,減少打包后的文件體積,加快首屏渲染速度。以下是常見的實現方式及其原理和應用場景:
1. 使用 ES6 的 import () 動態導入(推薦)
原理:import()
?是 ES6 提出的動態導入語法,允許在運行時異步加載模塊。在路由配置中,將組件的導入語句從靜態?import
?改為動態?import()
,打包工具(如 Webpack、Vite)會自動將對應的組件代碼拆分為獨立的 chunk 文件,僅在路由切換到該組件時加載。
示例(以 Vue Router 為例):
// 傳統靜態導入(全部組件一次性加載)
// import Home from './views/Home.vue';
// import About from './views/About.vue'; // 路由懶加載(動態導入)
const router = new VueRouter({ routes: [ { path: '/', name: 'Home', component: () => import('./views/Home.vue') // 動態導入組件 }, { path: '/about', name: 'About', component: () => import('./views/About.vue') } ]
});
打包效果:構建后會生成?Home.[hash].js
、About.[hash].js
?等獨立文件,首次訪問頁面時僅加載主 bundle 文件,切換到?/about
?路由時,瀏覽器會自動請求?About.[hash].js
?文件并渲染組件。
在 React 中使用(React Router):
import { Route, Routes } from 'react-router-dom';
function App() { return ( <Routes> <Route path="/" element={<LazyLoadComponent componentName="Home" />} /> <Route path="/about" element={<LazyLoadComponent componentName="About" />} /> </Routes> );
} // 封裝懶加載組件
const LazyLoadComponent = ({ componentName }) => { const [Component, setComponent] = useState(null); useEffect(() => { import(`./views/${componentName}.js`) .then(module => setComponent(module.default)); }, [componentName]); return Component ? <Component /> : <Spinner />; // 加載中顯示 Loading 組件
};
2. 使用 Webpack 的 require.ensure(歷史方案,逐步淘汰)
原理:Webpack 提供的?require.ensure
?方法可手動指定代碼拆分點,在指定的回調函數中異步加載模塊。該方法通過設置?webpackPrefetch
?或?webpackPreload
?注釋可優化加載時機(如預獲取資源)。
示例(Vue Router):
const router = new VueRouter({ routes: [ { path: '/about', name: 'About', component: resolve => { require.ensure([], () => { resolve(require('./views/About.vue')); }, 'about-chunk'); // 自定義 chunk 名稱 } } ]
});
注意:require.ensure
?是 Webpack 特有的語法,兼容性和可讀性不如?import()
,現代項目中已逐步被動態導入取代。
3. 結合路由配置的異步組件(框架特性)
部分框架(如 Vue)支持將路由組件定義為異步組件,框架內部會處理動態導入邏輯:
// Vue 2 中使用異步組件
const router = new VueRouter({ routes: [ { path: '/about', component: () => ({ // 異步組件配置 component: import('./views/About.vue'), loading: LoadingComponent, // 加載中顯示的組件 error: ErrorComponent, // 加載失敗顯示的組件 delay: 200, // 延遲顯示 loading 的時間 timeout: 3000 // 加載超時時間 }) } ]
});
4. 預加載(Prefetch)優化用戶體驗
為進一步提升用戶體驗,可結合?import()
?的?webpackPrefetch
?注釋,告知瀏覽器在空閑時提前加載后續可能需要的路由組件代碼:
const About = () => import(/* webpackPrefetch: true */ './views/About.vue');
原理:瀏覽器會在主線程空閑且網絡空閑時,提前獲取該 chunk 文件并緩存,當用戶切換到該路由時可直接使用緩存,減少加載延遲。注意避免濫用預加載,以免浪費流量和資源。
實現路由懶加載的關鍵步驟
- 修改路由配置:將靜態導入的組件改為動態導入(
import()
?或框架支持的異步組件寫法)。 - 配置打包工具:Webpack、Vite 等工具會自動處理代碼拆分,無需額外配置。若需自定義 chunk 名稱,可在動態導入語句中添加注釋:
const About = () => import(/* webpackChunkName: "about-page" */ './views/About.vue');
- 處理加載狀態:在組件加載過程中顯示 Loading 提示,避免頁面空白,提升用戶體驗。
- 優化網絡請求:結合 CDN 部署拆分后的 chunk 文件,利用瀏覽器緩存和并行加載進一步提升性能。
適用場景
- 大型單頁應用(SPA):當項目包含多個路由頁面時,懶加載可顯著減少初始加載時間。
- 非首屏關鍵路由:對于用戶可能不會立即訪問的頁面(如 “關于我們”、“用戶中心” 等),優先應用懶加載。
- 按需加載第三方庫:除路由組件外,對于 lodash、Chart.js 等大型庫,也可使用動態導入按需加載:
const loadChart = async () => { const Chart = await import('chart.js'); // 使用 Chart 庫創建圖表 };
通過路由懶加載,可有效優化首屏加載性能,尤其在移動端或網絡環境較差的場景中效果顯著。實際開發中需結合項目規模、框架特性和用戶行為分析,合理拆分代碼,平衡加載速度與開發復雜度。
Webpack 能否將某路由下的所有子路由一起打包?如何配置?
Webpack 可以將某路由下的所有子路由組件打包到同一個 Chunk 中,這種優化方式稱為 “按路由層級打包” 或 “分塊打包”,有助于減少 HTTP 請求次數,提升頁面加載性能。以下是具體實現思路和配置方法:
核心原理
Webpack 通過動態導入語句(如?import()
)拆分代碼時,默認會為每個動態導入的組件生成獨立的 Chunk。若希望將同一父路由下的所有子路由組件合并到同一個 Chunk 中,需通過?命名 Chunk(Named Chunks)或?手動分組?的方式,告訴 Webpack 哪些模塊應合并打包。
實現方式一:使用魔法注釋命名 Chunk(推薦)
在動態導入語句中添加?webpackChunkName
?注釋,為同一父路由下的子路由指定相同的 Chunk 名稱,Webpack 會將這些模塊打包到同一個文件中。
示例場景:假設路由結構如下:
- 父路由 /dashboard - 子路由 /dashboard/overview - 子路由 /dashboard/settings - 子路由 /dashboard/logs
配置步驟:
- 在路由配置中,為每個子路由的動態導入語句添加相同的 Chunk 名稱:
// 父路由無關組件(單獨打包) const Dashboard = () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'); // 子路由組件,指定相同的Chunk名稱“dashboard-child” const Overview = () => import(/* webpackChunkName: "dashboard-child" */ './views/Overview.vue'); const Settings = () => import(/* webpackChunkName: "dashboard-child" */ './views/Settings.vue'); const Logs = () => import(/* webpackChunkName: "dashboard-child" */ './views/Logs.vue');
- Webpack 構建后,會生成?
dashboard.[hash].js
(父組件)和?dashboard-child.[hash].js
(所有子路由組件合并后的 Chunk)。 - 當訪問?
/dashboard
?或其子路由時,瀏覽器會先加載?dashboard.[hash].js
,再按需加載?dashboard-child.[hash].js
(若子路由未被訪問,則無需加載)。
關鍵注釋說明:
webpackChunkName: "name"
:指定 Chunk 的名稱,同名 Chunk 會被合并。- 名稱中可使用?
~
?分隔符,實現層級分組(如?dashboard~child
?會生成?dashboard-child.[hash].js
)。
實現方式二:通過 Webpack 配置手動分組(高級)
若項目未使用動態導入(如仍用?require.ensure
),或需更精細的控制,可通過 Webpack 的?optimization.splitChunks
?配置手動分組。
示例配置(webpack.config.js):
module.exports = { optimization: { splitChunks: { cacheGroups: { // 定義一個名為“dashboard-child”的分組 dashboardChild: { test: (module) => { // 匹配所有屬于/dashboard子路由的組件路徑 return module.resource && module.resource.includes('/views/dashboard/'); }, name: 'dashboard-child', chunks: 'all', enforce: true } } } }
};
配置說明:
test
:通過正則或函數匹配需要合并的模塊路徑(如?/views/dashboard/
?目錄下的組件)。name
:指定分組名稱,Webpack 會將匹配的模塊打包到名為?dashboard-child.[hash].js
?的 Chunk 中。chunks: 'all'
:表示從所有 Chunk 中提取符合條件的模塊。enforce: true
:強制將匹配的模塊拆分到該分組,即使未被其他模塊引用。
實現方式三:利用目錄結構自動分組(適用于約定式路由)
若項目采用約定式路由(如根據文件目錄自動生成路由),可利用 Webpack 的?require.context
?或動態導入結合目錄匹配,自動將同一路由目錄下的組件打包到同一 Chunk。
示例(Vue CLI + 約定式路由):
// 在路由入口文件中,動態導入/dashboard目錄下的所有組件
const files = require.context('./views/dashboard', true, /\.vue$/);
files.keys().forEach(key => { const componentName = key.split('/').pop().replace(/\.vue$/, ''); const routePath = `/${componentName.toLowerCase()}`; routes.push({ path: routePath, component: () => import(`./views/dashboard/${componentName}.vue`), // 為同一目錄下的組件指定相同的Chunk名稱 meta: { chunkName: 'dashboard-child' } });
});
配合 Webpack 魔法注釋:
component: () => import(/* webpackChunkName: "dashboard-child" */ `./views/dashboard/${componentName}.vue`)
注意事項
- Chunk 粒度控制:合并子路由 Chunk 時需避免過度打包,若某子路由組件體積過大,合并后可能導致單個 Chunk 加載時間過長,建議按路由層級合理拆分(如父路由一個 Chunk,子路由按模塊功能分組)。
- 緩存策略:Chunk 名稱中包含哈希值(如?
dashboard-child.[hash].js
),修改子路由組件時僅會更新對應 Chunk 的哈希值,不影響其他 Chunk 的緩存。 - 兼容性:魔法注釋需 Webpack 2+ 支持,若使用舊版本需升級或改用其他方式。
- 按需加載時機:合并后的 Chunk 仍為異步加載,僅在訪問父路由或子路由時觸發下載,不會影響首屏性能。
打包效果驗證
構建完成后,可通過 Webpack 的?stats.json
?或可視化工具(如?webpack-bundle-analyzer
)查看 Chunk 分組情況:
npm run build -- --stats
# 或安裝分析工具
npm install webpack-bundle-analyzer --save-dev
在分析報告中搜索?dashboard-child
,確認所有子路由組件是否包含在同一個 Chunk 中。
通過以上方法,可靈活控制 Webpack 將某路由下的子組件打包到同一 Chunk,減少 HTTP 請求數量,提升路由切換時的加載速度。實際應用中需結合項目結構和路由設計,選擇最適合的分組策略,平衡性能優化與代碼可維護性。
為 objectType 對象編寫一個 TypeScript 接口(需明確屬性類型)
在 TypeScript 中,接口(Interface)用于定義對象的形狀,明確屬性的類型、可選性及函數類型等。為?objectType
?對象編寫接口時,需根據實際需求指定每個屬性的類型,包括基本類型(如字符串、數字、布爾值)、復雜類型(如數組、對象)、函數類型或聯合類型等。以下是不同場景的示例:
基礎屬性接口
若對象包含姓名(字符串)、年齡(數字)、是否激活(布爾值),接口可定義為:
interface ObjectType {name: string; // 必選字符串屬性age: number; // 必選數字屬性isActive?: boolean; // 可選布爾值屬性,問號表示可選
}
包含數組或對象的接口
若對象包含一個存儲用戶信息的數組(每個元素為對象)或嵌套對象,可進一步定義:
interface User { // 嵌套對象的接口userId: string;email: string;
}interface ObjectType {users: User[]; // 數組類型,元素為 User 接口類型settings: { // 直接定義嵌套對象的結構theme: 'light' | 'dark'; // 字面量聯合類型,限定取值fontSize: number;};
}
包含函數的接口
若對象包含方法(如獲取用戶信息的函數),需定義函數類型:
interface ObjectType {getUser: (id: number) => User; // 函數類型,參數為數字,返回值為 User 類型logMessage: (msg: string) => void; // 返回值為 void 的函數
}
可選屬性與只讀屬性
通過??
?標記可選屬性,通過?readonly
?標記只讀屬性(初始化后不可修改):
interface ObjectType {readonly id: string; // 只讀字符串屬性,創建后不可更改data?: any[]; // 可選數組屬性
}
索引簽名(動態屬性)
若對象包含不確定名稱的屬性,可通過索引簽名定義動態類型:
interface ObjectType {[key: string]: string | number; // 鍵為字符串,值為字符串或數字的動態屬性fixedProp: boolean; // 同時包含固定屬性
}
接口繼承
若需復用已有接口的屬性,可通過?extends
?關鍵字繼承:
interface BaseType {baseProp: string;
}interface ObjectType extends BaseType { // 繼承 BaseType 的屬性extendedProp: number;
}
注意事項
- 接口名稱通常以大寫字母開頭,符合駝峰命名法。
- 未標記為可選的屬性在創建對象時必須存在,否則會觸發 TypeScript 類型檢查錯誤。
- 函數類型需嚴格匹配參數類型和返回值類型,包括參數數量和類型順序。
使用 TypeScript 定義一個函數接口或泛型類型
TypeScript 中定義函數接口或泛型類型可增強代碼的類型安全性和復用性,適用于函數參數、返回值或復雜邏輯的類型約束。以下分場景說明:
一、函數接口(Function Interface)
函數接口用于定義函數的參數類型和返回值類型,可通過接口明確函數形狀。
示例 1:普通函數接口
定義一個加法函數接口,要求接收兩個數字參數,返回數字:
interface AddFunction {(a: number, b: number): number; // 函數簽名,參數和返回值類型
}const add: AddFunction = (x, y) => x + y; // 正確,符合接口定義
// const add: AddFunction = (x: string, y: string) => x + y; // 錯誤,參數類型不匹配
示例 2:帶可選參數的函數接口
若函數參數可選,需在參數名后加??
:
interface GreetFunction {(name?: string, isFormal?: boolean): string; // 可選參數
}const greet: GreetFunction = (name = 'Guest', isFormal = false) => {return isFormal ? `Hello, ${name}` : `Hi, ${name}`;
};
示例 3:帶默認參數的函數接口
接口中可定義參數默認值(需配合函數實現):
interface MultiplyFunction {(a: number, b: number = 1): number; // b 有默認值 1
}const multiply: MultiplyFunction = (x, y) => x * y;
multiply(2); // 有效,y 取默認值 1,返回 2
二、泛型類型(Generic Types)
泛型通過?<T>
?占位符定義類型變量,使函數或類型可適應多種數據類型,避免重復定義類似邏輯。
示例 1:泛型函數
定義一個返回數組最后一個元素的函數,支持任意類型數組:
function getLastItem<T>(arr: T[]): T | undefined { // T 為數組元素類型return arr[arr.length - 1];
}const numbers = [1, 2, 3];
const lastNum = getLastItem(numbers); // 推斷為 number 類型
const strings = ['a', 'b', 'c'];
const lastStr = getLastItem(strings); // 推斷為 string 類型
示例 2:泛型接口
將泛型函數封裝為接口,明確泛型參數的位置:
interface GetItemFunction {<T>(arr: T[]): T | undefined; // 泛型參數在接口名稱后聲明
}const getItem: GetItemFunction = (arr) => arr[arr.length - 1];
示例 3:泛型類
定義一個棧(Stack)類,支持存儲任意類型數據:
class Stack<T> {private items: T[] = [];push(item: T): void {this.items.push(item);}pop(): T | undefined {return this.items.pop();}
}const numberStack = new Stack<number>(); // 實例化為數字棧
numberStack.push(10);
const poppedNum = numberStack.pop(); // 類型為 numberconst stringStack = new Stack<string>(); // 實例化為字符串棧
stringStack.push('hello');
const poppedStr = stringStack.pop(); // 類型為 string
示例 4:泛型約束(Generic Constraints)
限制泛型類型必須包含特定屬性,例如要求類型具有?length
?屬性(如字符串、數組):
interface HasLength {length: number;
}function logLength<T extends HasLength>(arg: T): T { // T 必須繼承 HasLength 接口console.log('Length:', arg.length);return arg;
}logLength('abc'); // 有效,string 有 length 屬性
logLength([1, 2, 3]); // 有效,array 有 length 屬性
// logLength(123); // 錯誤,number 沒有 length 屬性
三、泛型與函數接口結合
定義一個處理泛型數據的函數接口,適用于多種類型操作:
interface Processor {<T, U>(data: T, converter: (item: T) => U): U; // 雙泛型參數
}const process: Processor = (data, converter) => converter(data);// 示例調用:將數字轉為字符串
const result = process(42, (num) => num.toString()); // 類型推導為 string
關鍵總結
- 函數接口通過?
(參數: 類型) => 返回值類型
?定義函數簽名,確保函數實現符合類型約束。 - 泛型通過?
<T>
?動態指定類型,提升代碼復用性,避免類型 “硬編碼”。 - 泛型約束(
extends
)可限制泛型的類型范圍,結合接口實現更精準的類型檢查。
Cookie 和 Token 的區別是什么?各自的使用場景有哪些?
Cookie 和 Token 是 Web 開發中用于狀態管理和身份驗證的重要機制,但設計理念、存儲方式和應用場景差異顯著。以下從技術特性和實際應用兩方面對比分析:
一、核心區別
維度 | Cookie | Token |
---|---|---|
本質 | 存儲于客戶端的鍵值對數據(HTTP 協議標準) | 服務端生成的令牌(通常為 JWT 或自定義字符串) |
存儲位置 | 瀏覽器內存(會話級)或磁盤(持久化) | 客戶端(如 localStorage、sessionStorage、內存) |
數據格式 | 簡單字符串(鍵值對,用分號分隔) | 結構化數據(如 JWT 包含頭部、載荷、簽名) |
自動傳輸 | 每次 HTTP 請求自動攜帶(受限域和路徑) | 需手動在請求頭中添加(如 Authorization 字段) |
安全性 | 較低(易受 XSS、CSRF 攻擊,需配合 HttpOnly) | 較高(無狀態,不依賴客戶端存儲特性) |
時效性 | 可設置過期時間(秒級至數年) | 通常短期有效(需定期刷新,防止令牌泄露) |
數據大小限制 | 單個 Cookie 不超過 4KB,每個域名限制約 20 個 | 無固定限制(建議控制在合理范圍,避免影響性能) |
二、關鍵特性對比
-
Cookie 的局限性
- 自動傳輸的雙刃劍:瀏覽器自動攜帶 Cookie 節省開發成本,但會增加請求頭冗余(如靜態資源請求也會攜帶 Cookie),影響性能。
- 安全風險:
- 若未設置?
HttpOnly
,易被 XSS 攻擊竊取; - 需配合?
SameSite
?屬性防范 CSRF 攻擊(如設置為?strict
?或?lax
)。
- 若未設置?
- 存儲限制:數據量小,不適合存儲復雜信息。
-
Token 的優勢
- 無狀態設計:服務端無需存儲會話狀態,可橫向擴展(如分布式架構),適合高并發場景。
- 靈活的驗證方式:可通過簽名(如 JWT 的 HMAC 算法)驗證令牌合法性,無需查詢數據庫。
- 跨域友好:不依賴瀏覽器原生 Cookie 機制,可在 AJAX 請求中手動攜帶,適用于前后端分離項目。
三、使用場景
Cookie 的典型應用
-
會話管理(傳統服務端渲染項目)
- 場景:用戶登錄后,服務端生成?
sessionId
?存儲于 Cookie,后續請求通過?sessionId
?查找服務端會話數據。 - 示例:PHP 的?
session_start()
、Java 的?HttpSession
?機制。
- 場景:用戶登錄后,服務端生成?
-
個性化配置
- 存儲用戶偏好(如語言、主題),每次請求自動攜帶以返回定制化內容。
-
跨站點請求偽造(CSRF)防護
- 通過 Cookie 存儲 CSRF Token,與表單提交的 Token 對比驗證(需配合?
SameSite
?屬性)。
- 通過 Cookie 存儲 CSRF Token,與表單提交的 Token 對比驗證(需配合?
注意:Cookie 不適合存儲敏感信息(如密碼),且在移動端 SDK 中支持較差。
Token 的典型應用
-
前后端分離項目的身份驗證
- 場景:用戶登錄后,服務端返回 JWT Token,前端存儲于?
localStorage
?或內存,每次請求在?Authorization
?頭中攜帶?Bearer ${token}
。 - 優勢:無狀態,適合 RESTful API 和微服務架構。
- 場景:用戶登錄后,服務端返回 JWT Token,前端存儲于?
-
第三方服務鑒權(OAuth 2.0)
- 如微信、GitHub 登錄,通過 Access Token 訪問用戶資源,避免直接暴露用戶憑證。
-
臨時權限發放
- 生成短期有效的 Token 用于文件下載、密碼重置等場景,到期后自動失效。
-
跨域通信
- 在非同源請求中(如前端調用后端 API),手動傳遞 Token 避免瀏覽器同源策略限制。
四、實踐建議
-
選擇 Cookie 的情況:
- 需要兼容老舊瀏覽器(如 IE 6-10 對 localStorage 支持有限);
- 需利用瀏覽器自動發送機制(如簡單的會話管理)。
-
選擇 Token 的情況:
- 前后端分離架構或移動端開發;
- 需支持分布式部署或微服務;
- 對安全性要求較高(如避免 CSRF 風險)。
-
混合使用場景:
- 用 Cookie 存儲 CSRF Token,用 Token 進行身份驗證(如 Django、Ruby on Rails 的默認方案)。
什么是 JWT(JSON Web Token)?簡述其工作原理和應用場景。
JWT(JSON Web Token)是一種基于 JSON 的開放標準(RFC 7519),用于在網絡通信中安全地傳輸信息。其設計目標是實現無狀態身份驗證和數據交換,廣泛應用于前后端分離、分布式系統和第三方登錄等場景。
一、JWT 的結構
JWT 由三部分組成,通過?.
?分隔,格式為:
header.payload.signature
-
Header(頭部)
- 描述令牌的元數據,包含兩部分:
typ
:令牌類型,固定為?JWT
;alg
:簽名算法,常用?HS256
(HMAC-SHA256)或?RS256
(RSA-SHA256)。
- 示例:
json
{ "typ": "JWT", "alg": "HS256" }
- 頭部會被 Base64Url 編碼(非加密),生成第一部分字符串。
- 描述令牌的元數據,包含兩部分:
-
Payload(載荷)
- 存儲實際數據(Claims,聲明),分為三類:
- 注冊聲明:預定義字段(如?
iss
- 簽發者,exp
- 過期時間,sub
- 主題); - 公共聲明:自定義字段(如用戶 ID、角色);
- 私有聲明:用于雙方約定的自定義數據(非標準,需避免沖突)。
- 注冊聲明:預定義字段(如?
- 示例:
json
{ "sub": "123456", "name": "John Doe", "admin": true, "exp": 1689345600 // 過期時間(Unix 時間戳) }
- 載荷同樣經過 Base64Url 編碼,生成第二部分字符串。
- 存儲實際數據(Claims,聲明),分為三類:
-
Signature(簽名)
- 用于驗證令牌的完整性和合法性,生成方式:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret // 服務端私有的密鑰(HS256 算法)或私鑰(RS256 算法) )
- 簽名結果經 Base64Url 編碼后成為第三部分字符串。
- 用于驗證令牌的完整性和合法性,生成方式:
二、工作原理
-
簽發令牌(Login)
- 用戶登錄時,客戶端向服務端發送憑證(如用戶名 / 密碼)。
- 服務端驗證通過后,根據用戶信息生成 JWT,包含有效期等聲明,通過響應返回給客戶端。
-
攜帶令牌(Request)
- 客戶端收到 JWT 后,存儲于?
localStorage
、sessionStorage
?或內存中。 - 后續每次請求(如訪問 API)時,在請求頭中添加?
Authorization: Bearer <JWT>
。
- 客戶端收到 JWT 后,存儲于?
-
驗證令牌(Validation)
- 服務端接收請求,提取 JWT 并拆解為三部分。
- 用相同算法和密鑰對頭部和載荷重新計算簽名,與令牌中的簽名對比:
- 一致則驗證通過,解析載荷獲取用戶信息;
- 不一致或過期則返回 401 未授權錯誤。
-
無狀態特性
- 服務端無需存儲會話數據,每次請求完全依賴 JWT 自身攜帶的信息,支持水平擴展和微服務架構。
三、核心優勢
- 輕量級:數據格式為 JSON,體積小,傳輸效率高。
- 自包含:載荷攜帶用戶權限等必要信息,避免頻繁查詢數據庫。
- 跨語言兼容:基于標準協議,可在多種后端語言(如 Node.js、Python、Java)中統一實現。
- 安全可靠:通過簽名防止數據篡改,配合 HTTPS 可避免令牌在傳輸中被竊取。
四、應用場景
-
身份驗證(最核心場景)
- 前后端分離項目中,替代傳統的 Cookie + Session 機制,實現無狀態登錄。
- 示例:用戶登錄后,前端攜帶 JWT 訪問?
/api/user
?接口,服務端驗證后返回用戶信息。
-
單點登錄(SSO)
- 多個子系統共享一個 JWT 簽發中心,用戶登錄后可憑同一令牌訪問所有子系統(需注意跨域問題)。
-
權限管理
- 在載荷中存儲用戶角色(如?
admin
、user
),服務端根據角色控制接口訪問權限。
// 示例:驗證用戶是否有權限訪問管理員接口 const token = request.headers.authorization.split(' ')[1]; const payload = verifyToken(token); // 解析并驗證令牌 if (!payload.admin) { throw new Error('無管理員權限'); }
- 在載荷中存儲用戶角色(如?
-
數據傳遞(非敏感信息)
- 例如在 URL 中傳遞臨時令牌(如密碼重置鏈接),避免明文傳輸敏感數據。
https://example.com/reset-password?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
-
第三方服務集成
- 在 OAuth 2.0 中,作為 Access Token 訪問用戶資源(如微信開放平臺獲取用戶頭像)。
五、注意事項
- 令牌安全存儲:避免將 JWT 存儲于 Cookie(易受 XSS 攻擊),優先使用?
HttpOnly
?標記的 Cookie 或內存存儲(如 Vuex、Redux)。 - 有效期控制:設置較短的過期時間(如 15 分鐘),配合刷新令牌(Refresh Token)機制延長會話。
- 簽名算法選擇:
HS256
:適用于單體應用(密鑰僅服務端持有);RS256
:適用于分布式系統(公鑰公開,私鑰僅簽發中心持有)。
- 防止 CSRF 攻擊:在使用 Cookie 存儲 JWT 時,需結合?
SameSite
?屬性和 CSRF Token 雙重防護。
項目中 UI 設計的流程是怎樣的?是否獨立完成過 UI 設計?
在前端項目中,UI 設計流程通常與產品需求、交互邏輯緊密結合,需經歷從需求分析到視覺落地的多階段協作。以下是標準流程的詳細說明,以及關于獨立設計能力的闡述:
一、UI 設計核心流程
1. 需求分析與目標定義
- 明確業務目標:與產品經理、后端開發團隊溝通,理解項目定位(如企業官網、電商平臺、管理系統)和用戶群體(如 C 端消費者、B 端企業用戶)。
- 拆解功能模塊:根據需求文檔梳理頁面結構(如首頁、列表頁、詳情頁)和交互場景(如表單提交、數據可視化)。
- 關鍵輸出:功能清單、用戶畫像、頁面流程圖。
示例:若開發一款電商 APP,需優先考慮商品瀏覽、購物車、支付等核心流程的易用性,針對年輕用戶群體設計輕量化視覺風格。
2. 交互設計(低保真原型)
- 繪制線框圖:使用 Figma、Axure 或 Sketch 工具創建低保真原型,聚焦頁面布局、信息層級和操作邏輯。
- 確定導航欄位置(頂部或底部)、按鈕交互狀態(默認 / 點擊 / 禁用)、列表項展示方式(圖文混排或純文本)。
- 交互邏輯說明:標注頁面跳轉規則(如點擊按鈕打開模態窗)、異常狀態處理(如網絡錯誤提示)。
- 關鍵輸出:可交互原型圖、交互說明文檔。
示例:在管理系統中,數據表格需支持篩選、排序、分頁功能,線框圖需明確篩選條件的觸發位置和展示形式。
3. 視覺設計(高保真設計)
- 建立設計系統:
- 基礎樣式:定義品牌色(主色、輔助色)、字體規范(標題 / 正文的字號、字重、行高)、按鈕尺寸(大 / 中 / 小);
- 組件庫:設計通用組件(輸入框、下拉菜單、模態框),確保交互一致性;
- 圖標體系:選擇或繪制符合產品調性的圖標(如線性圖標、填充圖標)。
- 頁面視覺落地:
- 將線框圖轉化為高保真設計稿,注重色彩對比、留白比例、動效預覽(如按鈕點擊反饋動畫);
- 適配多端設備(如 PC 端 1920px 分辨率、移動端 iPhone 14 尺寸),考慮響應式布局規則。
- 關鍵輸出:視覺設計稿(標注版)、組件庫文件、設計規范文檔。
示例:在官網首頁設計中,主按鈕采用品牌色 #2B6CB0,hover 狀態透明度降低 10%,點擊時添加輕微縮放動畫,增強操作反饋。
4. 開發協作與交付
- 切圖與資源輸出:
- 使用 Figma 的插件自動導出圖片資源(PNG、SVG),確保視網膜屏適配(2x、3x 圖);
- 標注元素尺寸、間距、字體樣式(如標題 font-size: 24px,line-height: 32px)、陰影參數(box-shadow: 0 2px 4px rgba (0,0,0,0.1))。
- 前端對接:
- 與開發團隊溝通交互細節(如列表滾動加載的觸發距離),解答樣式實現疑問(如 CSS 彈性布局的兼容性處理);
- 跟蹤開發進度,進行視覺走查,確保頁面還原度(如按鈕圓角半徑、輸入框邊框顏色是否與設計稿一致)。
- 關鍵輸出:切圖壓縮包、標注鏈接(如 Zeplin 或藍湖)、動效開發說明(如使用 CSS3 animation 或 Lottie)。
5. 測試與迭代優化
- 可用性測試:邀請真實用戶體驗產品,收集反饋(如按鈕位置不便于單手操作、配色導致視覺疲勞)。
- 數據驅動優化:分析頁面熱力圖、用戶點擊流,調整高流量頁面的布局(如將核心功能按鈕上移至首屏)。
- 版本迭代:根據業務需求更新設計(如節日活動主題換膚),維護組件庫的擴展性(如新增加載中狀態的骨架屏)。
二、獨立完成 UI 設計的能力
是否具備獨立設計經驗?
-
前端開發視角的設計能力:
多數前端開發者具備基礎視覺感知能力,可使用 Figma 完成簡單頁面布局(如官網單頁、表單頁面),但復雜交互(如數據大屏可視化、3D 動效)需依賴專業 UI/UX 設計師。- 優勢:熟悉前端實現邏輯,設計時會考慮技術可行性(如避免使用 CSS 不支持的濾鏡效果);
- 局限:缺乏用戶體驗理論支撐(如尼爾森可用性原則)、色彩搭配專業性(如未系統學習色輪理論)。
-
協作場景下的角色定位:
在中小型團隊中,前端開發者可能承擔部分輕量化設計任務(如調整現有組件樣式、適配移動端界面),但大型項目仍需專業設計師主導全流程。- 示例:獨立開發個人博客時,可自主設計頁面結構和配色;參與企業級項目時,需基于現有設計系統開發組件,不涉及從零構建視覺體系。
在項目中使用過哪些 Vue 生命周期鉤子?在 created 鉤子中如何訪問 DOM 元素?
Vue 的生命周期鉤子是組件實例從創建到銷毀的各個階段中自動執行的函數,貫穿組件的整個生命周期。項目中常用的鉤子函數涵蓋組件創建階段、掛載階段、更新階段和卸載階段,不同階段適用于不同的業務場景。
常用的 Vue 生命周期鉤子
-
beforeCreate
在組件實例初始化之后、數據觀測(data observer)和事件配置之前調用。此時組件的?data
?和?methods
?尚未初始化,無法訪問組件狀態,通常用于插件初始化或執行與組件狀態無關的操作。 -
created
組件實例創建完成后調用,此時?data
?和?methods
?已初始化,可以訪問組件的響應式數據和方法,但組件尚未掛載到 DOM 上(即?$el
?不存在)。這一階段常用于數據獲取(如調用 API 加載初始數據)、事件監聽或非 DOM 依賴的邏輯預處理。 -
beforeMount
在組件即將掛載到 DOM 前調用,此時?$el
?已生成(通過?template
?或?render
?函數編譯),但尚未插入真實 DOM。可用于在渲染前對?$el
?進行最后的修改,或在服務端渲染(SSR)場景中做特殊處理。 -
mounted
組件掛載到真實 DOM 后調用,此時可以通過?$refs
?或原生 DOM API 訪問 DOM 元素,適合執行依賴 DOM 的操作(如初始化第三方庫、設置滾動監聽、獲取元素尺寸等)。需要注意的是,若組件存在異步更新或嵌套子組件,mounted
?會在所有子組件掛載完成后觸發,因此若需操作子組件的 DOM,可能需要結合?$nextTick
。 -
beforeUpdate
組件數據更新之前調用,此時數據已發生變化,但 DOM 尚未更新。可用于在更新前獲取現有 DOM 狀態,或執行一些與狀態變更相關的預處理邏輯。 -
updated
組件數據更新且 DOM 重新渲染完成后調用,此時可以訪問更新后的 DOM。需注意避免在此鉤子中修改狀態,否則會觸發新一輪的更新循環。常見場景包括基于新 DOM 結構的重新計算或動畫觸發。 -
beforeUnmount
組件即將卸載前調用,用于清理副作用(如移除事件監聽、取消定時器、銷毀第三方實例等),避免內存泄漏。 -
unmounted
組件卸載完成后調用,此時組件實例已被銷毀,所有子組件也已卸載,通常無需在此階段執行操作,但可用于執行最終的清理工作。
此外,Vue 還提供了錯誤處理鉤子(如?errorCaptured
)和服務端渲染鉤子(如?ssrRendered
),適用于特定場景。
在 created 鉤子中如何訪問 DOM 元素?
created 鉤子中無法直接訪問 DOM 元素,因為此時組件尚未掛載到 DOM 上,$el
?屬性尚未生成。若強行通過?document.querySelector
?等方法直接操作 DOM,可能會因 DOM 未渲染而導致錯誤或無效操作。
若業務需求需要在數據初始化后立即操作 DOM,可通過以下方式實現:
-
使用 mounted 鉤子
將 DOM 操作邏輯轉移到?mounted
?鉤子中,此時組件已掛載,$refs
?和 DOM 元素均已可用。<template> <div ref="targetDiv">示例文本</div> </template> <script> export default { data() { return { /* 數據 */ }; }, created() { // 此處無法通過 $refs 訪問 DOM // 若需預處理數據,可在此處執行 }, mounted() { const div = this.$refs.targetDiv; // 正確方式:在 mounted 中訪問 console.log(div.textContent); } }; </script>
-
?結合 Vue 的?若需在數據更新后立即獲取更新后的(如在中觸發了數據變更),可通過nextTick` 延遲到下一個 DOM 更新周期執行。
?<script> export default { created() { this.fetchData(); // 假設 fetchData 會修改響應式數據 }, methods: { async fetchData() { const data = await api.getData(); this.data = data; // 修改數據后,DOM 尚未更新 // 使用 $nextTick 確保 DOM 已更新 this.$nextTick(() => { const div = document.querySelector('.target'); // 在此處執行 DOM 操作 }); } } }; </script>
$nextTick
?的原理是將回調函數延遲到 Vue 的異步更新隊列之后執行,確保此時 DOM 已完成渲染。 -
避免在 created 中依賴 DOM
組件的生命周期設計決定了?created
?階段應專注于數據邏輯和非 DOM 操作。若業務邏輯強依賴 DOM,需調整架構,例如將邏輯封裝到自定義指令、組合式函數(Vue 3 的 Composition API)或單獨的 DOM 操作服務中,通過依賴注入的方式在合適的生命周期階段調用。