بهینه‌سازی کامپوننت‌های کاربردی و کلاس-محور

Optimization of Functional and Class-Oriented Components

06 آبان 1398
بهینه سازی کامپوننت های کاربردی و کلاس-محور

بهینه سازی کامپوننت های کلاس-محور با shouldComponentUpdate

بگذارید یک مثال از نقص برنامه مان برایتان بزنم. در فایل Persons.js از shouldComponentUpdate استفاده می کنیم و در هر شرایطی true را برمی گردانیم:

    shouldComponentUpdate(nextProps, nextState) {
        console.log('[Persons.js] shouldComponentUpdate');
        return true;
    }

یعنی هر زمانی که چیزی در این کامپوننت تغییر کرد، بروزرسانی (update) اتفاق می افتد. در حال حاضر کامپوننت Persons درون کامپوننت App.js قرار دارد:

    return (
      <div className={classes.App}>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ?
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            persons={this.state.persons}
            clicked={this.togglePersonsHandler} /> : null}
        {persons}
      </div>
    );

بنابراین اگر App.js نیز تغییر کند Persons.js نیز تغییر می کند. مشکل اینجاست که تغییر ایجاد شده در App.js ممکن است هیچ ربطی به Persons نداشته باشد، مثلا فقط cockpit حذف شود!

برای درک بهتر، برنامه را در مرورگر باز می کنیم و سپس کنسول را clear می کنیم تا کدی باقی نماند:

قبل از کلیک روی remove cockpit قسمت کنسول مرورگر را پاک کرده ایم تا فقط کد های جدید را ببینیم
قبل از کلیک روی remove cockpit قسمت کنسول مرورگر را پاک کرده ایم تا فقط کدهای جدید را ببینیم.

حالا اگر دکمه remove cockpit را بزنیم:

پس از حذف cockpit
پس از حذف cockpit

همانطور که می بینید Persons و تک تک Person ها دوباره render می شوند. بنابراین حالت ایده آل این است که جلوی این بروزرسانی و render چند باره و بی هدف را بگیریم. برای این کار به فایل Persons.js و متد shouldComponentUpdate می رویم. در این متد می توانیم با یک شرط ساده بررسی کنیم و ببینیم چه چیزی تغییر کرده است!

    shouldComponentUpdate(nextProps, nextState) {
        console.log('[Persons.js] shouldComponentUpdate');
        if (nextProps.persons !== this.props.person) {
            return true;
        } else {
            return false;
        }
    }

این کد می گوید persons را از nextProps بگیر و آن را با persons فعلی مقایسه کن. اگر props فعلی با props بعدی فرقی دارد آن را دوباره render کن (برگرداندن true) در غیر این صورت به آن دست نزن. حالا اگر به مرورگر برویم می بینیم که با حذف cockpit دیگر شاهد render شدن دوباره persons نخواهیم بود (البته خود shouldComponentUpdate اجرا خواهد شد تا شرط را چک کند). این قابلیت در برنامه های بزرگ بسیار کاربردی است و سرعت برنامه را شدیدا افزایش می دهد!

نکته مهمی را نباید فراموش کنید؛ Persons که در شرط بالا آن را چک کرده ایم (nextProps.persons) یک آرایه است و آرایه ها مانند اشیاء در جاوا اسکریپت از نوع reference-type هستند و در حافظه ذخیره می شوند. بنابراین چیزی که در متغیرهای ما ذخیره می شود تنها pointer یا نشانگری به محل ذخیره این اشیاء یا آرایه ها در حافظه هستند (قبلا به صورت مفصل در مورد reference-type ها صحبت کرده ایم). ما در اینجا فقط pointer را مقایسه می کنیم نه خود آرایه را! بنابراین اگر چیزی در Persons تغییر کند اما pointer مثل قبل باقی بماند دیگر این روش کار نخواهد کرد. با این حساب چرا کد ما کار می کند؟ دلیلش کد زیر است:

nameChangedHandler = (event, id) => {
    const personIndex = this.state.persons.findIndex(p => {
      return p.id === id;
    });

    const person = {
      ...this.state.persons[personIndex]
    };

    // const person = Object.assign({}, this.state.persons[personIndex]);

    person.name = event.target.value;

    const persons = [...this.state.persons];
    persons[personIndex] = person;

    this.setState({ persons: persons });
  }

ما در این کد که در App.js قرار دارد با استفاده از اپراتور... یک شیء و یک آرایه جدید ایجاد می کنیم:

    const persons = [...this.state.persons];

و

    const person = {
      ...this.state.persons[personIndex]
    };

از آنجایی که این شیء و آرایه جدید هستند، مکانی جدید برای آن ها در حافظه در نظر گرفته می شود که یعنی یک pointer جدید به آن ها داده می شود.

اگر بدون ایجاد شیء جدید، state (شیء اصلی) را ویرایش میکردیم شیء جدیدی تولید نمیشد بنابراین pointer جدیدی هم در کار نبود و دیگر shouldComponentUpdate ما کار نخواهد کرد.

اگر در مورد reference-type ها در جاوا اسکریپت آشنایی ندارید به مقالات قبلی برگشته و یا به این مقاله انگلیسی مراجعه کنید.

نکته: اگر در کروم از قسمت Dev tools روی علامت سه نقطه کلیک کرده و سپس از منوی ظاهر شده more tools و سپس rendering را انتخاب کرده و در آخر Paint flashing را فعال کنید، می توانید عناصری که render می شوند را با رنگ خاص مشاهده کنید. به شما کمک می کند که بفهمید کدام قسمت ها در حال render می باشند (البته آنچه که در DOM واقعی بروزرسانی می شود نه virtual DOM مربوط به react).

بهینه سازی کامپوننت های کاربردی با memo

shouldComponentUpdate یک متد بسیار عالی برای بهینه سازی کامپوننت ها می باشد اما تنها در کامپوننت های کلاس-محور در دسترس است. به نظر شما برای بهینه سازی کامپوننت های کاربردی چه باید کرد؟ در حال حاضر اگر در input های افراد چیزی تایپ کنیم، cockpit هم render می شود که یک عملیات بیهوده است چرا که ما اصلا به cockpit دست نزدیم و آن را تغییر نداده ایم.

به فایل Cockpit.js رفته و به کد های آن با دقت نگاه کنید؛ تنها عناصری که cockpit به صورت داخلی از آن ها استفاده می کند و ممکن است باعث re-render شدن شوند این موارد هستند:

  • persons.length
  • title
  • showPersons

بنابراین باید آن ها را کنترل کنیم چرا که هیچ ربطی به اسم افراد (تغییر با input) ندارند. یک راه عالی برای این کار استفاده از memo است؛ باید تمام کامپوننت خود را درون آن قرار دهیم:

export default React.memo(cockpit);

memo یک snapshot از این کامپوننت را ذخیره می کند و تنها در صورتی که ورودی آن (مانند props.title و ...) تغییر کند آن را re-render خواهد کرد. در غیر این صورت اگر ورودی ها تغییری نکند ولی کامپوننت پدر بخواهد آن را دوباره render کند، React همان نسخه ذخیره شده (snapshot) را به کامپوننت پدر خواهد داد.

در حال حاضر اگر به مرورگر برویم و دکمه Toggle Persons را کلیک کنیم، باز هم می بینیم که قسمت cockpit دوباره اجرا می شود. به نظر شما چرا؟ دلیلش این است که props.persons.length یکی از متغیرهای ما بود. درست است که فقط از length آن استفاده می کنیم اما react هنوز توانایی تشخیص این مورد را ندارد بنابراین برای تصحیح این مورد باید نحوه ارسال داده به Cockpit را تصحیح کنیم.

به فایل App.js و قسمت JSX آن بروید:

    return (
      <div className={classes.App}>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ?
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            persons={this.state.persons}
            clicked={this.togglePersonsHandler} /> : null}
        {persons}
      </div>
    );

به جای ارسال کل persons به cockpit می توانیم فقط همان length را ارسال کنیم:

    return (
      <div className={classes.App}>
        <button
          onClick={() => { this.setState({ showCockpit: false }) }}
        >Remove Cockpit</button>
        {this.state.showCockpit ?
          <Cockpit
            title={this.props.appTitle}
            showPersons={this.state.showPersons}
            personsLength={this.state.persons.length}
            clicked={this.togglePersonsHandler} /> : null}
        {persons}
      </div>
    );

حالا به فایل Cockpit.js برگردید و تمامی موارد props.persons را به props.personsLength تغییر دهید:

    if (props.personsLength <= 2) {
        assignedClasses.push(classes.red); // classes = ['red']
    }
    if (props.personsLength <= 1) {
        assignedClasses.push(classes.bold); // classes = ['red', 'bold']
    }

حالا اگر به مرورگر برویم و در یکی از input ها تایپ کنیم متوجه می شویم که هیچ پیامی در کنسول از طرف cockpit.js نمایش داده نمی شود.

دانلود کدهای این جلسه

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.