ReactJS访问Virtual DOM协调结果

时间:2017-01-28 19:54:26

标签: javascript reactjs

我正在通过标准this.setState机制更新页面的一部分。我想抓住网页上已更改的元素,并向用户提供视觉反馈。

我们假设我们有一个组件RichText可以获得data道具。要呈现富文本,它会将渲染委托给较小的组件,如ParagraphHeaderBulletPointsText等。最终结果是正确呈现的富文本。

稍后data道具更改(例如套接字推送)。因此可以添加Paragraph,或者更改文本,或者可以移动。我想通过简单地突出显示已更改的HTML节点来向用户提供视觉反馈。

简而言之,我希望在您查看HTML树时实现Chrome检查器所显示的内容。它使DOM变化闪烁。

ReactJS知道改变了什么。理想情况下,我希望能够获得这些知识。

虽然像Paragraph这样较小的组件可以负责突出自身内部的差异,但我认为他们没有足够的外部知识来使其按预期工作。

格式(简化版)

{
  content: [{
    type: 'Document',
    content: [{
      type: 'Paragraph',
      content: [{
        type: 'Text', 
        text: 'text text'
      }, {
        type: 'Reference', 
        content: 'text text'
      },
    ]}, {
        type: 'BulletPoints', 
        content: [{
          type: 'ListEntry', content: [{
            type: 'Paragraph', content: [{
              type: 'Text', 
              text: 'text text'
            }, {
              type: 'Reference', 
              content: 'text text'
            }]
          }]
        }]
      }]

我目前的解决方案

我有一个顶级组件,它知道如何通过将作业委托给其他组件来呈现整个Document。我有一个实时版本的HOC:LiveDocument负责更改可视化。

所以我在setState之前和setState之后捕获DOM。然后我使用HtmlTreeWalker来发现第一个区别(当我走树时忽略某些元素)。

11 个答案:

答案 0 :(得分:6)

React已经为这些情况添加了一个插件。 ReactCSSTransitionGroup

  

ReactCSSTransitionGroup是一个基于ReactTransitionGroup的高级API,是一种在React组件进入或离开DOM时执行CSS转换和动画的简单方法。它的灵感来自优秀的ng-animate库。

您可以轻松设置进入或离开特定父级的项目的动画。

b2
c1
c2

答案 1 :(得分:4)

上次编辑

好了,现在你终于包含了解它所需的数据。您可以使用componentDidMountcomponentWillReceivePropscomponentDidUpdate完全处理它,并使用一些实例变量来保持某些内部状态与“内容”组件中的呈现无关。

这里有一个工作片段。我正在使用一些假按钮将新内容添加到列表的末尾并修改任何项目。这是你的JSON消息的模拟,但我希望你能得到它的要点。

我的样式非常基本,但你可以添加一些CSS过渡/关键帧动画,使突出显示只持续一段时间,而不是被修复。然而,这是一个CSS问题而不是React问题。 ;)

const Component = React.Component

class ContentItem extends Component {
  constructor(props){
    super(props)
    this.handleClick = this.handleClick.bind(this)
    //new by default
    this._isNew = true
    this._isUpdated = false
  }
  componentDidMount(){
    this._isNew = false
  }
  componentDidUpdate(prevProps){    
    this._isUpdated = false     
  }
  componentWillReceiveProps(nextProps){
    if(nextProps.content !== this.props.content){
      this._isUpdated = true
    }
  }
  handleClick(e){
    //hack to simulate a change in a specific content
    this.props.onChange(this.props.index)
  }
  render(){
    const { content, index } = this.props
    const newStyle = this._isNew ? 'new' : ''
    const updatedStyle = this._isUpdated ? 'updated': ''
         
    return (
      <p className={ [newStyle, updatedStyle].join(' ') }>
        { content }
        <button style={{ float: 'right' }} onClick={ this.handleClick}>Change me</button>
      </p>
     )
  }
}

class Document extends Component {
  constructor(props){
    super(props)
    this.state = {
      content: [
        { type: 'p', content: 'Foo' },
        { type: 'p', content: 'Bar' }
      ]
    }
    this.addContent = this.addContent.bind(this)
    this.changeItem = this.changeItem.bind(this)
  }
  addContent(){
    //mock new content being added
    const newContent = [ ...this.state.content, { type: 'p', content: `Foo (created at) ${new Date().toLocaleTimeString()}` }]
    this.setState({ content: newContent })
  }
  changeItem(index){
    //mock an item being updated
    const newContent = this.state.content.map((item, i) => {
      if(i === index){
        return { ...item, content: item.content + ' Changed at ' + new Date().toLocaleTimeString() }
      }
      else return item
    })
    this.setState({ content: newContent })
  }
  render(){
    return (
      <div>
        <h1>HEY YOU</h1>
        <div className='doc'>
          {
            this.state.content.map((item, i) => 
              <ContentItem key={ i } index={ i } { ...item } onChange={ this.changeItem } />)
          }
        </div>
        <button onClick={ this.addContent }>Add paragraph</button>
      </div>
    )    
  }
}

ReactDOM.render(<Document />, document.getElementById('app'));
.new {
  background: #f00
}
.updated {
  background: #ff0
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

答案 2 :(得分:3)

我认为你应该使用 componentDidUpdate

来自文档:

  更新发生后立即调用

componentDidUpdate(prevProps,prevState)。这种方法不是   要求初始渲染。

     

使用此作为在组件上操作DOM的机会   已经更新。这也是网络请求的好地方   只要你将当前道具与以前的道具进行比较(例如a   如果道具没有改变,则可能不需要网络请求。)

您可以比较哪个组件确实发生了变化,然后在状态中设置装饰器样式,以便在您的页面中使用。

答案 3 :(得分:3)

您可以编写一个HOC,它将您的叶子组件包装在PureComponent中。然后,当通过componentDidUpdate检测到更改时,此包装器将使用特殊样式呈现包装组件。它使用内部标志来打破componentDidUpdate + setState情况下的无限循环。

以下是示例代码 -

import React, {PureComponent} from "react";

let freshKid = (Wrapped, freshKidStyle) => {
    return class FreshKid extends PureComponent{
        state = {"freshKid" : true},
        componentDidUpdate(){
            if (this.freshKid){
                return;
            }
            this.freshKid = true;
            setTimeout(()=>this.setState(
                    {"freshKid" : false}, 
                    ()=>this.freshKid = false
                ), 
                100
            );
        }
        render(){
            let {freshKid} = this.state,
            {style, ..rest} = this.props,
            style = freshKid ? Object.assign({}, style, freshKidStyle) : style;

            return <Wrapped style={style} {...rest} />;
        }
    }
}

您可以使用它来包装像这样的叶子组件 -

let WrappedParagraph = freshKid(Paragraph, {"color":"orangered"});

或者导出预包装的所有叶子组件。

请注意,代码只是一个想法。此外,在调用setState之前,您应该在超时主体中添加一些检查以验证组件尚未卸载的ID。

答案 4 :(得分:3)

我认为你应该使用shouldComponentUpdate,据我所知,只有在这里你才能准确地发现你的情况。

这是我的例子:

class Text extends React.Component {
    constructor(props) {
        super(props);
        this.state = {textVal: this.props.text, className: ''};
    }
    shouldComponentUpdate(nextProps, nextState) {
        // Previous state equals to new state - so we have update nothing.
        if (this.state.textVal === nextProps.text) {
            this.state.className = '';
            return false;
        }
        // Here we have new state, so it is exactly our case!!!
        this.state.textVal = nextProps.text;
        this.state.className = 'blink';
        return true;
    }
    render() {
        return (<i className={this.state.className}>{this.state.textVal}</i>);
    }
}

它只是组件Text(我在场景后面留下了css和其他组件),我认为这段代码最有趣, 但您可以在codepen上尝试我的工作版,也可以在循环中使用jquery和更新的here示例。

答案 5 :(得分:3)

在呈现组件之前,您必须检查组件的道具是否已更改。如果他们这样做了,你必须在组件中添加一个类,然后在渲染后删除该类。将css transition添加到该类可以实现像Chrome开发工具一样的闪烁效果。

要检测属性中的更改,您应该使用componentWillReceiveProps(nextProps)组件钩子:

  在挂载的组件接收新道具之前调用

componentWillReceiveProps()。如果您需要更新状态以响应prop更改(例如,重置它),您可以比较this.propsnextProps并使用此方法中的this.setState()执行状态转换。 / p>

此挂钩不会触发组件安装,因此您需要设置初始&#34;突出显示&#34;构造函数中的状态。

要在渲染后删除该类,您需要将状态重置为&#34;未突出显示&#34;在setTimeout调用中,它发生在调用堆栈之外,并在组件呈现之后。

在下面的示例中,在输入中键入内容以查看突出显示的段落:

&#13;
&#13;
class Paragraph extends React.Component {
  constructor(props) {
    super(props);
    this.state = { highlighted: true };
    this.resetHighlight();
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.text !== this.props.text) {
      this.setState({ highlighted: true });
      this.resetHighlight();
    }
  }

  resetHighlight() {
    setTimeout(() => {
      this.setState({ highlighted: false });
    }, 0);
  }

  render() {
    let classes = `paragraph${(this.state.highlighted) ? ' highlighted' : ''}`;
    return (
      <div className={classes}>{this.props.text}</div>
    );

  }
}
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "type in me" };
  }
  handleInput(e) {
    this.setState({ text: e.target.value });
  }
  render() {
    return (
      <div className="App">
        <Paragraph text={this.state.text} />
        <input type="text" onChange={this.handleInput.bind(this)} value={this.state.text} />
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);
&#13;
.paragraph {
  background-color: transparent;
  transition: 1s;
}

.paragraph.highlighted {
  background-color: red;
  transition: 0s;
}
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
&#13;
&#13;
&#13;

答案 6 :(得分:2)

您可以将回调作为ref传递给节点,每次创建/重新创建DOM节点时,都会使用DOM节点调用它。

您可以使用公共回调来跟踪创建的节点。

答案 7 :(得分:2)

不幸的是,React并没有提供一个钩子来监听外部组件的状态变化。您可以使用componentDidUpdate(prevProps, nextProps)通知组件的状态更改,但您必须保留先前生成的DOM的引用,并手动将其与新DOM进行比较(例如,使用dom-compare)。我认为您已经使用当前的解决方案了。

我尝试了使用MutationObserverthis technique的替代解决方案来获取相对于文档的修改元素位置,并在mutated元素上方显示一个有边框的图层。它似乎运作良好,但我没有检查表现。

<强> mutationObserver.js

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

const observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    if (mutation.addedNodes) {
      mutation.addedNodes.forEach(showMutationLayer);
    }
  });
});

const showMutationLayer = (node) => {
  const mutationLayer = document.createElement('div');
  mutationLayer.style.position = 'absolute';
  mutationLayer.style.border = '2px solid red';
  document.body.appendChild(mutationLayer);
  if (node.nodeType === Node.TEXT_NODE) {
    node = node.parentNode;
  } 
  if (node.nodeType !== Node.ELEMENT_NODE) {
    return;
  }
  const { top, left, width, height } = getCoords(node);
  mutationLayer.style.top = `${top}px`;
  mutationLayer.style.left = `${left}px`;
  mutationLayer.style.width = `${width}px`;
  mutationLayer.style.height = `${height}px`;
  setTimeout(() => {
    document.body.removeChild(mutationLayer);
  }, 500);
};

function getCoords(elem) { // crossbrowser version
    const box = elem.getBoundingClientRect();
    const body = document.body;
    const docEl = document.documentElement;
    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
    const clientTop = docEl.clientTop || body.clientTop || 0;
    const clientLeft = docEl.clientLeft || body.clientLeft || 0;
    const top  = box.top +  scrollTop - clientTop;
    const left = box.left + scrollLeft - clientLeft;
    return { 
      top: Math.round(top), 
      left: Math.round(left), 
      width: box.width,
      height: box.height
    };
}

export default {
   init(container) {
     observer.observe(container, {
       attributes: true,
       childList: true,
       characterData: true,
       subtree: true
     });
   } 
}

<强> main.js

import React from 'react';
import {render} from 'react-dom';
import App from './App.js';
import mutationObserver from './mutationObserver.js';

const appContainer = document.querySelector('#app');

// Observe mutations when you are in a special 'debug' mode
// for example
if (process.env.NODE_ENV === 'debug') {
   mutationObserver.init(appContainer);
}

render(<App />, appContainer);

此技术的优点是您无需修改​​每个组件代码来观察更改。您也不会修改生成的组件DOM(该层在#app元素之外)。可以轻松启用/禁用此功能以保持应用程序性能。

See it in action in this fiddle (您可以编辑图层样式,为更好的图层添加CSS过渡)

答案 8 :(得分:2)

我最近在网络应用上遇到过同样的问题。我的要求是chrome like change notifier。我需要全局的唯一区别。由于UI上所需的功能(对于服务器渲染而言并不重要)使用观察者来挽救我的生命。

我设置&#34;通知 - 更改&#34;我要跟踪的组件和/或元素的css类。我的观察员会听取更改并检查更改后的dom和/或其父母是否已通知&#34;通知 - 更改&#34;类。如果条件匹配,那么它只需在&#34;中添加&#34;等级&#34;通知 - 更改&#34;标记元素以开始淡化效果。并删除&#34; in&#34;特定时间范围内的课程

&#13;
&#13;
(function () {
    const observer = new MutationObserver(function (mutations) {
        mutations.forEach(function (m) {
            let parent = m.target && m.target.parentNode;
            while (parent) {
                if (parent.classList && parent.classList.contains('notify-change')) {
                    break;
                }
                parent = parent.parentNode;
            }
            if (!parent) return;
            parent.classList.add('in');
            setTimeout(function () {
                try {
                    parent.classList.remove('in');
                } catch (err) {
                }
            }, 300);
        });
    });
    observer.observe(document.body, {subtree: true, characterData: true, characterDataOldValue: true});
})();

// testing

function test(){
  Array.from(document.querySelectorAll(".notify-change"))
  .forEach(e=>
    e.innerHTML = ["lorem", "ipsum", "sit" , "amet"][Math.floor(Math.random() * 4)]
  );
}

setInterval(test, 1000);
test();
&#13;
.notify-change {
  transition: background-color 300ms ease-out;
  background-color:transparent;
}

.notify-change.in{
  background-color: yellow !important;
}
&#13;
<div>Lorem ipsum dolor sit amet, eu quod duis eius sit, duo commodo impetus an, vidisse cotidieque an pro. Usu dicat invidunt et. Qui eu <span class="notify-change">Ne</span> impetus electram. At enim sapientem ius, ubique labore copiosae sea eu, commodo persecuti instructior ad his. Mazim dicit iisque sit ea, vel te oblique delenit.

Quo at <span class="notify-change">Ne</span> saperet <span class="notify-change">Ne</span>, in mei fugit eruditi nonumes, errem clita volumus an sea. Elitr delicatissimi cu quo, et vivendum lobortis usu. An invenire voluptatum his, has <span class="notify-change">Ne</span> incorrupte ad. Sensibus maiestatis necessitatibus sit eu, tota veri sea eu. Mei inani ocurreret maluisset <span class="notify-change">Ne</span>, mea ex mentitum deleniti.

Quidam conclusionemque sed an. <span class="notify-change">Ne</span> omnes utinam salutatus ius, sea quem necessitatibus no, ad vis antiopam tractatos. Ius cetero gloriatur ex, id per nisl zril similique, est id iriure scripta. Ne quot assentior theophrastus eum, dicam soleat eu ius. <span class="notify-change">Ne</span> vix nullam fabellas apeirian, nec odio convenire ex, mea at hinc partem utamur. In cibo antiopam duo.

Stet <span class="notify-change">Ne</span> no mel. Id sea adipisci assueverit, <span class="notify-change">Ne</span> erant habemus sit ei, albucius consulatu quo id. Sit oporteat argumentum ea, eam pertinax constituto <span class="notify-change">Ne</span> cu, sed ad graecis posidonium. Eos in labores civibus, has ad wisi idque.

Sit dolore <span class="notify-change">Ne</span> ne, vis eu perpetua vituperata interpretaris. Per dicat efficiendi et, eius appetere ea ius. Lorem commune mea an, at est exerci senserit. Facer viderer vel in, etiam putent alienum vix ei. Eu vim congue putent constituto, ad sit agam <span class="notify-change">Ne</span> integre, his ei veritus tacimates.</div>
&#13;
&#13;
&#13;

答案 9 :(得分:1)

我知道这个答案超出了你的问题的范围,但它是善意的另一种解决你问题的方法。

您可能正在根据您编写的内容创建中型或大型应用程序,正如我在这种情况下可以猜测的那样,您应该考虑使用Flux或Redux架构。

考虑到这种架构,您的控制器组件可以订阅Application Store更新,并根据您可以更新您的演示组件。

答案 10 :(得分:1)

您可以创建一个装饰器函数(如果您更喜欢该术语,则使用HOC),它使用部分应用程序根据提供的观察者函数观察更改。

(非常)简单的笔来演示这个概念:http://codepen.io/anon/pen/wgjJvO?editors=0110

笔的关键部分:

// decorator/HOC that accepts a change observer function
// and then a component to wrap
function observeChanges(observer) {
  return function changeObserverFactory(WrappedComponent) {
    return class ChangeObserver extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          changed: false
        }
      }

      componentWillReceiveProps(nextProps) {
        if (typeof observer === 'function') {
          observer(this.props, nextProps) && this.setState({ changed: true })
        } else if (this.props !== nextProps) {
          this.setState({ changed: true })
        }
      }

      componentDidUpdate() {
        if (this.state.changed) {
          setTimeout(() => this.setState({ changed: false }), 300)
        }
      }

      render() {
        return <WrappedComponent {...this.props} changed={this.state.changed} />
      }
    }
  }
}

// a simple component for showing a paragraph
const Paragraph = ({ changed, text }) => (
  <p className={`${changed ? 'changed' : ''}`}>{text}</p>
)

// a decorated change observer version of the paragraph,
// with custom change observer function
const ChangingParagraph = observeChanges(
  (props, nextProps) => props.text !== nextProps.text
)(Paragraph)

这将允许每个单独的组件确定什么构成自身的变化。

一些附注:

  • 您应该避免在componentDidUpdate中执行状态更新 componentWillUpdateshouldComponentUpdate componentWillReceiveProps就是这个地方。

      

    If you need to update state in response to a prop change, use componentWillReceiveProps()

  • 当您已经将状态作为事实来源和现有方法来比较构建在组件生命周期中的当前状态和下一个状态时,直接查看DOM以查找差异似乎是很多不必要的工作。 / p>