perf(console): large object expansion

This commit is contained in:
surunzi
2020-01-02 08:34:05 +08:00
parent 7637f0b778
commit e9ec1f3983
6 changed files with 302 additions and 27 deletions

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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 `<li>${wrapKey(key)}<span class="eruda-null">null</span></li>`
@@ -103,7 +102,7 @@ export default class JsonViewer extends Emitter {
} else if (val === '(...)') {
return `<li>${wrapKey(key)}<span class="eruda-special">${val}</span></li>`
} 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 + '</ul><span class="eruda-close"></span></li>'
}
@@ -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', ''))

267
src/lib/ObjViewer.js Normal file
View File

@@ -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 `<li>${wrapKey(key)}<span class="eruda-null">null</span></li>`
} else if (isNum(val) || isBool(val)) {
return `<li>${wrapKey(key)}<span class="eruda-${t}">${encode(
val
)}</span></li>`
}
if (valType === 'RegExp') t = 'regexp'
if (valType === 'Number') t = 'number'
if (valType === 'Number' || valType === 'RegExp') {
return `<li>${wrapKey(key)}<span class="eruda-${t}">${encode(
val.value
)}</span></li>`
} else if (valType === 'Undefined' || valType === 'Symbol') {
return `<li>${wrapKey(key)}<span class="eruda-special">${lowerCase(
valType
)}</span></li>`
} else if (val === '(...)') {
return `<li>${wrapKey(key)}<span class="eruda-special">${val}</span></li>`
} 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 = `<li ${
firstLevel ? 'data-first-level="true"' : ''
} ${'data-object-id="' + id + '"'}>
<span class="${
firstLevel ? '' : 'eruda-expanded eruda-collapsed'
}"></span>
${wrapKey(key)}
<span class="eruda-open">${
firstLevel ? '' : objAbstract
}</span>
<ul class="eruda-${t}" ${
firstLevel ? '' : 'style="display:none"'
}>`
if (firstLevel) obj += this._objToHtml(val)
return obj + '</ul><span class="eruda-close"></span></li>'
}
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 `<span class="${keyClass}">${encode(key)}</span>: `
}
return `<li>${wrapKey(key)}<span class="eruda-${typeof val}">"${encode(
val
)}"</span></li>`
}
_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
}
}

View File

@@ -8,9 +8,9 @@ import {
each,
getObjType,
endWith,
isEmpty,
evalCss
isEmpty
} from './util'
import evalCss from './evalCss'
let hasEvalCss = false

View File

@@ -13,7 +13,7 @@ export default {
contrast: '#f2f7fd',
varColor: '#c80000',
stringColor: '#1a1aa6',
keywordColor: '#0d22aa',
keywordColor: '#881280',
numberColor: '#1c00cf',
operatorColor: '#808080',
linkColor: '#1155cc',