前阵子因为需求改动,把之前写过的可配置抽奖转盘的抽奖判断逻辑重写了一遍,还解决了一个兼容性问题,简单记录下。
目录
一、setTimeout制作动画在Safari中掉帧
二、用状态机(伪)管理不同抽奖逻辑
一、setTimeout制作动画在Safari中掉帧
代码的实现逻辑是使用canvas根据奖品数量动态绘制转盘角度生成静止的一帧,然后使用定时器修改绘制的起始角度,每隔一段时间重绘一张转盘实现动画效果。
问题出在最开始是使用setTimeout实现这个定时绘制的功能,但是转动效果在苹果手机上非常糟糕,掉帧极其严重。在微信开发者工具上也复现不出问题,就猜测是Safari的兼容性问题,发现setTimeout和setInterval在Safari上的兼容性不大好。在同事的提示下换成了requestAnimationFrame,动画就流畅了很多。
至于原因懒得再总结了,我觉得这个人讲的很清楚。requestAnimationFram和setTimeout执行的先后
- requestAnimationFrame 执行步伐跟着系统的绘制频率走,就是说屏幕分辨率 和 屏幕尺寸会影响requestAnimationFrame的回调函数执行时间。
- setTimeout 执行只是在内存中通过设置一个间隔时间来运行代码,HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。同时setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以 setTimeout 的实际执行时机一般要比其设定的时间晚一些。
两者执行的快慢影响因素:
- requestAnimationFrame受系统的绘制频率影响,即屏幕分辨率 和 屏幕尺寸
- setTimeout 受任务队列和页面渲染有关
比较麻烦的点在于动画逻辑需要修改,大概修改如下。
使用setTimeout的大概写法:
1 | const interval = 10; |
使用requestAnimationFrame的大概写法:
1 | const interval = 10; |
二、用状态机(伪)管理不同抽奖逻辑
因为这是个可配置的抽奖活动,导致灵活度非常高。点击抽奖按钮之后需要做的条件判断非常之多,而且因为可配置的原因也非常复杂.再加上需求的变化,原本分散在不同组件中的判断逻辑需要整合到一个按钮上,于是就干脆重写了判断逻辑。
比如点击抽奖按钮之后,需要查看用户的抽奖次数、信息填写情况、配置填写用户信息的时机、活动时间等等等。
原先的做法是在不同组件内进行判断,并将不同判断结果发到一个中心化的数据中心管理,再在不同需要判断的地方调用这些结果进行一层一层判断。这样写的麻烦之处在于有很多条件的判断是耦合的,不是很好拆,而且如果需求一改动,在耦合的判断逻辑下(而且还是不同组件),改起来很恶心。虽然主要原因还是我原先代码写的太烂了。
后来想起来隔壁后端同事之前在搞可配置的发布流程管理,这一层层的条件逻辑判断以及状态改变达到不同页面(对抽奖而言就是不同提示),似乎有点眼熟。记得之前听到测试妹子说是用有限状态机写的,于是走投无路(并没有)的我研究了起状态机。
鉴于我很懒,只摘录了我觉得比较有用的概念。
- 第一个是 State ,状态。一个状态机至少要包含两个状态。例如上面自动门的例子,有 open 和 closed 两个状态。
- 第二个是 Event ,事件。事件就是执行某个操作的触发条件或者口令。对于自动门,“按下开门按钮”就是一个事件。
- 第三个是 Action ,动作。事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。编程的时候,一个 Action 一般就对应一个函数。
- 第四个是 Transition ,变换。也就是从一个状态变化为另一个状态。例如“开门过程”就是一个变换。
比如抽奖流程:点击开始抽奖=>验证身份=>验证活动时间=>验证抽奖剩余次数=>抽奖
上面这个是我随便写的,大概意思是一个抽奖行为是有流程的,虽然只是一个点击按钮其实是有非常多状态的改变的。然后在不同状态下会有不同需要触发的行为,并且它还要改变到下一个状态。
当使用这套逻辑时,整个流程的判断就可以都放到一起,代码会好理解很多,改起来也没那么恶心了。
为啥说伪呢,因为下面这个是一个简陋实现版乞丐版:
1 | const [lotteryState, setLotteryState] = useState<string | undefined>(); |