我正在尝试制作一个可重用的React文本夹组件。用户输入要渲染的行数和要显示的文本,组件将渲染其文本,以指定的行数将其 chop ,并在末尾插入省略号(...)。

我计算在何处 chop 文本并插入省略号的方法是一次添加一个单词,直到文本的clientHeight大于容器div的clientHeight

当它起作用时,我在chrome开发工具中看到了以下内容:
[Violation] Forced reflow while executing JavaScript took 179ms

这可能是由于读取了clientHeight forces reflow造成的。

这是我的代码:

class TextClamp extends React.PureComponent {

    constructor(props) {
        super(props);
        this.renderText = this.renderText.bind(this);
        this.state = {
            words: this.props.textToDisplay.split(' '),
        };
    }

    componentDidMount() {
        this.renderText();
    }

    renderText(isResizing = false) {
        const textEl = this.displayedText;
        const clampContainer = this.clampContainer;
        const heightToStop = isResizing ? clampContainer.style.height : this.letterHeightText.clientHeight * this.props.linesToRender;
        const dummyText = this.dummyText;
        const dummyDiv = this.dummyDiv;
        const words = this.state.words;
        const numWords = words.length;
        dummyDiv.style.cssText = `width: ${clampContainer.clientWidth}px; position: absolute; left: -1000px;`;

        let i = this.props.estimatedWordCount || 20;
        let strToRender = words.slice(0, i).join(' ');
        dummyText.textContent = strToRender;
        if (dummyText.clientHeight <= heightToStop && i>=numWords) {
            return;
        }
        while (dummyText.clientHeight <= heightToStop && i<numWords) {
           dummyText.textContent += ' ' + words[i++];
        };
        strToRender = dummyText.textContent;
        while (dummyText.clientHeight > heightToStop) {
            strToRender = strToRender.substring(0, strToRender.lastIndexOf(' '));
            dummyText.textContent = strToRender + '\u2026';
        }
        textEl.textContent = dummyText.textContent;
    }

    render() {
        const estimatedHeight = this.props.estimatedHeight || 20 * this.props.linesToRender;
        const containerStyle = { height: estimatedHeight, overflow: 'hidden'};
        if (typeof window !== 'undefined') {
            const dummyDiv = document.createElement('div');
            const dummyText = document.createElement('p');
            dummyDiv.appendChild(dummyText);
            this.dummyDiv = dummyDiv
            this.dummyText = dummyText
            document.body.appendChild(dummyDiv);
        }
        return (
            <div style={containerStyle} ref={(input) => {this.clampContainer = input;}}>
                <p ref={(input) => {this.displayedText = input;}}>{this.props.textToDisplay}</p>
                <p style={{visibility: 'hidden'}} ref={(input) => {this.letterHeightText = input;}}>Q</p>
            </div>
        );
    }
}

因此,基本上,该组件的主要功能是renderText()函数。在其中,我每次添加一个单词,直到文本的高度大于其容器的高度。从那里,我删除最后一个单词并添加省略号。

我所做的优化如下:
  • EstimatedWordCount允许每次添加一个单词的循环不必每次都从头开始。
  • 我通过将实际容器div的尺寸复制到屏幕外的position:absolute div来计算应该显示的文本,这样它就不会与其他DOM元素交互。

  • 但是,即使进行了优化,chrome仍然提示由于javascript造成的重排花费了太长时间。

    我可以对自己的renderText()函数进行任何优化,以避免如此频繁地阅读clientHeight吗?

    最佳答案

    这是解决此问题的一种非常快速的解决方案,它使用一种技术来存储文本中每个单词的宽度,然后基于该行上单词的maxWidth和累积宽度来构建每行。很少的DOM操作,因此非常快。甚至可以在不限制大小的情况下使用调整大小选项,看起来很棒:)



    希望您喜欢并随时提出任何问题!下面的代码是es6,这是一个有效的Codepen,已稍作修改以在Codepen.io上工作。

    我建议加载Codepen并调整窗口大小,以查看重新计算的速度。

    编辑:我更新了此组件,以便您可以为扩展和折叠添加自定义功能。这些是完全可选的,您可以提供所需的控件对象的任何部分。 IE。仅提供折叠选项的文本。

    您现在可以提供控件对象作为<TextClamp controls={ ... }。这是控件对象的耻辱:

    controls = {
        expandOptions: {
            text: string, // text to display
            func: func // func when clicked
        },
        collapseOptions: {
            text: string, // text to display
            func: func // func when clicked
        }
    }
    
    textlines都是必需 Prop 。


    import React, { PureComponent } from "react";
    import v4 from "uuid/v4";
    import PropTypes from "prop-types";
    
    import "./Text-clamp.scss"
    
    export default class TextClamp extends PureComponent {
        constructor( props ) {
            super( props );
    
            // initial state
            this.state = {
                displayedText: "",
                expanded: false
            }
    
            // generate uuid
            this.id = v4();
    
            // bind this to methods
            this.produceLines = this.produceLines.bind( this );
            this.handleExpand = this.handleExpand.bind( this );
            this.handleCollapse = this.handleCollapse.bind( this );
            this.updateDisplayedText = this.updateDisplayedText.bind( this );
            this.handleResize = this.handleResize.bind( this );
    
            // setup default controls
            this.controls = {
                expandOptions: {
                    text: "Show more...",
                    func: this.handleExpand
                },
                collapseOptions: {
                    text: "Collapse",
                    func: this.handleCollapse
                }
            }
    
            // merge default controls with provided controls
            if ( this.props.controls ) {
                this.controls = mergedControlOptions( this.controls, this.props.controls );
                this.handleExpand = this.controls.expandOptions.func;
                this.handleCollapse = this.controls.collapseOptions.func;
            }
        }
    
        componentDidMount() {
            // create a div and set some styles that will allow us to measure the width of each
            // word in our text
            const measurementEl = document.createElement( "div" );
            measurementEl.style.visibility = "hidden";
            measurementEl.style.position = "absolute";
            measurementEl.style.top = "-9999px";
            measurementEl.style.left = "-9999px";
            measurementEl.style.height = "auto";
            measurementEl.style.width = "auto";
            measurementEl.style.display = "inline-block";
    
            // get computedStyles so we ensure we measure with the correct font-size and letter-spacing
            const computedStyles = window.getComputedStyle( this.textDisplayEl, null );
            measurementEl.style.fontSize = computedStyles.getPropertyValue( "font-size" );
            measurementEl.style.letterSpacing = computedStyles.getPropertyValue( "letter-spacing" );
    
            // add measurementEl to the dom
            document.body.appendChild( measurementEl );
    
            // destructure props
            const { text, lines, resize } = this.props;
    
            // reference container, linesToProduce, startAt, and wordArray on this
            this.container = document.getElementById( this.id );
            this.linesToProduce = lines;
            this.startAt = 0;
            this.wordArray = text.split( " " );
    
    
            // measure each word and store reference to their widths
            let i, wordArrayLength = this.wordArray.length, wordArray = this.wordArray, wordWidths = { };
            for ( i = 0; i < wordArrayLength; i++ ) {
                measurementEl.innerHTML = wordArray[ i ];
                if ( !wordWidths[ wordArray[ i ] ] ) {
                    wordWidths[ wordArray[ i ] ] = measurementEl.offsetWidth;
                }
            }
    
            const { expandOptions } = this.controls;
    
            measurementEl.innerHTML = expandOptions.text;
            wordWidths[ expandOptions.text ] = measurementEl.offsetWidth;
            measurementEl.innerHTML = "&nbsp;";
            wordWidths[ "WHITESPACE" ] = measurementEl.offsetWidth;
    
            // reference wordWidths on this
            this.wordWidths = wordWidths;
    
            // produce lines from ( startAt, maxWidth, wordArray, wordWidths, linesToProduce )
            this.updateDisplayedText();
    
            this.resize = resize === false ? reisze : true
    
            // if resize prop is true, enable resizing
            if ( this.resize ) {
                window.addEventListener( "resize", this.handleResize, false );
            }
        }
    
        produceLines( startAt, maxWidth, wordArray, wordWidths, linesToProduce, expandOptions ) {
            // use _produceLine function to recursively build our displayText
            const displayText = _produceLine( startAt, maxWidth, wordArray, wordWidths, linesToProduce, expandOptions );
    
            // update state with our displayText
            this.setState({
                ...this.state,
                displayedText: displayText,
                expanded: false
            });
        }
    
        updateDisplayedText() {
            this.produceLines(
                this.startAt,
                this.container.offsetWidth,
                this.wordArray,
                this.wordWidths,
                this.linesToProduce,
                this.controls.expandOptions
            );
        }
    
        handleResize() {
            // call this.updateDisplayedText() if not expanded
            if ( !this.state.expanded ) {
                this.updateDisplayedText();
            }
        }
    
        handleExpand() {
            this.setState({
                ...this.state,
                expanded: true,
                displayedText: <span>{ this.wordArray.join( " " ) } - <button
                    className="_text_clamp_collapse"
                    type="button"
                    onClick={ this.handleCollapse }>
                        { this.controls.collapseOptions.text }
                    </button>
                </span>
            });
        }
    
        handleCollapse() {
            this.updateDisplayedText();
        }
    
        componentWillUnmount() {
            // unsubscribe to resize event if resize is enabled
            if ( this.resize ) {
                window.removeEventListener( "resize", this.handleResize, false );
            }
        }
    
        render() {
            // render the displayText
            const { displayedText } = this.state;
            return (
                <div id={ this.id } className="_text_clamp_container">
                    <span className="_clamped_text" ref={ ( el ) => { this.textDisplayEl = el } }>{ displayedText }</span>
                </div>
            );
        }
    }
    
    TextClamp.propTypes = {
        text: PropTypes.string.isRequired,
        lines: PropTypes.number.isRequired,
        resize: PropTypes.bool,
        controls: PropTypes.shape({
            expandOptions: PropTypes.shape({
                text: PropTypes.string,
                func: PropTypes.func
            }),
            collapseOptions: PropTypes.shape({
                text: PropTypes.string,
                func: PropTypes.func
            })
        })
    }
    
    function mergedControlOptions( defaults, provided ) {
        let key, subKey, controls = defaults;
        for ( key in defaults ) {
            if ( provided[ key ] ) {
                for ( subKey in provided[ key ] ) {
                    controls[ key ][ subKey ] = provided[ key ][ subKey ];
                }
            }
        }
    
        return controls;
    }
    
    function _produceLine( startAt, maxWidth, wordArray, wordWidths, linesToProduce, expandOptions, lines ) {
        let i, width = 0;
        // format and return displayText if all lines produces
        if ( !( linesToProduce > 0 ) ) {
    
            let lastLineArray = lines[ lines.length - 1 ].split( " " );
            lastLineArray.push( expandOptions.text );
    
            width = _getWidthOfLastLine( wordWidths, lastLineArray );
    
            width - wordWidths[ "WHITESPACE" ];
    
            lastLineArray = _trimResponseAsNeeded( width, maxWidth, wordWidths, lastLineArray, expandOptions );
    
            lastLineArray.pop();
    
            lines[ lines.length - 1 ] = lastLineArray.join( " " );
    
            let formattedDisplay = <span>{ lines.join( " " ) } - <button
                className="_text_clamp_show_all"
                type="button"
                onClick={ expandOptions.func }>{ expandOptions.text }</button></span>
    
            return formattedDisplay;
        }
    
        // increment i until width is > maxWidth
        for ( i = startAt; width < maxWidth; i++ ) {
            width += wordWidths[ wordArray[ i ] ] + wordWidths[ "WHITESPACE" ];
        }
    
        // remove last whitespace width
        width - wordWidths[ "WHITESPACE" ];
    
        // use wordArray.slice with the startAt and i - 1 to get the words for the line and
        // turn them into a string with .join
        let newLine = wordArray.slice( startAt, i - 1 ).join( " " );
    
        // return the production of the next line adding the lines argument
        return _produceLine(
            i - 1,
            maxWidth,
            wordArray,
            wordWidths,
            linesToProduce - 1,
            expandOptions,
            lines ? [ ...lines, newLine ] : [ newLine ],
        );
    }
    
    function _getWidthOfLastLine( wordWidths, lastLine ) {
        let _width = 0, length = lastLine.length, i;
        _width = ( wordWidths[ "WHITESPACE" ] * 2 )
        for ( i = 0; i < length; i++ ) {
            _width += wordWidths[ lastLine[ i ] ] + wordWidths[ "WHITESPACE" ];
        }
    
        return _width;
    }
    
    function _trimResponseAsNeeded( width, maxWidth, wordWidths, lastLine, expandOptions ) {
        let _width = width,
            _maxWidth = maxWidth,
            _lastLine = lastLine;
    
        if ( _width > _maxWidth ) {
            _lastLine.splice( length - 2, 2 );
            _width = _getWidthOfLastLine( wordWidths, _lastLine );
            if ( _width > _maxWidth ) {
                _lastLine.push( expandOptions.text );
                return _trimResponseAsNeeded( _width, _maxWidth, wordWidths, _lastLine, expandOptions );
            } else {
                _lastLine.splice( length - 2, 2 );
                _lastLine.push( expandOptions.text );
                if ( _getWidthOfLastLine( wordWidths, lastLine ) > maxWidth ) {
                    return _trimResponseAsNeeded( _width, _maxWidth, wordWidths, _lastLine, expandOptions );
                }
            }
        } else {
            _lastLine.splice( length - 1, 1 );
        }
    
        return _lastLine;
    }
    


    ._text_clamp_container {
        ._clamped_text {
            ._text_clamp_show_all, ._text_clamp_collapse {
                background-color: transparent;
                padding: 0px;
                margin: 0px;
                border: none;
                color: #2369aa;
                cursor: pointer;
                &:focus {
                    outline: none;
                    text-decoration: underline;
                }
                &:hover {
                    text-decoration: underline;
                }
            }
        }
    }
    

    关于javascript - 对React Text Clamp的性能改进?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/45091779/

    10-11 14:07