关于前端:精读React-useEvent-RFC

50次阅读

共计 3358 个字符,预计需要花费 9 分钟才能阅读完成。

useEvent 要解决一个问题:如何同时放弃函数援用不变与拜访到最新状态。

本周咱们联合 RFC 原文与解读文章 What the useEvent React hook is (and isn’t) 一起理解下这个提案。

借用提案里的代码,一下就能说分明 useEvent 是个什么货色:

function Chat() {const [text, setText] = useState('');

  // ✅ Always the same function (even if `text` changes)
  const onClick = useEvent(() => {sendMessage(text);
  });

  return <SendButton onClick={onClick} />;
}

onClick 既放弃援用不变,又能在每次触发时拜访到最新的 text 值。

为什么要提供这个函数,它解决了什么问题,在概述里缓缓道来。

概述

定义一个拜访到最新 state 的函数不是什么难事:

function App() {const [count, setCount] = useState(0)

  const sayCount = () => {console.log(count)
  }

  return <Child onClick={sayCount} />
}

sayCount 函数援用每次都会变动,这会间接毁坏 Child 组件 memo 成果,甚至会引发其更重大的连锁反应(Child 组件将 onClick 回调用在 useEffect 里时)。

想要保障 sayCount 援用不变,咱们就须要用 useCallback 包裹:

function App() {const [count, setCount] = useState(0)

  const sayCount = useCallback(() => {console.log(count)
  }, [count])

  return <Child onClick={sayCount} />
}

但即便如此,咱们仅能保障在 count 不变时,sayCount 援用不变。如果想放弃 sayCount 援用稳固,就要把依赖 [count] 移除,这会导致拜访到的 count 总是初始值,逻辑上引发了更大问题。

一种无奈的方法是,保护一个 countRef,使其值与 count 放弃同步,在 sayCount 中拜访 countRef

function App() {const [count, setCount] = useState(0)
  const countRef = React.useRef()
  countRef.current = count

  const sayCount = useCallback(() => {console.log(countRef.current)
  }, [])

  return <Child onClick={sayCount} />
}

这种代码能解决问题,但相对不举荐,起因有二:

  1. 每个值都要加一个配套 Ref,十分冗余。
  2. 在函数内间接同步更新 ref 不是一个好主见,但写在 useEffect 里又太麻烦。

另一种方法就是借鉴 hook,如 useStableCallback,这实质上就是这次提案的配角 – useEvent

function App() {const [count, setCount] = useState(0)

  const sayCount = useEvent(() => {console.log(count)
  })

  return <Child onClick={sayCount} />
}

所以 useEvent 的外部实现很可能相似于自定义 hook useStableCallback。在提案内也给出了可能的实现思路:

// (!) Approximate behavior
function useEvent(handler) {const handlerRef = useRef(null);

  // In a real implementation, this would run before layout effects
  useLayoutEffect(() => {handlerRef.current = handler;});

  return useCallback((...args) => {
    // In a real implementation, this would throw if called during render
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

其实很好了解,咱们将需要一分为二看:

  1. 既然要返回一个稳固援用,那最初返回的函数肯定应用 useCallback 并将依赖数组置为 []
  2. 又要在函数执行时拜访到最新值,那么每次都要拿最新函数来执行,所以在 Hook 里应用 Ref 存储每次接管到的最新函数援用,在执行函数时,实际上执行的是最新的函数援用。

留神两段正文,第一个是 useLayoutEffect 局部实际上要比 layoutEffect 执行机会更提前,这是为了保障函数在一个事件循环中被间接生产时,可能拜访到旧的 Ref 值;第二个是在渲染时被调用时要抛出异样,这是为了防止 useEvent 函数被渲染时应用,因为这样就无奈数据驱动了。

精读

其实 useEvent 概念和实现都很简略,上面咱们聊聊提案里一些有意思的细节吧。

为什么命名为 useEvent

提案里提到,如果不思考名称长短,齐全用性能来命名的话,useStableCallbackuseCommittedCallback 会更加适合,都示意拿到一个稳固的回调函数。但 useEvent 是从使用者角度来命名的,即其生成的函数个别都被用于组件的回调函数,而这些回调函数个别都有“事件个性”,比方 onClickonScroll,所以当开发者看到 useEvent 时,能够下意识揭示本人在写一个事件回调,还算比拟直观。(当然我感觉次要起因还是为了缩短名称,好记)

值并不是真正意义上的实时

尽管 useEvent 能够拿到最新值,但和 useCallbackref 还是有区别的,这个差别体现在:

function App() {const [count, setCount] = useState(0)

  const sayCount = useEvent(async () => {console.log(count)
    await wait(1000)
    console.log(count)
  })

  return <Child onClick={sayCount} />
}

await 前后输入值肯定是一样的,在实现上,count 值仅是调用时的快照,所以函数内异步期待时,即使内部又把 count 改了,以后这次函数调用还是拿不到最新的 count,而 ref 办法是能够的。在了解上,为了防止夜长梦多,回调函数尽量不要写成异步的。

useEvent 也救不了手残

如果你保持写出 onSomething={cond ? handler1 : handler2} 这样的代码,那么 cond 变动后,传下去的函数援用也肯定会变动,这是 useEvent 无论如何也防止不了的,兴许拯救计划是 Lint and throw error。

其实将 cond ? handler1 : handler2 作为一个整体包裹在 useEvent 就能解决援用变动的问题,但除了 Lint,没有人能避免你绕过它。

能够用自定义 hook 代替 useEvent 实现吗?

不能。尽管提案里给了一个近似解决方案,但实际上存在两个问题:

  1. 在赋值 ref 时,useLayoutEffect 机会仍然不够提前,如果值变动后了解拜访函数,拿到的会是旧值。
  2. 生成的函数被用在渲染并不会给出谬误提醒。

总结

useEvent 显然又给 React 减少了一个官网概念,在严严实实减少了了解老本的同时,也补齐了 React Hooks 在实践中缺失的重要一环,无论你喜不喜欢,问题就在那,解法也给了,挺好。

探讨地址是:精读《React useEvent RFC》· Issue #415 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

正文完
 0