From 697ba4e484c15e0dd427d6db40c7b55a47bec301 Mon Sep 17 00:00:00 2001 From: Furybean Date: Tue, 16 Jan 2018 19:49:20 +0800 Subject: [PATCH 1/2] improve table render time in some condition --- packages/table/src/layout-observer.js | 68 +++++++ packages/table/src/table-body.js | 21 +- packages/table/src/table-column.js | 45 +++-- packages/table/src/table-footer.js | 27 ++- packages/table/src/table-header.js | 26 +-- packages/table/src/table-layout.js | 102 +++++++--- packages/table/src/table-store.js | 33 ++- packages/table/src/table.vue | 279 +++++++++++++++++--------- packages/theme-chalk/src/table.scss | 26 ++- 9 files changed, 407 insertions(+), 220 deletions(-) create mode 100644 packages/table/src/layout-observer.js diff --git a/packages/table/src/layout-observer.js b/packages/table/src/layout-observer.js new file mode 100644 index 0000000000..6e9a1c30bc --- /dev/null +++ b/packages/table/src/layout-observer.js @@ -0,0 +1,68 @@ +export default { + created() { + this.tableLayout.addObserver(this); + }, + + destroyed() { + this.tableLayout.removeObserver(this); + }, + + computed: { + tableLayout() { + let layout = this.layout; + if (!layout && this.table) { + layout = this.table.layout; + } + if (!layout) { + throw new Error('Can not find table layout.'); + } + return layout; + } + }, + + mounted() { + this.onColumnsChange(this.tableLayout); + this.onScrollableChange(this.tableLayout); + }, + + updated() { + if (this.__updated__) return; + this.onColumnsChange(this.tableLayout); + this.onScrollableChange(this.tableLayout); + this.__updated__ = true; + }, + + methods: { + onColumnsChange() { + const cols = this.$el.querySelectorAll('colgroup > col'); + if (!cols.length) return; + const flattenColumns = this.tableLayout.getFlattenColumns(); + const columnsMap = {}; + flattenColumns.forEach((column) => { + columnsMap[column.id] = column; + }); + for (let i = 0, j = cols.length; i < j; i++) { + const col = cols[i]; + const name = col.getAttribute('name'); + const column = columnsMap[name]; + if (column) { + col.setAttribute('width', column.realWidth || column.width); + } + } + }, + + onScrollableChange(layout) { + const cols = this.$el.querySelectorAll('colgroup > col[name=gutter]'); + for (let i = 0, j = cols.length; i < j; i++) { + const col = cols[i]; + col.setAttribute('width', layout.scrollY ? layout.gutterWidth : '0'); + } + const ths = this.$el.querySelectorAll('th.gutter'); + for (let i = 0, j = ths.length; i < j; i++) { + const th = ths[i]; + th.style.width = layout.scrollY ? layout.gutterWidth + 'px' : '0'; + th.style.display = layout.scrollY ? '' : 'none'; + } + } + } +}; diff --git a/packages/table/src/table-body.js b/packages/table/src/table-body.js index f859eb1c55..26da721e59 100644 --- a/packages/table/src/table-body.js +++ b/packages/table/src/table-body.js @@ -3,8 +3,13 @@ import { hasClass, addClass, removeClass } from 'element-ui/src/utils/dom'; import ElCheckbox from 'element-ui/packages/checkbox'; import ElTooltip from 'element-ui/packages/tooltip'; import debounce from 'throttle-debounce/debounce'; +import LayoutObserver from './layout-observer'; export default { + name: 'ElTableBody', + + mixins: [LayoutObserver], + components: { ElCheckbox, ElTooltip @@ -16,9 +21,6 @@ export default { }, stripe: Boolean, context: {}, - layout: { - required: true - }, rowClassName: [String, Function], rowStyle: [Object, Function], fixed: String, @@ -35,11 +37,7 @@ export default { border="0"> { - this._l(this.columns, column => - ) + this._l(this.columns, column => ) } @@ -112,9 +110,6 @@ export default { } }) } - { - !this.fixed && this.layout.scrollY && this.layout.gutterWidth ? : '' - } , this.store.isRowExpanded(row) ? ( @@ -344,7 +339,7 @@ export default { if (hasClass(cellChild, 'el-tooltip') && cellChild.scrollWidth > cellChild.offsetWidth && this.$refs.tooltip) { const tooltip = this.$refs.tooltip; - + // TODO 会引起整个 Table 的重新渲染,需要优化 this.tooltipContent = cell.textContent || cell.innerText; tooltip.referenceElm = cell; tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none'); @@ -363,7 +358,7 @@ export default { const cell = getCell(event); if (!cell) return; - const oldHoverState = this.table.hoverState; + const oldHoverState = this.table.hoverState || {}; this.table.$emit('cell-mouse-leave', oldHoverState.row, oldHoverState.column, oldHoverState.cell, event); }, diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js index 017de994c2..7f78e63fd8 100644 --- a/packages/table/src/table-column.js +++ b/packages/table/src/table-column.js @@ -116,6 +116,26 @@ const DEFAULT_RENDER_CELL = function(h, { row, column }) { return value; }; +const parseWidth = (width) => { + if (width !== undefined) { + width = parseInt(width, 10); + if (isNaN(width)) { + width = null; + } + } + return width; +}; + +const parseMinWidth = (minWidth) => { + if (minWidth !== undefined) { + minWidth = parseInt(minWidth, 10); + if (isNaN(minWidth)) { + minWidth = 80; + } + } + return minWidth; +}; + export default { name: 'ElTableColumn', @@ -205,25 +225,12 @@ export default { let parent = this.columnOrTableParent; let owner = this.owner; this.isSubColumn = owner !== parent; - this.columnId = (parent.tableId || (parent.columnId + '_')) + 'column_' + columnIdSeed++; + this.columnId = (parent.tableId || parent.columnId) + '_column_' + columnIdSeed++; let type = this.type; - let width = this.width; - if (width !== undefined) { - width = parseInt(width, 10); - if (isNaN(width)) { - width = null; - } - } - - let minWidth = this.minWidth; - if (minWidth !== undefined) { - minWidth = parseInt(minWidth, 10); - if (isNaN(minWidth)) { - minWidth = 80; - } - } + const width = parseWidth(this.width); + const minWidth = parseMinWidth(this.minWidth); let isColumnGroup = false; @@ -353,14 +360,14 @@ export default { width(newVal) { if (this.columnConfig) { - this.columnConfig.width = newVal; + this.columnConfig.width = parseWidth(newVal); this.owner.store.scheduleLayout(); } }, minWidth(newVal) { if (this.columnConfig) { - this.columnConfig.minWidth = newVal; + this.columnConfig.minWidth = parseMinWidth(newVal); this.owner.store.scheduleLayout(); } }, @@ -368,7 +375,7 @@ export default { fixed(newVal) { if (this.columnConfig) { this.columnConfig.fixed = newVal; - this.owner.store.scheduleLayout(); + this.owner.store.scheduleLayout(true); } }, diff --git a/packages/table/src/table-footer.js b/packages/table/src/table-footer.js index f5166c215e..d6832a3a45 100644 --- a/packages/table/src/table-footer.js +++ b/packages/table/src/table-footer.js @@ -1,6 +1,10 @@ +import LayoutObserver from './layout-observer'; + export default { name: 'ElTableFooter', + mixins: [LayoutObserver], + render(h) { const sums = []; this.columns.forEach((column, index) => { @@ -41,16 +45,10 @@ export default { border="0"> { - this._l(this.columns, column => - ) + this._l(this.columns, column => ) } { - !this.fixed && this.layout.gutterWidth - ? - : '' + this.hasGutter ? : '' } @@ -70,9 +68,7 @@ export default { ) } { - this.hasGutter - ? - : '' + this.hasGutter ? : '' } @@ -85,9 +81,6 @@ export default { store: { required: true }, - layout: { - required: true - }, summaryMethod: Function, sumText: String, border: Boolean, @@ -103,6 +96,10 @@ export default { }, computed: { + table() { + return this.$parent; + }, + isAllSelected() { return this.store.states.isAllSelected; }, @@ -124,7 +121,7 @@ export default { }, hasGutter() { - return !this.fixed && this.layout.gutterWidth; + return !this.fixed && this.tableLayout.gutterWidth; } }, diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index 889e18a617..ba112c62f5 100644 --- a/packages/table/src/table-header.js +++ b/packages/table/src/table-header.js @@ -3,6 +3,7 @@ import ElCheckbox from 'element-ui/packages/checkbox'; import ElTag from 'element-ui/packages/tag'; import Vue from 'vue'; import FilterPanel from './filter-panel.vue'; +import LayoutObserver from './layout-observer'; const getAllColumns = (columns) => { const result = []; @@ -65,13 +66,14 @@ const convertToRows = (originColumns) => { export default { name: 'ElTableHeader', + mixins: [LayoutObserver], + render(h) { const originColumns = this.store.states.originColumns; const columnRows = convertToRows(originColumns, this.columns); // 是否拥有多级表头 const isGroup = columnRows.length > 1; if (isGroup) this.$parent.isGroup = true; - return ( { - this._l(this.columns, column => - ) + this._l(this.columns, column => ) } { - !this.fixed && this.layout.gutterWidth - ? - : '' + this.hasGutter ? : '' } @@ -137,12 +133,7 @@ export default { ) } { - this.hasGutter - ? - : '' + this.hasGutter ? : '' } ) @@ -157,9 +148,6 @@ export default { store: { required: true }, - layout: { - required: true - }, border: Boolean, defaultSort: { type: Object, @@ -211,7 +199,7 @@ export default { }, hasGutter() { - return !this.fixed && this.layout.gutterWidth; + return !this.fixed && this.tableLayout.gutterWidth; } }, diff --git a/packages/table/src/table-layout.js b/packages/table/src/table-layout.js index 1276145ffd..9f229cb327 100644 --- a/packages/table/src/table-layout.js +++ b/packages/table/src/table-layout.js @@ -1,7 +1,9 @@ import scrollbarWidth from 'element-ui/src/utils/scrollbar-width'; +import Vue from 'vue'; class TableLayout { constructor(options) { + this.observers = []; this.table = null; this.store = null; this.columns = null; @@ -43,7 +45,7 @@ class TableLayout { const bodyWrapper = this.table.bodyWrapper; if (this.table.$el && bodyWrapper) { const body = bodyWrapper.querySelector('.el-table__body'); - this.scrollY = body.offsetHeight > bodyWrapper.offsetHeight; + this.scrollY = body.offsetHeight > this.bodyHeight; } } @@ -52,19 +54,19 @@ class TableLayout { if (typeof value === 'string' && /^\d+$/.test(value)) { value = Number(value); } - this.height = value; - if (!el) return; + if (!el && value) return Vue.nextTick(() => this.setHeight(value, prop)); + if (typeof value === 'number') { el.style[prop] = value + 'px'; - this.updateHeight(); + this.updateElsHeight(); } else if (typeof value === 'string') { if (value === '') { el.style[prop] = ''; } - this.updateHeight(); + this.updateElsHeight(); } } @@ -72,37 +74,33 @@ class TableLayout { return this.setHeight(value, 'max-height'); } - updateHeight() { - const height = this.tableHeight = this.table.$el.clientHeight; - const noData = !this.table.data || this.table.data.length === 0; + updateElsHeight() { + if (!this.table.$ready) return Vue.nextTick(() => this.updateElsHeight()); const { headerWrapper, appendWrapper, footerWrapper } = this.table.$refs; - const footerHeight = this.footerHeight = footerWrapper ? footerWrapper.offsetHeight : 0; this.appendHeight = appendWrapper ? appendWrapper.offsetHeight : 0; + if (this.showHeader && !headerWrapper) return; - if (!this.showHeader) { - this.headerHeight = 0; - if (this.height !== null && (!isNaN(this.height) || typeof this.height === 'string')) { - this.bodyHeight = height - footerHeight + (footerWrapper ? 1 : 0); - } - this.fixedBodyHeight = this.scrollX ? height - this.gutterWidth : height; - } else { - const headerHeight = this.headerHeight = headerWrapper.offsetHeight; - const bodyHeight = height - headerHeight - footerHeight + (footerWrapper ? 1 : 0); - if (this.height !== null && (!isNaN(this.height) || typeof this.height === 'string')) { - this.bodyHeight = bodyHeight; - } - this.fixedBodyHeight = this.scrollX ? bodyHeight - this.gutterWidth : bodyHeight; + const headerHeight = this.headerHeight = !this.showHeader ? 0 : headerWrapper.offsetHeight; + if (this.showHeader && headerWrapper.offsetWidth > 0 && headerHeight < 2) { + return Vue.nextTick(() => this.updateElsHeight()); } - this.viewportHeight = this.scrollX ? height - (noData ? 0 : this.gutterWidth) : height; - } + const tableHeight = this.tableHeight = this.table.$el.clientHeight; + if (this.height !== null && (!isNaN(this.height) || typeof this.height === 'string')) { + const footerHeight = this.footerHeight = footerWrapper ? footerWrapper.offsetHeight : 0; + this.bodyHeight = tableHeight - headerHeight - footerHeight + (footerWrapper ? 1 : 0); + } + this.fixedBodyHeight = this.scrollX ? this.bodyHeight - this.gutterWidth : this.bodyHeight; - update() { - const fit = this.fit; - const columns = this.table.columns; - const bodyWidth = this.table.$el.clientWidth; - let bodyMinWidth = 0; + const noData = !this.table.data || this.table.data.length === 0; + this.viewportHeight = this.scrollX ? tableHeight - (noData ? 0 : this.gutterWidth) : tableHeight; + this.updateScrollY(); + this.notifyObservers('scrollable'); + } + + getFlattenColumns() { const flattenColumns = []; + const columns = this.table.columns; columns.forEach((column) => { if (column.isColumnGroup) { flattenColumns.push.apply(flattenColumns, column.columns); @@ -111,8 +109,21 @@ class TableLayout { } }); + return flattenColumns; + } + + updateColumnsWidth() { + const fit = this.fit; + const bodyWidth = this.table.$el.clientWidth; + let bodyMinWidth = 0; + + const flattenColumns = this.getFlattenColumns(); let flexColumns = flattenColumns.filter((column) => typeof column.width !== 'number'); + flattenColumns.forEach((column) => { // Clean those columns whose width changed from flex to unflex + if (typeof column.width === 'number' && column.realWidth) column.realWidth = null; + }); + if (flexColumns.length > 0 && fit) { flattenColumns.forEach((column) => { bodyMinWidth += column.width || column.minWidth || 80; @@ -169,7 +180,7 @@ class TableLayout { if (fixedColumns.length > 0) { let fixedWidth = 0; fixedColumns.forEach(function(column) { - fixedWidth += column.realWidth; + fixedWidth += column.realWidth || column.width; }); this.fixedWidth = fixedWidth; @@ -179,11 +190,40 @@ class TableLayout { if (rightFixedColumns.length > 0) { let rightFixedWidth = 0; rightFixedColumns.forEach(function(column) { - rightFixedWidth += column.realWidth; + rightFixedWidth += column.realWidth || column.width; }); this.rightFixedWidth = rightFixedWidth; } + + this.notifyObservers('columns'); + } + + addObserver(observer) { + this.observers.push(observer); + } + + removeObserver(observer) { + const index = this.observers.indexOf(observer); + if (index !== -1) { + this.observers.splice(index, 1); + } + } + + notifyObservers(event) { + const observers = this.observers; + observers.forEach((observer) => { + switch (event) { + case 'columns': + observer.onColumnsChange(this); + break; + case 'scrollable': + observer.onScrollableChange(this); + break; + default: + throw new Error(`Table Layout don't have event ${event}.`); + } + }); } } diff --git a/packages/table/src/table-store.js b/packages/table/src/table-store.js index c551611485..58333a53b2 100644 --- a/packages/table/src/table-store.js +++ b/packages/table/src/table-store.js @@ -94,7 +94,6 @@ const TableStore = function(table, initialState = {}) { fixedLeafColumnsLength: 0, rightFixedLeafColumnsLength: 0, isComplex: false, - _data: null, filteredData: null, data: null, sortingColumn: null, @@ -137,15 +136,6 @@ TableStore.prototype.mutations = { states.filteredData = data; states.data = sortData((data || []), states); - // states.data.forEach((item) => { - // if (!item.$extra) { - // Object.defineProperty(item, '$extra', { - // value: {}, - // enumerable: false - // }); - // } - // }); - this.updateCurrentRow(); if (!states.reserveSelection) { @@ -252,8 +242,10 @@ TableStore.prototype.mutations = { states.reserveSelection = column.reserveSelection; } - this.updateColumns(); // hack for dynamics insert column - this.scheduleLayout(); + if (this.table.$ready) { + this.updateColumns(); // hack for dynamics insert column + this.scheduleLayout(); + } }, removeColumn(states, column, parent) { @@ -266,8 +258,10 @@ TableStore.prototype.mutations = { array.splice(array.indexOf(column), 1); } - this.updateColumns(); // hack for dynamics remove column - this.scheduleLayout(); + if (this.table.$ready) { + this.updateColumns(); // hack for dynamics remove column + this.scheduleLayout(); + } }, setHoverRow(states, row) { @@ -370,7 +364,9 @@ TableStore.prototype.clearSelection = function() { const states = this.states; states.isAllSelected = false; const oldSelection = states.selection; - states.selection = []; + if (states.selection.length) { + states.selection = []; + } if (oldSelection.length > 0) { this.table.$emit('selection-change', states.selection); } @@ -531,8 +527,11 @@ TableStore.prototype.updateAllSelected = function() { states.isAllSelected = isAllSelected; }; -TableStore.prototype.scheduleLayout = function() { - this.table.debouncedLayout(); +TableStore.prototype.scheduleLayout = function(updateColumns) { + if (updateColumns) { + this.updateColumns(); + } + this.table.debouncedUpdateLayout(); }; TableStore.prototype.setCurrentRowKey = function(key) { diff --git a/packages/table/src/table.vue b/packages/table/src/table.vue index 5de902a393..f134af704f 100644 --- a/packages/table/src/table.vue +++ b/packages/table/src/table.vue @@ -7,147 +7,207 @@ 'el-table--hidden': isHidden, 'el-table--group': isGroup, 'el-table--fluid-height': maxHeight, + 'el-table--scrollable-x': layout.scrollX, + 'el-table--scrollable-y': layout.scrollY, 'el-table--enable-row-hover': !store.states.isComplex, 'el-table--enable-row-transition': (store.states.data || []).length !== 0 && (store.states.data || []).length < 100 }, tableSize ? `el-table--${ tableSize }` : '']" @mouseleave="handleMouseLeave($event)">
-
+
+ :style="{ + width: layout.bodyWidth ? layout.bodyWidth + 'px' : '' + }">
+ :style="{ + width: bodyWidth + }"> -
- {{ emptyText || t('el.table.emptyText') }} +
+ + {{ emptyText || t('el.table.emptyText') }} +
-
+
-