laclys / Front-End_practice

Daily practice
5 stars 0 forks source link

react hook开发的一些纠结点 #152

Open laclys opened 3 years ago

laclys commented 3 years ago

官方eslint

官方eslint配置之后,副作用函数有props或者state变量未存在于依赖数组中,会有 exhaustive-deps规则的提醒。 然而一些场景下又与这些规则不符。 比如时间类函数类似componentDidMount的功能一般会这么写:

useEffect(() => {
// ....
    console.log('var1', var1)
    console.log('var12, var2)
}, [])

假设var1& var2 是我们通过 useState或者props传进来的变量。再或者,里面有函数执行,比如一些请求异步函数。然而这里我们没有在依赖数组中加入这些变量(deps)。就触发了 exhaustive-deps规则的提醒。黄色的小warning,大可不必理会。(自动lint --fix一股脑把依赖添加到依赖数组里有时会出错)。强迫症出于对eslint的规则遵守。给出的解决方案就是 通过useRef来回避这个问题。对副作用函数中的每个变量都创建对应的useRef值。这样怎么说又麻烦有不合理。比如我们封装一个useMount

function useMount(fn) {
  const fnRef = useRef()
  fnRef.current = fn;
  useEffect(() => {
    fnRef.current();
  }, [fn]);
}

warning解除

再举个栗子

function Modal({visible,  value}) {
   useEffect(() => {
     visible && console.log('value', value)
   }, [visible])

}

这样一个简单的例子。依旧触发了就触发了 exhaustive-deps规则的提醒,有黄线。 但确确实实有这样的需求和问题。例子中Modal组件需要根据visible变量变化执行 ·”xxxxxx“一系列操作。操作中会引用其他变量比如这里的value。但是我们确实不希望value加在deps中,因为我们并不关心它的变化。(需要引用到其它的 props 或 state 变量)如果把所有变量都加入deps中带来的副作用可能会造成重复执行,这是非预期的也是没必要的事情(为了解决eslint小黄线而重复执行一次。这里 我们又迫于无奈需要一个辅助变量来记录 visible 变量的前一状态值,用来在副作用函数中判断是否因为 visible 变量变动触发的函数执行。我们又要用useRef封装一个前值函数。

const usePrevious = (value) => {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}
function Modal({visible,  value}) {
  const prev = usePrevious(visible)
   useEffect(() => {
     prev !==visible &&  visible && console.log('value', value)
   }, [visible, prev, value])

}

希望 useEffect 的依赖数组中是与副作用函数更有效的变量,而不是副作用函数中全部引用的变量。

再说useCallback

在 类组件中,传递给子组件的props变量中函数大多数都是this.xxx形式,即引用都是稳定的。useCallback 函数,只要依赖数组保持稳定,会返回一个引用稳定的函数。useCallback在项目中使用有时候会耗尽心思,一单一单 组件树上层一个不小心出了问题,有是前功尽弃 比如

function Button({ child, disabled, onClick }) {
  const handleBtnClick = useCallback(() => {
  !disabled && onClick?.()
  }, [disabled, onClick]);

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}

function App() {
  const onBtnClick = () => {}
  return (
    <Button onClick={onBtnClick} />
  )
}

这么一个App组件包裹着Button组件。我们希望disabled 和 onClick 变量的引用变动时才返回一个新的引用的函数。但是 APP组件这一层 没有用useCallback包裹onBtnClick保持函数稳定,每次传递给Button的都是新函数(或者说全新的函数引用)子组件接收触发deps,生成一个全新引用的 handleBtnClick 函数。怎么办,这里我们再在子组件Button中用useRef解决问题

function Button({ child, disabled, onClick }) {
  const handleBtnClickRef = useRef()
  handleBtnClickRef.current = () => {
    !disabled && onClick?.()
  };

  const handleBtnClick = useCallback(() => {
    handleBtnClickRef.current()
  }, [handleBtnClickRef])

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}

我们用useRef- handleBtnClickRef保存最新的函数。变量引用是固定的,所以 handleBtnClick 函数的引用也是固定的。触发 onClick 回调函数也能拿到最新的 disabled 和 onClick 值

这里再举一个例子:需要得到一个不变的函数引用,但这个不变的函数执行的时候,执行的是传递的最新函数。这个实在wukong这各项目里用的。直接复制过来

export function useCallbackRef(fn) {
  const fnRef = useRef(fn)
  fnRef.current = fn

  return useCallback(((...args) => fnRef.current(...args)), [])
}

useRef

这里useRef成为了解决问题的关键。很像class里面的this.xxxmutable的方式去解决问题。副作用、依赖数组真的增加了很多心智负担

laclys commented 3 years ago

这样写法 也很丑。感觉hooks就是从一个坑跳入了一个更大的坑

laclys commented 3 years ago

note: https://zh-hans.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback

laclys commented 3 years ago

note: useDelayState

// useStateRef 消除依赖
function useStateRef<T>(val: T) {
  const valRef = useRef(val);
  useEffect(() => {
    valRef.current = val;
  }, [val]);
  return valRef;
}

// useEndRef 检测销毁
function useEndRef() {
  const endRef = useRef(false);
  useEffect(() => {
    return () => {
      endRef.current = true;
    };
  }, []);
  return endRef;
}

function useDelayState<T>(val: T, n = 100, ignore = false) {
  const nRef = useStateRef(n);
  const ignoreRef = useStateRef(ignore);
  const endRef = useEndRef();
  const [delayVal, setDelayVal] = useState<T | undefined>();
  const handleTimeout = useCallback(() => {
    if (endRef.current) return;
    setDelayVal(val);
  }, [val, endRef]);
  useEffect(() => {
    const timer = setTimeout(handleTimeout, nRef.current);
    const ifIgnore = ignoreRef.current;
    return () => {
      if (ifIgnore) clearTimeout(timer);
    };
  }, [handleTimeout, nRef, ignoreRef]);
  return delayVal;
}
laclys commented 3 years ago

所有函数式组件,服务中的视图元素,只要不是纯组件或者纯节点,原则上必须全部加 useMemo

Vue props 自带检查,v-memo 也是自动的。要用好React 心智负担确实比vue大很多