首頁 > 軟體

列印Proxy物件和ref物件的包實現詳解

2022-11-17 14:00:38

前因

平時工作的時候,我喜歡用console.log偵錯大法。但Vue3更新後,控制檯都是列印的Proxy物件和ref物件,想看裡邊的值,就需要很麻煩的一層一層的展開。

為了解決這個問題,我試過在編輯器中寫一個新的快捷鍵,快速寫出console.log(JSON.parse(JSON.stringify()))

但我用的是webStorm,它自帶的.log快捷鍵太舒服了,比如這樣:abc.log 點選tab鍵,就自動替換為console.log(abc)

我試了好久,終究還是沒能拓展類似的程式碼。所以才有了重寫console.log()的想法。

目標

我希望新的console.log可以像現在的console.log一模一樣,只是當列印Proxyref物件時可以直接輸出它的源物件或ref.value。並且,還保留記錄當前檔案和行數的功能,可以讓我看到到底是哪個檔案哪個步驟執行的列印。

結果

先說結果:

我翻了好久的檔案,終究還是不能達到我想要的效果,控制檯右側展示出的列印檔案及行號終究還是不能直接顯示原始檔,如果有大神能看到這篇文章的話,希望告訴我怎麼怎麼才能實現這個想法。

但退而求其次,我用console.traceError.stack兩種方式十分簡陋的完成了這個目標。

各位可以去 下載試試,原始碼也就不到200行,有興趣的同學可以看看。

實現(直接看原始碼的同學可以略過)

判斷一個物件是否是Proxy

這個不好判斷,Vue3新增了isProxy 方法,但如果不是Vue環境的話,那這個方法就失效了。 而且就這麼一個簡單的小功能,實在沒必要依賴其他的包。 最終是選擇在使用者new Proxy之前,把Proxy物件改造。

// 記錄使用者new Proxy操作的所有物件
// WeakSet,WeakMap,都是弱參照,不干預其他模組的垃圾回收機制
export const proxyMap = new WeakMap()
let OriginalProxy = null
export function listenProxy() {
    if (OriginalProxy) { // 防止使用者多次呼叫監聽
        return
    }
    OriginalProxy = window.Proxy
    window.Proxy = new Proxy(Proxy, {
        construct(target, args) {
            const newProxy = new OriginalProxy(...args)
            proxyInstances.set(newProxy, target)
            return newProxy
        },
        get(obj, prop) {
            // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/hasInstance
            if (prop === Symbol.hasInstance) { // 監控 `instanceof` 關鍵字
                return instance => proxyMap.has(instance)
            }
            // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
            return Reflect.get(...arguments)
        }
    })
}
export function unListenProxy() {
    window.Proxy = OriginalProxy || window.Proxy
}

輸出使用者log的源物件

按說我們上一步已經監控了使用者動作,可以獲取源物件,等使用者log的時候,我們直接輸出源物件就可以了。但這也有個問題,Proxy畢竟不是普通的物件,通過Proxy獲取的結果,很可能跟源物件沒有一毛錢關係。所以只能通過深克隆返回源物件的值,但這也有個問題,就是對於某些不能遍歷的物件或屬性,就列印不了了……

問題貌似鎖死了,但,我們實際運用中,只是為了簡簡單單輸出一個不用展開的源物件而已,甚至運用場景都特別單一:Vue3! 使用者如果覺得列印的不準確,換一個api不完了嗎,比如我們監控的是console.log,那使用者就用console.info一樣能輸出相同的結果。 把選擇權交給使用者就好了。在參照包的時候,再寫多一個設定項,讓使用者自己選平時的使用場景哪個正確結果比較多,就選哪個。想要完全正確,就換一個其他的api。

我簡直是個天才,哈哈哈

export function getOrg(obj) {
    return proxyMap.get(obj)
}
// 深克隆
export function clone(obj, _refs = new WeakSet()) {
    if (obj === null || obj === undefined) return null
    if (typeof obj !== 'object') return obj
    if (obj.constructor === Date) return new Date(obj)
    if (obj.constructor === RegExp) return new RegExp(obj)
    const newObj = new obj.constructor() //保持繼承的原型
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            const val = obj[key]
            if (typeof val === 'object' && !_refs.has(val)) {
                newObj[key] = clone(val)
            } else {
                newObj[key] = val
            }
        }
    }
    return newObj
}

最後暴露出去給使用者呼叫

import { listenProxy, unListenProxy, clone, getOrg } from "./until";
let config = {
    key: 'log', // any String
    type: 'trace', // 'trace' | 'error' | 'any String'
    cloneProxy: getOrg
}
let Vue = {}
export default function (obj = {}, vue) {
    Vue = vue || {}
    config = { ...config, ...obj }
    if (obj.copy === 'clone') {
        config.cloneProxy = clone
    }
    listenLog(config)
}
// ----------------------------------------
const { groupCollapsed, groupEnd, trace, log } = console
// const type = 'trace' | 'error' | ''
function listenLog() {
    const isRef = Vue.isRef || (obj => {
        return typeof obj === 'object' && !!obj.constructor && obj.constructor.name === 'RefImpl'
    })
    const unref = Vue.unref || (obj => obj.value)
    const { key, type, cloneProxy } = config
    if (!key) {
        console.error('Missing required parameter: key')
    }
    listenProxy() // 為 new Proxy 物件新增 `instanceof` 支援
    console[key] = function (...arr) {
        const newArr = arr.map(i => {
            if (isRef(i)) {
                return unref(i)
            } else if (i instanceof Proxy) {
                return cloneProxy(i)
            } else {
                return i
            }
        })
        groupCollapsed(...newArr)
        // 以 trace
        if (type === 'trace') {
            // trace(...newArr)
            console.log('第二行即為呼叫者所在的檔案位置')
            trace('The second line is the file location of the caller')
            groupEnd()
            return
        }
        let stack = new Error().stack || ''
        // stack = stack.replace('Error', 'Log')
        if (type === 'error') {
            log('%c這不是一個錯誤,請點選第二行的"at",跳轉到對應的檔案', 'color: #008000')
            log('%cThis is not an error. Please click "at" in the second line to jump to the corresponding file', 'color: #008000')
            log(stack)
            groupEnd()
            return;
        }
        // 簡單輸入模式,控制檯看起來是簡單了,卻失去了點選連結直接跳轉到對應檔案的功能
        const stackArr = stack.match(/at.*s/g) || []
        log(stackArr[1])
        groupEnd()
    }
}

至此已全部結束。

再加上一點ts的解釋檔案,那這個庫就能執行在所有平臺了

以上就是列印Proxy物件和ref物件的包實現詳解的詳細內容,更多關於列印Proxy ref物件包的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com