React

Благодаря JavaScript привязка обработчиков событий в React может быть достаточно сложным занятием. Для тех, кто знаком с историей Perl или Python, аббревиатуры TMTOWTDI (There’s More Than One Way To Do It есть больше одного способа сделать это) и TOOWTDI (There’s Only One Way To Do It  есть только один способ сделать это) должны быть знакомыми. К несчастью, по крайней мере для привязывания событий, JavaScript является TMTOWTDI-языком, что всегда запутывает разработчиков.

В этом посте мы рассмотрим самые обычные способы привязки событий в React, а также рассмотрим их плюс и минусы. И, что самое важное, я помогу вам найти “единственный способ” или по крайней мере мой любимый.

В этом посте подразумевается, что вы понимаете необходимость привязок, почему нам нужно выполнять this.handler.bind(this) или в чём разница между function() { console.log(this); } и () => { console.log(this); }. Если эти вопросы вас смущают, то можете поискать ответы в этой отличной статье.

Динамическое привязывание в render()

В первом случае обычно вызывается .bind(this) внутри функции render(). Например:

class HelloWorld extends Component {
  handleClick(event) {}
  render() {
    return (
      <p>Hello, {this.state.name}!</p>
      <button onClick={this.handleClick.bind(this)}>Click</button>
    );
  }
}

Конечно же это будет работать. Но подумайте об одной вещи: что случится, если this.state.name изменится? Вы можете сказать, что изменение this.state.name заставит компонент использовать render() повторно. Хорошо. Компонент обработает и обновит часть с названием, но обработается ли кнопка?

Учтём тот факт, что React использует Virtual DOM. Когда выполняется рендеринг, то происходит сравнение обновленного Virtual DOM с предыдущим Virtual DOM, а затем обновляются только изменённые элементы настоящего DOM-дерева.

В нашем случае, когда после вызова render() также вызывается и this.handleClick.bind(this), чтобы привязать обработчик. Это приведёт к тому, что будет сгенерирован новый обработчик, который полностью отличается от обработчика создаваемого функцией render() при первом запуске!

Virtual DOM для динамической привязки. Голубые элементы будут обработаны повторно

 

Как видно в диаграмме выше, после вызова render()this.handleClick.bind(this возвращает funcA. Таким образом React знал, что значение onChange было funcA.

Позже, когда render() вызвано снова, this.handleClick.bind(this)возвращает funcB(заметьте, что оно возвращает новую функцию каждый раз после вызова). Таким образом React знает, что onChange больше не хранит значение funcA, что в свою очередь значит, что button требуется обновить.

Одна кнопка может и не быть проблемой, но что, если у вас 100 кнопок обрабатывается внутри списка?

render() {
  return (
    {this.state.buttons.map(btn => (
      <button key={btn.id} onChange={this.handleClick.bind(this)}>
        {btn.label}
      </button>
    ))}
  );
}

В примере выше изменение названия любой кнопки будет вызывать обновление всех кнопок, поскольку все кнопки будут генерировать новый onChange-обработчик.

Привязка в constructor()

Один из старых методов решения — это привязка внутри конструктора. Ничего красивого:

class HelloWorld extends Component {
  constructor() {
    this.handleClick = this.handleClickFunc.bind(this);
  }
  render() {
    return (<button onClick={this.handleClick}/>);
  }
}

Этот способ намного лучше предыдущего. Вызов render() не будет генерировать новый обработчик для onClick(), а значит <button> не будет обрабатываться заново до тех пор, пока кнопка не претерпит изменений.

Virtual DOM для привязки в конструкторе. Голубые элементы будут обработаны повторно

 

Привязки с использованием стрелочных функций

Со свойствами класса ES7 (в данный момент поддерживаемых Babel) мы можем делать привязывания как описание метода:

class HelloWorld extends Component {
  handleClick = (event) => {
    console.log(this.state.name);
  }
  render() {
    return (<button onClick={this.handleClick}/>)
  }
}

В коде выше handleClick является описанием, которое эквивалентно коду ниже:

constructor() {
  this.handleClick = (event) => { ... };
}

После того, как компонент инициализирован, this.handleClick больше не будет изменён. Таким способ вы гарантируете себе то, что <button> больше не будет обрабатываться снова. Этот подход, вероятно, один из лучших способ делать привязывания. Он прост, легко читаем и, самое важное, работает.

Динамические привязывания со стрелочными функция для нескольких элементов

Используя тот же трюк со стрелочными функциями мы можем использовать тот же обработчик для нескольких функций:

class HelloWorld extends Component {
  handleChange = name => event => {
    this.setState({ [name]: event.target.value });
  }
  render() {
    return (
      <input onChange={this.handleChange('name')}/>
      <input onChange={this.handleChange('description')}/>
    )
  }
}

На первый взгляд этот код выглядит потрясающе в своей простоте. Однако, если вы рассмотрите его тщательнее, то увидите, что в нём такая же проблема, как и в первом случае: каждый раз после вызова render() оба <input>обрабатываются по новой.

На самом деле мне действительно кажется, что этот подход умный и я не хочу писать множество handleXXXChange для каждого поля. К счастью, этот вид “многоразового обработчика” имеет маленькие шансы появиться внутри списка. Это значит, что там будет только пара <input>-компонентов, которые будут заново обработаны и, скорее всего, там не будет проблем с производительностью.

Тем не менее, преимущества, которые мы получаем от этого способа перевешивают потери производительности. Поэтому я рекомендую вам использовать этот подход.

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

class HelloWorld extends Component {
  handleChange = name => {
    if (!this.handlers[name]) {
      this.handlers[name] = event => {
        this.setState({ [name]: event.target.value });
      };
    }
    return this.handlers[name];  
  } 
  render() {
    return (
      <input onChange={this.handleChange('name')}/>
      <input onChange={this.handleChange('description')}/>
    )
  }
}

Заключение

Во время создания привязок в React мы должны очень тщательно следить, генерируются ли обработчики динамически. Обычно это не является проблемой, когда задействованные компоненты появляются только раз или два. Но когда обработчики событий появляются в списках — это может послужить ряду проблем с производительностью.

Решения

  • Используйте привязывание стрелочных функция всякий раз, когда это возможно
  • Если вам нужно сгенерировать привязывания динамически — рассмотрите возможность кеширования обработчиков, если привязывания становятся проблемой с производительностью

P.S. Omri Luzon и Shesh обратили внимание на пакеты lodash-decorators и react-autobind для более удобных привязок. Я лично не большой фанат автоматического выполнения чего-либо (я всегда стараюсь пользоваться такими вещами, как привязывания, как можно меньше), но автопривязка по-настоящему крутой способ для написания чистого кода и сохранения сил. Код будет выглядеть следующим образом:

import autoBind from 'react-autobind';
class HelloWorld() {
  constructor() {
    autoBind(this);
  }
handleClick() {
    ...
  }
  render() {
    return (<button onClick={this.handleClick}/>);
  }
}

Поскольку autoBind будет обрабатывать привязки автоматически, то совсем не обязательно использовать трюк со стрелочными функциями ( handleClick = () => {} ) для создания привязок, а в функции render() this.handleClickможет использоваться напрямую.

Перевод статьи Charlee LiThe best way to bind event handlers in React