我正在参与掘金创作者训练营第6期, 点击了解活动详情
正逢周六,泡一杯浓茶,绿色叶子慢慢从皱皱巴巴样子慢慢展开,随着热气浮沉,借着等接口的时间,聊一聊之前手底下小朋友做代码优化引起的一次客诉。
问题出现
一次常规代码上线后不到二小时,有部分客户反馈出现重复提交单据的情况,经实施与客户沟通反馈,客户未重复点击。
问题定位
首先第一时间,通过查看trace日志,发现两次提交的时间间隔在10毫秒内,初步判断可能是出现了重复提交的情况,然后进行代码审查,发现已经加上了节流函数,时间间隔为1000毫秒,理论上是不会重复触发的。
节流与防抖
节流与防抖的前提,都是 某个行为持续地触发,这里采用节流函数,是否会有问题?首先就需要知道节流与防抖函数的实现以及他们各自起到的效果
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
这里我们采用的是节流函数,而节流又有时间戳和定时器两种实现
- 时间戳版,这里利用了闭包的原理,保持了oldtime这个变量,这里,规定延时内,函数不会重复进行调用
function throttled1(fn, delay = 500) {
let oldtime = Date.now()
return function (...args) {
let newtime = Date.now()
if (newtime - oldtime >= delay) {
fn.apply(null, args)
oldtime = Date.now()
}
}
}
2.定时器版,和时间戳的版的区别就是定时器不会立即执行,而是会等待延时结束再执行,延时结束后再点击,会继续延时后执行
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay);
}
}
}
问题解析
既然节流函数没有问题,那么我们就需要看一下代码里对节流函数的使用是否有问题,节流函数的本质,是通过闭包的方式,返回了一个匿名函数,通过闭包特性在匿名函数内实现对我们真实函数的节流调用。
那么,问题就出现了,我们的节流函数是否生效了呢?
项目采用的是uni-app的框架,写法也和vue语法基本一致,那么在vue内,我们如何进行函数的节流?和正常我们想的不一样
<view @click="throttled"></view>
import throttle from 'xx'
methods: {
throttled () {
throttle(this.click, 1000)
}
}
上面那样的写法能实现节流吗?答案是不能的,因为节流的本质,是 当我们事件触发的时候,实际上是在调用节流函数返回的匿名函数,这里,点击事件依然调用的是我们的普通函数而不是节流函数,导致的结果就是,我们的事件延迟触发了,但是,并没有节流的效果,所以这里需要进行改造
<view @click="click"></view>
import throttle from 'xx'
methods: {
click:throttle(function () {....some code}, 1000)
}
这样,我们才能起到节流的效果,这里注意传入的函数一定不要用箭头函数的写法,因为如果是箭头函数,会绑定执行期上下文,那么this指向的就不是你的vm实例了。
一波二折
移动端的click与tap事件是不同的,区别是tap会立即触发,而click则有300ms的延迟,这里会和事件穿透有关吗?于是我们又加上了防止事件穿透的相关代码
一波三折
虽然改动了节流函数,但是后续还是有客户反馈依然还是偶发重复单据的情况,这里我们不禁思考,会不会是因为微信服务器代理转发导致的呢?小程序发起请求是使用了wx.request方法,于是我在微信开放平台以wx.request()请求服务器重复提交数据 为关键字进行检索,发现了很多同类型的提问,所以,单纯在前端做节流可能已经不能完全解决这个问题了,所以,我们需要对请求做改造,针对同一个请求,让他携带一个trace标识,在request请求实例上维护一个全局的时间戳+业务类型的traceid,在请求拦截器的部分,判断当前请求的traceId是否等于全局的traceId,如果相等,则直接终止请求,同样的,后台也需要在redis和aop内针对我的traceId进行处理。
结语
知之愈明,则行之愈笃,行之愈笃,则知之益明
见本而知末,观指而睹归,执一而应万,握要而治详,谓之术