网站首页 > 基础教程 正文
大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
最近在写一个基于大模型的聊天页面,遇到的第一个问题就是当有新消息的时候如何自动滚动页面,从而展示最新的聊天消息。于是深入了解了在 React 中如何优雅操作 DOM ,最终也认识了 react-dom 提供的 flushSync 方法。
1. 事件处理程序内的滚动逻辑非同步执行
如果只是将滚动逻辑放入事件处理程序中,可能和预取的效果不符。
const onAdd = (newTask) => {
setTodos([...todos, { id: uuid(), task: newTask }]);
// 不会立即执行
listRef.current.scrollTop = listRef.current.scrollHeight;
};
这是因为 setTodos 并非同步,所以页面会先滚动然后 todos 才会更新。因此,视图中显示的始终不是最后一个 todos 而是倒数第二个。
2. 使用 useEffect Hooks
useEffect 是一个 React Hook,其根据 React state 控制非 React 组件,例如:设置服务器连接或在组件出现在屏幕上时发送分析日志等等。
为了自动滚动到最后一条记录,开发者可以使用 useEffect Hooks,比如下面的示例:
useEffect(() => {
// 副作用
listRef.current.scrollTop = listRef.current.scrollHeight;
}, [todos]);
或者通过下面的方法实现:
useEffect(() => {
const lastTodo = listRef.current.lastElementChild;
lastTodo.scrollIntoView();
}, [todos]);
上述两种滚动逻辑都能工作正常,然而 useEffect Hooks(或者下面的 useLayoutEffect)有时候并非最优解。主要包括以下原因:
- 尝试的 DOM 操作,例如:滚动,是一种副作用,即渲染期间不会发生。而且在 React 中,副作用通常发生在事件处理程序内部,所以将其放在 onAdd 方法中更加合理
- 按照官方文档声明,当尝试所有其他选项仍未找到正确的事件处理程序时才考虑 useEffect
3. 使用 useLayoutEffect Hooks
在 React 中元素测量还可以使用 useLayoutEffect 这个 Hooks,其在浏览器重新绘制(repaint)屏幕之前测量布局,防止用户看到 tooltip 等类似内容移动。该 Hooks 在 ToolTip 等场景非常重要,因为该场景需要分两遍进行渲染:
- 在任何地方渲染 tooltip 提示,即使位置错误
- 测量其高度并决定 tooltip 放置位置
- 在正确的位置再次渲染 tooltip 提示
下面示例代码也能够实现自动滚动到最后一条元素:
useLayoutEffect(() => {
console.log('scrollRef', scrollRef);
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [messages]);
4. 使用回调引用 (Callback ref) 测量元素位置和大小
测量 DOM 节点位置或大小的一种基本方法是使用回调引用,回调函数会在组件挂载 (mounted)、重新渲染 (re-rendered) 或卸载 (unmounted) 后运行。同时,每当 ref 附加到不同的节点时,React 也会调用该回调。
同时,使用回调引用可确保即使子组件稍后显示测量的节点,例如:响应点击,开发者仍然会在父组件中收到有关通知并更新测量值。
下面示例是实现滚动到最后一条元素的代码:
let scrollRef = useCallback(node => {
if (node !== null) {
node.scrollTop = node.scrollHeight;
}
}, [messages]);
<Chat messages={messages} ref={scrollRef} />
const Chat = forwardRef((props, ref) => {})
以上示例没有使用 useRef,因为对象 ref 不会通知当前 ref 值的更改。 使用回调引用可确保即使子组件稍后显示测量的节点,例如:正在响应用户点击时仍会在父组件中收到通知并更新测量值。
通常,这种回调引用用在 React 中动态添加或删除元素的地方。
5. 使用 flushSync
开发者还可以使用 flushSync,其会强制 React 同步刷新提供的回调中的任何更新,从而确保 DOM 立即生效。
但是,开发者需要从 react-dom 导入相应的方法,然后将 setTodos 调用包裹在 flushSync 处理程序中:
import {flushSync} from "react-dom";
const onAdd = (newTask) => {
flushSync(() => {
setTodos([...todos, { id: uuid(), task: newTask }]);
});
listRef.current.scrollTop = listRef.current.scrollHeight;
};
flushSync 可以确保状态更新同步发生,并且滚动逻辑仅在状态更新后执行。
注意:使用 flushSync 并不常见,经常使用会严重损害应用程序的性能。如果应用程序仅使用 React API,并且不与第三方库集成(例如:浏览器 API 或 UI 库),则不需要 flushSync
最后值得一提的是,开发者还可以使用 useImperativeHandle 这个 React Hook,其可让开发者自定义作为 ref 公开的句柄,从而避免暴露整个DOM:
const Chat = forwardRef((props, ref) => {
const scrollRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollIntoView() {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
},
};
}, []);
}
参考资料
https://dev.to/somshekhar/have-you-used-flushsync-in-react-4cpo
https://react.dev/learn/keeping-components-pure
https://react.dev/reference/react-dom/flushSync
https://dev.to/gilfink/quick-tip-using-callback-refs-in-react-4gef
https://react.dev/learn/synchronizing-with-effects
https://www.linkedin.cn/
https://react.dev/reference/react/useImperativeHandle
- 上一篇: React 最常用的函数(备忘查询)
- 下一篇: 「前端进阶」React系列九 - 受控非受控组件
猜你喜欢
- 2024-11-24 React源码分析与实现(一):组件的初始化与渲染「实践篇」
- 2024-11-24 React 最简单的入门应用项目
- 2024-11-24 「干货」深入浅出React组件逻辑复用的那些事儿
- 2024-11-24 「干货满满」React Hooks 最佳实践
- 2024-11-24 React开发必须知道的34个技巧
- 2024-11-24 React组件应该如何封装?
- 2024-11-24 React.js前端框架初学技术总结
- 2024-11-24 前端架构师成长之路:5 分分钟搞懂面试官必问 React 题
- 2024-11-24 只会Vue的我,用两天学会了react,这个方法您也可以
- 2024-11-24 react高质量笔记_9(Diffing算法)
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)