拓冰建站拓冰建站
首页 / 资讯中心 / 正文

layui下拉框重新渲染全解析:机制、误区与实战联动

先讲个真实场景。某天运营反馈后台一个按城市筛选的下拉框选完省份之后城市列表死活不刷新控制台里接口数据明明已经回来了。我打开页面一查发现代码里用$(#city).append(new Option(...))之后接着就form.render(select)看起来一条龙服务结果页面上城市下拉还是老面孔。排查到最后问题出在渲染时机上——那行 render 竟然写在 AJAX 回调外面数据还没回来layui 已经把旧选项重新渲染了一遍。类似这种因为layui select 重新渲染细节没处理好引发的灵异问题这四年里我至少帮人排查过几十次几乎每个刚接触 layui 的前端都会在这里绊一下。这篇文章就把这块掰开揉碎讲清楚从渲染机制到实战联动一次说透。1. 为什么追加了 option 页面纹丝不动layui 的下拉渲染机制1.1 原生 select 早就被藏起来了先看一段很典型的代码var sel document.getElementById(city); sel.appendChild(new Option(北京, 110000));放在原生 HTML 页面里这段代码会让下拉框肉眼可见地多出北京一项。但放进 layui 表单你打开下拉框列表还是老样子仿佛 append 操作根本没发生过。这不是代码不生效而是你看错了对象。layui 的 form 模块在初始化时会对容器内所有的 select、checkbox、radio 做一次外观改造。具体到 select流程是这样的把原生 select 隐藏掉在它后面生成一套用 div、dl、dd 拼出来的模拟下拉。平时你点开看到的选项列表其实是那套模拟 DOM不是原生 select 的 option 列表。原生 select 在这个体系里变成了隐形数据源只负责存选项模拟 DOM 负责展示和交互。两者之间的同步全靠form.render这个动作来触发。你只改了原生 select 的 options却没有通知 layui 重新读取模拟下拉自然不会变。这就好比你把菜单改好了但服务员没收到通知客人看到的还是旧菜单。1.2 form.render 到底做了哪三件事很多文章把form.render描述成魔法刷新实际拆开看它无非做了三件事读取原生 select 当前的完整 option 列表重建模拟下拉里的 dl 选项根据当前选中项同步 dd 上的layui-this高亮类并更新模拟下拉标题栏的文字把新生成的模拟下拉与内部事件处理逻辑挂接好保证后续点击、键盘操作还能正常响应。顺带一提form.render不只是管 selectcheckbox 和 radio 也在它的职责范围内。它的完整签名是form.render(type, filter)type 传select、checkbox、radio可以只刷新某一类控件不传则是全部刷新一遍。有一个容易被误解的点form.render并不是重新执行整个页面初始化它只做同步显示层不会清空你已经通过form.on绑定的事件。关于事件会不会丢这个问题第 4 节我会专门展开。1.3 为什么我明明调了 render 还是没变化既然同步机制已经清楚了那调了 render 没用多半出在这几个环节调用时机早于数据更新。AJAX 还没返回render 已经跑完了目标 select 不在classlayui-form容器内layui 默认管不到它在弹层、Tab、表格动态行这类场景里控件是后插入 DOM 的插入后没有立刻调用 render用form.render(select)做了全局刷新但 option 本身确实没塞进去问题压根出在数据组装环节。前两种好自查第三种在真实项目里最普遍也是最容易被忽视的。我在第 2.3 节专门讲弹层和 Tab 的渲染时点赶时间可以直接跳到那。2. form.render(select) 的正确打开方式与常见误区2.1 全量渲染和定向渲染怎么选先记住最基础一条所有动态操作完 option 之后跟着调用form.render(select)这是标准动作。项目里只有三五个下拉框全量刷新一点问题没有但后台页面一旦复杂起来几十个下拉加一堆表格任何小改动都全局刷新成本会明显上升有些隐藏的联动状态还可能被意外重置。layui 提供了定向渲染给 select 加上lay-filter属性然后用form.render(select, filter名字)精确刷新。举例select namecity idcity lay-filtercityFilter option value请选择城市/option /selectform.render(select, cityFilter);这里有个新手很容易踩的细节第二个参数匹配的是lay-filter的值不是 id也不是 name。写成form.render(select, city)是无效的。我在项目里的习惯是凡是会被动态改数据的下拉一律加上有意义的 lay-filter比如provinceFilter、cityFilter既方便定向渲染也方便form.on监听一举两得。这样即使页面有几十个下拉每一次重渲染也是最小范围不会误伤其他控件的状态。2.2 先改数据再渲染顺序一个都不能反先改数据再 render可以说是基本常识但实际代码里还藏着更隐蔽的顺序问题。看这个反面例子$.ajax({ url: /api/cities, success: function(res) { // 渲染写在了数据组装之前 form.render(select, cityFilter); $(#city).empty(); res.data.forEach(function(item) { $(#city).append(new Option(item.name, item.code)); }); } });AJAX 回调里先 render 再 append等于 layui 读取的还是旧 options 列表页面当然不会有新数据。这种错在本地开发时很容易被忽视因为接口快的时候几乎察觉不到一旦网络慢或者接口抖动问题就暴露了。正确的执行顺序应该是一条线走到底清空旧选项、填充新选项、设置默认选中值、调用form.render(select, filter)。默认值赋值必须放在 render 之前否则渲染完再设置 val 是不生效的。如果你用form.val(filter, { city: defaultCode })来赋值它内部已经触发了渲染后面就不用再单独调一次form.render了否则等于白白多渲染一遍。2.3 弹层和 Tab 里的 select 有额外的渲染时点如果是layer.open打开的弹层内容里带 select那么form.render()必须在弹层内容真正插入页面之后执行。常见写法是放在 success 回调里layer.open({ type: 1, content: $(#dialogHtml), success: function() { form.render(select); } });如果 content 是通过 AJAX 拿回来的 HTML 字符串更要等字符串 append 到弹层容器后再 render。顺序一错下拉就是白板——选项是有但 layui 的模拟下拉样式出不来看起来像原生控件一样丑陋。同样的道理也适用于 element 模块的 Tab。Tab 内容如果是后生成的切到对应 Tab 才出现的下拉要在切换事件里补一次 render。很多人问为什么 Tab 里的 select 没有样式绝大多数就是这个原因。从 layui 的角度看它只处理初始化时已经存在的 form 控件后插入的都需要手动叫醒。3. 动态填充选项的三种姿势append、innerHTML 与数据源重载3.1 姿势一原生 DOM API 追加最基础的方式适合选项量少、逻辑简单的场景var sel document.getElementById(city); sel.options.length 0; // 清空旧选项 var cityList [ { code: 110000, name: 北京 }, { code: 310000, name: 上海 } ]; cityList.forEach(function(city) { var opt document.createElement(option); opt.value city.code; opt.text city.name; sel.appendChild(opt); }); form.render(select, cityFilter);代码的要点是先清空再填充最后一次性 render。如果每 append 一个选项就 render 一次会白白增加 DOM 操作和样式重算用户甚至可能看到选项列表在眼前闪动。用原生 API 的好处是没有依赖不依赖 jQuery 也能跑。缺点是代码比较啰嗦选项一多写起来明显不如模板拼接清爽。这个方法我还挺常用尤其是写一些零依赖的二开页面时。3.2 姿势二jQuery 批量生成 option项目里如果已经引入了 jQuery批量生成 option 会干净很多var html option value请选择城市/option; cityList.forEach(function(city) { html option value city.code city.name /option; }); $(#city).html(html); form.render(select, cityFilter);或者用new Option直接追加$(#city).empty(); cityList.forEach(function(city) { $(#city).append(new Option(city.name, city.code)); }); form.render(select, cityFilter);这里有个很容易弄反的细节new Option(text, value)第一个参数是显示文本第二个才是 value。我见过不少人写成new Option(city.code, city.name)结果下拉里显示的是编码提交的却是中文名这种错位特别隐蔽接口联调时才会暴露。模板拼接的方式还有一点要注意接口返回的数据如果包含特殊字符拼接进 HTML 时一定要做转义否则可能造成 HTML 注入或者引号把属性截断。下拉选项这种不起眼的地方往往最容易放松安全这根弦。3.3 姿势三从接口拉数据后整体替换真实项目里选项数据几乎都来自远端。把 AJAX 和渲染串起来的完整链路是这样function loadCities(provinceCode) { $.ajax({ url: /api/cities, data: { provinceCode: provinceCode }, dataType: json, success: function(res) { var html option value请选择城市/option; if (res.code 0 Array.isArray(res.data)) { res.data.forEach(function(city) { html option value city.code city.name /option; }); } $(#city).html(html); form.render(select, cityFilter); }, error: function() { $(#city).html(option value加载失败请重试/option); form.render(select, cityFilter); } }); }我特意在示例里加了几处实战细节判断res.code而不是直接res.data用Array.isArray兜底数据结构异常失败时渲染一个加载失败的提示项而不是留白。很多初学者只写 success 分支接口一报错下拉区域就变成空白用户完全不知道发生了什么这是非常差的体验。另一个高频问题是重复请求。用户快速切换多次省份前一个请求还没返回后一个就发出去了响应顺序错乱就会出现 A 省的选项渲染到 B 省的下拉里。解决办法我在第 5 节的联动案例里详细讲这里先留个钩子。3.4 占位项和空白状态的处理不管你选哪种姿势我都建议在下拉头部保留一个value的占位 option比如请选择省份请选择城市。它有三个作用页面加载时下拉有一个合理的默认显示不会出现空白标题栏联动逻辑拿到空值可以明确判断为未选择方便清空下级下拉在 layui 的模拟下拉里占位项作为第一项展示在标题栏符合表单操作习惯。空白状态同样要处理接口返回空数组下拉只剩占位项接口报错至少给个加载失败提示。这些不是 UI 上的吹毛求疵而是弱网环境下用户的真实感受。你多写两行代码后台用户就少一句抱怨。4. 重新渲染后的隐藏雷区事件丢失、选中态残留与联动失序4.1 事件丢失一半是误解一半是真坑不少朋友反馈重新渲染之后下拉的 change 事件不触发了。我专门排查过这个问题结论很明确如果你用的是form.on(select(过滤器), callback)的标准写法重新渲染根本不会让事件丢失。form.on是 layui 模块内部监听了模拟下拉的交互它不依赖具体某一轮生成的 DOM 节点模拟 DOM 重建后事件依然有效。真正会丢事件的是两种写法你用了onchangexxx()这种内联事件挂在 select 上模拟 DOM 拦截了原生交互原生 change 不触发你手动在某个 option 或 dd 元素上用 jQuery 绑定了 click渲染后这些节点被整体替换绑定随之消失。解决方案很简单统一走form.on(select(filter))不要直接操作模拟层的节点。如果你接手的老项目里还在用内联事件建议尽快迁到form.on上否则每次动态渲染都是在给自己埋雷。form.on的回调参数里data.value就是当前选中值data.elem是原生 select 元素绝大多数场景不需要再去 DOM 里查询直接用这个对象就够了form.on(select(cityFilter), function(data) { console.log(data.value); // 选中的 value console.log(data.elem); // 原生 select 的 DOM 引用 });4.2 选中态残留渲染后下拉显示旧值动态填充场景里另一个高频问题是选项已经刷新了标题栏却还显示老选项的文字。原因通常出在数据的更新和选中态的赋值没有放在同一个执行块里或者赋值动作放在了 render 之后。标准做法是把更新数据、设置默认值、渲染三步串在同一个同步代码块里var html option value请选择城市/option; // ... 组装 options $(#city).html(html); $(#city).val(defaultCityCode); // 在 render 之前设置选中态 form.render(select, cityFilter);如果你用的是form.val(cityFilter, { city: defaultCityCode })它本身会触发渲染后面就不要再单独调form.render(select)否则重复渲染等于做了一轮无用功。这里没有绝对的范式核心是搞清楚你选的 API 是不是自带渲染能力然后避免叠加。4.3 表头、弹窗、表格行内 select 的渲染陷阱layui 的 table 组件里经常放 select比如给某一行加状态下拉。这里的渲染要点在于table 行是table.render动态生成的如果你在表格的 done 回调里修改了某一行 select 的选项必须在 done 回调里紧接着补一次form.render(select)。还有一种更隐蔽的情况是表头筛选。之前做个项目列筛选下拉用 layui select 实现筛选后想清空选项重新渲染表头和数据区域始终不同步。排查到最后发现是复杂表头在特定版本里对 select 容器做了额外的 DOM 处理导致后续 append 被还原。遇到这种问题先升级 layui 到较新版本再回到常规的 render 流程验证大概率能解决。弹层里的 select 前面已经说过这里补充一点layer.open的 content 如果是传入的 jQuery 对象要确保这个对象在弹层打开前没有被其他逻辑隐藏掉否则 success 里的form.render渲染完下拉可能定位错乱。我自己实践下来最稳妥的方式是让弹层内容始终存在于页面流中需要时用 type:1 展开而不是频繁地创建和销毁节点。5. 一个完整的省市区联动案例渲染时序设计5.1 需求与数据结构直接拿后台系统里最经典的省市区三级联动说事。需求是这样选中省份城市下拉刷新选中城市区县下拉刷新把省份切回默认项城市和区县一起清空。后端接口约定两个GET /api/cities?provinceCodexxx返回该省份下的城市数组GET /api/districts?cityCodexxx返回该城市下的区县数组。返回结构统一是{ code: 0, data: [{ code, name }] }。5.2 分步实现从 HTML 到 JSHTML 部分三个下拉放在同一个layui-form容器里分别加 lay-filterdiv classlayui-form select nameprovince idprovince lay-filterprovinceFilter option value请选择省份/option /select select namecity idcity lay-filtercityFilter option value请选择城市/option /select select namedistrict iddistrict lay-filterdistrictFilter option value请选择区县/option /select /divJS 部分核心是监听省份 change然后按顺序重载城市下拉form.on(select(provinceFilter), function(data) { var provinceCode data.value; if (!provinceCode) { resetCity(); resetDistrict(); return; } loadCities(provinceCode); });接下来是loadCities和loadDistricts。这里有个很关键的时序设计城市请求返回后要清空区县所以区县的清空动作要放在城市渲染之前。我是这样组织的function loadCities(provinceCode) { $(#city).html(option value请选择城市/option); $(#district).html(option value请选择区县/option); form.render(select); if (!provinceCode) return; $.ajax({ url: /api/cities, data: { provinceCode: provinceCode }, success: function(res) { if (res.code ! 0) return; var html option value请选择城市/option; res.data.forEach(function(city) { html option value city.code city.name /option; }); $(#city).html(html); form.render(select, cityFilter); } }); }我先做了一次全量form.render(select)把城市和区县都重置为占位项然后再拉接口。这样即使用户切换很快界面上也不会残留上一个省的数据新数据返回前有合理的反馈。等城市数据回来再定向渲染cityFilter区县此时保持清空状态。5.3 边界情况重复请求、空数据与默认值真实环境里用户不可能乖乖等接口返回。快速连续切换省份时如果前面发出的请求比后面的慢响应顺序错乱城市下拉就会显示成上一个省的数据。我处理的方案是加一个请求序号var cityRequestSeq 0; function loadCities(provinceCode) { var seq cityRequestSeq; $(#city).html(option value请选择城市/option); $(#district).html(option value请选择区县/option); form.render(select); if (!provinceCode) return; $.ajax({ url: /api/cities, data: { provinceCode: provinceCode }, success: function(res) { if (seq ! cityRequestSeq) return; // 已经有过更新的请求丢弃本次结果 if (res.code ! 0) return; // 渲染 options } }); }这个 seq 对比的思路朴素但特别好用。你也可以用xhr.abort()取消上一个请求效果类似但请求序号的写法在 jQuery 和原生 fetch 下都能通用我更喜欢用这个。空数据场景res.data是空数组时下拉只留占位项我通常会把占位项文案改成暂无数据让用户知道接口是通的但没内容。默认值场景编辑页回显时要先拉省份列表再拉城市列表再拉区县列表每步都要等上一级数据拿到后才能请求下一级这个天然就是一套层层递进的渲染时序设计。赋默认值的位置在对应层级的 render 之前链路就顺了。6. 调试与排查问题定位的系统化思路6.1 三步法先看下拉数据再看渲染层最后看时序遇到下拉没刷新这类问题我已经养成了固定的排查顺序你直接照这个思路来就行。第一步检查原生 select 的 options。控制台执行document.getElementById(city).options.length看看数量是否等于你拼接的选项个数。如果不一致说明数据组装环节就有问题跟 layui 没关系别在渲染上浪费时间。第二步检查模拟层。打开 DevTools 的 Elements 面板找到 select 后面那串div.layui-form-select展开里面的 dl看选项数量。如果原生 select 已经更新dl 里还是旧选项说明form.render没有在这之后执行或者定向渲染的 filter 没对上。第三步在代码里插桩。在组装 html、调用 render 前后各打一条 console.log确认调用时机是不是在数据更新之后、有没有被执行到。这一步能快速把没执行和执行了没生效区分开。这三步走完八成的问题都能定位到具体环节。剩下的要么是时序问题要么是控件不在layui-form容器范围内。6.2 一个容易被忽略的伪故障浏览器自动填充还有一个踩过很多次的坑跟 layui 本身没有关系浏览器的表单自动填充。某些浏览器会自动给 select 填上一个曾经使用过的选项而且填充发生在页面加载后如果你的初始化脚本把下拉重置成占位项用户打开页面却发现标题栏显示的是老数据第一反应就是js 没生效。排查方法很简单看下拉框是否在页面加载后立即出现异常选中值。如果是建议在 form 标签或 select 上加上autocompleteoff并在页面初始化时统一用form.val赋值占位项。这个坑在 Chrome 上比较典型尤其是后台系统这种经常登录同一账号的环境。6.3 常见问题速查表现象可能原因对策动态 append 后下拉无变化只改了原生 select没调 form.render数据更新后调用form.render(select, filter)渲染后仍是旧选项render 调在数据组装之前调整顺序先组装后渲染只有部分下拉刷新了定向渲染 filter 写错或没加 lay-filter核对 lay-filter 与传入 filter 一致弹层内下拉无样式/空白渲染发生在弹层内容插入 DOM 前在layer.open的 success 回调里 renderTab 内下拉空白Tab 内容是后插入的在 tab 切换回调里补一次 render切换选项时旧数据残留上一次请求未完成或顺序错乱用请求序号/防抖/abort 控制并发选中态显示旧值赋值在 render 之后或没设置默认选中态在 render 之前设置 val 或 form.valform.on 不触发使用了内联事件或直接绑定模拟 DOM统一迁到form.on(select(filter))这张表建议收藏遇到问题先翻一遍比去搜索引擎翻几百条帖子快得多。最后再多说一句个人体会。layui 的 select 重新渲染本身不复杂核心就是记住改完原生 DOM 之后务必让 form.render 重新同步一次这条主线。但正因为规则太简单很多人反而会在时序、filter、容器这些细节上栽跟头。这几年带项目我经常跟新人强调先搞清楚数据在哪一层、渲染在哪一层、事件在哪一层三层对上了下拉框想怎么联动都不慌。另外真心建议所有会被动态改数据的下拉从一开始就统一走lay-filterform.on的写法别图省事用内联事件越到后期你越会感谢自己这个决定。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门