Оригинал:how-does-react-tell-a-class-from-a-function
Оригинальный перевод:Как реагировать, если компонент является компонентом класса
Рассмотрим это как функциюGreeting
Компоненты:
function Greeting() {
return <p>Hello</p>;
}
react
Также поддерживается определение его как класса:
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
(донедавний, что является единственным способом использования таких функций, как состояние. )
когда вы хотите визуализировать<Greeting />
компонент, вам не нужно заботиться о том, как он определен:
//类或者函数,都可以
<Greeting />
Однако, какreact
Сам по себе он заботится об этих различиях!
еслиGreeting
это функция,react
Нужно позвонить ему:
// 你的代码
function Greeting() {
return <p>Hello</p>;
}
// React内
const result = Greeting(props); // <p>Hello</p>
но еслиGreeting
это класс, тоReact
просто нужно использоватьnew
чтобы создать его экземпляр, а затем вызвать экземплярrender
метод:
// 你的代码
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// React内
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
В обоих случаях,React
Цель состоит в том, чтобы получить узел рендеринга (в данном случае<p> Hello </ p>
).
Так как же React узнает, является ли что-то классом или функцией?
как во мнев предыдущем постеОпять же, вам не нужно знатьthis
Что делать в Реакте. Я не знал этого много лет. Пожалуйста, не превращайте это в вопрос интервью. На самом деле, эта статья больше оJavaScript
а не оReact
.
Этот блог создан для любопытстваReact
Читатели почему это работает определенным образом. Вы тот человек? Тогда давай копать вместе.
Это было долгое путешествие. пристегните ремень безопасности. В этой статье мало о чемReact
сама информация, но мы обсудимnew
,this
,class
,arrow function
,prototype
,__ proto__
,instanceof
и как эти вещиJavaScript
Некоторые аспекты совместной работы в К счастью, когда вы используете React, вам не нужно об этом думать. Если вы внедряете React...
(Если вы действительно просто хотите узнать ответ, пожалуйста, дотяните до конца.)
Во-первых, нам нужно понять, почему важно относиться к функциям и классам по-разному. Обратите внимание, как мы используем новый оператор при вызове класса:
// If Greeting is a function
const result = Greeting(props); // <p>Hello</p>
// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
Давайте иметь общее представлениеnew
Что делается в Javascript.
В старые времена (до ES6) в Javascript не было классов. Однако вы можете использовать обычные функции, чтобы вести себя по шаблону, подобному классу.В частности, вы можете использовать любую функцию в роли, аналогичной конструктору класса, добавив new перед вызовом:
// 只是一个function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 不会如期工作
Вы все еще можете писать такой код сегодня! существуетDevTools
попробуй.
Если вы не несетеnew
передачаPerson('Fred')
,this
Inside указывало бы на что-то глобальное и бесполезное (например, window или undefined). Таким образом, наш код вылетит или что-то вроде установкиwindow.name
Так же глупо.
добавив перед вызовомnew
, что эквивалентно фразе: "ПриветJavaScript
,Я знаюPerson
Просто функция, но давайте предположим, что это что-то вроде конструктора класса.Создайте объект {} и поместите его вPerson
внутри функцииthis
указать на этот объект, чтобы я мог назначить что-то вродеthis.name
такая вещь. Тогда верни мне этот предмет. "
Вышеупомянутыеnew
Операционный символ, что нужно сделать.
var fred = new Person('Fred'); // `Person`内,相同的对象作为`this`
new
Оператор возвращаетfred
объект может использоватьPerson.prototype
любой контент на нем.
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred');
fred.sayHi();
Вот как люди издеваются над классами до того, как JavaScript добавит их напрямую.
такnew
JavaScript существует уже некоторое время. но,class
добавляется заново. Теперь давайте использоватьclass
Перепишите приведенный выше код, чтобы он больше соответствовал нашему замыслу:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
Важно отразить намерения разработчика в дизайне языка и API.
Если вы пишете функцию, JavaScript не может догадаться, похожа ли она наalert()
называется подобным, или если он похожnew Person()
Он действует как конструктор. забыть какPerson
Такая функция указываетnew
Может привести к хаотическому поведению.
Синтаксис класса позволяет эквивалентно сказать: «Это не просто функция — это класс, и у него есть конструктор».Если вы забыли использовать при вызовеnew
, JavaScript выдаст ошибку:
let fred = new Person('Fred');
// ✅ If Person is a function: works fine
// ✅ If Person is a class: works fine too
let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately
Это помогает нам выявлять ошибки на ранней стадии, а не ждать чего-то вродеthis.name
какой-то расплывчатыйbug
Считаетсяwindow.name
вместоgeorge.name
.
Тем не менее, это означаетReact
необходимо использовать перед вызовом любого классаnew
. Он не может просто вызвать его как обычную функцию, потому что JavaScript воспримет это как ошибку!
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// 🔴 React can't just do this:
const instance = Counter(props);
Это означает неприятности.
Прежде чем мы увидим, как React решает эту проблему, важно помнить, что большинство людей используют компилятор, такой как Babel, в React для компиляции современных функций, таких как классы, для старых браузеров. Поэтому нам нужно учитывать компилятор в дизайне.
существуетBabel
В более ранних версиях можно былоnew
класс вызывается в случае . Однако это исправляется путем создания дополнительного кода:
function Person(name) {
// A bit simplified from Babel output:
if (!(this instanceof Person)) {
throw new TypeError("Cannot call a class as a function");
}
// Our code:
this.name = name;
}
new Person('Fred'); // ✅ Okay
Person('George'); // 🔴 Can’t call class as a function
Вы можете увидеть что-то подобное в упакованном коде, вот и все._classCallCheck
функция функции. (Вы можете уменьшить размер пакета, выбрав «свободный режим» без проверки, но это может усложнить ваше возможное преобразование в настоящие нативные классы.)
К настоящему времени вы должны иметь общее представление об использованииnew
или не использоватьnew
чтобы назвать разницу между чем-то:
new Person() |
Person() |
|
---|---|---|
class |
✅this is a Person instance |
🔴TypeError
|
function |
✅this is a Person instance |
😳this is window or undefined
|
Это важная причина, по которой React правильно называет компоненты.Если ваш компонент определен как класс, React должен использоватьnew
.
Итак, React может проверить, является ли что-то классом?
Не так просто!即使我们可以Расскажите классу с функцией в JavaScript, что по-прежнему не применяется к классам, обрабатываемым такими инструментами, как Babel. Для браузеров это просто простые функции.
Что ж, возможно, React сможет использовать его при каждом вызове.new
? К сожалению, это не всегда работает.
В качестве обычной функции используйтеnew
Вызов их дает им экземпляр объекта какthis
. Для функций, написанных как конструкторы (например, вышеPerson
), что идеально, но путает функциональные компоненты:
function Greeting() {
// 我们不希望“this”在这里成为任何一种情况下的实例
return <p>Hello</p>;
}
Но это может быть терпимо. Есть еще две причины, по которым постоянное использование может убитьnew
идея.
Первая причина, по которой он может убить, — это функции стрелок, попробуйте:
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
Такое поведение является преднамеренным и соответствует дизайну стрелочных функций. Одним из основных преимуществ стрелочных функций является то, что они не имеют собственныхthis
Bindings — вместо этого это решается из ближайшей обычной функции:
class Friends extends React.Component {
render() {
const friends = this.props.friends;
return friends.map(friend =>
<Friend
// `this` is resolved from the `render` method
size={this.props.size}
name={friend.name}
key={friend.id}
/>
);
}
}
ИтакСтрелочные функции не имеют собственныхthis
.Но это означает, что они совершенно бесполезны как конструкторы!
const Person = (name) => {
// 🔴 This wouldn’t make sense!
this.name = name;
}
Поэтому JavaScript не разрешено использоватьnew
Вызов функции стрелки. Если вы это сделаете, вы, вероятно, все равно допустили ошибку, и лучше сказать вам об этом раньше. Это похоже на то, что JavaScript не позволяет безnew
Способ вызова класса в случае.
Это здорово, но это также влияет на наши планы. React не может вызывать new для всего, потому что это нарушит функции стрелок! Мы можем попробовать, пропустивprototype
для обнаружения стрелочных функций, а не толькоnew
:
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
но этонепригодныйдля использованияbabel
скомпилированная функция. Это может не иметь большого значения, но есть еще одна причина, по которой этот подход является тупиковым.
мы не всегда можем использоватьnew
Другая причина заключается в том, что он не позволяет React поддерживать компоненты, возвращающие строки или другие примитивные типы.
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
это снова сnew
дизайнсвязанные причуды. Как мы видели ранее,new
Скажите движку JavaScript создать объект, создать объект внутри функции, а затем использовать объект какnew
результат.
Однако JavaScript также позволяет использоватьnew
Вызванная функция переопределяется, возвращая какой-либо другой объектnew
Возвращаемое значение. Предположительно, это считается полезным для таких шаблонов, где мы хотим повторно использовать пул экземпляров:
// Created lazily
var zeroVector = null;
function Vector(x, y) {
if (x === 0 && y === 0) {
if (zeroVector !== null) {
// Reuse the same instance
return zeroVector;
}
zeroVector = this;
}
this.x = x;
this.y = y;
}
var a = new Vector(1, 1);
var b = new Vector(0, 0);
var c = new Vector(0, 0); // 😲 b === c
Однако, если функция не является объектом,new
Возвращаемое значение функции также полностью игнорируется. Если вы возвращаете строку или число, как будто возврат не отображается.
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
использоватьnew
При вызове функции необработанные возвращаемые значения (такие как числа или строки) не могут быть прочитаны из функции. Так что, если React всегда использует new, он не сможет добавлять вспомогательные компоненты, возвращающие строки!
Это неприемлемо, поэтому нам нужно идти на компромисс.
Чему мы научились?Реакция должна использоватьnew
вызывающий класс (включаяBabel
вывод), но требует вызова обычных функций или стрелочных функций (включая вывод Babel) безnew
. Нет надежного способа различить их (классы и функции).
Если мы не можем решить общую проблему, можем ли мы решить более конкретную проблему?
При определении компонента как класса вы можете захотеть определить встроенный метод, такой какthis.setState()
) расширениеReact.Component
.Можем ли мы просто обнаружить потомков React.Component вместо того, чтобы пытаться обнаружить все классы?
Спойлер: это именно то, что делает React.
возможно, проверьтеGreeting
Идиоматический способ тестирования классов компонентов ReactGreeting.prototype instanceof React.Component
:
class A {}
class B extends A {}
console.log(B.prototype instanceof A); // true
Я знаю, о чем ты думаешь. что сейчас произошло? ! Чтобы ответить на этот вопрос, нам нужно понять прототипы JavaScript.
Вы, вероятно, знакомы с цепочками прототипов. Каждый объект в JavaScript может иметь «прототип». когда мы пишемfred.sayHi()
ноfred
объект неsayHi
имущество, мы находимся вfred
Найдите на прототипеsayHi
Атрибуты. Если мы не сможем найти его там, мы посмотрим на следующий прототип в цепочке --fred
Прототип прототипа. и т.п.
Как ни странно, свойство прототипа класса или функции не указывает на прототип значения.Я не шучу.
function Person() {}
console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype
Так что "цепочка прототипов" больше похожа__proto __.__ proto __.__ proto__
вместоprototype.prototype.prototype
. На это у меня ушли годы.
Так что же такое свойство прототипа функции или класса?Он __proto__ присваивается всем объектам, созданным с помощью этого класса или функции!
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`
А цепочка __proto__ — это то, как JavaScript ищет свойства:
fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!
fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!
На практике вам редко нужно касаться непосредственно из кода, если только вы не отлаживаете что-то, связанное с цепочкой прототипов.__proto__
. если вы хотитеfred .__ proto__
чтобы предоставить что-то, вы должны положить это вPerson.prototype
начальство. По крайней мере, так было задумано изначально.
__proto__
Свойства даже не должны открываться браузером, потому что цепочка прототипов считается внутренней концепцией. Но некоторые браузеры добавляют__proto__
, в конечном итоге он был едва стандартизирован (но предпочтение отдавалось Object.getPrototypeOf()).
Однако я все же нашел файл с именемprototype
Свойство не дает вам прототип значения (например,fred.prototype
не определено, потому чтоfred
это не функция), что меня очень сбивает с толку. Лично я считаю, что это главная причина, по которой даже опытные разработчики неправильно понимают прототипы JavaScript.
Это длинный пост, а? Я говорю, что мы на 80%. держать.
мы знаем, что когда говорятobj.foo
, JavaScript на самом деле находится вobj
,obj .__ proto__
,obj .__ proto __.__ proto__
находясь в поискеfoo
,Так далее и тому подобное.
Для классов вы не раскрываете этот механизм напрямую, но расширения также работают со старыми добрыми цепочками прототипов. Вот как осуществляется доступ к экземплярам наших классов ReactsetState
такие методы, как:
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype
c.render(); // Found on c.__proto__ (Greeting.prototype)
c.setState(); // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString(); // Found on c.__proto__.__proto__.__proto__ (Object.prototype)
Другими словами, при использовании класса экземпляр__proto__
Цепочка «отражает» иерархию классов:
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
из-за__proto__
Цепочки отражают иерархию классов, поэтому мы можемGreeting.prototype
начать проверкуGreeting
Это расширеноReact.Component
, затем следуйте его__proto__
цепь:
// `__proto__` chain
new Greeting()
→ Greeting.prototype // 🕵️ We start here
→ React.Component.prototype // ✅ Found it!
→ Object.prototype
Удобно,x instanceof Y
确实完成了这种搜索。 следуетx .__ proto__
где цепь ищетY.prototype
.
Обычно он используется, чтобы определить, является ли что-то экземпляром класса:
let greeting = new Greeting();
console.log(greeting instanceof Greeting); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype (✅ Found it!)
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype
console.log(greeting instanceof React.Component); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
console.log(greeting instanceof Object); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (✅ Found it!)
console.log(greeting instanceof Banana); // false
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (🙅 Did not find it!)
Но он также отлично работает, чтобы определить, расширяет ли класс другой:
console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
Эта проверка — то, как мы определяем, является ли что-то классом компонента React или обычной функцией.
Но это не то, что делает React. 😳
дляinstanceof
Одно предостережение в отношении решения заключается в том, что оно не работает, когда на странице есть несколько копий React, а проверяемый компонент наследуется от React.Component из другой копии React. Смешивание нескольких копий React в проекте плохо по нескольким причинам, но исторически мы стараемся избегать проблем, насколько это возможно. (Используя хуки, мыможет понадобитьсяПринудительная дедупликация. )
Другой возможной эвристикой может быть проверка существования метода рендеринга в прототипе. Однако в то времяне ясноКак будет развиваться API компонента. У каждого чека есть стоимость, поэтому мы не хотим добавлять больше одного. Это также не работает, если вы определяете рендеринг как метод экземпляра (например, используя синтаксис атрибута класса).
Поэтому React является базовым компонентомДобавить кспециальный логотип. React проверяет наличие этого флага, поэтому он узнает, является ли что-то классом компонента React или функцией.
Начальный флаг основан на классе React.Component:
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
Однако некоторые реализации классов, на которые мы хотим ориентироваться, не имеюткопироватьстатические свойства (или задать нестандартные__proto__
), поэтому флаг теряется.
Это реагирование на перемещение этого флагаReact.Component.prototype
причина:
// Inside React
class Component {}
Component.prototype.isReactComponent = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes
Собственно, в этом все дело.
Вы можете задаться вопросом, почему это объект, а не просто логическое значение. На практике это не имеет значения, но в более ранних версиях Jest (до того, как Jest стал Good™️) автоблокировка была включена по умолчанию. Сгенерированные макеты опускают исходное свойство,сломал чек. Спасибо Джест.
isReactComponent
Проверка используется в React сегодня.
React не найдет прототип без расширения React.ComponentisReactComponent
, и не рассматривает компонент как класс. теперь ты знаешь почемусамый популярный ответДа:Cannot call a class as a function
Неправильный ответ - добавитьextends React.Component
. Наконец, добавилпредупреждать,когдаprototype.render
Предупреждает, когда присутствует, ноprototype.isReactComponent
не существует.
Фактическое решение было довольно простым, но я продолжил объяснять, почему React остановился на этом решении и какие были альтернативы.
По моему опыту, это обычно происходит с библиотечными API. Чтобы сделать API простым в использовании, семантику языка (возможно, для нескольких языков, включая будущие направления), производительность во время выполнения, эргономику с шагами во время компиляции и без них, состояние экосистемы и пакетные решения часто необходимо учитывать на раннем этапе. Предупреждение и многое другое. Конечный результат не всегда может быть самым элегантным, но он должен быть практичным.
Если окончательный API будет успешным, его пользователям никогда не придется думать об этом процессе.Вместо этого они могут сосредоточиться на создании приложений.
Но если вам тоже любопытно... приятно знать, как это работает.