由项目需求中引出的思考,Promise链式调用如何防抖

发表于:2019-4-23 11:21

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Yukan    来源:掘金

  想和大家探讨一下最近项目上遇到的一个防抖问题。
  问题概述
  大致需求是:有一个表格,点击其中任意一行会加载一些与之相关的详细内容(与表格在同一页面)。加载这个步骤是一个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),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号