关于React useEvent Hook RFC的基础知识介绍及实例

234 阅读6分钟

在React中,参照平等是一个重要的概念,它影响到你的应用程序中的组件重新渲染的频率。在这篇文章中,我们将探讨来自React的useEvent Hook,它允许你定义一个带有函数身份的事件处理程序,这个函数身份始终是稳定的,有助于在你的应用程序中管理参考平等。

值得注意的是,在撰写本文时,useEvent Hook还不能使用,目前正在React社区讨论。

JavaScript中的引用平等

在JavaScript中,你可以使用身份运算符来比较两个值是否相等,这也被称为严格平等。例如,在下面的代码中,你正在比较值ab

a === b; // strict equality or indentity operator

其结果是一个布尔值,告诉你值a 是否等于b

对于原始数据类型,比较是用实际值进行的。例如,如果a =10 ,而b =10 ,身份运算符将返回true

像对象和函数一样,复合值是一种参考类型。即使它们有相同的代码,两个函数也是不相等的。看一下下面的例子,身份运算符将返回false

let add = (a, b) => a + b;
let anotherAdd = (a, b) => a + b;
console.log(add === anotherAdd); // false

然而,如果你比较同一个函数的两个实例,它将返回true

let thirdAdd = add;
console.log(add === thridAdd); // true

换句话说,thirdAddadd 在引用上是相等的。

为什么参考性平等在React中很重要?

理解React中的引用平等很重要,因为我们经常用相同的代码创建不同的函数。例如,考虑下面的代码。

function AComponent() {
  // handleEvent is created and destroyed on each re-render
  const handleEvent = () => {
    console.log('Event handler');
  }
  // ...
}

React会销毁当前版本的handleEvent 函数,并在每次AComponent re-renders时创建一个新版本。然而,在某些情况下,这种方法不是很有效,例如:

  • 你使用一个useEffect,在其依赖数组中接受事件处理程序的Hook
  • 你有一个接受事件处理程序的记忆化组件

在这两种情况下,你都想保持一个事件处理程序的单一实例。但是,每次重新渲染时,你都会得到一个新的函数实例,这将进一步影响性能,要么重新渲染一个备忘组件,要么启动useEffect 回调。

你可以通过使用useCallback Hook轻松解决这个问题,如下图所示:

function AComponent() {
  const handleEvent = useCallback(() => {
    console.log('Event handled');
  }, []);
  // ...
}

useCallback Hook对函数进行了备忘,也就是说,每当一个函数被调用时有一个唯一的输入,useCallback Hook就会保存该函数的一个副本。因此,如果在重新渲染的过程中输入没有变化,你会得到相同的函数实例。

但是,当你的事件处理程序依赖于一个状态或道具时,useCallback Hook会在每次变化时创建一个新的处理函数。例如,看一下下面的代码:

function AComponent() {
  const [someState, setSomeState] = useState(0);
  const handleEvent = useCallback(() => {
    console.log('log some state: `, someState);
  }, [someState]);

  // ...
}

现在,每次组件被重新渲染时,该函数将不会被创建。但是,如果someState 发生变化,它将创建一个新的handleEvent 的实例,即使该函数的定义没有变化。

useEvent 钩子

useEvent 钩子试图解决这个问题;你可以使用useEvent 钩子来定义一个事件处理程序,它的函数身份始终是稳定的。换句话说,在每次重新渲染时,事件处理程序的引用都是相同的。基本上,事件处理程序将有以下属性。

  • 该函数不会在每次道具或状态改变时被重新创建
  • 该函数将能够访问道具和状态的最新值。

你会按以下方式使用useEvent Hook:

function AComponent() {
  const [someState, setSomeState] = useState(0);
  const handleEvent = useEvent(() => {
    console.log('log some state: `, someState);
  });
  // ...
}

由于useEvent Hook确保了一个函数只有一个实例,所以你不需要提供任何依赖关系。

实现RFC中的useEvent Hook

下面的例子是对RFC中useEvent Hook的近似实现。

// (!) 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);
  }, []);
}

useEvent 钩子在使用它的组件的每次渲染时被调用。在每次渲染时,handler 函数被传递给useEvent Hook。handler 函数总是具有最新的propsstate 的值,因为当一个组件被渲染时,它基本上是一个新函数。

useEvent Hook里面,useLayoutEffect Hook也在每次渲染时被调用,并将handlerRef 改为handler 函数的最新值。

在真实版本中, 在所有的 useLayoutEffect函数被调用 之前, handlerRef 将被切换到最新的处理函数 。

最后一块是useCallback 的返回。useEvent 钩子返回一个被useCallback 钩子包裹的函数,其依赖数组为空[] 。这就是为什么这个函数总是具有稳定的参考身份。

你可能想知道这个函数怎么总是有propsstate 的新值。如果你仔细看看,用于useCallback Hook的匿名函数使用了handlerRef 的当前值。这个current 的值代表了handler 的最新版本,因为它是在调用useLayoutEffect 的时候切换的。

什么时候不应该使用useEvent 钩子?

在某些情况下,你不应该使用useEvent Hook。让我们了解一下什么时候和为什么。

首先,你不能在渲染过程中使用用useEvent Hook创建的函数。例如,下面的代码会失败。

function AComponent() { 
  const getListOfData = useEvent(() => {
    // do some magic and return some data
    return [1, 2, 3];
  });

  return <ul>
    {getListOfData().map(item => <li>{item}</li>}
  </ul>;
}

解除挂载useEffectuseLayoutEffect

解除挂载的useEffect Hook和useLayoutEffect Hook会有不同版本的useEvent handler。看一下下面的例子。

function Counter() {
  const [counter, setCounter] = React.useState(0);
  const getValue = useEvent(() => {
    return counter;
  });
  React.useLayoutEffect(() => {
    return () => {
      const value = getValue();
      console.log('unmounting layout effect:', value);
    };
  });
  React.useEffect(() => {
    return () => {
      const value = getValue();
      console.log('unmounting effect:', value);
    };
  });
  return (
    <React.Fragment>
      Counter Value: {counter}
      <button onClick={() => setCounter(counter + 1)}>+</button>
    </React.Fragment>
  );
}

如果你运行这个程序,你会看到unmountinguseLayoutEffect 有旧版本的getValue 事件处理程序。请随时查看Stackblitz的例子

结论

尽管useEffect Hook还不能使用,但对于React开发者来说,它绝对是一个有前途的发展。在这篇文章中,我们探讨了useEffect Hook背后的逻辑,回顾了你应该和不应该使用它的场景。

绝对值得关注useEffect Hook,我期待着最终能够将它整合到我的应用程序中。我希望你喜欢这篇文章。编码愉快