可以在没有持续回流的情况下动态调整高度的文本区域吗?
Possible to have a dynamically height adjusted textarea without constant reflows?
注意: 据我所知,这不是重复的,因为使用 contentEditable
div 似乎不是一个好主意选择。它有很多问题(没有占位符文本,需要使用 dangerouslySetInnerHTML
hack 来更新文本,选择光标很挑剔,其他浏览器问题等)我想使用文本区域。
我目前正在为我的 React textarea 组件做这件事:
componentDidUpdate() {
let target = this.textBoxRef.current;
target.style.height = 'inherit';
target.style.height = `${target.scrollHeight + 1}px`;
}
这有效并允许文本区域在添加和删除换行符时动态增大和缩小高度。
问题是每次文本更改时都会发生重排。这会导致应用程序出现大量滞后。如果我在文本区域中按住一个键,则会在附加字符时出现延迟和滞后。
如果我删除 target.style.height = 'inherit';
行,延迟就会消失,所以我知道这是由这种持续回流造成的。
我听说设置 overflow-y: hidden
可能会消除不断的回流,但在我的情况下却没有。同样,设置 target.style.height = 'auto';
不允许动态调整大小。
我目前已经开发出 a 解决方案,但我不喜欢它,因为它是每次文本更改时的 O(n) 操作。我只是计算换行符的数量并相应地设置大小,如下所示:
// In a React Component
handleMessageChange = e => {
let breakCount = e.target.value.split("\n").length - 1;
this.setState({ breakCount: breakCount });
}
render() {
let style = { height: (41 + (this.state.breakCount * 21)) + "px" };
return (
<textarea onChange={this.handleMessageChange} style={style}></textarea>
);
}
我个人无法想象阅读所有这些换行符是一个太大的问题,除非你写小说,但我不知道。您可以尝试根据击键调整中断次数。
沙盒here.
import React, { Component } from "react";
class TextBox extends Component {
state = {
breakCount: 0
};
handleKeyDown = e => {
if (e.key === "Enter") {
this.setState({ breakCount: this.state.breakCount + 1 });
}
// Note you will want something to better detect if a newline is being deleted. Could do this with more logic
// For quick testing of spamming enter/backspace key though this works.
if (e.key === "Backspace" && this.state.breakCount > 0) {
this.setState({ breakCount: this.state.breakCount - 1 });
}
};
render() {
const style = { height: 41 + this.state.breakCount * 21 + "px" };
return <textarea onKeyDown={this.handleKeyDown} style={style} />;
}
}
export default TextBox;
注意: 更好。
原文 Post: 我对 apachuilo 的代码进行了大量修改以达到预期的效果。它根据 textarea
的 scrollHeight
调整高度。当框中的文本更改时,它将框的行数设置为 minRows
的值并测量 scrollHeight
。然后,它计算文本的行数并更改 textarea
的 rows
属性以匹配行数。盒子在计算的时候没有"flash"
render()
只调用一次,只改变rows
属性。
当我输入 1000000(一百万)行每行至少 1 个字符时,添加一个字符大约需要 500 毫秒。在 Chrome 77.
中进行了测试
代码沙箱:https://codesandbox.io/s/great-cherry-x1zrz
import React, { Component } from "react";
class TextBox extends Component {
textLineHeight = 19;
minRows = 3;
style = {
minHeight: this.textLineHeight * this.minRows + "px",
resize: "none",
lineHeight: this.textLineHeight + "px",
overflow: "hidden"
};
update = e => {
e.target.rows = 0;
e.target.rows = ~~(e.target.scrollHeight / this.textLineHeight);
};
render() {
return (
<textarea rows={this.minRows} onChange={this.update} style={this.style} />
);
}
}
export default TextBox;
我认为三十点的推荐可能是最好的。他链接的 Material UI textarea 有一个非常聪明的解决方案。
他们创建了一个隐藏的绝对定位的文本区域,模仿实际文本区域的样式和宽度。然后他们将您键入的文本插入该文本区域并检索它的高度。因为它是绝对定位的,所以没有回流计算。然后他们使用该高度作为实际文本区域的高度。
我不完全理解他们的代码在做什么,但我已经针对我的需要进行了最少的重新调整,而且它似乎工作得很好。以下是一些片段:
.shadow-textarea {
visibility: hidden;
position: absolute;
overflow: hidden;
height: 0;
top: 0;
left: 0
}
<textarea ref={this.chatTextBoxRef} style={{ height: this.state.heightInPx + "px" }}
onChange={this.handleMessageChange} value={this.props.value}>
</textarea>
<textarea ref={this.shadowTextBoxRef} className="shadow-textarea" />
componentDidUpdate() {
this.autoSize();
}
componentDidMount() {
this.autoSize();
}
autoSize = () => {
let computedStyle = window.getComputedStyle(this.chatTextBoxRef.current); // this is fine apparently..?
this.shadowTextBoxRef.current.style.width = computedStyle.width; // apparently width retrievals are fine
this.shadowTextBoxRef.current.value = this.chatTextBoxRef.current.value || 'x';
let innerHeight = this.shadowTextBoxRef.current.scrollHeight; // avoiding reflow because we are retrieving the height from the absolutely positioned shadow clone
if (this.state.heightInPx !== innerHeight) { // avoids infinite recursive loop
this.setState({ heightInPx: innerHeight });
}
}
有点hacky,但似乎工作得很好。如果有人可以很好地改进它或用更优雅的方法清理它,我会接受他们的回答。但这似乎是考虑到 Material UI 使用它的最佳方法,并且它是迄今为止我尝试过的唯一一种消除了在足够复杂的应用程序中导致延迟的昂贵回流计算的方法。
Chrome 仅报告在高度变化时发生一次回流,而不是在每次按键时发生。因此,当文本区域增大或缩小时仍然存在 30 毫秒的滞后,但这比每次击键或文本更改都要好得多。采用这种方法时,滞后现象消除了 99%。
虽然不可能消除所有回流 — 浏览器必须在某个点计算高度 — 但可以显着减少回流。
Per Paul Irish (a Chrome developer), elem.scrollHeight
is among the property accesses & methods that cause a reflow. However, there is a significant note:
Reflow only has a cost if the document has changed and invalidated the style or layout. Typically, this is because the DOM was changed (classes modified, nodes added/removed, even adding a psuedo-class like :focus).
对于纯文本,textarea 实际上优于 <div contenteditable>
。对于 div,键入会更改 innerHTML
,这实际上是 Text
node。因此,以任何方式修改文本 也会修改 DOM,从而导致重排。在文本区域的情况下,键入仅会更改其 value
属性 — 不会触及 DOM,所需要的只是重新绘制,这(相对)非常便宜。这允许呈现引擎缓存上述引述所指示的值。
由于浏览器缓存 scrollHeight
,您可以使用 "classic" 建议 — 获取该值并立即将其设置为实际高度。
function resizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.style.scrollHeight}px`;
}
在值发生变化时使用该方法,这将确保文本区域保持在不会滚动的高度。不要担心 属性 的连续设置,因为浏览器会一起执行这些设置(类似于 requestAnimationFrame
)。
这在所有基于 WebKit 的浏览器中都是如此,目前是 Chrome 和 Opera,很快也会是 Edge。我认为 Firefox 和 Safari 有类似的实现。
仅使用 React 内置功能的“现代”hooks-apporach 将是 useRef and useLayoutEffects。这种方法在浏览器中进行任何渲染之前更新由值更改触发的文本区域的高度,因此避免了文本区域的任何 flickering/jumping。
import React from "react";
const MIN_TEXTAREA_HEIGHT = 32;
export default function App() {
const textareaRef = React.useRef(null);
const [value, setValue] = React.useState("");
const onChange = (event) => setValue(event.target.value);
React.useLayoutEffect(() => {
// Reset height - important to shrink on delete
textareaRef.current.style.height = "inherit";
// Set height
textareaRef.current.style.height = `${Math.max(
textareaRef.current.scrollHeight,
MIN_TEXTAREA_HEIGHT
)}px`;
}, [value]);
return (
<textarea
onChange={onChange}
ref={textareaRef}
style={{
minHeight: MIN_TEXTAREA_HEIGHT,
resize: "none"
}}
value={value}
/>
);
}
注意: 据我所知,这不是重复的,因为使用 contentEditable
div 似乎不是一个好主意选择。它有很多问题(没有占位符文本,需要使用 dangerouslySetInnerHTML
hack 来更新文本,选择光标很挑剔,其他浏览器问题等)我想使用文本区域。
我目前正在为我的 React textarea 组件做这件事:
componentDidUpdate() {
let target = this.textBoxRef.current;
target.style.height = 'inherit';
target.style.height = `${target.scrollHeight + 1}px`;
}
这有效并允许文本区域在添加和删除换行符时动态增大和缩小高度。
问题是每次文本更改时都会发生重排。这会导致应用程序出现大量滞后。如果我在文本区域中按住一个键,则会在附加字符时出现延迟和滞后。
如果我删除 target.style.height = 'inherit';
行,延迟就会消失,所以我知道这是由这种持续回流造成的。
我听说设置 overflow-y: hidden
可能会消除不断的回流,但在我的情况下却没有。同样,设置 target.style.height = 'auto';
不允许动态调整大小。
我目前已经开发出 a 解决方案,但我不喜欢它,因为它是每次文本更改时的 O(n) 操作。我只是计算换行符的数量并相应地设置大小,如下所示:
// In a React Component
handleMessageChange = e => {
let breakCount = e.target.value.split("\n").length - 1;
this.setState({ breakCount: breakCount });
}
render() {
let style = { height: (41 + (this.state.breakCount * 21)) + "px" };
return (
<textarea onChange={this.handleMessageChange} style={style}></textarea>
);
}
我个人无法想象阅读所有这些换行符是一个太大的问题,除非你写小说,但我不知道。您可以尝试根据击键调整中断次数。
沙盒here.
import React, { Component } from "react";
class TextBox extends Component {
state = {
breakCount: 0
};
handleKeyDown = e => {
if (e.key === "Enter") {
this.setState({ breakCount: this.state.breakCount + 1 });
}
// Note you will want something to better detect if a newline is being deleted. Could do this with more logic
// For quick testing of spamming enter/backspace key though this works.
if (e.key === "Backspace" && this.state.breakCount > 0) {
this.setState({ breakCount: this.state.breakCount - 1 });
}
};
render() {
const style = { height: 41 + this.state.breakCount * 21 + "px" };
return <textarea onKeyDown={this.handleKeyDown} style={style} />;
}
}
export default TextBox;
注意:
原文 Post: 我对 apachuilo 的代码进行了大量修改以达到预期的效果。它根据 textarea
的 scrollHeight
调整高度。当框中的文本更改时,它将框的行数设置为 minRows
的值并测量 scrollHeight
。然后,它计算文本的行数并更改 textarea
的 rows
属性以匹配行数。盒子在计算的时候没有"flash"
render()
只调用一次,只改变rows
属性。
当我输入 1000000(一百万)行每行至少 1 个字符时,添加一个字符大约需要 500 毫秒。在 Chrome 77.
中进行了测试代码沙箱:https://codesandbox.io/s/great-cherry-x1zrz
import React, { Component } from "react";
class TextBox extends Component {
textLineHeight = 19;
minRows = 3;
style = {
minHeight: this.textLineHeight * this.minRows + "px",
resize: "none",
lineHeight: this.textLineHeight + "px",
overflow: "hidden"
};
update = e => {
e.target.rows = 0;
e.target.rows = ~~(e.target.scrollHeight / this.textLineHeight);
};
render() {
return (
<textarea rows={this.minRows} onChange={this.update} style={this.style} />
);
}
}
export default TextBox;
我认为三十点的推荐可能是最好的。他链接的 Material UI textarea 有一个非常聪明的解决方案。
他们创建了一个隐藏的绝对定位的文本区域,模仿实际文本区域的样式和宽度。然后他们将您键入的文本插入该文本区域并检索它的高度。因为它是绝对定位的,所以没有回流计算。然后他们使用该高度作为实际文本区域的高度。
我不完全理解他们的代码在做什么,但我已经针对我的需要进行了最少的重新调整,而且它似乎工作得很好。以下是一些片段:
.shadow-textarea {
visibility: hidden;
position: absolute;
overflow: hidden;
height: 0;
top: 0;
left: 0
}
<textarea ref={this.chatTextBoxRef} style={{ height: this.state.heightInPx + "px" }}
onChange={this.handleMessageChange} value={this.props.value}>
</textarea>
<textarea ref={this.shadowTextBoxRef} className="shadow-textarea" />
componentDidUpdate() {
this.autoSize();
}
componentDidMount() {
this.autoSize();
}
autoSize = () => {
let computedStyle = window.getComputedStyle(this.chatTextBoxRef.current); // this is fine apparently..?
this.shadowTextBoxRef.current.style.width = computedStyle.width; // apparently width retrievals are fine
this.shadowTextBoxRef.current.value = this.chatTextBoxRef.current.value || 'x';
let innerHeight = this.shadowTextBoxRef.current.scrollHeight; // avoiding reflow because we are retrieving the height from the absolutely positioned shadow clone
if (this.state.heightInPx !== innerHeight) { // avoids infinite recursive loop
this.setState({ heightInPx: innerHeight });
}
}
有点hacky,但似乎工作得很好。如果有人可以很好地改进它或用更优雅的方法清理它,我会接受他们的回答。但这似乎是考虑到 Material UI 使用它的最佳方法,并且它是迄今为止我尝试过的唯一一种消除了在足够复杂的应用程序中导致延迟的昂贵回流计算的方法。
Chrome 仅报告在高度变化时发生一次回流,而不是在每次按键时发生。因此,当文本区域增大或缩小时仍然存在 30 毫秒的滞后,但这比每次击键或文本更改都要好得多。采用这种方法时,滞后现象消除了 99%。
虽然不可能消除所有回流 — 浏览器必须在某个点计算高度 — 但可以显着减少回流。
Per Paul Irish (a Chrome developer), elem.scrollHeight
is among the property accesses & methods that cause a reflow. However, there is a significant note:
Reflow only has a cost if the document has changed and invalidated the style or layout. Typically, this is because the DOM was changed (classes modified, nodes added/removed, even adding a psuedo-class like :focus).
对于纯文本,textarea 实际上优于 <div contenteditable>
。对于 div,键入会更改 innerHTML
,这实际上是 Text
node。因此,以任何方式修改文本 也会修改 DOM,从而导致重排。在文本区域的情况下,键入仅会更改其 value
属性 — 不会触及 DOM,所需要的只是重新绘制,这(相对)非常便宜。这允许呈现引擎缓存上述引述所指示的值。
由于浏览器缓存 scrollHeight
,您可以使用 "classic" 建议 — 获取该值并立即将其设置为实际高度。
function resizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.style.scrollHeight}px`;
}
在值发生变化时使用该方法,这将确保文本区域保持在不会滚动的高度。不要担心 属性 的连续设置,因为浏览器会一起执行这些设置(类似于 requestAnimationFrame
)。
这在所有基于 WebKit 的浏览器中都是如此,目前是 Chrome 和 Opera,很快也会是 Edge。我认为 Firefox 和 Safari 有类似的实现。
仅使用 React 内置功能的“现代”hooks-apporach 将是 useRef and useLayoutEffects。这种方法在浏览器中进行任何渲染之前更新由值更改触发的文本区域的高度,因此避免了文本区域的任何 flickering/jumping。
import React from "react";
const MIN_TEXTAREA_HEIGHT = 32;
export default function App() {
const textareaRef = React.useRef(null);
const [value, setValue] = React.useState("");
const onChange = (event) => setValue(event.target.value);
React.useLayoutEffect(() => {
// Reset height - important to shrink on delete
textareaRef.current.style.height = "inherit";
// Set height
textareaRef.current.style.height = `${Math.max(
textareaRef.current.scrollHeight,
MIN_TEXTAREA_HEIGHT
)}px`;
}, [value]);
return (
<textarea
onChange={onChange}
ref={textareaRef}
style={{
minHeight: MIN_TEXTAREA_HEIGHT,
resize: "none"
}}
value={value}
/>
);
}