打印Proxy對象和ref對象的包實現詳解

前因

平時工作的時候,我喜歡用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對象包的資料請關註WalkonNet其它相關文章!

推薦閱讀: