Как React различает класс и функцию?

React.js

Взгляните на это, определяемое функциейGreetingКомпоненты:

function Greeting() {
  return <p>Hello</p>;
}

React также поддерживает определение по классам:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

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

когда вы собираетесь визуализировать<Greeting />, вам все равно, как это определено:

// Class or function — whatever.
<Greeting />

ноРеагировать на себязаключается в том, чтобы рассмотреть разницу между ними.

еслиGreetingэто функция, React должен вызывать ее так:

// Your code
function Greeting() {
  return <p>Hello</p>;
}

// Inside React
const result = Greeting(props); // <p>Hello</p>

но еслиGreetingЭто класс, реагировать, необходимо использовать его первымnewУправляйте экземпляром объекта, а затем вызывайте экземпляр объектаrenderметод.

// Your code
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

Цель обеих категорий React — получить визуализированный узел (здесь<p>Hello</p>). Но точные шаги зависят от того, как вы определяетеGreeting.

Так как же React распознает, является ли компонент классом или функцией?

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

Эта статья для любознательныхЗачемReact — это одноклассник, который работает определенным образом. Ты? Давайте копать вместе.

Это долгое путешествие, так что пристегните ремни. В этом посте мало говорится о самом React, но мы рассмотрим несколько других аспектов:new,this,class,arrow function,prototype,__proto__,instanceofи их зависимости в JavaScript. К счастью, вам не нужно слишком много думать при использовании React.

(Если вам действительно нужен ответ, прокрутите вниз.)


Во-первых, нам нужно понять, почему важно по-разному обрабатывать функции и классы. Обратите внимание, как при вызове класса мы используемnewиз.

// 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.


Раньше в JavaScript не было классов. Однако вы можете аппроксимировать его простой функцией.В частности, вы можете добавить функцию так же, как вы создаете класс.newдля создания функции:

// Just a function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work

Вы все еще можете писать так сегодня! Попробуйте с инструментами разработчика.

если ты позвонишьPerson('Fred') нет new, в методеthisбудет указывать на глобальный или нулевой (например,windowилиundefined). Таким образом, наш код может ошибаться или неосознанно даватьwindow.nameНазначение.

добавить перед вызовом методаnew, мы говорим: "Эй, JavaScript, я знаюPersonПросто функция, но давайте представим, что это конструктор класса.добавить один{}объект, воляPersonвнутреннийthisуказывает на этот объект, и я могу сделать это какthis.nameприсвойте ему такое значение и верните мне этот объект. "

ЭтоnewГотово.

var fred = new Person('Fred'); // Same object as `this` inside `Person`

newоператор также сохранит нас вPerson.prototypeчто-то действует наfredОбъект:

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 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()Вроде как конструктор. Забудь перед функцией плюс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бытьgeorge.nameсталwindow.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 решает эту проблему, важно помнить, что когда большинство людей используют React, они используют компилятор, такой как Babel, для компиляции новых функций, совместимых со старыми браузерами. Поэтому мы должны учитывать компилятор в дизайне.

В старой версии 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');   // 🔴 Cannot call a 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 может просто судить, является ли он классом?

Не так просто! Даже если бы мы моглиРазличие между классом и функцией, что по-прежнему не применяется к классам, обрабатываемым такими инструментами, как Babel. Для браузеров это просто обычные функции. Действительно неудача для React.


Хорошо, возможно, React сможет использовать его для каждого вызова.new? К сожалению, это не всегда работает.

Общая функция, принестиnewвызывая их, чтобы получить эквивалентthisобъект экземпляра. Для функций, написанных как конструкторы (например, предыдущийPerson),Оно работает. Но есть проблема с функциональными компонентами:

function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return <p>Hello</p>;
}

Эта ситуация терпима. Но есть две причины убить идею.


Первая причина в том, что,newНе может применяться к собственным стрелочным функциям (не скомпилированным с помощью Babel) при вызове сnewвыдаст ошибку:

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor

Эта ситуация преднамеренна и соответствует дизайну стрелочных функций. Основная особенность стрелочных функций в том, что они не имеют собственныхthisзначение, в то время какthisОн определяется ближайшей к себе общей функцией.

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 создать объект, приравнивая объект к объекту внутри функции.this, а затем объект как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При вызове функции исходное возвращаемое значение данных (например, число или строка) не может быть прочитано, и она не может поддерживать компоненты, возвращающие строки.

Это неприемлемо, поэтому приходится идти на компромисс.


Чему мы научились? Реакция должна использоватьnewПозвоните в класс (включая вывод бабела), но не долженnewВызовите общую функцию (включая вывод Babel) или стрелочную функцию, и не существует надежного метода их различения.

Если мы не можем решить общие проблемы, можем ли мы решить более конкретные проблемы??

Когда вы объявляете компонент с классом, вы можете захотеть расширитьReact.Componentвстроенные методы, такие какthis.setState().Мы можем обнаружить только больше, чем обнаружение всех CLASReact.Componentкомпоненты-потомки?

Спойлер: это именно то, что делает React.


может быть, еслиGreetingЭто компонент класса, который может быть обнаружен обычным методом и пройден тест.Greeting.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. Если не найдем, будем искать следующий в цепочкеprototype——fredПрототип прототипа и так далее.

Удивительно, что класс или функцияprototypeАтрибутыне будетУказывает на прототип для этого значения. Я не шучу.

function Person() {}

console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype

так__proto__.__proto__.__proto__Сравниватьprototype.prototype.prototypeБольше похоже на "цепочку прототипов". Мне потребовались годы, чтобы понять это.

затем функция или классprototypeКаковы свойства?Он доступен для всех классов или функцийnewобъект__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это не функцияfred.prototypeстановится неопределенным). Я думаю, что это главная причина, по которой опытные разработчики также неправильно понимают прототипы JavaScript.


Это длинная статья, да? Я бы сказал, что мы на 80%, давайте двигаться дальше.

когда мы пишемobj.foo, JavaScript на самом делеobj, obj.__proto__, obj.__proto__.__proto__Поиск поfoo, и так далее.

В классе вы не увидите этот механизм напрямую, ноextendsОн также реализован на основе этой классической цепочки прототипов. Вот как экземпляр React, определенный нашим классом, может получить что-то вродеsetStateПричина метода:

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начать, с__proto__Проверьте цепочку, чтобы убедиться, чтоGreetingрасширенныйReact.Component:

// `__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 делает это не так. 😳

Одна из проблем заключается в том, что в React проверяемые нами компоненты могут быть унаследованы отразноеРеагировать компонентыReact.Componentкопировать,instanceofРешение не работает для такого количества реплицированных компонентов React на странице. Эмпирически есть несколько причин, по которым многократное смешивание компонентов React в проекте не является хорошим выбором, и мы хотим по возможности избегать этого. (В Крючках мыможет понадобиться) реализует идею удаления дубликатов.

Существует также эвристика для обнаружения наличия на прототипеrenderметод. Однако в то времяне ясноКак будет развиваться API компонента. Каждое обнаружение увеличивает время обнаружения на единицу, и мы не хотим тратить здесь больше двух раз. и когдаrenderКогда это метод, определенный для экземпляра (например, определенный синтаксическим сахаром атрибута класса), этот метод бесполезен.

Поэтому реагироватьДобавить кК компоненту базового класса добавляется специальный флаг. 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передачаприбыть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 был еще превосходным) функция автоматической блокировки была включена по умолчанию, а фиктивные данные, сгенерированные Jest, игнорировали исходный тип данных, что приводило к сбою проверки React. Спасибо, Ле. . Шутка.

isReactComponentобнаружены в сегодняшнемВсе еще используется в React.

если у вас нет расширенияReact.Component, React не смотрит в прототипisReactComponent, он не будет рассматриваться как компонент класса. теперь ты знаешь почемуCannot call a class as a functionНеправильноЛучший ответиспользуетсяextends React.ComponentНу давай же. Наконец, мы также добавляемпредупреждать,когдаprototype.renderсуществует, ноprototype.isReactComponentПредупреждение выдается, когда он не существует.


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

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

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

Но если вам все еще интересно. . . Также было бы неплохо узнать, как это работает.

перевести оригиналHow Does React Tell a Class from a Function?