Przeglądaj źródła

Table: improve performance (#9426)

* improve table render time in some condition

* Update table.vue
FuryBean 7 lat temu
rodzic
commit
9b9384214b

+ 68 - 0
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';
+      }
+    }
+  }
+};

+ 8 - 13
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">
         <colgroup>
           {
-            this._l(this.columns, column =>
-              <col
-                name={ column.id }
-                width={ column.realWidth || column.width }
-              />)
+            this._l(this.columns, column => <col name={ column.id } />)
           }
         </colgroup>
         <tbody>
@@ -112,9 +110,6 @@ export default {
                     }
                   })
                 }
-                {
-                  !this.fixed && this.layout.scrollY && this.layout.gutterWidth ? <td class="gutter" /> : ''
-                }
               </tr>,
               this.store.isRowExpanded(row)
                 ? (<tr>
@@ -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);
     },
 

+ 26 - 19
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);
       }
     },
 

+ 12 - 15
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">
         <colgroup>
           {
-            this._l(this.columns, column =>
-              <col
-                name={ column.id }
-                width={ column.realWidth || column.width }
-              />)
+            this._l(this.columns, column => <col name={ column.id } />)
           }
           {
-            !this.fixed && this.layout.gutterWidth
-              ? <col name="gutter" width={ this.layout.scrollY ? this.layout.gutterWidth : '' }></col>
-              : ''
+            this.hasGutter ? <col name="gutter" /> : ''
           }
         </colgroup>
         <tbody class={ [{ 'has-gutter': this.hasGutter }] }>
@@ -70,9 +68,7 @@ export default {
               )
             }
             {
-              this.hasGutter
-                ? <td class="gutter" style={{ width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0' }}></td>
-                : ''
+              this.hasGutter ? <th class="gutter"></th> : ''
             }
           </tr>
         </tbody>
@@ -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;
     }
   },
 

+ 7 - 19
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 (
       <table
         class="el-table__header"
@@ -80,16 +82,10 @@ export default {
         border="0">
         <colgroup>
           {
-            this._l(this.columns, column =>
-              <col
-                name={ column.id }
-                width={ column.realWidth || column.width }
-              />)
+            this._l(this.columns, column => <col name={ column.id } />)
           }
           {
-            !this.fixed && this.layout.gutterWidth
-              ? <col name="gutter" width={ this.layout.scrollY ? this.layout.gutterWidth : '' }></col>
-              : ''
+            this.hasGutter ? <col name="gutter" /> : ''
           }
         </colgroup>
         <thead class={ [{ 'is-group': isGroup, 'has-gutter': this.hasGutter }] }>
@@ -137,12 +133,7 @@ export default {
                   )
                 }
                 {
-                  this.hasGutter
-                    ? <th class="gutter" style={{
-                      width: this.layout.scrollY ? this.layout.gutterWidth + 'px' : '0',
-                      display: this.layout.scrollY ? '' : 'none'
-                    }}></th>
-                    : ''
+                  this.hasGutter ? <th class="gutter"></th> : ''
                 }
               </tr>
             )
@@ -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;
     }
   },
 

+ 71 - 31
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}.`);
+      }
+    });
   }
 }
 

+ 16 - 17
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 ? states.selection.slice() : []);
   }
@@ -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) {

+ 179 - 101
packages/table/src/table.vue

@@ -7,154 +7,213 @@
       '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)">
     <div class="hidden-columns" ref="hiddenColumns"><slot></slot></div>
-    <div class="el-table__header-wrapper" ref="headerWrapper" v-if="showHeader" v-mousewheel="handleHeaderFooterMousewheel">
+    <div
+      v-if="showHeader"
+      v-mousewheel="handleHeaderFooterMousewheel"
+      class="el-table__header-wrapper"
+      ref="headerWrapper">
       <table-header
         ref="tableHeader"
         :store="store"
-        :layout="layout"
         :border="border"
         :default-sort="defaultSort"
-        :style="{ width: layout.bodyWidth ? layout.bodyWidth + 'px' : '' }">
+        :style="{
+          width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
+        }">
       </table-header>
     </div>
     <div
       class="el-table__body-wrapper"
       ref="bodyWrapper"
-      :class="[layout.scrollX ? `is-scroll-${scrollPosition}` : 'is-scroll-none']"
+      :class="[layout.scrollX ? `is-scrolling-${scrollPosition}` : 'is-scrolling-none']"
       :style="[bodyHeight]">
       <table-body
         :context="context"
         :store="store"
         :stripe="stripe"
-        :layout="layout"
         :row-class-name="rowClassName"
         :row-style="rowStyle"
         :highlight="highlightCurrentRow"
-        :style="{ width: bodyWidth }">
+        :style="{
+           width: bodyWidth
+        }">
       </table-body>
-      <div :style="{ width: bodyWidth }" class="el-table__empty-block" v-if="!data || data.length === 0">
-        <span class="el-table__empty-text"><slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot></span>
+      <div
+        v-if="!data || data.length === 0"
+        class="el-table__empty-block"
+        ref="emptyBlock"
+        :style="{
+          width: bodyWidth
+        }">
+        <span class="el-table__empty-text">
+          <slot name="empty">{{ emptyText || t('el.table.emptyText') }}</slot>
+        </span>
       </div>
-      <div class="el-table__append-wrapper" ref="appendWrapper" v-if="$slots.append">
+      <div
+        v-if="$slots.append"
+        class="el-table__append-wrapper"
+        ref="appendWrapper">
         <slot name="append"></slot>
       </div>
     </div>
-    <div class="el-table__footer-wrapper" ref="footerWrapper" v-if="showSummary" v-show="data && data.length > 0" v-mousewheel="handleHeaderFooterMousewheel">
+    <div
+      v-if="showSummary"
+      v-show="data && data.length > 0"
+      v-mousewheel="handleHeaderFooterMousewheel"
+      class="el-table__footer-wrapper"
+      ref="footerWrapper">
       <table-footer
         :store="store"
-        :layout="layout"
         :border="border"
         :sum-text="sumText || t('el.table.sumText')"
         :summary-method="summaryMethod"
         :default-sort="defaultSort"
-        :style="{ width: layout.bodyWidth ? layout.bodyWidth + 'px' : '' }">
+        :style="{
+          width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
+        }">
       </table-footer>
     </div>
-    <div class="el-table__fixed" ref="fixedWrapper"
+    <div
       v-if="fixedColumns.length > 0"
       v-mousewheel="handleFixedMousewheel"
-      :style="[
-        { width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' },
-        fixedHeight
-      ]">
-      <div class="el-table__fixed-header-wrapper" ref="fixedHeaderWrapper" v-if="showHeader">
+      class="el-table__fixed"
+      ref="fixedWrapper"
+      :style="[{
+        width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
+      },
+      fixedHeight]">
+      <div
+        v-if="showHeader"
+        class="el-table__fixed-header-wrapper"
+        ref="fixedHeaderWrapper" >
         <table-header
           ref="fixedTableHeader"
           fixed="left"
           :border="border"
           :store="store"
-          :layout="layout"
-          :style="{ width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' }"></table-header>
+          :style="{
+            width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
+          }"></table-header>
       </div>
       <div
         class="el-table__fixed-body-wrapper"
         ref="fixedBodyWrapper"
-        :style="[
-          { top: layout.headerHeight + 'px' },
-          fixedBodyHeight
-        ]">
+        :style="[{
+          top: layout.headerHeight + 'px'
+        },
+        fixedBodyHeight]">
         <table-body
           fixed="left"
           :store="store"
           :stripe="stripe"
-          :layout="layout"
           :highlight="highlightCurrentRow"
           :row-class-name="rowClassName"
           :row-style="rowStyle"
-          :style="{ width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' }">
+          :style="{
+            width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
+          }">
         </table-body>
-        <div class="el-table__append-gutter" :style="{ height: layout.appendHeight + 'px' }" v-if="$slots.append"></div>
+        <div
+          v-if="$slots.append"
+          class="el-table__append-gutter"
+          :style="{
+            height: layout.appendHeight + 'px'
+          }"></div>
       </div>
-      <div class="el-table__fixed-footer-wrapper" ref="fixedFooterWrapper" v-if="showSummary" v-show="data && data.length > 0">
+      <div
+        v-if="showSummary"
+        v-show="data && data.length > 0"
+        class="el-table__fixed-footer-wrapper"
+        ref="fixedFooterWrapper">
         <table-footer
           fixed="left"
           :border="border"
           :sum-text="sumText || t('el.table.sumText')"
           :summary-method="summaryMethod"
           :store="store"
-          :layout="layout"
-          :style="{ width: layout.fixedWidth ? layout.fixedWidth + 'px' : '' }"></table-footer>
+          :style="{
+            width: layout.fixedWidth ? layout.fixedWidth + 'px' : ''
+          }"></table-footer>
       </div>
     </div>
-    <div class="el-table__fixed-right" ref="rightFixedWrapper"
+    <div
       v-if="rightFixedColumns.length > 0"
       v-mousewheel="handleFixedMousewheel"
-      :style="[
-        { width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' },
-        { right: layout.scrollY ? (border ? layout.gutterWidth : (layout.gutterWidth || 0)) + 'px' : '' },
-        fixedHeight
-      ]">
-      <div class="el-table__fixed-header-wrapper" ref="rightFixedHeaderWrapper" v-if="showHeader">
+      class="el-table__fixed-right"
+      ref="rightFixedWrapper"
+      :style="[{
+        width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '',
+        right: layout.scrollY ? (border ? layout.gutterWidth : (layout.gutterWidth || 0)) + 'px' : ''
+      },
+      fixedHeight]">
+      <div v-if="showHeader"
+        class="el-table__fixed-header-wrapper"
+        ref="rightFixedHeaderWrapper">
         <table-header
           ref="rightFixedTableHeader"
           fixed="right"
           :border="border"
           :store="store"
-          :layout="layout"
-          :style="{ width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' }"></table-header>
+          :style="{
+            width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : ''
+          }"></table-header>
       </div>
-      <div class="el-table__fixed-body-wrapper" ref="rightFixedBodyWrapper"
-        :style="[
-          { top: layout.headerHeight + 'px' },
-          fixedBodyHeight
-        ]">
+      <div
+        class="el-table__fixed-body-wrapper"
+        ref="rightFixedBodyWrapper"
+        :style="[{
+          top: layout.headerHeight + 'px'
+        },
+        fixedBodyHeight]">
         <table-body
           fixed="right"
           :store="store"
           :stripe="stripe"
-          :layout="layout"
           :row-class-name="rowClassName"
           :row-style="rowStyle"
           :highlight="highlightCurrentRow"
-          :style="{ width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' }">
+          :style="{
+            width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : ''
+          }">
         </table-body>
       </div>
-      <div class="el-table__fixed-footer-wrapper" ref="rightFixedFooterWrapper" v-if="showSummary" v-show="data && data.length > 0">
+      <div
+        v-if="showSummary"
+        v-show="data && data.length > 0"
+        class="el-table__fixed-footer-wrapper"
+        ref="rightFixedFooterWrapper">
         <table-footer
           fixed="right"
           :border="border"
           :sum-text="sumText || t('el.table.sumText')"
           :summary-method="summaryMethod"
           :store="store"
-          :layout="layout"
-          :style="{ width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : '' }"></table-footer>
+          :style="{
+            width: layout.rightFixedWidth ? layout.rightFixedWidth + 'px' : ''
+          }"></table-footer>
       </div>
     </div>
-    <div class="el-table__fixed-right-patch"
+    <div
       v-if="rightFixedColumns.length > 0"
-      :style="{ width: layout.scrollY ? layout.gutterWidth + 'px' : '0', height: layout.headerHeight + 'px' }"></div>
+      class="el-table__fixed-right-patch"
+      ref="rightFixedPatch"
+      :style="{
+        width: layout.scrollY ? layout.gutterWidth + 'px' : '0',
+        height: layout.headerHeight + 'px'
+      }"></div>
     <div class="el-table__column-resize-proxy" ref="resizeProxy" v-show="resizeProxyVisible"></div>
   </div>
 </template>
 
 <script type="text/babel">
   import ElCheckbox from 'element-ui/packages/checkbox';
-  import throttle from 'throttle-debounce/throttle';
   import debounce from 'throttle-debounce/debounce';
   import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
   import Mousewheel from 'element-ui/src/directives/mousewheel';
@@ -346,32 +405,44 @@
         });
 
         if (this.fit) {
-          this.windowResizeListener = throttle(50, () => {
-            if (this.$ready) this.doLayout();
-          });
-          addResizeListener(this.$el, this.windowResizeListener);
+          addResizeListener(this.$el, this.resizeListener);
+        }
+      },
+
+      resizeListener() {
+        if (!this.$ready) return;
+        let shouldUpdateLayout = false;
+        const el = this.$el;
+        const { width: oldWidth, height: oldHeight } = this.resizeState;
+
+        const width = el.offsetWidth;
+        if (oldWidth !== width) {
+          shouldUpdateLayout = true;
+        }
+
+        const height = el.offsetHeight;
+        if (this.height && oldHeight !== height) {
+          shouldUpdateLayout = true;
+        }
+
+        if (shouldUpdateLayout) {
+          this.resizeState.width = width;
+          this.resizeState.height = height;
+          this.doLayout();
         }
       },
 
       doLayout() {
-        this.store.updateColumns();
-        this.updateScrollY();
-        this.layout.update();
-        this.$nextTick(() => {
-          if (this.height) {
-            this.layout.setHeight(this.height);
-          } else if (this.maxHeight) {
-            this.layout.setMaxHeight(this.maxHeight);
-          } else if (this.shouldUpdateHeight) {
-            this.layout.updateHeight();
-          }
-        });
+        if (this.shouldUpdateHeight) {
+          this.layout.updateElsHeight();
+        }
+        this.layout.updateColumnsWidth();
       }
     },
 
     created() {
-      this.tableId = 'el-table_' + tableIdSeed + '_';
-      this.debouncedLayout = debounce(50, () => this.doLayout());
+      this.tableId = 'el-table_' + tableIdSeed++;
+      this.debouncedUpdateLayout = debounce(50, () => this.doLayout());
     },
 
     computed: {
@@ -384,7 +455,7 @@
       },
 
       shouldUpdateHeight() {
-        return typeof this.height === 'number' ||
+        return this.height ||
           this.fixedColumns.length > 0 ||
           this.rightFixedColumns.length > 0;
       },
@@ -409,34 +480,29 @@
         return this.store.states.rightFixedColumns;
       },
 
-      bodyHeight() {
-        let style = {};
+      bodyWidth() {
+        const { bodyWidth, scrollY, gutterWidth } = this.layout;
+        return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
+      },
 
+      bodyHeight() {
         if (this.height) {
-          style = {
+          return {
             height: this.layout.bodyHeight ? this.layout.bodyHeight + 'px' : ''
           };
         } else if (this.maxHeight) {
-          style = {
+          return {
             'max-height': (this.showHeader
               ? this.maxHeight - this.layout.headerHeight - this.layout.footerHeight
               : this.maxHeight - this.layout.footerHeight) + 'px'
           };
         }
-
-        return style;
-      },
-
-      bodyWidth() {
-        const { bodyWidth, scrollY, gutterWidth } = this.layout;
-        return bodyWidth ? bodyWidth - (scrollY ? gutterWidth : 0) + 'px' : '';
+        return {};
       },
 
       fixedBodyHeight() {
-        let style = {};
-
         if (this.height) {
-          style = {
+          return {
             height: this.layout.fixedBodyHeight ? this.layout.fixedBodyHeight + 'px' : ''
           };
         } else if (this.maxHeight) {
@@ -448,38 +514,40 @@
 
           maxHeight -= this.layout.footerHeight;
 
-          style = {
+          return {
             'max-height': maxHeight + 'px'
           };
         }
 
-        return style;
+        return {};
       },
 
       fixedHeight() {
-        let style = {};
-
         if (this.maxHeight) {
-          style = {
+          return {
             bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
           };
         } else {
-          style = {
+          return {
             height: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
           };
         }
-
-        return style;
       }
     },
 
     watch: {
-      height(value) {
-        this.layout.setHeight(value);
+      height: {
+        immediate: true,
+        handler(value) {
+          this.layout.setHeight(value);
+        }
       },
 
-      maxHeight(value) {
-        this.layout.setMaxHeight(value);
+      maxHeight: {
+        immediate: true,
+        handler(value) {
+          this.layout.setMaxHeight(value);
+        }
       },
 
       currentRowKey(newVal) {
@@ -488,8 +556,8 @@
 
       data: {
         immediate: true,
-        handler(val) {
-          this.store.commit('setData', val);
+        handler(value) {
+          this.store.commit('setData', value);
           if (this.$ready) {
             this.$nextTick(() => {
               this.doLayout();
@@ -509,13 +577,19 @@
     },
 
     destroyed() {
-      if (this.windowResizeListener) removeResizeListener(this.$el, this.windowResizeListener);
+      if (this.resizeListener) removeResizeListener(this.$el, this.resizeListener);
     },
 
     mounted() {
       this.bindEvents();
+      this.store.updateColumns();
       this.doLayout();
 
+      this.resizeState = {
+        width: this.$el.offsetWidth,
+        height: this.$el.offsetHeight
+      };
+
       // init filters
       this.store.states.columns.forEach(column => {
         if (column.filteredValue && column.filteredValue.length) {
@@ -542,11 +616,15 @@
         showHeader: this.showHeader
       });
       return {
-        store,
         layout,
+        store,
         isHidden: false,
         renderExpanded: null,
         resizeProxyVisible: false,
+        resizeState: {
+          width: null,
+          height: null
+        },
         // 是否拥有多级表头
         isGroup: false,
         scrollPosition: 'left'

+ 20 - 6
packages/theme-chalk/src/table.scss

@@ -83,6 +83,18 @@
     }
   }
 
+  @include m(scrollable-x) {
+    .el-table__body-wrapper {
+      overflow-x: auto;
+    }
+  }
+
+  @include m(scrollable-y) {
+    .el-table__body-wrapper {
+      overflow-y: auto;
+    }
+  }
+
   thead {
     color: $--table-header-color;
     font-weight: 500;
@@ -296,6 +308,7 @@
     top: 0;
     left: 0;
     overflow-x: hidden;
+    overflow-y: hidden;
     box-shadow: $--table-fixed-box-shadow;
 
     &::before {
@@ -372,6 +385,7 @@
 
   @include e((header, body, footer)) {
     table-layout: fixed;
+    border-collapse: separate;
   }
 
   @include e((header-wrapper, footer-wrapper)) {
@@ -384,36 +398,36 @@
   }
 
   @include e(body-wrapper) {
-    overflow: auto;
+    overflow: hidden;
     position: relative;
 
-    @include when(scroll-none) {
+    @include when(scrolling-none) {
       ~ .el-table__fixed,
       ~ .el-table__fixed-right {
         box-shadow: none;
       }
     }
 
-    @include when(scroll-left) {
+    @include when(scrolling-left) {
       ~ .el-table__fixed {
         box-shadow: none;
       }
     }
 
-    @include when(scroll-right) {
+    @include when(scrolling-right) {
       ~ .el-table__fixed-right {
         box-shadow: none;
       }
     }
 
     .el-table--border {
-      @include when(scroll-right) {
+      @include when(scrolling-right) {
         ~ .el-table__fixed-right {
           border-left: $--table-border;
         }
       }
 
-      @include when(scroll-left) {
+      @include when(scrolling-left) {
         ~ .el-table__fixed {
           border-right: $--table-border;
         }