为什么按钮点击触发器与 setTimeout() 触发器不同?

IT技术 reactjs rxjs rxjs5
2021-05-07 18:45:59

考虑以下几乎相同的两个片段。

区别在于:

  • 第一个用于setTimeout()触发事件
  • 第二个在按钮被点击时触发事件

如果您检查控制台,您会看到 Snippet 1 中的最后两行是:

App rendering 1 folder(s)
Observed js

在片段 2 中是:

Observed js
App rendering 1 folder(s)

问题:为什么顺序颠倒了?

setTimeout() 游乐场

按钮游乐场


代码段 1:setTimeout() 触发器

class App extends React.Component {
  constructor() {
    super();
    
    this.events$ = new Rx.Subject();
    this.eventsByName$ = this.events$.groupBy(e => e.name);
    
    this.state = {};
    
    setTimeout(() => {
      console.log('Emitting event');
      
      this.events$.next({
        type: 'ADD_FOLDER',
        name: 'js',
        permissions: 400
      });
    }, 1000);
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
      
      console.log(`New stream for "${folder}" created`);

      folderEvents$.subscribe(e => {
        console.log(`Observed ${e.name}`);
      });
      
      this.setState({
        [folder]: folderEvents$
      });
    });
  }
  
  render() {
    const folders = Object.keys(this.state);
    
    console.log(`App rendering ${folders.length} folder(s)`);
    
    return (
      <div>
        {
          folders.map(folder => (
            <div key={folder}>
              {folder}
            </div>
          ))
        }
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>

代码段 2:按钮触发器

class App extends React.Component {
  constructor() {
    super();
    
    this.events$ = new Rx.Subject();
    this.eventsByName$ = this.events$.groupBy(e => e.name);
    
    this.state = {};
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
      
      console.log(`New stream for "${folder}" created`);
      
      folderEvents$.subscribe(e => {
        console.log(`Observed ${e.name}`);
      });
      
      this.setState({
        [folder]: folderEvents$
      });
    });
  }
  
  onClick = () => {
    console.log('Emitting event');
    
    this.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  };
  
  render() {
    const folders = Object.keys(this.state);
    
    console.log(`App rendering ${folders.length} folder(s)`);
    
    return (
      <div>
        <button onClick={this.onClick}>
          Add event
        </button>
        <div>
          {
            folders.map(folder => (
              <div key={folder}>
                {folder}
              </div>
            ))
          }
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>

1个回答

它们以不同的顺序运行,因为 React 尝试一起批量setState()调用,因此调用setState()不会导致组件同步重新渲染,而是等待事件回调返回。

但是,只有当您调用setState是 React 驱动事件的结果时,它才会这样做,就像onClick是。当您使用 时setTimeout,React(当前)无法知道您何时完成,因此无法将它们批处理。相反,它会立即同步重新渲染。

我能说的最好的是,React 文档只是间接提到了这种行为:

setState() 不会立即改变 this.state 而是创建一个挂起的状态转换。调用此方法后访问 this.state 可能会返回现有值。

无法保证对 setState 调用的同步操作,并且可能会批量调用以提高性能。

https://facebook.github.io/react/docs/react-component.html#setstate

如果您希望 React 对事物进行批处理,则需要将回调代码包装在 中ReactDOM.unstable_batchedUpdates,顾名思义,这不是一个稳定的 API,因此它可以(并且可能会)在没有警告的情况下更改。

setTimeout(() => {
  ReactDOM.unstable_batchedUpdates(() => {
    console.log('Emitting event');

    this.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  });
}, 1000);

理想情况下,您的代码将以顺序无关紧要的方式构建。