ReactJS能否获取虚拟DOM协调结果?

13

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

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

稍后会更改 data 属性(例如,套接字推送)。由于这个原因,Paragraph 可能被添加,或者文字被更改,或者事物可能会移动。我想通过简单地突出显示已更改的 HTML 节点向用户提供视觉反馈。

简而言之,我想实现 Chrome 检查器在查看 HTML 树时显示的内容。它会闪烁 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)。我有一个名为LiveDocument的实时版本高阶组件(HOC),负责变更可视化。

因此,在setState之前和之后,我捕获DOM。然后,我使用HtmlTreeWalker来发现第一个差异(在遍历树时忽略某些元素)。


我理解你的问题,但是如果有一个代码示例,就可以给你一个准确的可能解决方案。 - damianfabian
目前最接近 ReactJS 隐藏实用程序的是 MutationObserver。 - Mykola Golubyev
我给了第一个提到MutationObserver的人赏金,因为这是我工具箱中没有的解决方案。 - Mykola Golubyev
@MykolaGolubyev 嘿!你可能看到了我的示例,我使用setInterval不断更新组件。而我看到你应用了基于componentDidUpdate的答案,但它在我的示例中无法正常工作。你可以在这里看到它。 - cn007b
@MykolaGolubyev 请接受这个答案。 - hazardous
11个回答

6
React已经为这些情况提供了一个插件。ReactCSSTransitionGroup ReactCSSTransitionGroup是一个基于ReactTransitionGroup的高级API,它是在React组件进入或离开DOM时执行CSS转换和动画的简单方法。 它受到优秀的ng-animate库的启发。
您可以轻松地对进入或离开特定父项的项目进行动画处理。

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const nextId = (() => {
  let lastId = 0;
  return () => ++lastId;
})();

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {items: [
      {id: nextId(), text: 'hello'}, 
      {id: nextId(), text: 'world'}, 
      {id: nextId(), text: 'click'}, 
      {id: nextId(), text: 'me'}
    ]};
    this.handleAdd = this.handleAdd.bind(this);
  }

  handleAdd() {
    const newItems = this.state.items.concat([
      {id: nextId(), text: prompt('Enter some text')}
    ]);
    this.setState({items: newItems});
  }

  handleRemove(toRemove) {
    let newItems = this.state.items.filter(item => item.id !== toRemove.id);
    this.setState({items: newItems});
  }

  render() {
    const items = this.state.items.map((item) => (
      <div key={item.id} onClick={() => this.handleRemove(item)}>
        {item.text}
      </div>
    ));

    return (
      <div>
        <button className="add-todo" onClick={this.handleAdd}>Add Item</button>        
        <ReactCSSTransitionGroup
          transitionName="example"
          transitionEnterTimeout={500}
          transitionLeaveTimeout={300}>
          {items}
        </ReactCSSTransitionGroup>
      </div>
    );
  }
}

ReactDOM.render(<TodoList/>, document.getElementById("app"));
.example-enter {  
  background-color: #FFDCFF;
  color: white;
}

.example-enter.example-enter-active {
  background-color: #9E1E9E;  
  transition: background-color 0.5s ease;
}

.example-leave {
  background-color: #FFDCFF;
  color: white;
}

.example-leave.example-leave-active {
  background-color: #9E1E9E;  
  transition: background-color 0.3s ease;
}

.add-todo {
  margin-bottom: 10px;
}
<script src="https://unpkg.com/react@15/dist/react-with-addons.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>

<div id="app"></div>


以前见过这个。我的结构不像待办事项清单那样扁平。而且项目本身也可能会改变。 - Mykola Golubyev
对于非扁平化结构问题,您需要在每个组件中添加ReactCSSTransitionGroup组件以动画化其子元素。至于项目本身的更改,您是指是否希望在任何属性/状态更改时向用户提供反馈? - Tiago Engel
所有这些孩子都代表了一个富文本,因此如果其中任何内容发生变化,我希望提供视觉提示。我不想将每个容器(如段落)元素都包装在 CSS 过渡中。 - Mykola Golubyev
2
我的看法是将所有子元素用CSS过渡包装起来。你不必在每个组件中手动进行操作,例如,你可以创建一个HOC(高阶组件)来添加过渡效果,并在所有需要的组件中使用它。 - Tiago Engel
我将idx作为children的键,因为没有自然键可用。在开头插入一个新元素将触发对所有现有元素的更新。 - Mykola Golubyev
是的,你不能有重复的键,因为React会在内部使用它们进行转换。实际上,使用索引作为ID可能会导致很多其他问题,并且不建议这样做。您可以使用函数生成唯一的ID,就像我在示例中所做的那样。 - Tiago Engel

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>


1
非常感谢您提供如此详细的回答。我已经更新了我的问题,希望这能更加清楚明确。 - Mykola Golubyev
嗨,Mykola,我刚刚也更新了自己。希望有所帮助。 - CharlieBrown
感谢更新。每次都会将整个文档发送。没有时间戳。服务器不知道哪一部分已更改。考虑到有人刚刚上传了文档的新版本。让我将格式添加到问题中。 - Mykola Golubyev
我认为你提出的最后一点不是一个纯CSS问题。你的代码会在段落上保留“updated”类,直到有其他内容更改它。然而,如果你在同一段落上两次点击“Change me”,“updated”类仍然存在于你的点击之间。我不明白你如何使用此行为来实现“闪烁”。 - jetpackpony
@CharlieBrown OP指定了要实现什么样的闪烁效果:“我想实现Chrome检查器在查看HTML树时显示的效果。它会闪烁DOM更改”。它应该改变背景色(你的代码可以实现),然后渐变回透明(你的代码无法实现)。如上所述,当您两次点击“更改我”时,从Chrome的角度来看,类属性的更改只会在第一次点击之后发生(您实际上可以打开Chrome检查器自行查看)。如果您认为我错了,那就按照您的代码实现闪烁效果,我敢赌。 - jetpackpony
显示剩余5条评论

3

我认为你应该使用 componentDidUpdate

来自文档:

componentDidUpdate(prevProps, prevState) 在更新后立即被调用。此方法不会 呼叫最初的渲染。

利用这个机会来操作 DOM,当组件已经更新时。这也是一个很好的地方去进行网络请求,只要你比较当前 props 和之前的 props(例如,如果 props 没有改变,则可能不需要进行网络请求)。

您可以比较哪些组件发生了更改,然后在状态中设置装饰样式,以在页面中使用。


我认为它将被调用,无论组件是否实际更新。如果添加新元素,这并不会有所帮助。 - Mykola Golubyev

3
在组件渲染之前,您需要检查组件的属性是否更改。如果更改了,您将需要为该组件添加一个类,然后在渲染后删除该类。将css过渡应用于该类将使您获得Chrome开发工具中的闪烁效果。
要检测属性更改,您应该使用componentWillReceiveProps(nextProps)组件钩子:

componentWillReceiveProps()在已挂载的组件接收到新属性之前被调用。如果您需要响应属性更改来更新状态(例如重置状态),则可以在此方法中比较this.propsnextProps,并使用this.setState()执行状态转换。

此钩子不会在组件挂载时触发,因此您还需要在构造函数中设置初始“突出显示”状态。
要在渲染后删除类,您需要在setTimeout调用中将状态重置回“未突出显示”,以便它发生在调用堆栈之外且在组件渲染后发生。
请参见以下示例,在输入框中键入内容以查看突出显示的段落:

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')
);
.paragraph {
  background-color: transparent;
  transition: 1s;
}

.paragraph.highlighted {
  background-color: red;
  transition: 0s;
}
<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>


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 之前组件是否已被卸载。


喜欢包装的想法。它对于像父容器(例如,段落具有文本子元素)添加新子项并没有帮助。 - Mykola Golubyev
这应该只用于包装叶子元素,例如在这种情况下,您应该包装文本。我希望这是您想要的效果?例如,如果段落中出现新文本,则仅突出显示该部分? - hazardous

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并在循环中更新的示例。


请使用以下代码修改您的示例,实现无闪烁效果:setInterval(function () { paragraphs[0] = {id: 1, header: 'Sport', text: Date.now()}; ReactDOM.render(, document.getElementById('root')); }, 2000);您能够实现吗? - prosti
@prosti 我的回答重点不在于动画,而在于方法!我决定使用CSS动画,因为它非常简单,但你也可以使用JS动画或jQuery。这里是更新后的示例链接:http://codepen.io/cn007b/pen/qRoJwO?editors=0010 我只是为了少写代码而使用了jQuery...但再次强调,我只是试图解释我的观点... - cn007b
@prosti 谢谢你,伙计!从你那里得到了一个好的建议,真是太棒了! - cn007b

2
你可以将回调函数作为ref附加到节点上,每当DOM节点被创建/重新创建时,它将被调用并传递该DOM节点。
你可以使用一个通用的回调函数来跟踪已创建的节点。

我有很多从数据递归创建的节点。在整个过程中传递ref回调可能有点繁琐。但我喜欢这个建议。谢谢。 - Mykola Golubyev
我尝试了这种方法。如果我只更改组件的值(我的键不会改变,因此DOM节点仍然相同),它不会触发我的ref回调函数。 - Mykola Golubyev
哦,是的,我假设你所说的“更改”节点实际上是更改节点标识。据我所知,React没有提供(文档完备的)高级钩子来拦截协调机制。另一种方法是在组件级别跟踪数据更改,并事先推断出组件是否会更改。还有MutationObserver,但处理起来可能很麻烦。 - lorefnon
如果您正在考虑使用 MutationObserver,请查看 Mutation Summary一个使观察 DOM 的变化变得快速、简便且安全的 JavaScript 库。 - Joel Purra
MutationObserver看起来很有前途。谢谢。 - Mykola Golubyev

2

最近我在一个 Web 应用上遇到了同样的问题。我的要求是像 Chrome 一样的变化通知器,但我需要这个功能在全局范围内使用。由于这个功能要求在 UI 上运行(对于服务器渲染不重要),使用观察者模式解决了我的问题。

我为组件和/或元素设置了“notify-change” CSS 类,以便跟踪它们。我的观察者监听这些更改,并检查已更改的 DOM 及其父元素是否具有“notify-change”类。如果条件匹配,它就会简单地为“notify-change”标记的元素添加“in”类以开始淡出效果,并在特定时间内删除“in”类。

(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();
.notify-change {
  transition: background-color 300ms ease-out;
  background-color:transparent;
}

.notify-change.in{
  background-color: yellow !important;
}
<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>


2

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

我尝试了一种替代方案,使用MutationObserver这个技术来获取相对于文档的修改元素位置,并在突变的元素上方显示一个带边框的层。它似乎工作得很好,但我没有检查性能。

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元素之外)。启用/禁用此功能以保持应用程序性能非常容易。
在这个fiddle中可以看到它的实际效果(您可以编辑图层样式,添加 CSS 过渡效果使图层更加美观)。 链接

据我所知,您的答案基于componentDidUpdate。但是,在使用setInterval连续更新组件的情况下,它将无法正常工作。您可以在这里看到它here,以及here我的示例,展示了我认为应该如何工作的方式。 - cn007b

1
我知道这个答案超出了你问题的范围,但它是另一种解决你问题的良好意图。根据你所写的内容,你可能正在创建中型或大型应用程序,在这种情况下,你应该考虑Flux或Redux架构。有了这个架构,你的控制组件可以订阅应用程序存储更新,并基于此更新你的演示组件。

不完全准确,组件将对 Store(Redux/Flux)的更改做出反应,而不是正在分派的操作! - CharlieBrown
我同意拦截操作不是一个好的术语。我会编辑@CharlieBrown,非常感谢。 - prosti
我更喜欢我的核心组件不依赖于Redux。 - Mykola Golubyev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接