Почему триггер нажатия кнопки отличается от триггера setTimeout()?

Рассмотрим следующие, почти идентичные, два фрагмента.

Разница в следующем:

  • первый использует setTimeout() для запуска события
  • второй запускает событие при нажатии кнопки

Если вы посмотрите консоль, вы увидите, что последние две строки в фрагменте 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/[email protected]/bundles/Rx.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/react.js"></script>
  <script src="https://unpkg.com/[email protected]/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/[email protected]/bundles/Rx.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/react.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>


person Misha Moroshko    schedule 07.03.2017    source источник


Ответы (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);

В идеале ваш код должен быть структурирован таким образом, чтобы порядок не имел значения.

person jayphelps    schedule 08.03.2017