Пролог
Если вы использовали 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()
может иметь значение.
Спасибо за прочтение! 🖖