Дизайн React — как он узнает, является ли он компонентом класса или компонентом функции

внешний интерфейс JavaScript React.js Babel

Оригинал: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'),thisInside указывало бы на что-то глобальное и бесполезное (например, 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 добавит их напрямую.


такnewJavaScript существует уже некоторое время. но,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

Такое поведение является преднамеренным и соответствует дизайну стрелочных функций. Одним из основных преимуществ стрелочных функций является то, что они не имеют собственныхthisBindings — вместо этого это решается из ближайшей обычной функции:

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 будет успешным, его пользователям никогда не придется думать об этом процессе.Вместо этого они могут сосредоточиться на создании приложений.

Но если вам тоже любопытно... приятно знать, как это работает.