借模改df开源插件的机会,第一次真正使用hook开发,目的是实现添加多策略自动执行. 此插件涉及的hook有useState, useEffect, useCallback
插件内容
一、首先,整理对比现有插件功能,第一个版本要模改的插件是crawl planets, 第二个版本参考的是range attack。
先看第一个版本:
crawl planets提供的功能是根据一些条件,筛选出合适的来源星球和目标星球进行单次攻击。第一版是直接在crawl planets上做了修改,实现多策略循环自动攻击。实现过程就不赘述,我们来看下修改后的插件样子:
这个插件使用的是原生js实现,缺点很明显—代码太多,样式丑陋。
接下来我们看下用hook实现的开源插件—range attack:
缺陷:
- Planet Type 不能多选
- Planet 过滤条件不全
- 没有多策略
- 没有定时循环执行策略
优点:
- Source Planet 和 Target Planet 分开,清晰明了
- 交互友好
- ui比较美观
二、 综合以上信息,我整理了一份脑图,可当做 To Do List 使用
三、根据脑图中的内容,最后开发完的插件是这样的:
添加与优化的内容有:
- 把原来 crawl planets 插件中的 Planet 筛选条件迁移过来
- 下拉框单选改为按钮多选
- 添加 Plans Information 模块(添加策略)
- Executions Times (执行次数) 和 Executions Interval (执行周期) 从 input range 改为按钮点击和可输入的形式
- 添加的策略展示在界面上,并可点击X来停止并删除该策略
本次模改df插件学到的hook知识
一、 useState
二、 useEffect
三、 useCallback
遇到的难点
难点1: useState更新问题
useState执行之后不能马上拿到更新后的值。
按照class component写法,setState第二个参数的函数中可以拿到最新的state值以供使用。
查阅hook各种文档后发现,useState之后一般使用useEffet来拿到最新的state值,当然useEffect第二个数组参数要写上这个state,来表示该state改变时,触发这个useEffect,以便执行一些操作。
难点2: 多个计时器问题
由于 plans list 是维护在列表里的,那刚开始按照我的编程思路,可以把计时器对象分别保存在数组的每个 plan 对象来维护,这样不会互相影响。代码如下:
const [planList, setPlanList] = useState(''); // 策略列表
// 添加策略
const onAddPlan = () => {
let arr = [...planList]
arr.push({
name: 'xxx',
times: 10, // 执行次数
interval: 10, // 执行间隔(秒)
timer: null // 计时器对象
})
setPlanList([...arr])
}
useEffect (()=>{
let newPlanList = [...planList]
newPlanList.forEach((planItem, planIndex) => {
if (planItem.times > 1) {
planItem.timer = setTimeout(() => {
planItem.times --
setPlanList([...newPlanList])
}, Number(planItem.executionsInterval) * 1000)
} else {
clearTimeout(planItem.timer)
newPlanList.splice(planIndex, 1)
setPlanList([...newPlanList])
}
})
}, [planList])
此时,单个策略可以正常运行,过程如下:
定时器更新 => 更新planList => useEffect执行 => 定时器更新 => 更新planList => useEffect执行…
但当我添加第二个策略时,此时 planList 长度为2,也就是有两个定时器了,这时候运行过程其实和上面一样,只不过更新 planList => useEffect 执行=> 导致 planList 所有的定时器都重新执行了,甚至执行了 N 次——>定时器已经乱套了….
是的,我犯了低级错误,每次定时任务执行前,应该先清空之前的定时器,此时 useEffect 代码如下:
useEffect (()=>{
let newPlanList = [...planList]
newPlanList.forEach((planItem, planIndex) => {
clearTimeout(planItem.timer) // 新增代码行
if (planItem.times > 1) {
// ...
} else {
// ...
}
})
}, [planList])
这时候解决了定时器执行N次的问题。但还存在一个问题,我每增加一个定时任务,所有的定时器都被初始化了。
真是令人头大且脱发…
我甚至在想,只要定时任务抽成一个组件,每个定时组件内部去控制自己的定时任务,就可以完美解决这个问题了,但我不打算自掏腰包去部署一个cdn来支持我在单文件js引入一个组件来使用。
于是我请教了hook大神—hook怎么管理多个定时任务?大神给出了思路:只需维护一个interval就可以解决我这个问题,我震惊了,并仔细阅读了大神的代码
const doIt = (args) => {
let newPlanList = [...planList]
newPlanList.forEach((planItem, planIndex) => {
const nowTime = new Date().getTime()
if (planItem.times > 1 && Math.floor(Math.abs(planItem.lastModifiedTime - nowTime) / 1000) == planItem.interval) {
planItem.times--
planItem.lastModifiedTime = nowTime
setPlanList([...planList])
}
if (!newPlanList.find(item => item.times > 1)) {
clearInterval(timer)
timer = null
}
})
}
const createInterval = () => {
if (timer) return
timer = setInterval((func, args) => {
func && func(args)
}, 1000, doIt)
}
useEffect(() => {
if (timer) {
clearInterval(timer)
timer = null
}
if (planList.length > 0) {
createInterval()
}
}, [planList])
妙啊!!!我在办公室都喊出声了!用一个每秒执行一次的 interval , 检查每个任务的最后执行时间,来确定是否要再次执行这个任务。这是什么脑子才能想出如此妙哉方法啊…这又一次印证了大学老师说的最多的一句话—编程最重要的是思想…
此时已经完美实现了这个【多策略自动循环执行】的插件功能,撒花