簡體   English   中英

可以在沒有不斷回流的情況下動態調整高度的 textarea 嗎?

[英]Possible to have a dynamically height adjusted textarea without constant reflows?

注意:據我所知,這不是重復的,因為使用contentEditable div 似乎不是一個好的選擇。 它有很多問題(沒有占位符文本,需要使用dangerouslySetInnerHTML SetInnerHTML hack來更新文本,選擇光標很挑剔,其他瀏覽器問題等)我想使用文本區域。

我目前正在為我的 React textarea 組件執行以下操作:

componentDidUpdate() {
  let target = this.textBoxRef.current;

  target.style.height = 'inherit';
  target.style.height = `${target.scrollHeight + 1}px`; 
}

這有效並允許 textarea 在添加和刪除換行符時動態增長和縮小高度。

問題是在每次文本更改時都會發生回流。 這會導致應用程序出現很多滯后。 如果我在 textarea 中按住一個鍵,則會在附加字符時出現延遲和滯后。

如果我刪除target.style.height = 'inherit'; 線滯后消失了,所以我知道它是由這種不斷回流引起的。

我聽說設置overflow-y: hidden可能會擺脫不斷的回流,但在我的情況下卻沒有。 同樣,設置target.style.height = 'auto'; 不允許動態調整大小。

我目前已經為此開發了一個可行解決方案,但我不喜歡它,因為每次文本更改時它都是 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>
  );
}

我認為三十多點的推薦可能是最好的。 他鏈接的Material UI textarea有一個非常聰明的解決方案。

他們創建了一個隱藏的絕對定位的 textarea,模仿實際 textarea 的樣式和寬度。 然后他們將您鍵入的文本插入該文本區域並檢索它的高度。 因為它是絕對定位的,所以沒有回流計算。 然后他們使用該高度作為實際 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 僅在高度變化時報告回流發生一次,而不是在每次按鍵時發生。 所以當 textarea 增長或縮小時仍然有一個 30ms 的延遲,但這比每次擊鍵或文本更改都要好得多。 使用這種方法,滯后消失了 99%。

注意: Ryan Peschel 的回答更好。

原帖:我對 apachuilo 的代碼進行了大量修改以達到預期的效果。 它根據textareascrollHeight調整高度。 當框中的文本更改時,它將框的行數設置為minRows的值並測量scrollHeight 然后,它計算文本的行數並更改textarearows屬性以匹配行數。 計算時該框不會“閃爍”。

render()只調用一次,並且只改變了rows屬性。

當我輸入 1000000(一百萬)行,每行至少 1 個字符時,添加一個字符大約需要 500 毫秒。 在 Chrome 77 中對其進行了測試。

CodeSandbox: 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;

雖然不可能消除所有回流——瀏覽器必須在某個時刻計算高度——但可以顯着減少它們。

根據 Paul Irish (Chrome 開發人員), elem.scrollHeight是導致回流的屬性訪問和方法之一。 但是,有一個重要的注意事項

僅當文檔已更改並使樣式或布局無效時,回流才會產生成本。 通常,這是因為 DOM 已更改(修改了類,添加/刪除了節點,甚至添加了像 :focus 這樣的偽類)。

這就是,對於純文本, textarea 實際上優於<div contenteditable> 對於 div,鍵入會更改innerHTML ,它實際上是一個Text節點 因此,以任何方式修改文本也會修改 DOM,從而導致回流。 在 textarea 的情況下,輸入只會改變它的value屬性——沒有任何東西觸及 DOM,所需要的只是重新繪制,這(相對)非常便宜。 這允許渲染引擎緩存上述引用所指示的值。

由於瀏覽器緩存scrollHeight ,您可以使用“經典”建議 - 獲取該值並立即將其設置為實際高度。

function resizeTextarea(textarea) {
    textarea.style.height = 'auto';
    textarea.style.height = `${textarea.style.scrollHeight}px`;
}

每當值更改時使用該方法,這將確保 textarea 保持在不滾動的高度。 不要擔心屬性的連續設置,因為瀏覽器會一起執行這些(類似於requestAnimationFrame )。

這適用於所有基於 WebKit 的瀏覽器,目前是 Chrome 和 Opera,很快也會是 Edge。 我認為 Firefox 和 Safari 有類似的實現。

就我個人而言,除非您寫小說,否則我無法想象像那樣閱讀所有這些換行符是一個太大的問題,但我不知道。 您可以嘗試根據擊鍵調整中斷次數。

沙箱在這里

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;

僅使用 react 的內置功能的“現代” hooks- apporach將是useRefuseLayoutEffects 這種方法在瀏覽器中進行任何渲染之前更新由值更改觸發的 textarea 的高度,從而避免 textarea 的任何閃爍/跳躍。

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}
    />
  );
}

https://codesandbox.io/s/react-textarea-auto-height-s96b2

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM