作者:盞燈
https://juejin.cn/post/7493539513106677769
前言
你說,為什么?尤雨溪搞響應(yīng)式,他為什么要換掉Object.defineProperty呢?
proxy什么來頭?
有一次??看他直播,說去面試人家問他原型鏈
,他不會,GG了面試黃了,你說他是不是無中生有暗度陳倉憑空想象憑空捏造new Proxy
來換掉Object.defineProperty
的呢?
還真不是,尤雨溪的響應(yīng)式,我們暫且叫成插一腳
吧??,請聽我細細道來??
在前端開發(fā)中,響應(yīng)式系統(tǒng)是現(xiàn)代框架的核心特性。無論是 Vue 還是 React,它們都需要實現(xiàn)一個基本功能:當(dāng)數(shù)據(jù)變化時,自動更新相關(guān)的視圖。用通俗的話說,就是要在數(shù)據(jù)被讀取或修改時"插一腳",去執(zhí)行一些額外的操作(比如界面刷新、計算屬性重新計算等)。
// 讀取屬性時
obj.a; // 需要知道這個屬性被讀取了
// 修改屬性時
obj.a = 3; // 需要知道這個屬性被修改了
但原生 JavaScript 對象不會告訴我們這些操作的發(fā)生。那么,尤雨溪是如何實現(xiàn)這種"插一腳"的能力的呢?
正文
Vue 2 的"插一腳"方案 - Object.defineProperty
基本實現(xiàn)原理
Vue 2 使用的是 ES5 的 `Object.defineProperty`[1] API。這個 API 允許我們定義或修改對象的屬性,并為其添加 getter 和 setter。
const obj = { a: 1 };
let v = obj.a;
Object.defineProperty(obj, 'a', {
get() {
console.log('讀取 a'); // 插一腳:知道屬性被讀取了
return v;
},
set(val) {
console.log('更新 a'); // 插一腳:知道屬性被修改了
v = val;
}
});
obj.a; // 輸出"讀取 a"
obj.a = 3; // 輸出"更新 a"
完整對象監(jiān)聽
為了讓整個對象可響應(yīng),Vue 2 需要遍歷對象的所有屬性:
function observe(obj) {
for (const k in obj) {
let v = obj[k];
Object.defineProperty(obj, k, {
get() {
console.log('讀取', k);
return v;
},
set(val) {
console.log('更新', k);
v = val;
}
});
}
}
處理嵌套對象
對于嵌套對象,還需要遞歸地進行觀察:
function _isObject(v) {
returntypeof v === 'object' && v !== null;
}
function observe(obj) {
for (const k in obj) {
let v = obj[k];
if (_isObject(v)) {
observe(v); // 遞歸處理嵌套對象
}
Object.defineProperty(obj, k, {
get() {
console.log('讀取', k);
return v;
},
set(val) {
console.log('更新', k);
v = val;
}
});
}
}
Vue 2 方案的兩大缺陷
缺陷一:效率問題
在這種模式下,他就必須要去遍歷這個對象里邊的每一個屬性...這是第一個缺陷:必須遍歷對象的所有屬性,對于大型對象或深層嵌套對象,這會帶來性能開銷。
缺陷二:新增屬性問題
無法檢測到對象屬性的添加或刪除:
obj.d = 2; // 這個操作不會被監(jiān)聽到
因為一開始遍歷的時候沒有這個屬性,后續(xù)添加的屬性不會被自動觀察。
Vue 3 的"插一腳"方案 - Proxy
基本實現(xiàn)原理
Vue 3 使用 ES6 的 `Proxy`[2] 來重構(gòu)響應(yīng)式系統(tǒng)。Proxy 可以攔截整個對象的操作,而不是單個屬性。
const obj = { a: 1 };
const proxy = newProxy(obj, {
get(target, k) {
console.log('讀取', k); // 插一腳
return target[k];
},
set(target, k, val) {
if (target[k] === val) returntrue;
console.log('更新', k); // 插一腳
target[k] = val;
returntrue;
}
});
proxy.a; // 輸出"讀取 a"
proxy.a = 3; // 輸出"更新 a"
proxy.d; // 輸出"讀取 d" - 連不存在的屬性也能監(jiān)聽到!
完整實現(xiàn)
function _isObject(v) {
returntypeof v === 'object' && v !== null;
}
function reactive(obj) {
const proxy = newProxy(obj, {
get(target, k) {
console.log('讀取', k);
const v = target[k];
if (_isObject(v)) {
return reactive(v); // 惰性遞歸
}
return v;
},
set(target, k, val) {
if (target[k] === val) returntrue;
console.log('更新', k);
target[k] = val;
returntrue;
}
});
return proxy;
}
Proxy 的優(yōu)勢
- 無需初始化遍歷:直接代理整個對象,不需要初始化時遍歷所有屬性
- 全面攔截:可以檢測到所有屬性的訪問和修改,包括新增屬性
- 性能更好:采用惰性處理,只在屬性被訪問時才進行響應(yīng)式處理
- 更自然的開發(fā)體驗:不需要特殊 API 處理數(shù)組和新增屬性
"proxy 它解決了什么問題?兩個問題。
第一個問題不需要深度遍歷了,因為它不再監(jiān)聽屬性了,而是監(jiān)聽的什么?整個對象。
同時也由于它監(jiān)聽了整個對象,就解決了第二個問題:能監(jiān)聽這個對象的所有操作,包括你去讀寫一些不存在的屬性,都能監(jiān)聽到。"
原理對比與源碼解析
原理對比
源碼實現(xiàn)差異
Vue 2 實現(xiàn):
Vue 3 實現(xiàn):
- 使用 Proxy 實現(xiàn)基礎(chǔ)響應(yīng)式
為什么 Proxy 是更好的選擇?
- 更全面的攔截能力:可以攔截對象的所有操作,包括屬性訪問、賦值、刪除等
- 更簡潔的 API:不再需要 Vue.set/Vue.delete 等特殊 API
- 更自然的開發(fā)體驗:開發(fā)者可以使用普通的 JavaScript 語法操作對象
總結(jié)
需顯式操作(defineProperty)-> 聲明式編程(Proxy)
局部監(jiān)聽(屬性級別)-> 全局攔截(對象級別)
。
從 Object.defineProperty 到 Proxy 的轉(zhuǎn)變,不僅是 API 的升級,更是前端框架設(shè)計理念的進步。Vue 3 的響應(yīng)式系統(tǒng)通過 Proxy 實現(xiàn)了更高效、更全面的數(shù)據(jù)監(jiān)聽。
該文章在 2025/7/25 14:48:31 編輯過