React hook奇淫技巧-模拟同步函数

678 阅读3分钟

最近在项目中做多情况下的触发接口请求(按钮触发,实时触发等)的最佳代码实践整理时,发现为了处理 清空条件->重新搜索 这个情况,会增加很多代码,让可读性变得很差。那是否有什么办法能对其进行优化呢?

先看问题代码

来一段最常见的列表请求展示代码

import React, { useState, useEffect } form 'react'

const example = () => {
  const [ search, setSearch ] = useState('')
  const [ list, setList ] = useState([])
  useEffect(() => {
    getList()
  }, [])
  const getList = async () => {
    const data = await ajax({
      search,
    }) // 请求方法
    setList(data)
  }
  cosnt clear = () => {
    setSearch('')
    getList()
  }
  return (
    <div>
      <input value={search} (e) => setSearch(e.target.value) />
      <button onClick="getList">搜索</button>
      <button onClick="clear">清空</button>
    </div>
  )
}

这里我们可以看到在input内输入内容后点击搜索,请求的方法内的获取的search的参数是最新的。

这时我需要在页面上加个清空条件重新搜索的功能,逻辑就是清空搜索条件setSearch('')然后调用请求方法getList()

我们知道因为setSearch是异步的且React hook的闭包问题不能直接写

cosnt clear = () => {
  setSearch('')
  getList()
}
return (
  <div>
    ...
    <button onClick="clear">清空</button>
  </div>
)

这时请求里的search还是原来的数据。

解决

遇上上述问题,我们当然可以通过useRef先对搜索条件的字段进行储存,每次更新state是同时修改useRef的值,但这肯定会增加代码量,降低代码的可读性。

或者只要能解决异步和闭包问题。

hook中如何判断state已修改, 且解决闭包问题

通过useEffect我们可以知道state的变化

const ready = useRef(false) // 用来标记第一次不执行
/** search更新后会重新渲染,getList方法会重新生成个实例,新的实例内的state将会是最新 */
useEffect(() => {
  if (ready.current) {
    getList()
  } else {
  	ready.current = true
  }
}, [search])

上面的方法就能在setSearch('')然后search更新调用请求方法getList()

然而如果这样写,会有两个问题。

1、会有其地方使用setSearch但并不希望重新getList 2、setSearch('')getList()代码编写的位置分离,难以阅读。

模拟同步

React中一般条件下(async/await、Promise除外)多个setState放到了一个队列里面,等待事件结束在执行。

所以在一个方法内我们就写多个setState,然后设置一个state作为useEffect绑定监听参数。

这里我们定义个flag作为监听参数

const ready = useRef(false)
const [flag, setFlag] = useState([]); // 设置[],之后再setFlag([]),由于数组的对应指针不同会被判断成新值触发重新渲染。
useEffect(() => {
  if (ready.current) {
    getList()
  } else {
  	ready.current = true
  }
}, [flag])

这样我们就解决上上面的问题1。

接下来的问题2就好解决,我们封装个hook,入参为要执行的方法。

看代码

/**
* @param callbacks:一个返回值为'函数数组'的函数 (...args) => [function]
*/
const useQueueFn = (callbacks) => {
  const ready = useRef(false)
  const params = useRef([]) // 记录参数
  const index = useRef(0); // 执行标记位置
  const [flag, setFlag] = useState([]);
  useEffect(() => {
    if (ready.current) {
      execute()
    } else {
      ready.current = true
    }
  }, [flag]);
  const init = useCallback(
    (...args) => {
      params.current = args;
      index.current = 0;
      execute();
    },
    [],
  );
  const execute = () => {
    const fnArr = callbacks(...params.current); // 每次调用重新获取参数实例
    fnArr[index.current]();
    if (index.current < fnArr.length - 1) {
      index.current++;
      setFlag([]);
    }
  };
  return init;
}

useQueueFn使用

const clear = useQueueFn(() => ([
  () => {
    setSearch('');
    ... // 其他setState
  },
  () => {
    getList();
  },
]));
return (
  <div>
    ...
    <button onClick="clear">清空</button>
  </div>
)

这样就完美了解决了上面的问题。