这是一个非常有趣的问题,鸡和蛋,你怎么能测量需要渲染的东西的高度才能测量它,而不会让用户先看到它闪现。对此的糟糕解决方案可能涉及在屏幕外渲染某些内容等并对其进行测量,但这与 React 所提倡的概念并不相符。
然而,它不仅可以通过了解组件生命周期来解决,还可以通过了解它如何适应浏览器生命周期来解决。
关键是componentDidMount
和componentDidUpdate
。一个常见的误解是这些发生在组件被渲染到浏览器之后,不是真的,它在发生之前触发
render
首先将虚拟 DOM 与您要求渲染的任何内容相协调(这里的另一个误解 - 虚拟 DOM 实际上不是真实的甚至是影子 DOM,它只是表示算法可以派生的组件及其 DOM 的 JS 对象树差异 - 此时您无法确定动态高度或与真实 DOM 元素交互)。
如果它感觉到有实际的 DOM 突变要完成,那么componentDidMount
/ componentDidUpdate
now 在实际浏览器渲染之前触发。
然后它将执行对真实 DOM 的派生更新(实际上rendering
是对浏览器 DOM)。至关重要的是,尽管浏览器生命周期意味着这个渲染的 DOM 还没有被绘制到屏幕上(即它不是rendered
从用户的角度来看的),直到下一次requestAnimationFrame
有机会拦截它的时候......
componentWillReceiveProps
|
V
shouldComponentUpdate
|
V
componentWillUpdate
|
V
render
|
V
componentDidUpdate
|
V
[BROWSER DOM rendered - can be interacted with programatically]
|
V
requestAnimationFrame callback
|
V
[BROWSER screen painted with updated DOM - can be seen and interacted by user]
所以在这一点上,componentDidX
我们有机会告诉它在浏览器绘制之前拦截浏览器,但我们可以与真正更新的 DOM 交互并执行测量!为此,我们需要使用window.requestAnimationFrame
which 接受一个回调函数,我们可以在其中执行测量。由于这是一个异步函数,它返回一个 id,您可以使用它来取消它,就像 setTimeout 所做的那样(我们应该记得清除任何挂起的请求componentWillUnmount
)。
我们还需要一个未设置样式的可测量父元素,并存储一个ref
存储的,以便我们可以在 DOM 节点上对其进行测量。还有一个高度动画的包装器。
无需 react-motion 即可简化(但外部div
可以包裹在 a 中Motion
并且可以将插值插入到动态样式道具中)...
// added type info for better clarity
constructor(props: SomeProps) {
super(props)
this.state = {
height: undefined
}
}
listRef: HTMLElement
bindListRef = (el: HTMLElement): void => {
this.listRef = el
}
rafId: number
interceptAndMeasure = (): void {
// use request animation frame to intercept and measure new DOM before the user sees it
this.rafId = window.requestAnimationFrame(this.onRaf)
}
onRaf = (): void => {
const height: number = this.listRef.getBoundingClientRect().height
// if we store this in the state - the screen will not get painted, as a new complete render lifecycle will ensue
// BEWARE you must have a guard condition against setting state unnecessarily here
// as it is triggered from within `componentDidX` which can if unchecked cause infinite re-render loops!
if (height !== this.state.height) this.setState({height})
}
render() {
const {data} = this.props
const {height} = this.state.height
// the ul will be pushed out to the height of its children, and we store a ref that we can measure it by
// the wrapper div can have its height dynamically set to the measured value (or an interpolated interim animated value!)
return (
<div style={height !== undefined ? {height: `${height}px`} : {}}>
<ul ref={this.bindListRef}>
{data.map((_, i) =>
<li key={i}>
{_.someMassiveText}
</li>
)}
</ul>
</div>
)
}
componentDidMount() {
this.interceptAndMeasure()
}
componentDidUpdate() {
this.interceptAndMeasure()
}
componentWillUnmount() {
// clean up in case unmounted while still pending
window.cancelAnimationFrame(this.rafId)
}
当然,这之前已经遇到过,幸运的是,您可以跳过不必担心自己实现它并使用优秀的react-collapse
包(https://github.com/nkbt/react-collapse),它使用react-motion
表面下做正是这个,并且还处理许多边缘情况,如嵌套动态高度等
但在你这样做之前,我真的强烈建议你至少自己试一试,因为它开启了对 React 和浏览器生命周期的更深入理解,并且将有助于深度交互体验,否则你将很难实现