1

真的很抱歉这个奇怪的问题。我正在寻找使用 react-motion 创建一个简短的弹簧动画。请多多包涵:

我有一个组件,它根据选定的基本目录加载框架内的本地目录和文件列表,有点像 mac finder 窗口。因此,根据该任意目录,获取的文件数量会有所不同,因此我的框架的高度也会有所不同(直到达到最大高度)。

现在,一旦获取了所选目录的内容,我想将这个组件的框架动态地打开到正确的高度(或最大高度)。如果我要更改目录,我需要根据获取的元素数量或直接调整到最大高度来调整框架高度。

以下是该组件的相关摘录:

  <Motion
    defaultStyle={{x: 30}}
    style={{x: spring(330, {stiffness: 270, damping: 17})}}
  >
    {({x}) =>  
      <List height={x}>
        {
          localTree.map(item =>
            <ListItem key={v4()}>
              <FileIcon isDirectory={item.isDirectory} />
              <span onClick={() => this.handleClick(item.id)}>
                {item.baseName}
              </span>
            </ListItem>
          )
        }
      </List>
    }
  </Motion>

您可以看到,此时defaultStylestyle变量是静态设置的。它工作得很好,特别是因为我已经将<List />组件的样式设置为overflow: scroll这样,如果有比330px这种情况更多的元素,稍微滚动一下就可以自然地浏览它。

但这不是我最终想要的。我希望defaultStyle={{x: 30}}and由子元素style={{x: spring(330, )}}的累积数量的高度动态设置。<ListItem />

我应该如何解决这个问题?我应该在某处添加父组件吗?

4

1 回答 1

2

这是一个非常有趣的问题,鸡和蛋,你怎么能测量需要渲染的东西的高度才能测量它,而不会让用户先看到它闪现。对此的糟糕解决方案可能涉及在屏幕外渲染某些内容等并对其进行测量,但这与 React 所提倡的概念并不相符。

然而,它不仅可以通过了解组件生命周期来解决,还可以通过了解它如何适应浏览器生命周期来解决。

关键是componentDidMountcomponentDidUpdate。一个常见的误解是这些发生在组件被渲染到浏览器之后,不是真的,它在发生之前触发

render首先将虚拟 DOM 与您要求渲染的任何内容相协调(这里的另一个误解 - 虚拟 DOM 实际上不是真实的甚至是影子 DOM,它只是表示算法可以派生的组件及其 DOM 的 JS 对象树差异 - 此时您无法确定动态高度或与真实 DOM 元素交互)。

如果它感觉到有实际的 DOM 突变要完成,那么componentDidMount/ componentDidUpdatenow 在实际浏览器渲染之前触发。

然后它将执行对真实 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.requestAnimationFramewhich 接受一个回调函数,我们可以在其中执行测量。由于这是一个异步函数,它返回一个 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 和浏览器生命周期的更深入理解,并且将有助于深度交互体验,否则你将很难实现

于 2017-07-18T19:45:48.780 回答