大家好,我是愛水文的蘇先生,一名從業(yè)5年+的前端愛好者,致力于用最通俗的文字分享前端知識的酸菜魚
github與好文
前言
在這么卷的2023,你還搞不懂vue響應式潭苞?一文中,我們通過手寫的形式實現(xiàn)了響應式的核心內(nèi)容真朗,了解了響應式的核心實現(xiàn)思路此疹。但文章只進行了橫向?qū)崿F(xiàn),對其實現(xiàn)細節(jié)并未提及遮婶。
本篇我們縱向深入蝗碎,將關注點放到數(shù)據(jù)本身,來進一步探討對Object類型的處理細節(jié)旗扑,并補充上一節(jié)中對屬性刪除和編輯邏輯的處理缺失
攔截器的選擇
前文中我們直接使用Proxy來進行實現(xiàn)蹦骑,這也是vue3中的方案,但在vue2中使用的其實是Object.defineProperty臀防。至于原因嘛眠菇,網(wǎng)上已經(jīng)被人說爛了,不過為了文章的完整性袱衷,我也簡單總結下:
- 對數(shù)據(jù)類型的非原生支持捎废,需要提供補救api,比如this.$set
- 需要在初始化階段執(zhí)行全量遞歸祟昭,影響性能
Proxy與Reflect
在分析數(shù)據(jù)類型的處理之前缕坎,我們還需要搞懂Proxy和Reflect這兩個的一些關鍵點問題,不過我不打算去長篇大論它們篡悟,我只會對與本文相關的特性或概念進行闡述說明,因為這對于后文的理解很重要
Proxy
Proxy可以創(chuàng)建一個代理對象匾寝,它允許我們攔截并重新定義對一個對象的基本操作搬葬,而所謂基本操作即一個動作,反過來說艳悔,如果一個操作由兩個動作完成急凰,那就不是基本操作,而叫復合操作了
以如下代碼來說明猜年,我們定義了對象obj抡锈,它包含一個名稱為say的屬性,并且其值為一個函數(shù)乔外。當我們執(zhí)行p.say時是一個基本操作床三,因為它只包含了獲取這一個動作,如果我們執(zhí)行的是p.say()杨幼,那它就是一個復合操作了撇簿,因為這包含了兩個動作:1-獲取p.say聂渊;2-對p.say的結果進行調(diào)用
const obj = {
say:function(){}
}
const p = new Proxy(obj,{
get(){
...
}
})
Reflect
如果你閱讀過它的相關文檔,你會發(fā)現(xiàn)任何能夠在Proxy中找到的方法四瘫,都能夠在Reflect中找到同名的函數(shù)汉嗽。對于本文來說,我們只關注它的第三個參數(shù):receiver
我們先回顧下這么卷的2023找蜜,你還搞不懂vue響應式饼暑?一文中我們實現(xiàn)的代碼
const obj = {
name:'spp'
}
const p = new Proxy(obj,{
get(target,key){
...
return target[key]
},
set(target,key,newValue){
target[key] = newValue
...
}
})
現(xiàn)在我把obj對象進行下改造,為其增加get訪問器,并在內(nèi)部打印this是否就是代理對象p
const obj = {
get getName(){
console.log(this === p)
}
}
const p = new Proxy(obj,{...})
如果你運行該示例洗做,你會發(fā)現(xiàn)其結果為fasle撵孤,這意味著,我們?nèi)绻趃et訪問器中通過this訪問對象上的name屬性時竭望,是無法正確觸發(fā)依賴收集的
那么是什么原因?qū)е碌哪匦奥耄课覀儊矸治鲆幌拢赑roxy內(nèi)我們是通過target[key]獲取返回值的咬清,我們知道在JavaScript中闭专,誰調(diào)用this就會指向誰,所以this指向的原始對象旧烧,而原始對象我們是不進行依賴追蹤的
因此影钉,我們要利用第三個參數(shù)修正下this指向,就像call掘剪、apply平委、bind所做的事情一樣
const obj = {
get getName(){
console.log(this === p)
}
}
const p = new Proxy(obj,{
get(target,key,receiver){
...
return Reflect.get(target,key,receiver)
}
})
可以看到,我們使用Reflect進行映射而不再直接返回target夺谁,此時再次打印你會發(fā)現(xiàn)結果就為true了
抽離依賴追蹤與更新派發(fā)
先不要著急嘛廉赔,小伙子!在真正開始之前匾鸥,我們還需要填個坑
在這么卷的2023蜡塌,你還搞不懂vue響應式?一文中我們將依賴追蹤和派發(fā)更新的代碼內(nèi)置到了get和set內(nèi)勿负,為了代碼的可復用與可維護性馏艾,我們需要先將其進行下抽離(見demo\vue\響應式設計與實現(xiàn)\07.js)
trace
function trace(target,key){
if (!actEffect) return target[key];
let reactiveObj = bucket.get(target);
if (!reactiveObj) bucket.set(target, (reactiveObj = new Map()));
let effects = reactiveObj.get(key);
if (!effects) reactiveObj.set(key, (effects = new Set()));
effects.add(actEffect);
actEffect.deps.push(effects);
}
trigger
function trigger(target, key, value){
target[key] = value;
const reactiveObj = bucket.get(target);
if (reactiveObj) {
const effects = reactiveObj.get(key) || [];
const t = new Set(effects);
t.forEach((v) => {
if(actEffect !== v){
taskQueue.add(v)
flushTask()
}
});
}
}
代理Object類型(見demo\vue\響應式設計與實現(xiàn)\08.js)
這么卷的2023,你還搞不懂vue響應式奴愉?一文中我們假設對象讀取操作只有一種琅摩,即obj.keyName,但實際上in操作符和for...in循環(huán)都是對象訪問的形式
處理in操作符
由于Proxy上并沒有一眼就能看出來是哪個攔截函數(shù)與之相對應,所以理論上來說我們需要去查閱相關規(guī)范才行锭硼。不過我比較懶房资,我選擇先去看下阮一峰的es6教程,事實上账忘,還真被我找到了
因此志膀,對于in操作符熙宇,我們使用has攔截器來實現(xiàn)依賴追蹤,并通過Reflect來判定是否存在
const obj = {
name:'spp'
}
const p = new Proxy(obj,{
has(target,key){
trace(target,key)
return Reflect.has(target,key)
}
})
處理for...in循環(huán)
同理溉浙,我們找到關于for...in的攔截器
模擬key
仔細觀察我們發(fā)現(xiàn)烫止,ownKeys攔截器只提供了target而缺失了key屬性,而key恰恰是我們構造bucket數(shù)據(jù)結構中最最重要的一環(huán)戳稽,它與具體的effect進行關聯(lián)
因此馆蠕,我們需要自己去構造一個唯一的值并當作key值使用燎潮,顯然Symbol很適合
const UNI_KEY_FOR_IN = Symbol()
為此瞻鹏,我們需要在依賴追蹤時向trace函數(shù)傳入該UNI_KEY_FOR_IN
const proxyObj = new Proxy(obj, {
...
ownKeys(target){
trace(target,UNI_KEY_FOR_IN)
return Reflect.ownKeys(target)
}
});
打call時間:
學了那么久,一定累了吧拟枚?那我們先來看一波推廣吧
我目前正在開發(fā)一個名為unplugin-router的項目,它是一個約定式路由生成的庫颂郎,目前已支持在webpack和vite中使用吼渡,也已完成對vue-router3.x和vue-router4.x的支持,且已經(jīng)接入到公司的一個vite3+vue3的項目中
不過受限于工作時間進度比較慢乓序,在此尋找志同道合的朋友一起來完成這件事寺酪,后續(xù)計劃對功能做進一步的完善,比如支持@hmr注解替劈、支持權限路由等寄雀,也有對react路由和svelte路由的支持計劃,以及除了webpack和vite這兩個之外的構建工具的支持陨献,還有單元測試的編寫.....
確認關聯(lián)關系
上一小節(jié)盒犹,我們使用一個Symbol值解決了ownKeys缺失key屬性的問題,但是這又引出了一個新的問題:什么時候應該觸發(fā)Symbol值對應的副作用函數(shù)重新執(zhí)行眨业?
這個問題其實等價于急膀,哪些情況是需要進行依賴追蹤的?現(xiàn)在我們分情況來進行下討論:
- 新增
當新增屬性時坛猪,我們希望能追蹤到依賴脖阵,為此我們需要在trigger中將與Symbol值關聯(lián)的effect取出執(zhí)行一遍
function trigger(target, key, value){
...
// 取出UNI_KEY_FOR_IN,兼容for...in
const forInEffects = reactiveObj.get(UNI_KEY_FOR_IN) || new Set()
forInEffects.forEach(v=>t.add(v))
...
}
- 修改
當修改時墅茉,由于屬性已經(jīng)被依賴收集過,所以我們不需要再次進行收集呜呐。不過對于Proxy而言就斤,對象屬性的新增和刪除統(tǒng)稱為對象的設置,因此我們需要能夠區(qū)分出當前是在進行哪種操作蘑辑,這一點洋机,我們只需要通過判斷對象上是否已經(jīng)存在即可做出區(qū)分,并且將其作為trigger的第三個參數(shù)傳入
...
const p = new Proxy(obj,{
set(target,key,value){
const type = target[key] ? 'edit' : 'add'
trigger(target, key, type)
...
}
})
然后在trigger中洋魂,我們根據(jù)type的類型為for...in的追蹤邏輯添加守衛(wèi)
function trigger(target, key, value){
...
// 當為新增時绷旗,取出UNI_KEY_FOR_IN喜鼓,兼容for...in
if(type === 'add'){
const forInEffects = reactiveObj.get(UNI_KEY_FOR_IN) || new Set()
forInEffects.forEach(v=>t.add(v))
}
...
}
- 刪除
在這么卷的2023,你還搞不懂vue響應式衔肢?一文中我們當時為了解決dead code問題實現(xiàn)了reset用于重新進行依賴收集庄岖,這剛好也可以用于屬性刪除上
鑒于目前我們還沒有處理過屬性值的刪除,因此老規(guī)矩角骤,我們先查閱下阮的文檔并找到deleteProperty攔截器
這里我們使用Object.property.hasOwnProperty來過濾原型上的屬性隅忿,當刪除成功后重新收集依賴,這樣在reset中就會切斷刪除的那個key所對應的effect了
...
const p = new Proxy(obj,{
deleteProperty(target,key){
const exist = Object.prototype.hasOwnProperty(target,key)
if(exist){
const isDel = Reflect.deleteProperty(target,key)
if(isDel){
trigger(target,key,'delete')
return true
}
}
return false
}
})
另外邦尊,你可能也注意到了背桐,trigger函數(shù)的第三個參數(shù)類型我們新增了delete類型,這主要對應for...in循環(huán)的兼容處理
function trigger(target, key, value){
...
// 當為新增或刪除時蝉揍,取出UNI_KEY_FOR_IN链峭,兼容for...in
if(type === 'add' || type === 'delete'){
const forInEffects = reactiveObj.get(UNI_KEY_FOR_IN) || new Set()
forInEffects.forEach(v=>t.add(v))
}
...
}
- 代碼實現(xiàn)
代碼比較多,感興趣的可以到根據(jù)前文提示到對應的文件下查看完整的實現(xiàn)哈又沾,我這里就不再貼了
總結
本文弊仪,我們通過引出前文對in和for...in處理的缺失,從而在對應的解決過程中順道實現(xiàn)了一個對象除了新增之外捍掺,對刪除撼短、編輯的處理。至此挺勿,關于Object類型的處理就基本完成了曲横。下一節(jié),我們將繼續(xù)探究關于Array類型的處理