Odată cu lansarea React Hooks, am văzut o mulțime de posturi comparând componentele clasei cu componentele funcționale. Componentele funcționale nu sunt nimic nou în React, cu toate acestea, înainte de versiunea 16.8.0, nu era posibil să se creeze o componentă de stare cu acces la cârligele ciclului de viață folosind doar o funcție. Sau a fost?

Spune-mi un pedant (mulți oameni o fac deja!), Dar când vorbim despre componentele clasei, vorbim tehnic despre componentele create de funcții. În această postare aș dori să folosesc React pentru a demonstra ce se întâmplă de fapt atunci când scriem o clasă în JavaScript.

Clase vs Funcții

În primul rând, aș dori să arăt foarte pe scurt modul în care ceea ce sunt denumite în mod obișnuit componente funcționale și componente de clasă se raportează între ele. Iată o componentă simplă scrisă ca o clasă:

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

Și aici este scris ca o funcție:

function Hello() {
  return <p>Hello!</p>
}

Observați că componenta funcțională este doar o metodă de redare. Din această cauză, aceste componente nu au putut niciodată să-și păstreze propria stare sau să efectueze efecte secundare în puncte din timpul ciclului lor de viață. De la React 16.8.0 a fost posibil să creăm componente funcționale stabile datorită cârligelor, ceea ce înseamnă că putem transforma o componentă de acest fel:

class Hello extends React.Component {
  
  state = {
    sayHello: false
  }

  componentDidMount = () => {
    fetch('greet')
      .then(response => response.json())
      .then(data => this.setState({ sayHello: data.sayHello });
  }

  render = () => {
    const { sayHello } = this.state;
    const { name } = this.props;

    return sayHello ? <p>{`Hello ${name}!`}</p> : null;
  }
}

Intr-o componenta functionala ca aceasta:

function Hello({ name }) {

  const [sayHello, setSayHello] = useState(false);

  useEffect(() => {
    fetch('greet')
      .then(response => response.json())
      .then(data => setSayHello(data.sayHello));
  }, []);

  return sayHello ? <p>{`Hello ${name}!`}</p> : null;
}

Scopul acestui articol nu este să argumentăm că una este mai bună decât cealaltă, deoarece există deja sute de postări pe acest subiect! Motivul pentru care sunt prezentate cele două componente de mai sus este astfel încât să putem fi clari despre ce face React cu ele.

În cazul componentei clasei, React creează o instanță a clasei folosind new cuvânt cheie:

const instance = new Component(props);

Această instanță este un obiect. Când spunem că o componentă este o clasă, ceea ce vrem să spunem este că este un obiect. Această nouă componentă obiect poate avea propria sa stare și metode, dintre care unele pot fi metode ale ciclului de viață (randare, componentDidMount etc.) pe care React le va apela în punctele corespunzătoare pe durata vieții aplicației.

Cu o componentă funcțională, React o numește doar ca o funcție obișnuită (deoarece este o funcție obișnuită!) Și returnează fie HTML, fie mai multe componente React.

Metodele cu care să gestionați starea componentei și să declanșeze efectele în puncte din timpul ciclului de viață al componentei trebuie acum importate dacă sunt necesare. Acestea funcționează în întregime pe baza ordinii în care sunt numite de fiecare componentă care le folosește, deoarece nu știu ce componentă le-a numit. Acesta este motivul pentru care puteți apela cârlige doar la nivelul superior al componentei și acestea nu pot fi apelate condiționat.

Funcția constructor

JavaScript nu are cursuri. Știu că se pare că are cursuri, tocmai am scris două! Dar sub capotă JavaScript nu este un limbaj bazat pe clase, este bazat pe prototip. Clasele au fost adăugate cu specificația ECMAScript 2015 (denumită și ES6) și sunt doar o sintaxă mai curată pentru funcționalitatea existentă.

Să încercăm să rescriem o componentă a clasei React fără a utiliza sintaxa clasei. Iată componenta pe care urmează să o recreăm:

class Counter extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    const { count } = this.state;
    this.setState({ count: count + 1 });
  }

  render() {
    const { count } = this.state;
    return (
      <>
        <button onClick={this.handleClick}>+1</button>
        <p>{count}</p>
      </>
    );
  }

}

Aceasta redă un buton care mărește un contor atunci când faceți clic, este un clasic! Primul lucru pe care trebuie să-l creăm este funcția constructor, aceasta va efectua aceleași acțiuni ca constructor metoda din clasa noastră funcționează în afară de apelul către super pentru că acesta este un lucru numai pentru clasă.

function Counter(props) {
  this.state = {
    count: 0
  }
  this.handleClick = this.handleClick.bind(this);
}

Aceasta este funcția pe care React o va apela cu new cuvânt cheie. Când se apelează o funcție cu new este tratat ca o funcție constructor; se creează un nou obiect, this variabila este îndreptată către ea și funcția este executată cu noul obiect fiind folosit oriunde this e mentionat.

Apoi, trebuie să găsim o casă pentru render și handleClick metode și pentru asta trebuie să vorbim despre lanțul prototip.

Lanțul prototip

JavaScript permite moștenirea proprietăților și metodelor între obiecte prin ceva cunoscut sub numele de lanț prototip.

Ei bine, spun moștenire, dar de fapt mă refer la delegare. Spre deosebire de alte limbi cu clase, unde proprietățile sunt copiate dintr-o clasă în instanțele sale, obiectele JavaScript au o legătură prototip internă care indică un alt obiect. Când apelați o metodă sau încercați să accesați o proprietate pe un obiect, JavaScript verifică mai întâi proprietatea obiectului în sine. Dacă nu îl găsește acolo, atunci verifică prototipul obiectului (legătura cu celălalt obiect). Dacă tot nu îl poate găsi, atunci verifică prototipul prototipului și așa mai departe în lanț până când îl găsește sau rămâne fără prototipuri de verificat.

În general vorbind, toate obiectele din JavaScript au Object în partea de sus a lanțului lor de prototip; acesta este modul în care aveți acces la metode precum toString și hasOwnProperty pe toate obiectele. Lanțul se termină când se ajunge la un obiect cu null ca prototip, acesta este în mod normal la Object.

Să încercăm să clarificăm lucrurile cu un exemplu.

const parentObject = { name: 'parent' };
const childObject = Object.create(parentObject, { name: { value: 'child' } });
console.log(childObject);

Mai întâi creăm parentObject. Deoarece am folosit sintaxa literală a obiectului la care va fi legat acest obiect Object. Apoi vom folosi Object.create pentru a crea un obiect nou folosind parentObject ca prototip al acestuia.

Acum, când folosim console.log pentru a tipări noastre childObject ar trebui să vedem:

ieșire consolă a childObject

Obiectul are două proprietăți, există name proprietate pe care tocmai am stabilit-o și __proto___ proprietate. __proto__ nu este o proprietate reală ca name, este o proprietate de acces la prototipul intern al obiectului. Le putem extinde pentru a vedea lanțul nostru de prototipuri:

ieșire extinsă a childObject

Primul __proto___ conține conținutul parentObject care are a sa __proto___ care conține conținutul Object. Acestea sunt toate proprietățile și metodele disponibile childObject.

Poate fi destul de confuz faptul că prototipurile se găsesc pe o proprietate numită __proto__! Este important să ne dăm seama de asta __proto__ este doar o referință la obiectul legat. Dacă folosești Object.create la fel ca mai sus, obiectul legat poate fi orice alegeți, dacă utilizați new cuvânt cheie pentru a apela o funcție constructor, atunci această legătură se întâmplă automat cu funcția constructor prototype proprietate.

Ok, înapoi la componenta noastră. Deoarece React ne cheamă funcția cu new cuvânt cheie, știm acum că pentru a pune la dispoziție metodele din lanțul prototip al componentei noastre, trebuie doar să le adăugăm la prototype proprietatea funcției constructor, astfel:

Counter.prototype.render = function() {
  const { count } = this.state;
  return (
    <>
      <button onClick={this.handleClick}>+1</button>
      <p>{count}</p>
    </>
  );
},

Counter.prototype.handleClick = function () {
  const { count } = this.state;
  this.setState({ count: count + 1 });
}

Metode statice

Acesta pare un moment bun pentru a menționa metodele statice. Uneori s-ar putea să doriți să creați o funcție care efectuează o acțiune care se referă la instanțele pe care le creați – dar nu are sens ca funcția să fie disponibilă pe fiecare obiect this. Atunci când sunt utilizate cu clase, acestea se numesc metode statice. Nu sunt sigur dacă au un nume atunci când nu sunt folosiți la cursuri!

Nu am folosit metode statice în exemplul nostru, dar React are câteva metode de ciclu de viață statice și am folosit una mai devreme cu Object.create. Este ușor să declarați o metodă statică pe o clasă, trebuie doar să prefixați metoda cu static cuvânt cheie:

class Example {
  static staticMethod() {
    console.log('this is a static method');
  }
}

Și este la fel de ușor să adăugați una la o funcție constructor:

function Example() {}
Example.staticMethod = function() { 
  console.log('this is a static method');
}

În ambele cazuri, numiți funcția astfel:

Example.staticMethod()

Extinderea React.Component

Componenta noastră este aproape gata, mai sunt doar două probleme de rezolvat. Prima problemă este că React trebuie să poată stabili dacă funcția noastră este o funcție constructor sau doar o funcție obișnuită. Acest lucru se datorează faptului că trebuie să știe dacă îl apelează cu new cuvânt cheie sau nu.

Dan Abramov a scris o postare grozavă pe blog despre asta, dar pentru a scurta o poveste lungă, React caută o proprietate pe componenta numită isReactComponent. Am putea ocoli acest lucru adăugând isReactComponent: {} la Counter.prototype (Știu, te-ai aștepta să fie un boolean, dar isReactComponentValoarea lui este un obiect gol. Va trebui să-i citiți articolul dacă doriți să știți de ce!), Dar asta ar fi doar înșelarea sistemului și nu ar rezolva problema numărul doi.

În handleClick metoda la care apelăm this.setState. Această metodă nu face parte din componenta noastră, este „moștenită” de la React.Component impreuna cu isReactComponent. Dacă vă amintiți secțiunea lanțului prototip de mai devreme, dorim ca instanța noastră componentă să moștenească mai întâi metodele Counter.prototype și apoi metodele din React.Component. Aceasta înseamnă că dorim să conectăm proprietățile React.Component.prototype la Counter.prototype.__proto__.

Din fericire există o metodă activată Object ceea ce ne poate ajuta în acest sens:

Object.setPrototypeOf(Counter.prototype, React.Component.prototype);

Funcționează!

Asta este tot ce trebuie să facem pentru ca această componentă să funcționeze cu React fără a utiliza sintaxa clasei. Iată codul pentru componentă într-un singur loc, dacă doriți să îl copiați și să-l încercați singur:

function Counter(props) {
  this.state = {
    count: 0
  };
  this.handleClick = this.handleClick.bind(this);
}

Counter.prototype.render = function() {
  const { count } = this.state;
  return (
    <>
      <button onClick={this.handleClick}>+1</button>
      <p>{count}</p>
    </>
  );
}

Counter.prototype.handleClick = function() {
  const { count } = this.state;
  this.setState({ count: count + 1 });
}

Object.setPrototypeOf(Counter.prototype, React.Component.prototype);

După cum puteți vedea, nu este la fel de frumos să priviți ca înainte. În plus față de a face JavaScript mai accesibil dezvoltatorilor obișnuiți să lucreze cu limbaje tradiționale bazate pe clase, sintaxa clasei face, de asemenea, codul mult mai ușor de citit.

Nu vă sugerez să începeți să vă scrieți componentele React în acest mod (de fapt, aș descuraja-l în mod activ!). M-am gândit doar că va fi un exercițiu interesant care va oferi o oarecare înțelegere a modului în care funcționează moștenirea JavaScript.


Deși nu este nevoie să înțelegeți aceste lucruri pentru a scrie componente React, cu siguranță nu poate face rău. Mă aștept să apară ocazii când remediați un bug dificil, în care înțelegerea modului în care funcționează moștenirea prototipală va face diferența.

Sper că ați găsit acest articol interesant și / sau plăcut. Puteți găsi mai multe postări pe care le-am scris pe blogul meu la hellocode.dev. Mulțumesc.