借模改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 = nowTimesetPlanList([...planList])}if (!newPlanList.find(item => item.times > 1)) {clearInterval(timer)timer = null}})}const createInterval = () => {if (timer) returntimer = setInterval((func, args) => {func && func(args)}, 1000, doIt)}useEffect(() => {if (timer) {clearInterval(timer)timer = null}if (planList.length > 0) {createInterval()}}, [planList])
妙啊!!!我在办公室都喊出声了!用一个每秒执行一次的 interval , 检查每个任务的最后执行时间,来确定是否要再次执行这个任务。这是什么脑子才能想出如此妙哉方法啊…这又一次印证了大学老师说的最多的一句话—编程最重要的是思想…
此时已经完美实现了这个【多策略自动循环执行】的插件功能,撒花
