在前端開發中,Vue 的數據響應機制、腳本加載策略以及函數式編程技巧是高頻考點和日常開發的核心基礎。本文將圍繞這幾個關鍵點展開詳細解析,幫助開發者深入理解其原理與應用。
一、Vue2 與 Vue3 的數據響應原理對比
Vue 的核心特性之一是數據響應式—— 當數據變化時,視圖自動更新。但 Vue2 和 Vue3 實現這一特性的底層原理存在顯著差異。
1. Vue2 的數據響應原理:Object.defineProperty
Vue2 通過 **Object.defineProperty
劫持對象的 getter 和 setter** 實現響應式。其核心邏輯是:
初始化時遍歷data
中的屬性,為每個屬性設置getter
(獲取值時收集依賴)和setter
(修改值時觸發更新)。
實現核心步驟:
- 遞歸遍歷
data
中的所有屬性(包括嵌套對象); - 對每個屬性調用
Object.defineProperty
,重寫get
和set
方法; - 當屬性被訪問時(
get
),收集當前依賴(如組件渲染函數); - 當屬性被修改時(
set
),通知所有依賴更新(觸發視圖重新渲染)。
代碼示例(簡化版):
function defineReactive(obj, key, value) {// 遞歸處理嵌套對象observe(value);Object.defineProperty(obj, key, {get() {console.log(`獲取${key}的值: ${value}`);// 收集依賴(實際中會關聯Dep和Watcher)Dep.target && dep.addSub(Dep.target);return value;},set(newVal) {if (newVal !== value) {console.log(`更新${key}的值: ${newVal}`);value = newVal;// 通知依賴更新dep.notify();}}});
}// 遍歷對象屬性,批量設置響應式
function observe(obj) {if (typeof obj !== 'object' || obj === null) return;Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key]);});
}
局限性:
- 無法監聽數組索引變化(如
arr[0] = 1
)和對象新增屬性(如obj.newKey = 1
); - 需通過
Vue.set
或this.$set
手動觸發響應式(本質是為新增屬性重新設置getter/setter
); - 對數組的監聽通過重寫
push/pop/splice
等 7 個方法實現(修改數組時觸發更新)。
2. Vue3 的數據響應原理:Proxy
Vue3 改用 **Proxy
代理對象 ** 實現響應式,解決了 Vue2 的局限性。Proxy
可以直接代理整個對象,而非單個屬性,支持監聽更多場景。
實現核心優勢:
- 代理整個對象:無需遞歸遍歷屬性,初始化性能更好;
- 支持監聽新增屬性 / 刪除屬性:如
obj.newKey = 1
或delete obj.key
; - 原生支持數組索引修改:如
arr[0] = 1
可直接被監聽; - 支持
Map
、Set
等復雜數據結構的響應式。
代碼示例(簡化版):
function reactive(obj) {return new Proxy(obj, {get(target, key) {console.log(`獲取${key}的值: ${target[key]}`);// 收集依賴(類似Vue2的Dep)track(target, key);return target[key];},set(target, key, value) {if (target[key] !== value) {console.log(`更新${key}的值: ${value}`);target[key] = value;// 通知更新(類似Vue2的Watcher)trigger(target, key);}},deleteProperty(target, key) {console.log(`刪除${key}`);delete target[key];trigger(target, key); // 刪除也觸發更新}});
}
總結:
特性 | Vue2(Object.defineProperty) | Vue3(Proxy) |
---|---|---|
監聽范圍 | 僅已定義的屬性 | 整個對象(含新增 / 刪除) |
數組支持 | 需重寫方法,不支持索引修改 | 原生支持索引修改和方法調用 |
初始化性能 | 遞歸遍歷屬性,性能較差 | 懶代理,性能更優 |
復雜數據結構(Map) | 不支持 | 支持 |
二、Vue2 如何監聽 data 里的數據變化
Vue2 對data
的監聽是一個遞歸劫持 + 依賴收集的過程,核心通過Observer
、Dep
、Watcher
三個類協同實現。
1. 核心流程:
- 初始化
data
:組件初始化時,data
函數返回的對象會被傳入observe
函數; - 創建
Observer
實例:observe
函數會為對象創建Observer
實例,負責劫持屬性; - 劫持屬性:
Observer
通過Object.defineProperty
重寫對象的getter/setter
; - 收集依賴(
Dep
):當屬性被訪問時(如渲染時),getter
會將當前Watcher
(依賴)添加到Dep
(依賴管理器)中; - 觸發更新:當屬性被修改時,
setter
會通知Dep
,Dep
再通知所有Watcher
執行更新(如重新渲染組件)。
2. 數組的特殊處理:
由于Object.defineProperty
無法監聽數組索引變化,Vue2 通過重寫數組原型方法實現監聽:
// 重寫數組的7個變更方法
const arrayMethods = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {arrayMethods[method] = function(...args) {// 執行原數組方法const result = Array.prototype[method].apply(this, args);// 通知更新(觸發Observer的dep)this.__ob__.dep.notify();return result;};
});// 為數組設置新原型
function observeArray(arr) {arr.__proto__ = arrayMethods; // 覆蓋數組原型for (let i = 0; i < arr.length; i++) {observe(arr[i]); // 遞歸監聽數組元素}
}
三、watch 與 computed 的區別
watch
和computed
都是 Vue 中監聽數據變化的工具,但應用場景截然不同。
1. 核心差異對比:
特性 | computed(計算屬性) | watch(監聽器) |
---|---|---|
本質 | 基于依賴的衍生數據(類似 “變量”) | 數據變化后的回調函數(類似 “事件”) |
緩存機制 | 有緩存,依賴不變則不重新計算 | 無緩存,數據變化即觸發回調 |
返回值 | 必須有返回值(用于頁面渲染) | 無返回值(用于執行副作用,如異步操作) |
異步支持 | 不支持(不能包含異步邏輯,否則緩存失效) | 支持(可執行異步操作,如接口請求) |
適用場景 | 簡單的衍生數據計算(如拼接字符串、計算總價) | 復雜的副作用處理(如數據變化后請求接口) |
2. 代碼示例:
// computed示例:計算全名(有緩存)
computed: {fullName() {// 依賴firstName和lastName,只有它們變化時才重新計算return `${this.firstName} ${this.lastName}`;}
}// watch示例:監聽name變化,執行異步操作
watch: {name(newVal, oldVal) {// 支持異步(如請求接口)this.$axios.get(`/user?name=${newVal}`).then(res => {this.userInfo = res.data;});}
}
3. 總結:
- 當需要根據已有數據生成新數據時,用
computed
(利用緩存提升性能); - 當需要在數據變化時執行異步操作或復雜邏輯時,用
watch
。
四、script 標簽的 defer 和 async 區別
script
標簽的defer
和async
屬性用于控制腳本的加載與執行時機,解決默認加載阻塞 HTML 解析的問題。
1. 默認行為:
不添加defer
或async
時,腳本加載會阻塞 HTML 解析:瀏覽器遇到script
標簽時,會暫停 HTML 解析,下載腳本并立即執行,執行完成后再繼續解析 HTML。
2. defer 與 async 的差異:
特性 | async | defer |
---|---|---|
加載時機 | 并行下載腳本(不阻塞 HTML 解析) | 并行下載腳本(不阻塞 HTML 解析) |
執行時機 | 下載完成后立即執行(可能打斷 HTML 解析) | 下載完成后等待 HTML 解析完畢再執行 |
執行順序 | 不保證順序(哪個先下載完就先執行) | 嚴格按照 HTML 中腳本的順序執行 |
適用場景 | 獨立腳本(如統計腳本、廣告腳本) | 依賴 DOM 或順序執行的腳本(如 jQuery 插件) |
3. 執行流程圖示:
- 默認(無屬性):加載阻塞解析 → 執行阻塞解析;
- async:加載不阻塞解析 → 執行阻塞解析(順序不確定);
- defer:加載不阻塞解析 → 執行在 HTML 解析完成后(順序與標簽一致)。
4. 總結:
- 若腳本無需依賴 DOM 且無順序要求(如獨立工具庫),用
async
; - 若腳本依賴 DOM 或需要按順序執行(如 jQuery 后加載插件),用
defer
。
五、函數柯里化及常見應用場景
函數柯里化(Currying)?是將多參數函數轉化為一系列單參數函數的技術,形如f(a,b,c)
?→?f(a)(b)(c)
。
1. 核心原理:
通過閉包收集參數,當參數數量滿足原函數需求時,執行原函數;否則返回一個新函數繼續收集參數。
實現示例:
// 將多參數函數柯里化
function curry(fn) {const argsLength = fn.length; // 原函數的參數數量return function curried(...args) {// 若收集的參數足夠,執行原函數if (args.length >= argsLength) {return fn.apply(this, args);}// 否則返回新函數,繼續收集參數return function(...nextArgs) {return curried.apply(this, args.concat(nextArgs));};};
}// 測試:柯里化add函數
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
2. 常見應用場景:
參數復用:固定部分參數,簡化函數調用。
例:計算商品稅后價格(固定稅率):const calculateTax = (taxRate, price) => price * (1 + taxRate); const withTax = curry(calculateTax)(0.1); // 固定稅率10% withTax(100); // 110(無需重復傳稅率)
延遲執行:分步傳遞參數,直到條件滿足再執行。
例:事件綁定中分步傳參:// 柯里化事件處理函數 const handleClick = curry((id, event) => {console.log(`點擊了ID為${id}的元素,事件:`, event); });// 綁定事件時先傳id,事件觸發時傳event document.getElementById('btn1').onclick = handleClick(1); document.getElementById('btn2').onclick = handleClick(2);
函數式編程:配合
compose
、pipe
等工具實現函數組合。
總結
本文解析了前端開發中的 5 個核心知識點:
- Vue2 通過
Object.defineProperty
劫持屬性,Vue3 通過Proxy
代理對象,后者更全面; - Vue2 對
data
的監聽是遞歸劫持 + 依賴收集的過程,數組需特殊處理; computed
適用于衍生數據計算(有緩存),watch
適用于異步副作用(無緩存);async
腳本加載完立即執行(亂序),defer
等待 HTML 解析后執行(順序);- 柯里化通過閉包分步收集參數,適用于參數復用、延遲執行等場景。