From 249d6db78ec94329acdb2666391db21bb4d1a2f5 Mon Sep 17 00:00:00 2001
From: morning-star <26325820+Sight-wcg@users.noreply.github.com>
Date: Tue, 18 Jun 2024 09:34:41 +0800
Subject: [PATCH] =?UTF-8?q?perf(table):=20=E4=BC=98=E5=8C=96=E8=A1=A8?=
=?UTF-8?q?=E6=A0=BC=E6=95=B0=E6=8D=AE=E9=87=8F=E8=BE=83=E5=A4=A7=E6=97=B6?=
=?UTF-8?q?=E8=A1=8C=E9=80=89=E4=B8=AD=E6=80=A7=E8=83=BD=20(#2004)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* perf(table): 优化表格数据量较大时行选中性能
* chore: 改进选择器
* refactor: 简化代码
* feat: 多选时禁用过渡效果以提高性能
* update code
* update code
---
src/css/layui.css | 3 ++
src/modules/form.js | 89 +++++++++++++++++++++++++++++++-------------
src/modules/table.js | 70 +++++++++++++++++++++++++++-------
3 files changed, 123 insertions(+), 39 deletions(-)
diff --git a/src/css/layui.css b/src/css/layui.css
index 45cff7d3..202c082a 100644
--- a/src/css/layui.css
+++ b/src/css/layui.css
@@ -1028,6 +1028,9 @@ hr.layui-border-black{border-width: 0 0 1px;}
.layui-table-checked{background-color: #dbfbf0;}
.layui-table-checked.layui-table-hover,
.layui-table-checked.layui-table-click{background-color: #abf8dd;}
+.layui-table-disabled-transition *,
+.layui-table-disabled-transition *:before,
+.layui-table-disabled-transition *:after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}
.layui-table th,
diff --git a/src/modules/form.js b/src/modules/form.js
index 4f40897e..b44a8a6a 100644
--- a/src/modules/form.js
+++ b/src/modules/form.js
@@ -843,20 +843,13 @@ layui.define(['lay', 'layer', 'util'], function(exports){
// 事件
var events = function(reElem, RE_CLASS){
var check = $(this);
+ var skin = check.attr('lay-skin') || 'primary';
+ var isSwitch = skin === 'switch';
+ var isPrimary = skin === 'primary';
// 勾选
reElem.on('click', function(){
- var othis = $(this);
var filter = check.attr('lay-filter') // 获取过滤器
- var title = (
- othis.next('*[lay-checkbox]')[0]
- ? othis.next().html()
- : check.attr('title') || ''
- );
- var skin = check.attr('lay-skin') || 'primary';
-
- // 开关
- title = skin === 'switch' ? title.split('|') : [title];
// 禁用
if(check[0].disabled) return;
@@ -864,19 +857,10 @@ layui.define(['lay', 'layer', 'util'], function(exports){
// 半选
if (check[0].indeterminate) {
check[0].indeterminate = false;
- reElem.find('.'+ CLASS.SUBTRA).removeClass(CLASS.SUBTRA).addClass('layui-icon-ok');
}
// 开关
- check[0].checked ? (
- check[0].checked = false,
- reElem.removeClass(RE_CLASS[1]),
- skin === 'switch' && reElem.children('div').html(title[1])
- ) : (
- check[0].checked = true,
- reElem.addClass(RE_CLASS[1]),
- skin === 'switch' && reElem.children('div').html(title[0])
- );
+ check[0].checked = !check[0].checked
// 事件
layui.event.call(check[0], MOD_NAME, RE_CLASS[2]+'('+ filter +')', {
@@ -885,6 +869,27 @@ layui.define(['lay', 'layer', 'util'], function(exports){
othis: reElem
});
});
+
+ that.syncAppearanceOnPropChanged(this, 'checked', function(isChecked){
+ if(isSwitch){
+ var title = (reElem.next('*[lay-checkbox]')[0]
+ ? reElem.next().html()
+ : check.attr('title') || ''
+ ).split('|');
+ reElem.children('div').html(isChecked ? title[0] : title[1] || title[0]);
+ }
+ reElem.toggleClass(RE_CLASS[1], isChecked);
+ });
+
+ if(isPrimary){
+ that.syncAppearanceOnPropChanged(this, 'indeterminate', function(isIndeterminate){
+ if(isIndeterminate){
+ reElem.children('.layui-icon-ok').removeClass('layui-icon-ok').addClass(CLASS.SUBTRA);
+ }else{
+ reElem.children('.'+ CLASS.SUBTRA).removeClass(CLASS.SUBTRA).addClass('layui-icon-ok');
+ }
+ })
+ }
};
// 遍历复选框
@@ -938,7 +943,7 @@ layui.define(['lay', 'layer', 'util'], function(exports){
''
].join(''),
// 开关
- "switch": '
'+ ((check.checked ? title[0] : title[1]) || '') +'
'
+ "switch": ''+ ((check.checked ? title[0] : (title[1] || title[0])) || '') +'
'
};
return type[skin] || type['checkbox'];
}(),
@@ -968,15 +973,10 @@ layui.define(['lay', 'layer', 'util'], function(exports){
if(radio[0].disabled) return;
layui.each(sameRadio, function(){
- var next = $(this).next('.' + CLASS);
this.checked = false;
- next.removeClass(CLASS + 'ed');
- next.children('.layui-icon').removeClass(ANIM + ' ' + ICON[0]).addClass(ICON[1]);
});
radio[0].checked = true;
- reElem.addClass(CLASS + 'ed');
- reElem.children('.layui-icon').addClass(ANIM + ' ' + ICON[0]);
layui.event.call(radio[0], MOD_NAME, 'radio('+ filter +')', {
elem: radio[0],
@@ -984,6 +984,16 @@ layui.define(['lay', 'layer', 'util'], function(exports){
othis: reElem
});
});
+
+ that.syncAppearanceOnPropChanged(this, 'checked', function(isChecked){
+ if(isChecked){
+ reElem.addClass(CLASS + 'ed');
+ reElem.children('.layui-icon').addClass(ANIM + ' ' + ICON[0]);
+ }else{
+ reElem.removeClass(CLASS + 'ed');
+ reElem.children('.layui-icon').removeClass(ANIM + ' ' + ICON[0]).addClass(ICON[1]);
+ }
+ })
};
// 初始渲染
@@ -1064,6 +1074,33 @@ layui.define(['lay', 'layer', 'util'], function(exports){
}
return that;
};
+
+ /**
+ * checkbox 和 radio 指定属性变化时自动更新 UI
+ * @param {HTMLInputElement} elem - HTMLInput 元素
+ * @param {'checked' | 'indeterminate'} propName - 属性名
+ * @param {(newValue: boolean, oldValue: boolean) => void} handler - 定义如何更新
+ * @see https://learn.microsoft.com/zh-cn/previous-versions//ff382725(v=vs.85)?redirectedfrom=MSDN
+ */
+ Form.prototype.syncAppearanceOnPropChanged = function(elem, propName, handler){
+ var originProps = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, propName);
+
+ Object.defineProperty(elem, propName,
+ lay.extend({}, originProps, {
+ // 此处的 get 是为了兼容 IE<9
+ get: function(){
+ return originProps.get.call(this);
+ },
+ set: function (newValue) {
+ var oldValue = this[propName];
+ originProps.set.call(this, newValue);
+ if(oldValue !== newValue){
+ handler(newValue, oldValue);
+ }
+ }
+ })
+ );
+ }
/**
* 主动触发验证
diff --git a/src/modules/table.js b/src/modules/table.js
index 02071f61..b7d101f1 100644
--- a/src/modules/table.js
+++ b/src/modules/table.js
@@ -147,7 +147,8 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
var ELEM_GROUP = 'laytable-cell-group';
var ELEM_COL_SPECIAL = 'layui-table-col-special';
var ELEM_TOOL_PANEL = 'layui-table-tool-panel';
- var ELEM_EXPAND = 'layui-table-expanded'
+ var ELEM_EXPAND = 'layui-table-expanded';
+ var DISABLED_TRANSITION = 'layui-table-disabled-transition';
var DATA_MOVE_NAME = 'LAY_TABLE_MOVE_DICT';
@@ -1571,7 +1572,6 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
checked: checkStatus.isAll,
indeterminate: !checkStatus.isAll && checkStatus.data.length // 半选
});
- form.render(checkAllElem);
};
// 标记当前活动行背景色
@@ -1593,14 +1593,28 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
var options = that.config;
var isCheckAll = opts.index === 'all'; // 是否操作全部
var isCheckMult = layui.type(opts.index) === 'array'; // 是否操作多个
+ var needDisableTransition= isCheckAll || isCheckMult; // 减少回流
+
+ if(needDisableTransition){
+ that.layBox.addClass(DISABLED_TRANSITION);
+ }
+
+ if(isCheckMult){
+ var makeMap = {}
+ layui.each(opts.index, function(i,v){
+ makeMap[v] = true;
+ })
+ opts.index = makeMap;
+ }
// 匹配行元素
+ var selector = (isCheckAll || isCheckMult) ? 'tr' : 'tr[data-index="'+ opts.index +'"]';
var tr = function(tr) {
return isCheckAll ? tr : tr.filter(isCheckMult ? function() {
var dataIndex = $(this).data('index');
- return opts.index.indexOf(dataIndex) !== -1;
+ return opts.index[dataIndex];
} : '[data-index="'+ opts.index +'"]');
- }(that.layBody.find('tr'));
+ }(that.layBody.find(selector));
// 默认属性
opts = $.extend({
@@ -1616,14 +1630,18 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
return opts.type === 'radio' ? true : (existChecked ? opts.checked : !value)
};
+ var ignoreTrIndex = {};
// 设置选中状态
layui.each(thisData, function(i, item){
// 绕过空项和禁用项
- if(layui.type(item) === 'array' || item[options.disabledName]) return;
+ if(layui.type(item) === 'array' || item[options.disabledName]){
+ ignoreTrIndex[i] = true;
+ return;
+ }
// 匹配条件
var matched = isCheckAll || (
- isCheckMult ? opts.index.indexOf(i) !== -1 : Number(opts.index) === i
+ isCheckMult ? opts.index[i] : Number(opts.index) === i
);
// 设置匹配项的选中值
@@ -1632,18 +1650,39 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
var checked = item[options.checkName] = getChecked(item[options.checkName]);
// 标记当前行背景色
- var currTr = tr.filter('[data-index="'+ i +'"]');
- currTr[checked ? 'addClass' : 'removeClass'](ELEM_CHECKED);
-
- // 若为 radio 类型,则取消其他行选中背景色
- if(opts.type === 'radio'){
- currTr.siblings().removeClass(ELEM_CHECKED);
+ // 此处只更新 radio 和 单个 checkbox
+ if(!isCheckAll && !isCheckMult){
+ var currTr = tr.filter('[data-index="'+ i +'"]');
+ currTr[checked ? 'addClass' : 'removeClass'](ELEM_CHECKED);
+
+ // 若为 radio 类型,则取消其他行选中背景色
+ if(opts.type === 'radio'){
+ currTr.siblings().removeClass(ELEM_CHECKED);
+ }
}
} else if(opts.type === 'radio') {
delete item[options.checkName];
}
});
+ if(isCheckAll){
+ tr.each(function(i){
+ var index = Number(this.getAttribute('data-index'));
+ if(!ignoreTrIndex[index]){
+ var el = $(this);
+ el.toggleClass(ELEM_CHECKED, getChecked(thisData[index][options.checkName]))
+ }
+ });
+ }else if(isCheckMult){
+ tr.each(function(i){
+ var index = Number(this.getAttribute('data-index'));
+ if(opts.index[index] && !ignoreTrIndex[index]){
+ var el = $(this);
+ el.toggleClass(ELEM_CHECKED, getChecked(thisData[index][options.checkName]))
+ }
+ });
+ }
+
// 若存在复选框或单选框,则标注选中状态样式
var checkedElem = tr.find('input[lay-type="'+ ({
radio: 'layTableRadio',
@@ -1657,7 +1696,12 @@ layui.define(['lay', 'laytpl', 'laypage', 'form', 'util'], function(exports){
: checkedElem ).prop('checked', getChecked(checkedSameElem.prop('checked')));
that.syncCheckAll();
- that.renderForm(opts.type);
+
+ if(needDisableTransition){
+ setTimeout(function(){
+ that.layBox.removeClass(DISABLED_TRANSITION);
+ },100)
+ }
};
// 数据排序