我正在尝试制作一个可重用的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()
函数。在其中,我每次添加一个单词,直到文本的高度大于其容器的高度。从那里,我删除最后一个单词并添加省略号。我所做的优化如下:
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
}
}
text
和lines
都是必需 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 = " ";
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/