如何實現vue3.0的響應式呢?本文實戰教你

来源:https://www.cnblogs.com/chengxuyuanaa/archive/2020/06/11/13096173.html
-Advertisement-
Play Games

之前寫了兩篇vue2.0的響應式原理,鏈接在此,對響應式原理不清楚的請先看下麵兩篇 和尤雨溪一起進階vue 和尤雨溪一起進階vue(二) 現在來寫一個簡單的3.0的版本吧 大家都知道,2.0的響應式用的是Object.defineProperty,結合發佈訂閱模式實現的,3.0已經用Proxy改寫了 ...


之前寫了兩篇vue2.0的響應式原理,鏈接在此,對響應式原理不清楚的請先看下麵兩篇

和尤雨溪一起進階vue

和尤雨溪一起進階vue(二)

現在來寫一個簡單的3.0的版本吧

大家都知道,2.0的響應式用的是Object.defineProperty,結合發佈訂閱模式實現的,3.0已經用Proxy改寫了

Proxy是es6提供的新語法,Proxy 對象用於定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數調用等)。

語法:

const p = new Proxy(target, handler)

target 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
handler 一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。

handler的方法有很多, 感興趣的可以移步到MDN,這裡重點介紹下麵幾個

handler.has()
in 操作符的捕捉器。
handler.get()
屬性讀取操作的捕捉器。
handler.set()
屬性設置操作的捕捉器。
handler.deleteProperty()
delete 操作符的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
複製代碼

基於上面的知識,我們來攔截一個對象屬性的取值,賦值和刪除

// version1
const handler = {
    get(target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('set', key, value)
        let res = Reflect.set(target, key, value, receiver)
        return res
    },
    deleteProperty(target, key) {
        console.log('deleteProperty', key)
        Reflect.deleteProperty(target, key)
    }
}
// 測試部分
let obj = {
    name: 'hello',
    info: {
       age: 20 
    }
}
const proxy = new Proxy(obj, handler)
// get name hello
// hello
console.log(proxy.name)
// set name world
proxy.name = 'world'
// deleteProperty name
delete proxy.name  我是08年出道的前端老鳥,想交流經驗可以進我的扣扣裙 519293536 有問題我都會儘力幫大家

上面已經可以攔截到對象屬性的取值,賦值和刪除了,我們來看看新增一個屬性可否攔截

proxy.height = 20
// 列印 set height 20
複製代碼

成功攔截!! 我們知道vue2.0新增data上不存在的屬性是不可以響應的,需要手動調用$set的,這就是Proxy的優點之一

現在來看看嵌套對象的攔截,我們修改info屬性的age屬性

proxy.info.age = 30
// 列印 get info
複製代碼

只可以攔截到info,不可以攔截到info的age屬性,所以我們要遞歸了,問題是在哪裡遞歸呢?

因為調用proxy.info.age會先觸發proxy.info的攔截,所以我們可以在get中攔截,如果proxy.info是對象的話,對象需要再被代理一次,我們把代碼封裝一下,寫成遞歸的形式

function reactive(target) {
    return createReactiveObject(target)
}
function createReactiveObject(target) {
    // 遞歸結束條件
    if(!isObject(target)) return target
    const handler = {
        get(target, key, receiver) {
            console.log('get', key)
            let res = Reflect.get(target, key, receiver)
            // res如果是對象,那麼需要繼續代理
            return isObject(res) ? createReactiveObject(res): res
        },
        set(target, key, value, receiver) {
            console.log('set', key, value)
            let res = Reflect.set(target, key, value, receiver)
            return res
        },
        deleteProperty(target, key) {
            console.log('deleteProperty', key)
            Reflect.deleteProperty(target, key)
        }
    }
    return new Proxy(target, handler)
}
function isObject(obj) {
    return obj != null && typeof obj === 'object'
}
// 測試部分
let obj = {
    name: 'hello',
    info: {
        age: 20
    }
}
const proxy = reactive(obj)
proxy.info.age = 30
複製代碼

運行上面的代碼,列印結果

get info
set age 30
複製代碼

Bingo! 嵌套對象攔截到了

vue2.0用的是Object.defineProperty攔截對象的getter和setter,一次將對象遞歸到底, 3.0用Proxy,是惰性遞歸的,只有訪問到某個屬性,確定了值是對象,我們才繼續代理下去這個屬性值,因此性能更好

現在我們來測試數組的方法,看看能否攔截到,以push方法為例, 測試部分代碼如下

let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)
複製代碼

列印結果

get push
get length
set 3 4
set length 4
複製代碼

和預期有點不太一樣,調用數組的push方法,不僅攔截到了push, 還攔截到了length屬性,set被調用了兩次,在set中我們是要更新視圖的,我們做了一次push操作,卻觸發了兩次更新,顯然是不合理的,所以我們這裡需要修改我們的handler的set函數,區分一下是新增屬性還是修改屬性,只有這兩種情況才需要更新視圖

set函數修改如下

set(target, key, value, receiver) {
        console.log('set', key, value)
        let oldValue = target[key]
        let res = Reflect.set(target, key, value, receiver)
        let hadKey = target.hasOwnProperty(key)
        if(!hadKey) {
            // console.log('新增屬性', key)
            // 更新視圖
        }else if(oldValue !== value) {
            // console.log('修改屬性', key)
             // 更新視圖
        }
        return res
    }
複製代碼

至此,我們對象操作的攔截我們基本已經完成了,但是還有一個小問題, 我們來看看下麵的操作

let obj = {
    some: 'hell'
}
let proxy = reactive(obj)
let proxy1 = reactive(obj)
let proxy2 = reactive(obj)
let proxy3 = reactive(obj)
let p1 = reactive(proxy)
let p2 = reactive(proxy)
let p3 = reactive(proxy)
複製代碼

我們這樣寫,就會一直調用reactive代理對象,所以我們需要構造兩個hash表來存儲代理結果,避免重覆代理

function reactive(target) {
   return createReactiveObject(target)
}
let toProxyMap = new WeakMap()
let toRawMap = new WeakMap()
function createReactiveObject(target) {
    let dep = new Dep()
    if(!isObject(target)) return target
    // reactive(obj)
    // reactive(obj)
    // reactive(obj)
    // target已經代理過了,直接返回,不需要再代理了
    if(toProxyMap.has(target)) return toProxyMap.get(target)
    // 防止代理對象再被代理
    // reactive(proxy)
    // reactive(proxy)
    // reactive(proxy)
    if(toRawMap.has(target)) return target
    const handler = {
        get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver)
            // 遞歸代理
            return isObject(res) ? reactive(res) : res
        },
        // 必須要有返回值,否則數組的push等方法報錯
        set(target, key, val, receiver) {
            let hadKey = hasOwn(target, key)
            let oldVal = target[key]
            let res = Reflect.set(target, key, val,receiver)
            if(!hadKey) {
                // console.log('新增屬性', key)
            } else if(oldVal !== val) {
                // console.log('修改屬性', key)
            }
            return res
        },
        deleteProperty(target, key) {
            Reflect.deleteProperty(target, key)
        }
    }
    let observed = new Proxy(target, handler)
    toProxyMap.set(target, observed)
    toRawMap.set(observed, target)
    return observed

}
function isObject(obj) {
    return obj != null && typeof obj === 'object'
}
function hasOwn(obj, key) {
    return obj.hasOwnProperty(key)
}
複製代碼

接下來就是修改數據,觸發視圖更新,也就是實現發佈訂閱,這一部分和2.0的實現部分一樣,也是在get中收集依賴,在set中觸發依賴

完整代碼如下

class Dep {
    constructor() {
        this.subscribers = new Set(); // 保證依賴不重覆添加
    }
    // 追加訂閱者
    depend() {
        if(activeUpdate) { // activeUpdate註冊為訂閱者
            this.subscribers.add(activeUpdate)
        }

    }
    // 運行所有的訂閱者更新方法
    notify() {
        this.subscribers.forEach(sub => {
            sub();
        })
    }
}
let activeUpdate
function reactive(target) {
   return createReactiveObject(target)
}
let toProxyMap = new WeakMap()
let toRawMap = new WeakMap()
function createReactiveObject(target) {
    let dep = new Dep()
    if(!isObject(target)) return target
    // reactive(obj)
    // reactive(obj)
    // reactive(obj)
    // target已經代理過了,直接返回,不需要再代理了
    if(toProxyMap.has(target)) return toProxyMap.get(target)
    // 防止代理對象再被代理
    // reactive(proxy)
    // reactive(proxy)
    // reactive(proxy)
    if(toRawMap.has(target)) return target
    const handler = {
        get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver)
            // 收集依賴
            if(activeUpdate) {
                dep.depend()
            }
            // 遞歸代理
            return isObject(res) ? reactive(res) : res
        },
        // 必須要有返回值,否則數組的push等方法報錯
        set(target, key, val, receiver) {
            let hadKey = hasOwn(target, key)
            let oldVal = target[key]
            let res = Reflect.set(target, key, val,receiver)
            if(!hadKey) {
                // console.log('新增屬性', key)
                dep.notify()
            } else if(oldVal !== val) {
                // console.log('修改屬性', key)
                dep.notify()
            }
            return res
        },
        deleteProperty(target, key) {
            Reflect.deleteProperty(target, key)
        }
    }
    let observed = new Proxy(target, handler)
    toProxyMap.set(target, observed)
    toRawMap.set(observed, target)
    return observed

}
function isObject(obj) {
    return obj != null && typeof obj === 'object'
}
function hasOwn(obj, key) {
    return obj.hasOwnProperty(key)
}
function autoRun(update) {
    function wrapperUpdate() {
        activeUpdate = wrapperUpdate
        update() // wrapperUpdate, 閉包
        activeUpdate = null;
    }
    wrapperUpdate();
}
let obj = {name: 'hello', arr: [1, 2,3]}
let proxy = reactive(obj)
// 響應式
autoRun(() => {
    console.log(proxy.name)
})
我是08年出道的前端老鳥,想交流經驗可以進我的扣扣裙 519293536 有問題我都會儘力幫大家
proxy.name = 'xxx' // 修改proxy.name, 自動執行autoRun的回調函數,列印新值 複製代碼

最後總結下vue2.0和3.0響應式的實現的優缺點:

  • 性能 : 2.0用Object.defineProperty攔截對象的屬性的修改,在getter中收集依賴,在setter中觸發依賴更新,一次將對象遞歸到底攔截,性能較差, 3.0用Proxy攔截對象,惰性遞歸,性能好
  • Proxy可以攔截數組的方法,Object.defineProperty無法攔截數組的pushunshift,shiftpop,slice,splice等方法(2.0內部重寫了這些方法,實現了攔截), proxy可以攔截攔截對象的新增屬性,Object.defineProperty不可以(開發者需要手動調用$set)
  • 相容性 : Object.defineProperty支持ie8+,Proxy的相容性差,ie瀏覽器不支持
    本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 安裝Homebrew(已安裝跳過) raw.githubusercontent.com功能變數名稱被污染,如果報錯,host文件添加 199.232.68.133 raw.githubusercontent.com 安裝安卓工具 brew cask install android-platform-tool ...
  • 2020年,整個資本市場風起雲涌,大環境下,互聯網更是風聲鶴唳,大多數公司面臨著裁員,結構重構,他們收緊資本,為自己取暖。在漫長的寒冬下,互聯網人只有自己修煉內功,才能在寒風中屹立不倒。作為一名iOS開發者,要時時刻刻保持學習的衝勁,新的知識每年都是海量增長,要學的東西真的太多太多。很多公司收緊資本 ...
  • 目錄:andorid jar/庫源碼解析 Frida體驗: 作用: android手機上可以對,java和so層代碼,進行hook.監控數據和處理記憶體數據。 官譯:面向開發人員、逆向工程師和安全研究人員的動態工具工具包。 慄子: 運行步驟: 1、https://github.com/frida/fr ...
  • 前言: 眾所周知,Android廠商非常多,各種尺寸的android手機、平板層出不窮。導致了Android生態環境的碎片化現象越來越嚴重。Google公司為瞭解決解析度過多的問題,在Android的開發文檔中定義了px、dp、sp,方便開發者適配不同解析度的Android設備。對於初級程式員來說理 ...
  • 一、回調函數 1.含義:如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針用來調用所指向的函數時,我們就說這是回調函數。 2.函數也是有類型的,下麵舉個回調函數以及函數的類型演示。 function fn() { ​ } console.log(typeof fn); console.l ...
  • 2020/6/11 23:23 今天做系統時,用到二級菜單,菜單下方放了一個<iframe>標簽,但二級菜單的菜單項太多,導致一部分菜單項被<iframe>覆蓋,從而無法再選中,之後查詢了一下,瞭解到了z-index屬性。 z-index屬性是用來調整元素堆疊順序,在手冊中的取值如下: 預設的優先順序 ...
  • #最近項目中,要用到element-ui的無限級分類菜單,根據角色生成不同的遞歸數據,查閱了網上很多資料,發現很多都不太完整並且沒有很多的延伸性。 ###梳理遞歸數據 我們一般拿到後臺的數據是:1.扁平化數據格式 2.遞歸式數據格式 複製代碼 let arr = [ { name:小七, id:1 ...
  • 位元組跳動 一面 1.說出以下代碼的原型關係,以及 Object 和 Function 的原型關係 Function; function test() { } var obj = new test(); __proto__ prototype 2.合併兩個已排序的鏈表:遞歸解法、迭代解法3.判斷數組類 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...