perf(DataTable): implement lazy wrapping and optimize initialization
This commit is contained in:
parent
0bae1edceb
commit
3f19fb3a83
70
dist/datatable.js
vendored
70
dist/datatable.js
vendored
@ -1,4 +1,4 @@
|
||||
import { Component, NewState, Util, RefreshState } from "@web/state";
|
||||
import { Component, NewState, Util } from "@web/state";
|
||||
import { VirtualScroll } from "@web/base";
|
||||
Component.register("DataTable", (container) => {
|
||||
const vs = VirtualScroll();
|
||||
@ -21,19 +21,24 @@ Component.register("DataTable", (container) => {
|
||||
const scrollEl = container.querySelector(".dt-body");
|
||||
if (!scrollEl) return;
|
||||
const res = vs.calc(scrollEl, state.list);
|
||||
if (res) Object.assign(state, {
|
||||
prevHeight: res.prevHeight,
|
||||
postHeight: res.postHeight,
|
||||
_listStartIndex: res.listStartIndex,
|
||||
renderedList: res.renderedList
|
||||
});
|
||||
if (res) {
|
||||
res.renderedList.forEach((item, i) => {
|
||||
if (!item.__watch) {
|
||||
const wrapped = NewState(item);
|
||||
res.renderedList[i] = wrapped;
|
||||
state.list[res.listStartIndex + i] = wrapped;
|
||||
}
|
||||
});
|
||||
Object.assign(state, {
|
||||
prevHeight: res.prevHeight,
|
||||
postHeight: res.postHeight,
|
||||
_listStartIndex: res.listStartIndex,
|
||||
renderedList: res.renderedList
|
||||
});
|
||||
}
|
||||
};
|
||||
container.refresh = refresh;
|
||||
state.__watch("list", (list) => {
|
||||
if (list && list.length > 0 && !list[0].__watch) {
|
||||
state.list = list.map((item) => NewState(item));
|
||||
return;
|
||||
}
|
||||
state._listStartIndex = 0;
|
||||
const scrollEl = container.querySelector(".dt-body");
|
||||
state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || [];
|
||||
@ -56,8 +61,10 @@ Component.register("DataTable", (container) => {
|
||||
};
|
||||
container.clearAllActive = (keepSelection = false) => {
|
||||
state.list.forEach((row) => {
|
||||
if (row._editingF !== null) row._editingF = null;
|
||||
if (row._activeF !== null) row._activeF = null;
|
||||
if (row && row.__watch) {
|
||||
if (row._editingF !== null) row._editingF = null;
|
||||
if (row._activeF !== null) row._activeF = null;
|
||||
}
|
||||
});
|
||||
if (!keepSelection) {
|
||||
state.selStartR = -1;
|
||||
@ -85,13 +92,14 @@ Component.register("DataTable", (container) => {
|
||||
state.selStartF = state.selEndF = f;
|
||||
}
|
||||
state.isSelecting = true;
|
||||
state.list[r]._activeF = f;
|
||||
if (state.list[r] && state.list[r].__watch) state.list[r]._activeF = f;
|
||||
}
|
||||
};
|
||||
container.updateSelect = (r, f) => state.isSelecting && (state.selEndR = r, state.selEndF = f);
|
||||
container.endSelect = () => state.isSelecting = false;
|
||||
container.editCell = (row, f, fIdx) => {
|
||||
var _a;
|
||||
if (!row.__watch) return;
|
||||
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR);
|
||||
const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF);
|
||||
const rIdx = state.list.indexOf(row);
|
||||
@ -106,7 +114,12 @@ Component.register("DataTable", (container) => {
|
||||
if (count > 1) {
|
||||
const unwatch = row.__watch(f.id, (val) => {
|
||||
for (let r = rMin; r <= rMax; r++) {
|
||||
if (state.list[r] !== row) state.list[r][f.id] = val;
|
||||
const cur = state.list[r];
|
||||
if (cur !== row) {
|
||||
const wrapped = cur.__watch ? cur : NewState(cur);
|
||||
state.list[r] = wrapped;
|
||||
wrapped[f.id] = val;
|
||||
}
|
||||
}
|
||||
unwatch();
|
||||
});
|
||||
@ -160,15 +173,21 @@ Component.register("DataTable", (container) => {
|
||||
const fStart = Math.min(state.selStartF, state.selEndF);
|
||||
if (rStart === -1) return;
|
||||
rows.forEach((rowData, i) => {
|
||||
const row = state.list[rStart + i];
|
||||
if (row) rowData.forEach((val, j) => {
|
||||
const field = state.fields[fStart + j];
|
||||
if (field) {
|
||||
if (typeof row[field.id] === "boolean") row[field.id] = val.toLowerCase() === "true";
|
||||
else if (typeof row[field.id] === "number") row[field.id] = Number(val);
|
||||
else row[field.id] = val;
|
||||
let row = state.list[rStart + i];
|
||||
if (row) {
|
||||
if (!row.__watch) {
|
||||
row = NewState(row);
|
||||
state.list[rStart + i] = row;
|
||||
}
|
||||
});
|
||||
rowData.forEach((val, j) => {
|
||||
const field = state.fields[fStart + j];
|
||||
if (field) {
|
||||
if (typeof row[field.id] === "boolean") row[field.id] = val.toLowerCase() === "true";
|
||||
else if (typeof row[field.id] === "number") row[field.id] = Number(val);
|
||||
else row[field.id] = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
container.addEventListener("keydown", (e) => {
|
||||
@ -242,8 +261,3 @@ Component.register("DataTable", (container) => {
|
||||
</div>
|
||||
`
|
||||
));
|
||||
if (typeof document !== "undefined") {
|
||||
const initDataTable = () => RefreshState(document.documentElement);
|
||||
if (document.readyState !== "loading") initDataTable();
|
||||
else document.addEventListener("DOMContentLoaded", initDataTable, true);
|
||||
}
|
||||
|
||||
2
dist/datatable.min.js
vendored
2
dist/datatable.min.js
vendored
File diff suppressed because one or more lines are too long
68
src/index.js
68
src/index.js
@ -17,20 +17,28 @@ Component.register('DataTable', container => {
|
||||
const scrollEl = container.querySelector('.dt-body')
|
||||
if (!scrollEl) return
|
||||
const res = vs.calc(scrollEl, state.list)
|
||||
if (res) Object.assign(state, {
|
||||
prevHeight: res.prevHeight, postHeight: res.postHeight,
|
||||
_listStartIndex: res.listStartIndex, renderedList: res.renderedList
|
||||
})
|
||||
if (res) {
|
||||
// 仅对渲染列表中的项进行响应式包装(如果尚未包装)
|
||||
res.renderedList.forEach((item, i) => {
|
||||
if (!item.__watch) {
|
||||
const wrapped = NewState(item)
|
||||
res.renderedList[i] = wrapped
|
||||
// 同步回原列表
|
||||
state.list[res.listStartIndex + i] = wrapped
|
||||
}
|
||||
})
|
||||
Object.assign(state, {
|
||||
prevHeight: res.prevHeight, postHeight: res.postHeight,
|
||||
_listStartIndex: res.listStartIndex, renderedList: res.renderedList
|
||||
})
|
||||
}
|
||||
}
|
||||
container.refresh = refresh
|
||||
|
||||
state.__watch('list', list => {
|
||||
if (list && list.length > 0 && !list[0].__watch) {
|
||||
state.list = list.map(item => NewState(item))
|
||||
return
|
||||
}
|
||||
state._listStartIndex = 0
|
||||
const scrollEl = container.querySelector('.dt-body')
|
||||
// 初始预览,不全量包装
|
||||
state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || []
|
||||
if (scrollEl) {
|
||||
vs.init(list, refresh)
|
||||
@ -55,8 +63,10 @@ Component.register('DataTable', container => {
|
||||
|
||||
container.clearAllActive = (keepSelection = false) => {
|
||||
state.list.forEach(row => {
|
||||
if (row._editingF !== null) row._editingF = null
|
||||
if (row._activeF !== null) row._activeF = null
|
||||
if (row && row.__watch) {
|
||||
if (row._editingF !== null) row._editingF = null
|
||||
if (row._activeF !== null) row._activeF = null
|
||||
}
|
||||
})
|
||||
if (!keepSelection) {
|
||||
state.selStartR = -1; state.multiSelections = []
|
||||
@ -82,7 +92,7 @@ Component.register('DataTable', container => {
|
||||
state.selStartF = state.selEndF = f
|
||||
}
|
||||
state.isSelecting = true
|
||||
state.list[r]._activeF = f
|
||||
if (state.list[r] && state.list[r].__watch) state.list[r]._activeF = f
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +100,7 @@ Component.register('DataTable', container => {
|
||||
container.endSelect = () => state.isSelecting = false
|
||||
|
||||
container.editCell = (row, f, fIdx) => {
|
||||
if (!row.__watch) return; // 理论上双击的行一定已经包装过
|
||||
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR)
|
||||
const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF)
|
||||
const rIdx = state.list.indexOf(row)
|
||||
@ -107,7 +118,12 @@ Component.register('DataTable', container => {
|
||||
if (count > 1) {
|
||||
const unwatch = row.__watch(f.id, (val) => {
|
||||
for (let r = rMin; r <= rMax; r++) {
|
||||
if (state.list[r] !== row) state.list[r][f.id] = val
|
||||
const cur = state.list[r]
|
||||
if (cur !== row) {
|
||||
const wrapped = cur.__watch ? cur : NewState(cur)
|
||||
state.list[r] = wrapped
|
||||
wrapped[f.id] = val
|
||||
}
|
||||
}
|
||||
unwatch()
|
||||
})
|
||||
@ -156,15 +172,21 @@ Component.register('DataTable', container => {
|
||||
const fStart = Math.min(state.selStartF, state.selEndF)
|
||||
if (rStart === -1) return
|
||||
rows.forEach((rowData, i) => {
|
||||
const row = state.list[rStart + i]
|
||||
if (row) rowData.forEach((val, j) => {
|
||||
const field = state.fields[fStart + j]
|
||||
if (field) {
|
||||
if (typeof row[field.id] === 'boolean') row[field.id] = val.toLowerCase() === 'true'
|
||||
else if (typeof row[field.id] === 'number') row[field.id] = Number(val)
|
||||
else row[field.id] = val
|
||||
let row = state.list[rStart + i]
|
||||
if (row) {
|
||||
if (!row.__watch) {
|
||||
row = NewState(row)
|
||||
state.list[rStart + i] = row
|
||||
}
|
||||
})
|
||||
rowData.forEach((val, j) => {
|
||||
const field = state.fields[fStart + j]
|
||||
if (field) {
|
||||
if (typeof row[field.id] === 'boolean') row[field.id] = val.toLowerCase() === 'true'
|
||||
else if (typeof row[field.id] === 'number') row[field.id] = Number(val)
|
||||
else row[field.id] = val
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -232,9 +254,3 @@ Component.register('DataTable', container => {
|
||||
</style>
|
||||
</div>
|
||||
`))
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const initDataTable = () => RefreshState(document.documentElement)
|
||||
if (document.readyState !== 'loading') initDataTable()
|
||||
else document.addEventListener('DOMContentLoaded', initDataTable, true)
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div id="debug-log" style="position:fixed; top:0; right:0; background:rgba(0,0,0,0.8); color:white; z-index:9999; font-family:monospace; padding:10px; max-height:200px; overflow:auto;"></div>
|
||||
<div class="demo-container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 text-primary fw-bold">DataTable Professional</h4>
|
||||
@ -49,50 +50,72 @@
|
||||
import '@web/base'
|
||||
import '../src/index.js'
|
||||
|
||||
const table = document.getElementById('myTable')
|
||||
const logEl = document.getElementById('debug-log');
|
||||
const log = (msg) => {
|
||||
console.log(msg);
|
||||
logEl.innerText += msg + '\n';
|
||||
};
|
||||
|
||||
const fields = [
|
||||
{ id: 'id', name: 'ID', width: 60, type: 'text', pinned: 'left' },
|
||||
{ id: 'name', name: 'User Name', width: 150, type: 'text', pinned: 'left' },
|
||||
{ id: 'role', name: 'Role', width: 120, type: 'select', options: ['Admin', 'Editor', 'Viewer'] },
|
||||
{ id: 'active', name: 'Active', width: 80, type: 'switch' },
|
||||
{ id: 'bio', name: 'Bio', width: 250, type: 'textarea' },
|
||||
{ id: 'gender', name: 'Gender', width: 120, type: 'radio', options: ['Male', 'Female', 'Other'] },
|
||||
{ id: 'tags', name: 'Tags', width: 150, type: 'TagsInput' },
|
||||
{ id: 'score', name: 'Score', width: 100, type: 'number' },
|
||||
{ id: 'created', name: 'Created At', width: 150, type: 'date' },
|
||||
{ id: 'actions', name: 'Actions', width: 100, type: 'text', pinned: 'right' }
|
||||
]
|
||||
log('Script starting after imports...');
|
||||
|
||||
try {
|
||||
const table = document.getElementById('myTable')
|
||||
|
||||
const generateData = (count) => Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'User ' + (i + 1),
|
||||
role: ['Admin', 'Editor', 'Viewer'][i % 3],
|
||||
active: i % 2 === 0,
|
||||
bio: 'This is the bio for user ' + (i + 1) + '. It might be a long text that needs a textarea for editing.',
|
||||
gender: ['Male', 'Female', 'Other'][i % 3],
|
||||
tags: ['State.js', 'Vite', 'Playwright'].slice(0, (i % 3) + 1),
|
||||
score: Math.floor(Math.random() * 100),
|
||||
created: new Date().toISOString().split('T')[0],
|
||||
actions: '...'
|
||||
}))
|
||||
const fields = [
|
||||
{ id: 'id', name: 'ID', width: 60, type: 'text', pinned: 'left' },
|
||||
{ id: 'name', name: 'User Name', width: 150, type: 'text', pinned: 'left' },
|
||||
{ id: 'role', name: 'Role', width: 120, type: 'select', options: ['Admin', 'Editor', 'Viewer'] },
|
||||
{ id: 'active', name: 'Active', width: 80, type: 'switch' },
|
||||
{ id: 'bio', name: 'Bio', width: 250, type: 'textarea' },
|
||||
{ id: 'gender', name: 'Gender', width: 120, type: 'radio', options: ['Male', 'Female', 'Other'] },
|
||||
{ id: 'tags', name: 'Tags', width: 150, type: 'TagsInput' },
|
||||
{ id: 'score', name: 'Score', width: 100, type: 'number' },
|
||||
{ id: 'created', name: 'Created At', width: 150, type: 'date' },
|
||||
{ id: 'actions', name: 'Actions', width: 100, type: 'text', pinned: 'right' }
|
||||
]
|
||||
|
||||
const data = generateData(500)
|
||||
const generateData = (count) => Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'User ' + (i + 1),
|
||||
role: ['Admin', 'Editor', 'Viewer'][i % 3],
|
||||
active: i % 2 === 0,
|
||||
bio: 'This is the bio for user ' + (i + 1) + '. It might be a long text that needs a textarea for editing.',
|
||||
gender: ['Male', 'Female', 'Other'][i % 3],
|
||||
tags: ['State.js', 'Vite', 'Playwright'].slice(0, (i % 3) + 1),
|
||||
score: Math.floor(Math.random() * 100),
|
||||
created: new Date().toISOString().split('T')[0],
|
||||
actions: '...'
|
||||
}))
|
||||
|
||||
// 数据初始化逻辑
|
||||
setTimeout(() => {
|
||||
Object.assign(table.state, {
|
||||
fields: fields,
|
||||
list: data
|
||||
})
|
||||
const data = generateData(500)
|
||||
|
||||
// 数据初始化逻辑
|
||||
setTimeout(() => {
|
||||
window.testStatus = 'passed'
|
||||
console.log('DataTable initialized in test page')
|
||||
}, 500)
|
||||
}, 200)
|
||||
try {
|
||||
log('Triggering RefreshState before init...');
|
||||
RefreshState(table);
|
||||
|
||||
log('Initializing table state...');
|
||||
if (!table.state) table.state = NewState({});
|
||||
|
||||
Object.assign(table.state, {
|
||||
fields: fields,
|
||||
list: data
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
window.testStatus = 'passed'
|
||||
log('DataTable initialized in test page: PASSED');
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
log('ERROR in state init: ' + e.message + '\n' + e.stack);
|
||||
}
|
||||
}, 200)
|
||||
} catch (e) {
|
||||
log('ERROR during startup: ' + e.message);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user