关于react.js:解读-React-useEvent-RFC

40次阅读

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

谈谈 React 的新提案:useEvent

2022 年 5 月 5 日,Dan Abramov 在 React RFC 上提交了一个新 hook 的提案:useEvent。其目标是返回一个永远援用不变 (always-stable) 的事件处理函数。

没有 useEvent 时咱们如何写事件函数

首先咱们来看一下这段代码

function Chat() {const  = useState("");

  const onClick = () => {sendMessage(text);
  };

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

为了拜访最新的 state,onClick在每次 Chat 组件产生更新时,都会申明一个新的函数 (援用变动),这会导致SendButton 组件每次都承受一个新的 prop,React 的比拟两个组件节点是否要 diff 前,会对 props 做浅比拟(Object.is),所以每次 props 无意义的变动显然是对 diff 性能不利的。

同时它还会毁坏你的 memo 优化,比方你的 SendButton 做了如下设计:

const SendButton = React.memo(() => {});

这时你可能会想到应用 useMemo 或者 useCallback 来优化父组件的 onClick 函数

function Chat() {const  = useState("");

  const onClick = useCallback(() => {sendMessage(text);
  }, );

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

然而这样当 text 变动时,援用还是会变动,仍然会带来子组件的不必要更新,设计不当甚至会触发子组件 useEffect 的 re-fired。SendButton基本不关怀 text 的变动。而且当函数非常复杂时,可能会漏写依赖(当然你能够通过 eslint 来保障),导致每次应用的都是初始 state,从而造成难以追踪的 bug。

而新的 hook 提案 useEvent,你能够做到这样:

function Chat() {const  = useState("");

  const onClick = useEvent(() => {sendMessage(text);
  });

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

onClick曾经始终是援用不变的了,而且能够拜访到最新的 text。

useEvent 是如何实现的

它看上去如同很神奇,你也能够本人简略实现一个相似的 hook,最外围的中央就是应用 useRef 维持最新援用以及缓存住外层的 function:

const useEvent = (eventHandler) => {const eventHandlerRef = useRef(eventHandler);

  // 每次 useEvent 被调用都返回不变的值,但外部理论执行的是最新的函数
  return useMemo((...args) => {return eventHandlerRef.current(...args);
  }, []);
};

官网给的一个相似实现是这样的:

// (!) 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. 在组件 render 时应用被 useEvent 包裹的函数须要抛出谬误。因为它的设计是为了包裹事件函数,事件函数不应该在 render 时调用。这也是为什么上述代码有 useLayoutEffect,它也保障了每次事件触发时都是最新的,因为视图 / 事件的更新肯定在useLayoutEffect 之后。同时,useEvent 外部批改 state 也是平安的,因为它不会在 render 期间被调用,不会批改组件的 output。
  2. 其实 handlerRef.current 的更新产生在比所有 useLayoutEffect 更提前的时刻,这个保障了当 layout 时,不会存在旧版本的 handler,不会呈现状态割裂的问题
  3. 第 1 处的设计还间接的优化了服务端渲染的平安和性能,因为它不能在 render 时运行,而服务端是不存在事件的,防止了报错。同时,既然 useEvent 对服务端渲染没有意义,那么服务端构建的包里能够跳过 useEvent 的打包,优化了包体积。

你什么时候不应该应用 useEvent

  1. 一般的函数(非事件回调)仍然用原来的 useCallback
function ThemedGrid() {const theme = useContext(ThemeContext);
  const renderItem = useCallback((item) => {
      // Called during rendering, so it's not an event.
      return <Row {...item} theme={theme} />;
    },
    [theme]
  );
  return <Grid renderItem={renderItem} />;
}

因为有 render 期间的报错机制,开发者也不太可能在这种场景下用 useEvent

  1. 不是所有的 useEffect 依赖函数都应该是事件
function Chat({selectedRoom}) {const { createKeys} = useContext(EncryptionSettings);
  // ...
  useEffect(() => {const socket = createSocket("/chat/" + selectedRoom, createKeys());
    // ...
    socket.connect();
    return () => socket.disconnect();
  }, [selectedRoom, createKeys]); // ✅ Re-runs when room or createKeys changes
}

这里的 createKeys 不应该应用 useEvent,因为 effect 中的函数不是事件,也不须要放弃援用不变,因为它须要在 createKeys 变动时从新建设 socket

  1. 可能会导致 useEffect 不再响应式

上面是一个谬误的写法

function Chat({selectedRoom, theme}) {
  // ...
  // 🔴 This should not be an event!
  const createSocket = useEvent(() => {const socket = createSocket("/chat/" + selectedRoom);
    socket.on("connected", async () => {await checkConnection(selectedRoom);
      onConnected(selectedRoom);
    });
    socket.on("message", onMessage);
    socket.connect();
    return () => socket.disconnect();
  });
  useEffect(() => {return createSocket();
  }, []);
}

要晓得一点的是,useEvent 是非响应式的。因为它是事件,最终会被动调用,并不需要随着状态变动而立刻响应。所以当 selectedRoom 变动时,effect 不再从新建设 socket 了,只管 createSocket 始终能够拿到最新的selectedRoom,但它须要的是被动触发。

正确的写法应该是应用 useCallback 且依赖 selectedRoomuseEffect 依赖useCallback

useEvent 的『毛病』是什么

  1. 毫无疑问它减少了 hooks 的概念,带来了更多的心智累赘,你须要判断这里该不该用 useEvent,还是用 useCallback
  2. 因为须要一个比 layoutEffect 更提前的期间,它不可避免的须要改变 fiber tree commit 阶段的逻辑。然而相比于让社区在第三方库中自行提供各自的不完满的解决方案,这种付出还是值得的。
  3. 它的体现仿佛超出了单纯的 event 边界,更应该叫 useStableCallback 或者 useCommittedCallback,官网给它取useEvent 这一名字,是为了帮忙开发者们更容易建设『它应该被用于事件』这一心智模式。
  4. 它有一些非凡的边界条件下会呈现问题,不过这次要是因为代码编写有问题带来的,并不是它本身的问题。但正因为人是最难管制的,所以这种问题也是最难阻止的,开发者应该更留神本人的书写标准:

比方 useEvent 外面有异步逻辑

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 前后输入值是一样的,因为 await 前面的回调保留了 count 闭包。count 仅仅是本次 render 的状态快照,所以函数内异步期待时,即使内部又把 count 改了,以后这次函数调用还是拿不到最新的 count,而 ref 办法是能够的。所以事件中尽量不要有异步。

另外还有『条件判断式的 event』,比方你写出了这样的代码onSomething={cond ? handler1 : handler2},天然是没方法帮你放弃援用不变的。

此外在 react 更新中也会有『割裂』问题,unmounting layout effects 时应用的是上一次 render 时的 event,然而 非 layout effect 卸载时应用的是新版本的 event(下一次更新时的 event 可能发生变化了)。这就相似于在 unmounting layout 和 non-layout effects 期间读 ref 后果不统一的状况。

集体对 useEvent 的认识

useEvent 次要作用是维持援用不变的事件,能够用非常简洁的代码缩小援用变动带来的问题。然而它自身也带来了更多的概念。正如下面的毛病里写的,你须要时刻留神那些问题。而且目前官网也仍然有一些待解决的问题 https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#unresolved-questions。总之对于这个 RFC 集体并没有太多欣慰,未来有则用,毕竟是官网给出的最佳实际,没有也能够有其余解决办法。

正文完
 0