Пролог

Если вы использовали React до версии 15, вы наверняка помните старый способ объявления классов компонентов, при котором вы могли бы написать что-то вроде этого:

var MyComponent = React.createClass({
  methodOne: function(someValue) {
    /* ... */
    this.setState({ someState: someValue})
  },
  methodTwo: function(someOtherValue) {
    /* ... */
    this.setState({ someOtherState: someOtherValue})
  },
  render: function() {
    return (
      /* ... */
      <button onClick={this.methodOne}>
        Click me!
      </button>    
    ) ;
  }
});

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

Но затем появился React 15.5, а вместе с ним и отказ от поддержки. Внезапно React.createClass() больше не был гражданином первого класса, и, двигаясь вперед, вам пришлось начать использовать новую class функцию ES6. Итак, теперь вы должны написать свой код выше:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state={/*...*/};
  }
  
  methodOne(someValue) {
    /* ... */
    this.setState({ someState: someValue})
  }
  methodTwo(someOtherValue) {
    /* ... */
    this.setState({ someOtherState: someOtherValue})
  }
  render() {
    return (
      /* ... */
      <button onClick={this.methodOne}>
        Click me!
      </button>    
    ) ;
  }
}

Все выглядит хорошо, и ваше приложение строится правильно. Что ж, это было легко. Но затем вы нажимаете на эту кнопку, которая постоянно призывает вас нажать на нее, и ничего не происходит. Вы открываете консоль и видите что-то среди строк Uncaught TypeError: this.setState is not a function.

Итак, вы понимаете (возможно, немного погуглив об ошибке), что когда вы нажимаете кнопку и вызывается methodOne, контекст this неверен.

"Да, конечно!" вы говорите себе. Новый React не связывает автоматически this с правильным контекстом, как это делала старая версия.

«Мне просто нужно .bind() все методы моего компонента this внутри конструктора, и все снова будет работать нормально»

И вы это делаете. И вы достигнете точки, когда большинство ваших constructor функций будут выглядеть так:

constructor(props) {
  super(this);
  this.state = {/*...*/};
  this.methodOne = this.methodOne.bind(this);
  this.methodTwo = this.methodTwo.bind(this);
  this.methodThree = this.methodThree.bind(this);
  /*.....*/
  this.methodGorillionMinusOne =    this.methodGorillionMinusOne.bind(this);
  this.methodGorillion = this.methodGorillion.bind(this);
}

Но все работает, значит, все хорошо. Либо это?

Магия прототипного наследования и цепочка прототипов

Если вы хотите пропустить некоторые технические объяснения, не стесняйтесь пропустить этот раздел и сразу перейти к «Привязка всего этого к React» ниже.

Давайте рассмотрим простой класс JavaScript, написанный на ES6:

class SomeClass {
  constructor(config) {
    this.instancePropertyOne = config.one;
    this.instancePropertyTwo = config.two;
    this.instancePropertyThree = config.one + config.two;
  }
classMethodOne = function classMethodOne() {
    console.log(this.instancePropertyOne);
  }
classMethodTwo = function classMethodTwo() {
    console.log(this.instancePropertyTwo);
  }
}

А теперь давайте немного отступим и посмотрим, как мы будем писать это в стиле ES5:

function SomeClass(config) {
  this.instancePropertyOne = config.one;
  this.instancePropertyTwo = config.two;
  this.instancePropertyThree = config.one + config.two;
}
SomeClass.prototype.classMethodOne = function classMethodOne() {
  console.log(this.instancePropertyOne);
}
SomeClass.prototype.classMethodTwo = function classMethodTwo() {
  console.log(this.instancePropertyTwo);
}

И чтобы проверить это:

const config = {one: 1, two: 2}
const someClassInstance = new SomeClass(config);
someClassInstance.classMethodOne(); // prints 1
someClassInstance.classMethodOne(); // prints 2
someClassInstance.instancePropertyThree; //prints 3

А теперь давайте посмотрим поближе.

Когда мы определяем класс, внутренне движок JavaScript делает что-то вроде этого:

function SomeClass(config) {
  this.instancePropertyOne = config.one;
  this.instancePropertyTwo = config.two;
  this.instancePropertyThree = config.one + config.two;
}
// The following is an approximation of what happens internally:
// SomeClass.prototype = {constructor: SomeClass};
SomeClass.prototype.classMethodOne = function classMethodOne() {
  console.log(this.instancePropertyOne);
}
SomeClass.prototype.classMethodTwo = function classMethodTwo() {
  console.log(this.instancePropertyTwo);
}

И когда мы создаем новый экземпляр SomeClass с new SomeClass(), происходит примерно следующее:

function SomeClass(config) {
  // The following is an approximation of what happens internally:
  //this = {
    __proto__: SomeClass.prototype
    };
  this.instancePropertyOne = config.one;
  this.instancePropertyTwo = config.two;
  this.instancePropertyThree = config.one + config.two;
}

Как вы можете видеть, каждый экземпляр SomeClass указывает на объект prototype (или __proto__), который присоединен к функции конструктора SomeClass. Слово «точки» имеет здесь важное значение, поскольку почти каждая переменная, свойство или метод в JavaScript является просто указателем на экземпляр объекта или примитивное значение.

Это цепочка прототипов.

Каждый экземпляр объекта, созданный в среде JavaScript, указывает на другой экземпляр объекта-прототипа с именем __proto__, который был предоставлен ему из его функции-конструктора, таким образом создавая цепочку.

Итак, мы объяснили цепочку прототипов, но что такое прототипное наследование?

Возможно, вы заметили, что в нашем предыдущем примере использования класса экземпляр SomeClass на самом деле не имеет никаких методов. У него всего три свойства: instancePropertyOne, instancePropertyTwo и instancePropertyThree.

Однако, когда мы создаем новый экземпляр SomeClass и вызываем someClassInstance.classMethodOne(), он работает. Почему это так?

Это потому, что всякий раз, когда вы оцениваете свойство или метод в экземпляре объекта, механизм JavaScript, начиная с самого экземпляра объекта, будет проходить все __proto__ объекты в цепочке прототипов экземпляра, пытаясь найти соответствующий ключ свойства / метода в любом из них. Если он найдет его, он прекратит поиск и будет использовать его значение.

Это прототипное наследование.

Пока любой объект __proto__ в цепочке прототипов экземпляра объекта имеет свойство или метод с ключом / именем, которое мы ищем, он действует так, как если бы сам экземпляр объекта имел свойство / метод сам по себе.

Если бы мы пытались использовать метод, который существует в одном из экземпляров объекта __proto__, и мы вызвали его как SomeClassInstance.somePrototypeMethod, контекст для this будет экземпляром объекта, для которого был вызван метод (в данном случае SomeClassInstance).

Так что все это значит?

Прелесть прототипного наследования и цепочки прототипов в том, что даже если у вас есть бесконечное количество экземпляров класса, и все они наследуют метод от своего прототипа (класса), тогда все они будут просто указывать по единственному адресу памяти, где находится метод-прототип.

По сути, вы экономите память, позволяя всем экземплярам вашего класса использовать один и тот же экземпляр функции по ссылке.

С другой стороны, каждый раз, когда вызывается .bind(), он создает новый экземпляр дискретной функции, который должен отдельно храниться в памяти.

function SomeClass(config) {
  this.instancePropertyOne = config.one;
  this.instancePropertyTwo = config.two;
  this.instancePropertyThree = config.one + config.two;
  
  // Each instance creates two new functions and references them   
  this.classMethodOne = this.classMethodOne.bind(this);
  this.classMethodTwo = this.classMethodTwo.bind(this);
}
SomeClass.prototype.classMethodOne = function classMethodOne() {
  console.log(this.instancePropertyOne);
}
SomeClass.prototype.classMethodTwo = function classMethodTwo() {
  console.log(this.instancePropertyTwo);
}

В приведенном выше коде, если мы создадим 3 SomeClassInstance, в памяти будут храниться 6 новых функций, тогда как мы могли бы использовать только 2 начальные функции для каждого SomeClassInstance. Сделайте это тысячи раз вместо 3, и память начнет постепенно заполняться.

Связывание всего этого с React

Применяя все вышеперечисленное к React, мы теперь понимаем, что .bind использование каждого метода компонента для каждого экземпляра компонента не очень эффективный способ делать что-то, если мы хотим минимизировать объем нашей памяти.

(tl; dr, если вы пропустили два раздела выше: каждый раз, когда вы вызываете .bind() в методе компонента, вы сохраняете дополнительную функцию в памяти, при этом вы можете использовать ссылку на исходный метод и воспользоваться преимуществами прототипного наследования)

Это особенно верно при работе с большими приложениями React, где часто бывает несколько экземпляров некоторых компонентов.

Так что же нам делать? Вот хорошее практическое правило:

Есть ли у вашего компонента метод класса, который нужно передать в качестве обработчика дочернему компоненту? Затем этот метод необходимо связать с правильным экземпляром компонента.

Это потому, что когда вы передаете метод дочернему компоненту в качестве функции-обработчика, вы, по сути, выполняете эту функцию из контекста другого экземпляра объекта, где this не тот, который ожидает метод.

Пример:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state={/*...*/};
    
    this.methodOne = this.methodOne.bind(this);
  }
  
  methodOne(someValue) {
    /* ... */
    this.setState({ someState: someValue})
  }
  render() {
    return (
      /* ... */
      <button onClick={this.methodOne}>
        Click me!
      </button>    
    ) ;
  }
}

Любой другой метод класса, который используется только внутри экземпляра компонента, не должен быть привязан к экземпляру компонента.

Пример:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state={/*...*/};
    this.methodOne = this.methodOne.bind(this);
  }
  
  methodOne(someValue) {
    /* ... */
    this.methodTwo();
    this.setState({ someState: someValue})
  }
  methodTwo(someOtherValue) {
    /* ... */
    this.methodThree()
  }
  methodThree() {
  // Do something with "this"
  }
  render() {
    return (
      /* ... */
      <button onClick={this.methodOne}>
        Click me!
      </button>    
    ) ;
  }
}

Как видите, только methodOne необходимо связать с экземпляром компонента, поскольку он должен быть передан дочернему компоненту в качестве onClick обработчика. methodTwo и methodThree связывать не нужно. Таким образом, каждый MyComponent экземпляр, который вы создаете, будет добавлять в память только одну дополнительную функцию, а для остальных он может использовать те же MyComponent.prototype методы.

Итак, в следующий раз, когда вы попытаетесь оптимизировать компоненты React, имейте в виду, что ненужное использование .bind() может иметь значение.

Спасибо за прочтение! 🖖

дальнейшее чтение