如果您覺得這篇文章有幫助的話!給個點贊和評論支持下吧,感謝~
作者:前端小王hs
阿里云社區博客專家/清華大學出版社簽約作者/csdn百萬訪問前端博主/B站千粉前端up主
此篇文章是博主于2022年學習《Vue.js設計與實現》時的筆記整理而來
書籍:《Vue.js設計與實現》 作者:霍春陽
本篇博文將在書第5.1節至5.4節的基礎上進一步總結所提到的基礎概念,附加了測試的代碼運行示例,方便正在學習Vue3或想分析Vue3源碼的朋友快速閱讀
如有幫助,不勝榮幸
5.1 Proxy和Reflect
代理與被代理
5.1節開篇給到了一個信息:Proxy 只能代理對象,無法代理非對象值,例如字符串、布爾值等(原文)
關于什么是代理,可以舉個簡單的例子,有a
和b
兩個對象,如果我們想通過b
去訪問a
里的屬性,那么這個b
就是代理
可以理解這個b
為中介,而這個b
,就是new Proxy
的proxy
對象,如下代碼所示:
const b = new Proxy(a, {
})
對象a
就是被代理對象(a
被b
代理了),位于new Proxy
第一個參數,而(new Proxy
,下同)第二個參數是一個對象,里面包含了如get()
、set()
之類的攔截方法,關于這一點,在之前的筆記中深入理解Vue3.js響應式系統基礎邏輯也提到了
代理的作用
代理的作用在于可以攔截一些基本操作,如讀取、修改對象等
邏輯也非常簡單,原來通過a.foo
可以訪問到a
對象里的foo
屬性,現在代理了就變成通過b.foo
去訪問了,當訪問時就會觸發第二個參數里設置的如get()
、set()
之類的攔截方法,那么經過這些方法的攔截,就可以進行一些額外的操作,例如前文筆記里提到的響應式
Proxy攔截函數
函數也是對象,那么同理可以把一個函數fn
當作a
變為被代理對象,那么同理,當調用fn
時會觸發第二個參數內的攔截方法
apply與call
書中舉例了一個使用代理對象去調用fn
的例子,代碼如下:
function fn(name) {return '我是:' + name;
}// 使用 Proxy 攔截對 fn 的調用
const p = new Proxy(fn, {apply(target, thisArg, argArray) {return target.call(thisArg, ...argArray);}
})p('123') // 我是123
這里的apply
是proxy
支持的13個攔截方法之一,在阮一峰大佬的ES6中對apply
也有詳細的介紹:
apply(target, object, args) :攔截 Proxy 實例作為函數調用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。
這里的三個參數分別是:
- target:目標對象
- object:目標對象的上下文對象(this)
- args:目標對象的參數數組
回看上面的例子,apply
接收的三個形參的內容分別是:fn
、this
(undefined)、包含123的參數數組
接著返回了target.call(thisArg, ...argArray);
,也就是fn.call(undefined,'123')
,或者說fn('123')
這里的一個問題是,我們知道此時this
指向的是調用者(使用了call
),也就是fn
,那第一個參數thisArg
其實也就沒有發揮作用,或者說一直都是undefined
這里其實有個潛在條件就是,使用proxy.apply
的基本場景就是去攔截函數
整個流程其實就是:
- 用
p
代理了fn
- 傳入
123
,被apply
攔截 - 在攔截的邏輯里執行
fn('123')
Reflect的作用
Proxy
的方法,在Reflect
都能找到,也就是說Proxy
有get
,Reflect
里也有
Reflect.get(target, name, receiver)
和Reflect.set(target, name, value, receiver)
的作用和Proxy
內的get
、set
相同
書中提到Reflect
的原因是,使用target
即原始對象去完成對屬性的讀取,無法完成與副作用函數的綁定
我們來分析一下書中的例子,下面是代碼:
const obj = {foo: 1,get bar() {return this.foo;}
}; const p = new Proxy(obj, {get(target, key) {track(target, key);return target[key];},set(target, key, newVal) { target[key] = newVal;trigger(target, key);}
}); effect(() => {console.log(p.bar);
}); p.foo++;
這段代碼的問題是,執行p.foo++
,不會觸發副作用函數,這是為什么?
直接看當前的執行邏輯是怎么樣的:
- 執行
effect
,那么會輸出p.bar
,那么會觸發get
攔截 - 在
track
中,target
是obj
,key
是字符串bar
,也就是obj
的bar
會和當前effect
建立聯系(這一步不重要) - 返回
target[key]
,那么觸發getter
,此時這里的this
是指obj
,也就是最終返回的是obj.foo
給副作用函數
effect(()=>{ obj.foo })
也就是說,匿名函數輸出p.bar
,返回的是obj.foo
,那就很好理解了,obj
不是代理對象,在副作用函數里輸出obj.foo
不會觸發Proxy.get
攔截,自然就不會與副作用函數進行聯系了
解決的辦法就是使用Reflect.get(target, key, receiver)
代替target[key]
Reflect.get(target, key, receiver)
返回也是obj.foo
,但它的第三個參數receiver
可以指出是誰在調用
那么代碼就更新如下:
const p = new Proxy(obj, {get(target, key,receiver) {track(target, key);return Reflect.get(target, key, receiver);},// ...
});
那么現在,就可以把obj.foo
變為p.foo
了
5.2 JavaScript 對象及 Proxy 的工作原理
對象分為兩種,常規對象和異質對象
區分常規對象和異質對象的區別在于其內部方法是使用ECMA的哪一種規范決定的,這里不去詳細贅述。需要明白的是Proxy
是一個異質對象
如何區分普通對象和函數對象呢?文中提到:對象的實際語義是由對象的內部方法(internal method)指定的(原文)
最簡單的一個區分方法是,函數對象有call()
方法
內部方法具有多態性
在書中提到了代理透明性質,也就是如果定義了一個代理對象p
,但是內部沒有指定get()
,那么通過p
去訪問被代理對象的某個屬性,會調用原始對象的內部方法[[Get]]
。這一點在阮一峰的ES6關于proxy.get
一節也有記載
Proxy對象部署的所有內部方法
內部方法 | 處理器函數 | 使用場景 |
---|---|---|
[[GetPrototypeOf]] | getPrototypeOf | 獲取對象原型 |
[[SetPrototypeOf]] | setPrototypeOf | 設置對象原型 |
[[IsExtensible]] | isExtensible | 判斷對象是否可以新增屬性 |
[[PreventExtensions]] | preventExtensions | 阻止對象新增屬性 |
[[GetOwnProperty]] | getOwnProperty | 獲取對象自有屬性的屬性描述符 |
[[DefineOwnProperty]] | defineProperty | 定義對象新屬性或修改現有屬性,返回鍍錫 |
[[HasProperty]] | has | 判斷對象是否有指定的屬性 |
[[Get]] | get | 獲取對象的屬性值 |
[[Set]] | set | 設置對象的屬性值 |
[[Delete]] | deleteProperty | 刪除對象的屬性 |
[[OwnPropertyKeys]] | ownKeys | 獲取對象所有自有屬性的鍵 |
[[Call]] | apply | 調用函數 |
[[OwnPropertyKeys]] | Construct | 創建一個新的實例 |
書上的表格無使用場景,這里加上便于理解
在書上的例子是舉例了deleteProperty
,代碼如下:
const obj = { foo: 1 }
const p = new Proxy(obj, {deleteProperty(target, key) {return Reflect.deleteProperty(target, key)}
})console.log(p.foo) // 1
delete p.foo
console.log(p.foo) // undefined
那么需要注意的是deleteProperty
是Proxy
對象即p
的內部方法,只有刪除p
的屬性時才會被調用,其實就和p
讀取obj
屬性時才會調用get
一個意思。在上述代碼中,上下文環境要刪除的是obj.foo
,所以調用了Reflect.deleteProperty
,回想一下,Proxy
的方法和Reflect
里的方法是一樣的名字
問題總結
- JS中的代理是什么
- 結合Vue3響應式理解apply和call
- 了解ES6的Reflect在攔截函數中的作用
- 什么是常規對象和異質對象?
- 如何區分普通對象和函數對象
- Proxy對象的內部方法及其使用場景