今天一位朋友在群里问到一个问题「app 中设置界面修改设置的时候,每修改一项,就会触发 loading,导致用户体验不佳,如何避免?」
这是一个非常常见的「编辑 - 保存」页面,一般来说,这类页面的逻辑分成两种:一类是单独有个保存按钮进行保存;另一类是修改一项生效一项,无需额外保存。
单独按钮保存,常见于后台管理系统、或者是移动端的资料编辑页面上。以 Form 为单位,一次把所有内容提交到后台。单独设置保存按钮,我们可以在保存逻辑执行前通过弹窗,让用户对操作进行确认,通过点击「保存」或者「取消」,来让用户决定是否执行保存。试想,对于一个编辑用户页面,如果走的是立即生效的逻辑,很可能我们无意间的一些随意的修改,就把数据改掉了,比如无意间删除了用户名字中的一个字,除非我们记得这个字是什么然后手动改回来,我们是无法「撤销」我们的操作的。另外,对于一些依赖于其他输入项的表单验证行为,我们也需要当用户修改完所有项目后,统一进行验证和提交服务器,例如修改密码页面。
而像手机的设置页面上,当我们设置屏幕亮度的时候,我们希望立即看到效果,而不是点击保存按钮,才能看到亮度变化了。我们也不希望修改这些非常基础的设置,系统还要我们确认是否确定我们的操作。因此对于希望能够即时看到修改效果,并且无需额外确认的页面,我们可以讲请求后台的逻辑放到onChange,或者onLostFocus的时候。
回到最初的问题,朋友说的「每修改一项,就会触发loading」的情况,就是使用了无需保存按钮的方法,来设计的页面。
和手机上保存设置不同,app 里面的一些设置,是保存在服务器上的,每次改变设置,都需要去发起 API 调用,如果没有 loading, 当我们频繁的去切换设置,app 会连续发送多个请求到后台,给服务器造成压力,并且对于 Http 请求来说,如果没有设置 keep-alive,每次请求都会建立新的 TCP 连接,可能我们 app 上快速操作发送的请求是 开 关 开,到了服务器端的顺序变成了 开 开 关,导致设置与我们的想法相违背。通过加入 loading,实际是人为加了限制,必须在前一次 API 调用结束后,才可以发起下一次调用,减少服务器压力并防止频繁请求导致异常情况,loading 的伪代码如下:
if (!loading) {
loading = true;
await queryBackend(args);
loading = false;
}
复制代码
很多表单页面,也会在提交的时候,将 submit 按钮给禁用,避免重复点击造成数据异常。
但对于立即生效式的表单,频繁的 loading 就像每修改一项就弹出 confirm 一样的让人反感,在开发中,我们可以通过函数的节流(throttle)和防抖(debounce),来实现逻辑的优化,总的来说,节流和防抖都是在时间轴上控制执行的次数。
节流(throttle)
让一个函数无法在很短的时间间隔内连续调用,当上一次函数执行后过了规定的时间间隔,才能进行下一次该函数的调用。 节流逻辑的伪代码如下
function throttle(method, time){
var timer = null;
var startTime = new Date();
return function(){
var context = this;
var endTime = new Date();
var resTime = endTime - startTime;
//判断大于等于我们给的时间采取执行函数;
if(resTime >= time){
method.call(context);
//执行完函数之后重置初始时间,等于最后一次触发的时间
startTime = endTime;
}
}
}
复制代码
防抖(debounce)
让一个函数在一定间隔内没有被调用时,才开始执行被调用方法。 防抖逻辑的伪代码付下
function debounce(method,time){
var timer = null ;
return function(){
var context = this;
//在函数执行的时候先清除timer定时器;
clearTimeout(timer);
timer = setTimeout(function(){
method.call(context);
},time);
}
}
复制代码
具体是该用防抖还是节流,要看具体的场景,假如说我们需要在页面上调节一盏灯光的亮度,通过节流,我们可以在边调节边看到灯光明暗的变化,如果使用了防抖,这意味着只有当我们停止了调节,灯光才会产生变化。假如说我们调节的是一个温度旋钮,比如把空调温度从 22° 调节到 26°,我们不需要将 23°、24°、25°这些中间的状态发送给服务器,只需要最终的设置,这种时候防抖就更适合了。