手寫vue響應式之Object類型的處理

大家好,我是愛水文的蘇先生,一名從業(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了

image.png

抽離依賴追蹤與更新派發(fā)

先不要著急嘛廉赔,小伙子!在真正開始之前匾鸥,我們還需要填個坑

image.png

這么卷的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教程,事實上账忘,還真被我找到了

image.png

因此志膀,對于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的攔截器

image.png

模擬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時間:

學了那么久,一定累了吧拟枚?那我們先來看一波推廣吧

image.png

我目前正在開發(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í)行眨业?

image.png

這個問題其實等價于急膀,哪些情況是需要進行依賴追蹤的?現(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攔截器

image.png

這里我們使用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類型的處理

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末不瓶,一起剝皮案震驚了整個濱河市禾嫉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚊丐,老刑警劉巖熙参,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異麦备,居然都是意外死亡孽椰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門凛篙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來黍匾,“玉大人,你說我怎么就攤上這事呛梆∪裱模” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵填物,是天一觀的道長纹腌。 經(jīng)常有香客問我霎终,道長,這世上最難降的妖魔是什么升薯? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任莱褒,我火速辦了婚禮,結果婚禮上覆劈,老公的妹妹穿的比我還像新娘保礼。我一直安慰自己,他們只是感情好责语,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布炮障。 她就那樣靜靜地躺著,像睡著了一般坤候。 火紅的嫁衣襯著肌膚如雪胁赢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天白筹,我揣著相機與錄音智末,去河邊找鬼。 笑死徒河,一個胖子當著我的面吹牛系馆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顽照,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼由蘑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了代兵?” 一聲冷哼從身側響起尼酿,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎植影,沒想到半個月后裳擎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡思币,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年鹿响,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谷饿。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡抢野,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出各墨,到底是詐尸還是另有隱情,我是刑警寧澤启涯,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布贬堵,位于F島的核電站恃轩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏黎做。R本人自食惡果不足惜叉跛,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒸殿。 院中可真熱鬧筷厘,春花似錦、人聲如沸宏所。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爬骤。三九已至充石,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霞玄,已是汗流浹背骤铃。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坷剧,地道東北人惰爬。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像惫企,于是被迫代替她去往敵國和親撕瞧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容