想和大家探讨一下最近项目上遇到的一个防抖问题。
问题概述
大致需求是:有一个表格,点击其中任意一行会加载一些与之相关的详细内容(与表格在同一页面)。加载这个步骤是一个Promise链,会依次从2个不同的服务器端获取相关信息(存在依赖关系无法同时发送请求)。
在短时间内多次点击时,由于加载的时间每次不一样,可能会造成最终显示的不是最后一次点击的内容,且每一次点击都会有DOM操作从而造成浏览器性能的损失。
我们认为最合理的当然是加载过程中阻止用户继续点击,然而此方案被客户否决了:用户不应该被限制自由,假如用户点错了,还要等加载完才能改吗等等。
到这里,我们很自然的想到了利用防抖来进行延迟执行。但问题来了,加载的时间是个很大的区间(几百毫秒到几秒都有可能),传统的防抖在这个情况下并不适用。
举个例子,我们延迟500毫秒执行,第一次点击加载花了2秒,1秒后我们又点了一次加载,这次只花了500毫秒,结果就是最终先显示后一次结果,然后被前一次结果覆盖。如果我们设置一个过大的延迟值,那将会极大的降低用户体验。
由此引出今天讨论的话题,如何实现当Promise链未获取最终结果前,只有最后一次点击能够操作DOM改变页面。
P.S.由于实际工程比较复杂,http请求被封装在其他的模块中,所以在这里不考虑通过abort来终止请求以达到更好的优化。
以下为实际问题简化版:p1、p2、p3形成Promise链,可以看到,每次点击都会执行改变页面。(固定了Promise执行时间,且多加了一个Promise来更好的扩展假设有n个Promise的情况)
const p1 = (data) => { return new Promise(resolve => { setTimeout(() => resolve(data + 1), 200); }); }; const p2 = (data) => { return new Promise(resolve => { setTimeout(() => resolve(data + 2), 300); }); }; const p3 = (data) => { return new Promise(resolve => { setTimeout(() => resolve(data + 3), 500); }); }; const onClick = (data) => { p1(data) .then(data => p2(data)) .then(data => p3(data)) .then(result => { // 实际情况为操作返回值改变页面 console.log(result); }) .catch(err => { // 处理错误 }); }; // 模拟点击 onClick(1); setTimeout(() => onClick(2), 400); setTimeout(() => onClick(3), 2000); // 7 // 8 // 9 |
方案一
我们可以在onClick上设置一个counter,每次点击加1,只有当前值匹配counter时才改变页面。
// 省略p1,p2,p3申明 let counter = 0; const onClick = (data) => { const current = ++counter; p1(data) .then(data => p2(data)) .then(data => p3(data)) .then(result => { if (current === counter) { // 实际情况为操作返回值改变页面 console.log(result); } }) .catch(err => { if (current === counter) { // 处理错误 } }); }; onClick(1); setTimeout(() => onClick(2), 400); setTimeout(() => onClick(3), 2000); // 第一个onClick不会刷新页面 // 8 // 9 第三个点击时第二个已经刷新,所以第三个继续刷新页面 |
这个方案解基本解决了问题,但是仔细想想,实际上在每次点击时,所有的Promise链还是完全都执行了。
比如在第二个onClick时,第一个的Promise链才执行到p2,那么能不能不执行p3来达到更好的优化呢?
方案二:在方案一的基础上进一步优化
通过在每个Promise上嵌套一个函数来实现进一步优化,如果不匹配counter,直接reject中断Promise链。
// 省略p1,p2,p3申明 let counter = 0; const onClick = (data) => { const current = ++counter; p1(data) .then(wrapWithCancel(p2)) .then(wrapWithCancel(p3)) .then(result => { if (current === counter) { // 实际情况为操作返回值刷新页面 console.log(result); } }) .catch(err => { if (current === counter && err !== 'cancelled') { // 处理除了cancelled以外的错误 } }); function wrapWithCancel(fn) { return (data) => { if (current === counter) { return fn(data); } else { return Promise.reject('cancelled'); } } } }; onClick(1); setTimeout(() => onClick(2), 100); setTimeout(() => onClick(3), 400); // 第一个onClick的p2和p3都不会执行 // 第二个onClick的p3不会执行 // 9 |
方案三:加上常规的防抖延迟执行
我们同样可以在这基础上加上常规的防抖延迟执行,进一步优化:
// 省略p1,p2,p3申明 let counter = 0; const onClick = (data) => { const current = ++counter; p1(data) .then(wrapWithCancel(p2)) .then(wrapWithCancel(p3)) .then(result => { if (current === counter) { // 实际情况为操作返回值刷新页面 console.log(result); } }) .catch(err => { if (current === counter && err !== 'cancelled') { // 处理除了cancelled以外的错误 } }); function wrapWithCancel(fn) { return (data) => { if (current === counter) { return fn(data); } else { return Promise.reject('cancelled'); } } } }; const debounce = function (fn, wait) { var timer = null; return function () { const context = this; const args = arguments; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, wait); } }; const debounced = debounce(onClick, 200); debounced(1); setTimeout(() => debounced(2), 100); setTimeout(() => debounced(3), 200); setTimeout(() => debounced(4), 600); // 前两个onClick的p1,p2和p3都不会执行 // 第三个onClick的p3不会执行 // 10 |
第一次发文,不足之处还请轻喷,欢迎指出错误,如果你有更好的方法,也希望大家一起共同探讨共同进步~
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。