From e9ec1f39837a58e5104ed4c60529a2a00cff02e6 Mon Sep 17 00:00:00 2001 From: surunzi Date: Thu, 2 Jan 2020 08:34:05 +0800 Subject: [PATCH] perf(console): large object expansion --- src/Console/Log.js | 26 ++-- src/Network/Network.scss | 3 +- src/lib/JsonViewer.js | 27 ++-- src/lib/ObjViewer.js | 267 +++++++++++++++++++++++++++++++++++++++ src/lib/getAbstract.js | 4 +- src/lib/themes.js | 2 +- 6 files changed, 302 insertions(+), 27 deletions(-) create mode 100644 src/lib/ObjViewer.js diff --git a/src/Console/Log.js b/src/Console/Log.js index 2f34292..a81efc8 100644 --- a/src/Console/Log.js +++ b/src/Console/Log.js @@ -2,6 +2,7 @@ import stringify from './stringify' import origGetAbstract from '../lib/getAbstract' import beautify from 'js-beautify' import JsonViewer from '../lib/JsonViewer' +import ObjViewer from '../lib/ObjViewer' import { isObj, isStr, @@ -185,7 +186,8 @@ export default class Log extends Emitter { } } click(logger) { - const { type, src, args } = this + const { type, src } = this + let { args } = this const $el = this._$el switch (type) { @@ -198,23 +200,29 @@ export default class Log extends Emitter { case 'dir': case 'group': case 'groupCollapsed': - if (src) { + if (src || args) { const $json = $el.find('.eruda-json') if ($json.hasClass('eruda-hidden')) { if ($json.data('init') !== 'true') { - const jsonViewer = new JsonViewer(src, $json) - jsonViewer.on('change', () => this.updateSize(false)) + if (src) { + const jsonViewer = new JsonViewer(src, $json) + jsonViewer.on('change', () => this.updateSize(false)) + } else { + if (type === 'table' || args.length === 1) { + if (isObj(args[0])) args = args[0] + } + const objViewer = new ObjViewer(args, $json, { + showUnenumerable: Log.showUnenumerable, + showGetterVal: Log.showGetterVal + }) + objViewer.on('change', () => this.updateSize(false)) + } $json.data('init', 'true') } $json.rmClass('eruda-hidden') } else { $json.addClass('eruda-hidden') } - } else if (args) { - this.extractObj(() => { - this.click(logger) - delete this.args - }) } else if (type === 'group' || type === 'groupCollapsed') { logger.toggleGroup(this) } diff --git a/src/Network/Network.scss b/src/Network/Network.scss index fe7939d..fcb4444 100644 --- a/src/Network/Network.scss +++ b/src/Network/Network.scss @@ -121,7 +121,8 @@ bottom: 0; color: var(--foreground); width: 100%; - background: var(--accent); + border-top: 1px solid var(--border); + background: var(--darker-background); display: block; height: 40px; line-height: 40px; diff --git a/src/lib/JsonViewer.js b/src/lib/JsonViewer.js index 1e3e43f..e4d26cb 100644 --- a/src/lib/JsonViewer.js +++ b/src/lib/JsonViewer.js @@ -53,7 +53,7 @@ export default class JsonViewer extends Emitter { this._appendTpl() this._bindEvent() } - jsonToHtml(data, firstLevel) { + _jsonToHtml(data, firstLevel) { let ret = '' each(['enumerable', 'unenumerable', 'symbol'], type => { @@ -63,23 +63,22 @@ export default class JsonViewer extends Emitter { typeKeys.sort(sortObjName) for (let i = 0, len = typeKeys.length; i < len; i++) { const key = typeKeys[i] - ret += this.createEl(key, data[type][key], type, firstLevel) + ret += this._createEl(key, data[type][key], type, firstLevel) } }) if (data.proto) { if (ret === '') { - ret = this.jsonToHtml(data.proto, firstLevel) + ret = this._jsonToHtml(data.proto) } else { - ret += this.createEl('__proto__', data.proto, 'proto', firstLevel) + ret += this._createEl('__proto__', data.proto, 'proto') } } return ret } - createEl(key, val, keyType, firstLevel = false) { + _createEl(key, val, keyType, firstLevel = false) { let type = typeof val - let id if (val === null) { return `
  • ${wrapKey(key)}null
  • ` @@ -103,7 +102,7 @@ export default class JsonViewer extends Emitter { } else if (val === '(...)') { return `
  • ${wrapKey(key)}${val}
  • ` } else if (isObj(val)) { - id = val.id + const id = val.id const referenceId = val.reference const objAbstract = getObjAbstract(val) || upperFirst(type) @@ -121,7 +120,7 @@ export default class JsonViewer extends Emitter { firstLevel ? '' : 'style="display:none"' }>` - if (firstLevel) obj += this.jsonToHtml(this._map[id]) + if (firstLevel) obj += this._jsonToHtml(this._map[id]) return obj + '' } @@ -149,7 +148,7 @@ export default class JsonViewer extends Emitter { _appendTpl() { const data = this._map[this._data.id] - this._$el.html(this.jsonToHtml(data, true)) + this._$el.html(this._jsonToHtml(data, true)) } _bindEvent() { const map = this._map @@ -165,7 +164,7 @@ export default class JsonViewer extends Emitter { if ($this.data('first-level')) return if (circularId) { - $this.find('ul').html(self.jsonToHtml(map[circularId], false)) + $this.find('ul').html(self._jsonToHtml(map[circularId], false)) $this.rmAttr('data-object-id') } @@ -268,14 +267,14 @@ function objToArr(data, id, type) { return ret } -const encode = str => { +export const encode = str => { return escape(toStr(str)) .replace(/\n/g, '↵') .replace(/\f|\r|\t/g, '') } // $, upperCase, lowerCase, _ -function sortObjName(a, b) { +export function sortObjName(a, b) { const numA = toNum(a) const numB = toNum(b) if (!isNaN(numA) && !isNaN(numB)) { @@ -320,7 +319,7 @@ function transCode(code) { return code } -function getObjAbstract(data) { +export function getObjAbstract(data) { const { type, value } = data if (!type) return @@ -344,7 +343,7 @@ function extractFnHead(str) { return str } -function getFnAbstract(str) { +export function getFnAbstract(str) { if (str.length > 500) str = str.slice(0, 500) + '...' return 'ƒ ' + trim(extractFnHead(str).replace('function', '')) diff --git a/src/lib/ObjViewer.js b/src/lib/ObjViewer.js new file mode 100644 index 0000000..a273480 --- /dev/null +++ b/src/lib/ObjViewer.js @@ -0,0 +1,267 @@ +import { + Emitter, + getProto, + isNum, + isBool, + lowerCase, + isObj, + upperFirst, + keys, + each, + toSrc, + isPromise, + type, + $, + difference, + allKeys, + filter +} from './util' +import { encode, getFnAbstract, sortObjName } from './JsonViewer' +import evalCss from './evalCss' + +let hasEvalCss = false + +export default class ObjViewer extends Emitter { + constructor( + data, + $el, + { showUnenumerable = false, showGetterVal = false } = {} + ) { + super() + + if (!hasEvalCss) { + evalCss(require('./json.scss')) + hasEvalCss = true + } + + this._data = [data] + this._$el = $el + this._visitor = new Visitor() + this._map = {} + this._showUnenumerable = showUnenumerable + this._showGetterVal = showGetterVal + + this._appendTpl() + this._bindEvent() + } + _objToHtml(data, firstLevel) { + let ret = '' + + const types = ['enumerable'] + const enumerableKeys = keys(data) + let unenumerableKeys = [] + let symbolKeys = [] + + if (this._showUnenumerable && !firstLevel) { + types.push('unenumerable') + types.push('symbol') + unenumerableKeys = difference( + allKeys(data, { + prototype: false, + unenumerable: true + }), + enumerableKeys + ) + symbolKeys = filter( + allKeys(data, { + prototype: false, + symbol: true + }), + key => { + return typeof key === 'symbol' + } + ) + } + + each(['enumerable', 'unenumerable', 'symbol'], type => { + let typeKeys = [] + if (type === 'symbol') { + typeKeys = symbolKeys + } else if (type === 'unenumerable') { + typeKeys = unenumerableKeys + } else { + typeKeys = enumerableKeys + } + typeKeys.sort(sortObjName) + for (let i = 0, len = typeKeys.length; i < len; i++) { + const key = typeKeys[i] + let val = '' + try { + val = data[key] + if (isPromise(val)) { + val.catch(() => {}) + } + } catch (e) { + val = e.message + } + ret += this._createEl(key, val, type, firstLevel) + } + }) + + const proto = getProto(data) + if (!firstLevel && proto) { + if (ret === '') { + ret = this._objToHtml(proto) + } else { + ret += this._createEl('__proto__', proto, 'proto') + } + } + + return ret + } + _createEl(key, val, keyType, firstLevel = false) { + const visitor = this._visitor + let t = typeof val + const valType = type(val, false) + + if (val === null) { + return `
  • ${wrapKey(key)}null
  • ` + } else if (isNum(val) || isBool(val)) { + return `
  • ${wrapKey(key)}${encode( + val + )}
  • ` + } + + if (valType === 'RegExp') t = 'regexp' + if (valType === 'Number') t = 'number' + + if (valType === 'Number' || valType === 'RegExp') { + return `
  • ${wrapKey(key)}${encode( + val.value + )}
  • ` + } else if (valType === 'Undefined' || valType === 'Symbol') { + return `
  • ${wrapKey(key)}${lowerCase( + valType + )}
  • ` + } else if (val === '(...)') { + return `
  • ${wrapKey(key)}${val}
  • ` + } else if (isObj(val)) { + const visitedObj = visitor.get(val) + let id + if (visitedObj) { + id = visitedObj.id + } else { + id = visitor.set(val) + this._map[id] = val + } + const objAbstract = getObjAbstract(val, valType) || upperFirst(t) + + let obj = `
  • + + ${wrapKey(key)} + ${ + firstLevel ? '' : objAbstract + } +
  • ' + } + + function wrapKey(key) { + if (firstLevel) return '' + if (isObj(val) && val.jsonSplitArr) return '' + + let keyClass = 'eruda-key' + if ( + keyType === 'unenumerable' || + keyType === 'proto' || + keyType === 'symbol' + ) { + keyClass = 'eruda-key-lighter' + } + + return `${encode(key)}: ` + } + + return `
  • ${wrapKey(key)}"${encode( + val + )}"
  • ` + } + _appendTpl() { + this._$el.html(this._objToHtml(this._data, true)) + } + _bindEvent() { + const map = this._map + + const self = this + + this._$el.on('click', 'li', function(e) { + const $this = $(this) + const circularId = $this.data('object-id') + const $firstSpan = $(this) + .find('span') + .eq(0) + + if ($this.data('first-level')) return + if (circularId) { + $this.find('ul').html(self._objToHtml(map[circularId], false)) + $this.rmAttr('data-object-id') + } + + e.stopImmediatePropagation() + + if (!$firstSpan.hasClass('eruda-expanded')) return + + const $ul = $this.find('ul').eq(0) + if ($firstSpan.hasClass('eruda-collapsed')) { + $firstSpan.rmClass('eruda-collapsed') + $ul.show() + } else { + $firstSpan.addClass('eruda-collapsed') + $ul.hide() + } + + self.emit('change') + }) + } +} + +function getObjAbstract(data, type) { + if (!type) return + + if (type === 'Function') { + return getFnAbstract(toSrc(data)) + } + if (type === 'Array') { + return `Array(${data.length})` + } + + return type +} + +class Visitor { + constructor() { + this.id = 0 + this.visited = [] + } + set(val) { + const { visited, id } = this + const obj = { + id, + val + } + visited.push(obj) + + this.id++ + + return id + } + get(val) { + const { visited } = this + + for (let i = 0, len = visited.length; i < len; i++) { + const obj = visited[i] + if (val === obj.val) return obj + } + + return false + } +} diff --git a/src/lib/getAbstract.js b/src/lib/getAbstract.js index b782351..98bebae 100644 --- a/src/lib/getAbstract.js +++ b/src/lib/getAbstract.js @@ -8,9 +8,9 @@ import { each, getObjType, endWith, - isEmpty, - evalCss + isEmpty } from './util' +import evalCss from './evalCss' let hasEvalCss = false diff --git a/src/lib/themes.js b/src/lib/themes.js index 7669be6..7ebc11d 100644 --- a/src/lib/themes.js +++ b/src/lib/themes.js @@ -13,7 +13,7 @@ export default { contrast: '#f2f7fd', varColor: '#c80000', stringColor: '#1a1aa6', - keywordColor: '#0d22aa', + keywordColor: '#881280', numberColor: '#1c00cf', operatorColor: '#808080', linkColor: '#1155cc',