反应“渲染后”代码?

Posted

技术标签:

【中文标题】反应“渲染后”代码?【英文标题】:React "after render" code? 【发布时间】:2014-12-20 19:03:51 【问题描述】:

我有一个应用程序,我需要在其中动态设置元素的高度(比如说“应用程序内容”)。它取应用程序“chrome”的高度并减去它,然后将“app-content”的高度设置为 100% 适应这些约束。这对于 vanilla JS、jQuery 或 Backbone 视图来说非常简单,但我正在努力弄清楚在 React 中执行此操作的正确过程是什么?

下面是一个示例组件。我希望能够将app-content 的高度设置为窗口的 100% 减去ActionBarBalanceBar 的大小,但是我怎么知道什么时候渲染所有内容以及我将计算的东西放在哪里在这个 React 类中?

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass(
  render: function () 
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance=balance />
          <div className="app-content">
            <List items=items />
          </div>
        </div>
      </div>
    );
  
);

module.exports = AppBase;

【问题讨论】:

【参考方案1】:

componentDidMount()

这个方法在你的组件被渲染后被调用一次。所以你的代码看起来像这样。

var AppBase = React.createClass(
  componentDidMount: function() 
    var $this = $(ReactDOM.findDOMNode(this));
    // set el height and width etc.
  ,

  render: function () 
    return (
      <div className="wrapper">
        <Sidebar />
          <div className="inner-wrapper">
            <ActionBar title="Title Here" />
            <BalanceBar balance=balance />
            <div className="app-content">
              <List items=items />
          </div>
        </div>
      </div>
    );
  
);

【讨论】:

componentDidUpdate 如果值在第一次渲染后可以更改。 我正在尝试更改设置为过渡的 css 属性,以便动画在渲染后开始。不幸的是,更改 componentDidMount() 中的 css 不会导致转换。 谢谢。这个名字非常直观,我想知道为什么我要尝试像“init”甚至“initialize”这样荒谬的名字。 在 componentDidMount 中更改它对浏览器来说太快了。将它包装在一个 setTimeout 中并且不给它实际时间。即componentDidMount: () =&gt; setTimeout(addClassFunction()),或者使用rAF,下面这个答案提供了这个答案。 这肯定行不通。如果你得到一个节点列表,然后尝试遍历节点列表,你会发现长度等于 0。执行 setTimeout 并等待 1 秒对我有用。不幸的是,react 似乎没有真正等到渲染 DOM 之后的方法。【参考方案2】:

使用 componentDidUpdatecomponentDidMount 的一个缺点是它们实际上是在 dom 元素绘制完成之前执行,但在它们从 React 传递到浏览器的 DOM 之后执行。

例如,如果您需要将 node.scrollHeight 设置为渲染的 node.scrollTop,那么 React 的 DOM 元素可能还不够。您需要等到元素绘制完成才能获取它们的高度。

解决方案:

使用requestAnimationFrame 确保您的代码在绘制新渲染的对象之后运行

scrollElement: function() 
  // Store a 'this' ref, and
  var _this = this;
  // wait for a paint before running scrollHeight dependent code.
  window.requestAnimationFrame(function() 
    var node = _this.getDOMNode();
    if (node !== undefined) 
      node.scrollTop = node.scrollHeight;
    
  );
,
componentDidMount: function() 
  this.scrollElement();
,
// and or
componentDidUpdate: function() 
  this.scrollElement();
,
// and or
render: function() 
  this.scrollElement()
  return [...]

【讨论】:

window.requestAnimationFrame 对我来说还不够。我不得不用 window.setTimeout 破解它。啊啊啊啊!!!!!! 奇数。也许它在最新版本的 React 中发生了变化,我认为对 requestAnimationFrame 的调用是不必要的。文档说:“在组件的更新刷新到 DOM 后立即调用。初始渲染不调用此方法。当组件更新时,将此作为对 DOM 进行操作的机会。” ...即,它已刷新,DOM 节点应该存在。 -- facebook.github.io/react/docs/… @JimSoho,我希望你是对的,这是已修复的,但该文档中实际上没有任何新内容。这是针对dom更新不够的边缘情况,重要的是我们等待绘制周期。我试图创建一个新版本和旧版本的小提琴,但我似乎无法创建一个足够复杂的组件来演示这个问题,甚至返回几个版本...... @neptunian 严格来说“[RAF] 在下一次重绘之前被称为 [...]...” -- [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/… ]。在这种情况下,节点仍然需要由 DOM 计算其布局(又名“重排”)。这使用 RAF 作为从布局前跳转到布局后的一种方式。 Elm 的浏览器文档是了解更多信息的好地方:elmprogramming.com/virtual-dom.html#how-browsers-render-html _this.getDOMNode is not a function 这个代码到底是什么?【参考方案3】:

根据我的经验,window.requestAnimationFrame 不足以确保 DOM 已从 componentDidMount 完全渲染/回流完成。我运行的代码在 componentDidMount 调用后立即访问 DOM,仅使用 window.requestAnimationFrame 会导致元素出现在 DOM 中;但是,由于尚未发生回流,因此尚未反映对元素尺寸的更新。

唯一真正可靠的方法是将我的方法包装在 setTimeoutwindow.requestAnimationFrame 中,以确保在注册下一帧的渲染之前清除 React 的当前调用堆栈。

function onNextFrame(callback) 
    setTimeout(function () 
        requestAnimationFrame(callback)
    )

如果我不得不推测为什么会发生这种情况/这是必要的,我可以看到 React 批处理 DOM 更新,并且直到当前堆栈完成后才真正将更改应用到 DOM。

最终,如果您在 React 回调后触发的代码中使用 DOM 测量值,您可能会想要使用此方法。

【讨论】:

您只需要 setTimeout 或 requestAnimationFrame,而不是两者都需要。 通常 - 你是对的。但是,在 React 的 componentDidMount 方法的上下文中,如果您在堆栈完成之前附加一个 requestAnimationFrame,则 DOM 实际上可能不会完全更新。我的代码在 React 的回调上下文中始终如一地重现此行为。在 DOM 更新之后,确保您的代码正在执行的唯一方法(再次说明,在这个特定的 React 用例中)是让调用堆栈首先使用 setTimeout 清除。 您会注意到上面提到的其他 cmets 需要相同的解决方法,即:***.com/questions/26556436/react-after-render-code/… 这是此 React 用例唯一 100% 可靠的方法。如果我不得不冒险猜测,这可能是由于 React 批处理更新本身可能不会在当前堆栈中应用(因此将 requestAnimationFrame 推迟到下一帧以确保应用批处理)。 我认为您可能需要重新了解您的 JS 内部知识...altitudelabs.com/blog/what-is-the-javascript-event-loop***.com/questions/8058612/… 作为嵌套的requestAnimationFrame 调用会更好吗?例如; function onNextFrame(cb) window.requestAnimationFrame(_ =&gt; window.requestAnimationFrame(cb)) 。根据规范 (html.spec.whatwg.org/multipage/webappapis.html#animation-frames),这将保证它在初始渲染后的下一帧上运行(特别是在“运行动画帧回调”中查看执行列表的顺序)。它避免了下一帧何时执行 setTimeout 的歧义。【参考方案4】:

只是用新的 Hook 方法更新这个问题,你可以简单地使用 useEffect 钩子:

import React,  useEffect  from 'react'

export default function App(props) 

     useEffect(() => 
         // your post layout code (or 'effect') here.
         ...
     ,
     // array of variables that can trigger an update if they change. Pass an
     // an empty array if you just want to run it once after component mounted. 
     [])

此外,如果您想在布局绘制之前运行,请使用 useLayoutEffect 挂钩:

import React,  useLayoutEffect  from 'react'

export default function App(props) 

     useLayoutEffect(() => 
         // your pre layout code (or 'effect') here.
         ...
     , [])

【讨论】:

根据 React 的文档,useLayoutEffect 发生在 所有 DOM 突变 reactjs.org/docs/hooks-reference.html#uselayouteffect 是的,但它会在布局有机会绘制之前运行Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.我会编辑。 你是否知道 useEffect 是否在浏览器的重排之后运行(而不是 React 所谓的“绘制”)?使用 useEffect 请求元素的 scrollHeight 是否安全? 安全使用效果 是的,从类中重构我的组件并使用 useEffect 为我工作【参考方案5】:

您可以更改状态,然后在setState callback 中进行计算。根据 React 文档,这是“保证在应用更新后触发”。

这应该在componentDidMount 或代码中的其他地方(例如在调整大小事件处理程序上)而不是在构造函数中完成。

这是window.requestAnimationFrame 的一个很好的替代方案,它没有一些用户在这里提到的问题(需要将它与setTimeout 结合使用或多次调用它)。例如:

class AppBase extends React.Component 
    state = 
        showInProcess: false,
        size: null
    ;

    componentDidMount() 
        this.setState( showInProcess: true , () => 
            this.setState(
                showInProcess: false,
                size: this.calculateSize()
            );
        );
    

    render() 
        const appStyle = this.state.showInProcess ?  visibility: 'hidden'  : null;

        return (
            <div className="wrapper">
                ...
                <div className="app-content" style=appStyle>
                    <List items=items />
                </div>
                ...
            </div>
        );
    

【讨论】:

这是我最喜欢的答案。干净且惯用的 React 代码。 这是一个很好的答案!谢谢!【参考方案6】:

我觉得这个解决方案很脏,但我们开始吧:

componentDidMount() 
    this.componentDidUpdate()


componentDidUpdate() 
    // A whole lotta functions here, fired after every render.

现在我只是坐在这里等待反对票。

【讨论】:

你应该尊重 React 组件的生命周期。 @TúbalMartín 我知道。如果您有更好的方法来达到相同的结果,请随时分享。 嗯,“坐在这里等待反对票”的比喻 +1。勇敢的人。 ;^) 宁可从两个生命周期调用一个方法,那么您不必从其他周期触发周期。 componentWillReceiveProps 应该这样做【参考方案7】:

React 很少有生命周期方法可以在这些情况下提供帮助,列表包括但不限于 getInitialState、getDefaultProps、componentWillMount、componentDidMount 等。

在你的情况和需要与 DOM 元素交互的情况下,你需要等到 dom 准备好,所以使用 componentDidMount 如下:

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass(
  componentDidMount: function() 
    ReactDOM.findDOMNode(this).height = /* whatever HEIGHT */;
  ,
  render: function () 
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance=balance />
          <div className="app-content">
            <List items=items />
          </div>
        </div>
      </div>
    );
  
);

module.exports = AppBase;

此外,有关 react 生命周期的更多信息,您可以查看以下链接: https://facebook.github.io/react/docs/state-and-lifecycle.html

【讨论】:

我的组件在页面呈现之前挂载运行,导致数据中的 api 调用加载时出现很大延迟。【参考方案8】:

我遇到了同样的问题。

在大多数情况下,在 componentDidMount() 中使用 hack-ish setTimeout(() =&gt; , 0) 是有效的。

但不是在特殊情况下;而且我不想使用ReachDOM findDOMNode,因为文档说:

注意:findDOMNode 是一个用于访问底层 DOM 的逃生舱口 节点。在大多数情况下,不鼓励使用此逃生舱口,因为 它穿透了组件抽象。

(来源:findDOMNode)

所以在那个特定的组件中我不得不使用componentDidUpdate() 事件,所以我的代码最终是这样的:

componentDidMount() 
    // feel this a little hacky? check this: http://***.com/questions/26556436/react-after-render-code
    setTimeout(() => 
       window.addEventListener("resize", this.updateDimensions.bind(this));
       this.updateDimensions();
    , 0);

然后:

componentDidUpdate() 
    this.updateDimensions();

最后,就我而言,我必须删除在componentDidMount 中创建的侦听器:

componentWillUnmount() 
    window.removeEventListener("resize", this.updateDimensions.bind(this));

【讨论】:

【参考方案9】:

实际上有比使用请求动画帧或超时更简单和更简洁的版本。我很惊讶没有人提出它: vanilla-js onload 处理程序。 如果可以,使用组件确实挂载,如果没有,只需在 jsx 组件的 onload 处理程序上绑定一个函数。如果您希望该函数运行每个渲染,请在将结果返回渲染函数之前执行它。代码如下所示:

runAfterRender = () => 

  const myElem = document.getElementById("myElem")
  if(myElem)
  
    //do important stuff
  


render()

  this.runAfterRender()
  return (
    <div
      onLoad = this.runAfterRender
    >
      //more stuff
    </div>
  )

【讨论】:

非常感谢!代码中的错字?应该是onLoad = this.runAfterRender() 即调用函数。 我认为您可以在 render() 函数开始时删除 this.runAfterRender() 调用。 onLoad=this.runAfterRender 应该是 onLoad=this.runAfterRender()。这确实会在加载时触发该功能。【参考方案10】:

我实际上遇到了类似行为的问题,我在组件中渲染了一个带有 id 属性的视频元素,因此当 RenderDOM.render() 结束时,它会加载一个需要 id 来查找占位符的插件,但它无法找到它。

componentDidMount() 中 0ms 的 setTimeout 修复了它:)

componentDidMount() 
    if (this.props.onDidMount instanceof Function) 
        setTimeout(() => 
            this.props.onDidMount();
        , 0);
    

【讨论】:

【参考方案11】:

渲染后,可以像下面这样指定高度,也可以给对应的react组件指定高度。

render: function () 
    var style1 = height: '100px';
    var style2 =  height: '100px';

   //window. height actually will get the height of the window.
   var hght = $(window).height();
   var style3 = hght - (style1 + style2) ;

    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar style=style1 title="Title Here" />
          <BalanceBar style=style2 balance=balance />
          <div className="app-content" style=style3>
            <List items=items />
          </div>
        </div>
      </div>
    );`
  

或者您可以使用 sass 指定每个反应组件的高度。用固定宽度指定前 2 个反应组件主 div,然后用 auto 指定第三个组件主 div 的高度。因此,将根据第三个 div 的内容分配高度。

【讨论】:

【参考方案12】:

对我来说,window.requestAnimationFramesetTimeout 的组合没有产生一致的结果。有时它有效,但并非总是如此——或者有时为时已晚。

我根据需要多次循环 window.requestAnimationFrame 来修复它。 (通常为 0 或 2-3 次)

关键是diff &gt; 0:这里我们可以确保页面更新的准确时间。

// Ensure new image was loaded before scrolling
if (oldH > 0 && images.length > prevState.images.length) 
    (function scroll() 
        const newH = ref.scrollHeight;
        const diff = newH - oldH;

        if (diff > 0) 
            const newPos = top + diff;
            window.scrollTo(0, newPos);
         else 
            window.requestAnimationFrame(scroll);
        
    ());

【讨论】:

【参考方案13】:

来自ReactDOM.render() 文档:

如果提供了可选的回调,它将在 组件被渲染或更新。

【讨论】:

你能添加一个如何使用它的例子吗?我主要从 render 方法返回元素,我不调用 render 并提供值。 不幸的是,您提到的回调仅适用于the toplevel ReactDOM.render,不适用于component level's ReactElement.render(这是这里的主题)。 这里的例子会有帮助 我点击了您答案中的链接,但找不到您引用的那一行,而且您的答案没有包含足够的信息,无法在没有它的情况下使用。请参阅***.com/help/how-to-answer 获取有关如何写出好问题的建议【参考方案14】:

当我需要打印接收大量数据并在画布上绘制的反应组件时,我遇到了奇怪的情况。我已经尝试了所有提到的方法,但没有一种方法对我来说可靠,在 setTimeout 中使用 requestAnimationFrame 我有 20% 的时间得到空画布,所以我做了以下操作:

nRequest = n => range(0,n).reduce(
(acc,val) => () => requestAnimationFrame(acc), () => requestAnimationFrame(this.save)
);

基本上我做了一个 requestAnimationFrame 链,不确定这是不是个好主意,但到目前为止,这对我来说 100% 的情况下都有效(我使用 30 作为 n 变量的值)。

【讨论】:

【参考方案15】:

我不会假装我知道为什么这个特定的函数会起作用,但是每当我需要使用 Ref 在 useEffect 中——我只能假设它也适用于 componentDidMount

我把它放在 useEffect 中的代码顶部,它出现就好像它强制效果等待元素被绘制然后继续下一行代码,但没有任何明显的延迟,例如使用 setTimeout 或异步睡眠功能。如果没有这个,当我尝试访问 Ref 元素时,它会以 undefined 的形式返回。

const ref = useRef(null);

useEffect(()=>
    window.getComputedStyle(ref.current);
    // Next lines of code to get element and do something after getComputedStyle().
);

return(<div ref=ref></div>);

【讨论】:

【参考方案16】:

对于功能组件,您可以react-use-call-onnext-render,它是一个自定义钩子,允许在以后的渲染中安排回调。

在one of my other projects上使用成功。

用于要求 dom 元素的尺寸, 看这个例子,它是react-use-call-onnext-render examples上的第三个例子:

假设我们想要获取可移动 DOM 元素的尺寸,假设 divshowBox 状态控制 多变的。为此,我们可以使用getBoundingClientRect()。但是,我们只想在元素之后调用这个函数 安装到 dom 中,因此将在负责显示此元素的变量之后安排此调用一次渲染 在 dom 中发生了变化,这个变量是showBox,所以他将是useCallOnNextRender的依赖:

const YourComponent = () => 
    const [showBox, setShowBox] = useState(false)
    const divRef = useRef()
    const callOnNextShowBoxChange = useCallOnNextRender()
    return (
        <>
            <div style=canvasStyle id="canvas">
                <button style=boxStyle onClick=() => 
                    setShowBox(!showBox)
                    callOnNextShowBoxChange(() => console.log(divRef.current.getBoundingClientRect())) //right value
                >toggle show box
                </button>
                <div style=border: "black solid 1px" ref=divRef>
                    showBox ? <div style=boxStyle>box2</div> : null
                </div>
            </div>
        </>
    );
;

【讨论】:

【参考方案17】:

在尝试了上述所有建议的解决方案后,我发现中间的一个元素有 CSS 过渡,这就是为什么我在道具更改后未能获得正确的计算几何图形。 所以我不得不使用onTransitionEnd 监听器等待片刻,然后尝试获取容器元素的 DOM 高度。 希望这可以节省某人的工作日哈哈。

【讨论】:

【参考方案18】:

对我来说,单独使用 componentDidUpdate 或单独使用 window.requestAnimationFrame 并不能解决问题,但以下代码有效。

// Worked but not succinct
    componentDidUpdate(prevProps, prevState, snapshot) 
        if (this.state.refreshFlag)   // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
            window.requestAnimationFrame(() => 
                this.setState(
                    refreshFlag: false   // Set the refreshFlag back to false so this only runs once.
                );
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");  // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
            );
        
    

后来我用 requestAnimationFrame 进行了评论,它仍然可以正常工作:

// The best solution I found
    componentDidUpdate(prevProps, prevState, snapshot) 
        if (this.state.refreshFlag)   // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
            // window.requestAnimationFrame(() => 
                this.setState(
                    refreshFlag: false   // Set the refreshFlag back to false so this only runs once.
                );
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");  // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
            // );
        
    

我不确定额外的setState是否只是巧合导致了时间延迟,因此在检索图像时,绘图已经完成(如果我删除@987654326,我将得到旧的画布图像@)。

或者更可能是因为setState需要在渲染完所有内容后执行,所以它强制等待渲染完成。

-- 我倾向于相信后者,因为根据我的经验,在我的代码中连续调用 setState 会导致每个调用仅在最后一次渲染完成后触发。

最后,我测试了以下代码。如果this.setState(); 不更新组件,而是等到渲染完成,我想这将是最终的最佳解决方案。然而,它失败了。即使传递一个空的setState() 仍然会更新组件。

// This one failed!
    componentDidUpdate(prevProps, prevState, snapshot) 
        // if (this.state.refreshFlag) 
            // window.requestAnimationFrame(() => 
                this.setState();
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");
            // );
        // 
    

【讨论】:

【参考方案19】:

我建议你使用钩子。 它们从 16.8.0 版本开始提供。

你可以在官方react documentation查看这个钩子的行为。

类似这样的:

import React,  useEffect  from 'react'


const AppBase = ( ) => 

    useEffect(() => 
        // set el height and width etc.
    , [])

    return (
        <div className="wrapper">
            <Sidebar />
            <div className="inner-wrapper">
                <ActionBar title="Title Here" />
                <BalanceBar balance=balance />
                <div className="app-content">
                    <List items=items />
                </div>
            </div>
        </div>
    );


export default AppBase

【讨论】:

【参考方案20】:

使用ES6 类而不是React.createClass 进行一点更新

import React,  Component  from 'react';

class SomeComponent extends Component 
  constructor(props) 
    super(props);
    // this code might be called when there is no element avaliable in `document` yet (eg. initial render)
  

  componentDidMount() 
    // this code will be always called when component is mounted in browser DOM ('after render')
  

  render() 
    return (
      <div className="component">
        Some Content
      </div>
    );
  

另外 - 检查 React 组件生命周期方法:The Component Lifecycle

每个组件都有很多类似componentDidMount的方法,例如

componentWillUnmount() - 组件即将从浏览器 DOM 中移除

【讨论】:

没有不尊重,但这如何回答这个问题?在 ES6 上显示更新与问题无关/不会改变任何东西。所有更早的答案都已经谈到 componentDidMount 不能单独工作。

以上是关于反应“渲染后”代码?的主要内容,如果未能解决你的问题,请参考以下文章

反应:渲染后无法获得正确的元素宽度

仅在初始渲染后反应 CSS 动画

渲染后将原生重置 ScrollView 反应到顶部

在useEffect中反应丢失变量?

使用 cypress.io 模拟 s-s-r 反应应用程序 e2e 测试的服务器

渲染后如何在输入字段上设置焦点?