مفهوم Referential Equality در React

Referential Equality چیست و چه کاربردی دارد؟ | Referential Equality در ری‌اکت | Referential Equality در React

 مفهوم Referential Equality در React -  آرین حسینی  - Arian Hosseini
مفهوم Referential Equality در React - آرین حسینی - Arian Hosseini


سلام، ممکنه در حال یادگیری کاربرد های هوک useMemo, useCallback یا هوک های دیگر بوده باشید و کلمه Referential Equality به گوشتون خورده باشه، همونطور که ممکنه بدونید دومین کاربرد هوک useMemo در React همین مفهوم Referential Equality هست که در این مقاله قراره با هم بررسی کنیم؛

پس اگر علاقه دارید بیشتر در مورد Referential Equality بدونید، تا آخر این مقاله همراه من باشید...

قبل از اینکه بخوام در مورد این مفهوم صحبت کنم، دوست دارم یکسری مقدمه و مفاهیمی که در ادامه کار نیاز داریم بدونیم رو همینجا توضیح بدم که نیاز به مطالعه مقالات و منابع مظاعف نداشته باشید، اگر شما این مفاهیم رو میدونید، میتونید این قسمت رو Skip کنید.

در ضمن این مقاله یک مقداری طولانی هست پس حتما حین خوندنش باید پر انرژی و سر حال باشید که بتونید کامل تا آخر دنبال کنید و مباحث رو درک کنید، اگر الان شرایطش رو ندارید میتونید براحتی این صفحه رو bookmark کنید و بعدا بهش مراجعه کنید. قهوه فراموش نشه :)

بریم که شروع کنیم:

بخش اول:‌ Primitive and Reference Data Types

در زبان JavaScript ما دو نوع Data type داریم:

1. Primitive Data types || انواع داده های اولیه

2. Reference Data types

خودتون رو زیاد درگیر ترجمه فارسی این کلمات نکنید و سعی کنید به همون شکل انگلیسی اون ها رو به خاطر بسپارید.

به طور کلی زمانی که شما یک متغیر در JavaScript تعریف میکنید، اون متغیر میتونه یکی از این نوع داده ها یا data type هارو داخل خودش ذخیره کنه،

اگر مقداری که داخل متغیر ذخیره میکنید number, string, boolean, undefined, null یا یک symbol باشه، شما در واقع یک نوع داده Primitive یا یک Primitive Data type رو داخل اون متغیر ذخیره کردید،

ولی اگر اون مقدار یک Object, Array, Function یا هر نوع داده دیگری باشه، شما در واقع یک نوع داده Reference یا Reference Data type رو ذخیره کردید. (بهتر بگم هر چیزی که از نوع Object در JavaScript هست مثل فانکشن ها و آرایه ها که میتونید با operator یا عملگر typeof اون رو چک کنید)

مثال:‌

const age = 20; // primitive
const name = &quotArian Hosseini&quot // primitive
const isLoggedIn = false; // primitive
const user = undefined; // primitive
const response = null; // primitive
const counter = Symbol(&quotcounter&quot); // primitive

const person = { firstName: &quotArian&quot  }; // reference
const coaches = [&quotClarian&quot,  &quotMax&quot]; // reference
const getSomething = () => {}; // reference

وقتی شما به کد بالا نگاه می‌کنید، خب تمامی این نوع داده هایی که در متغیر ها ذخیره شده، یک شکل به نظر میاد اما تفاوت به قول انگلیسی ها under the hood هست یا یک جورایی در پشت صحنه و در اعماق ماجرا، منظورم چیه؟!

ساده تر بگم مقادیری که Primitive هستند، به شکلی همون مقدار داخل اون متغیر ذخیره میشه اما مقادیری که از نوع داده Reference هستند، آدرسشون (Memory Address) داخل اون متغیر ذخیره میشه نه خود مقدار، در واقع آدرس اون نقطه‌ای در حافظه که این مقدار داخلش ذخیره شده!

یک مثال میزنم که این مورد رو خیلی بهتر درک کنید:

let lastName = &quotHosseini&quot
let displayName = lastName;

lastName = &quotNorth&quot

console.log(lastName); // &quotNorth&quot
console.log(displayName); // 'Hosseini'

چی شد؟ در مثال بالا من یک متغیر تعریف کردم با نام lastName و مقدار "Hosseini" رو که یک string هست رو داخلش ذخیره کردم و بعد یک متغیر جدید ساختم با نام displayName و مقدارش رو برابر قرار دادم با مقدار متغیر lastName، خیلی ساده بود نه؟!

حالا در خط بعدی اومدم و مقدار اون متغیر lastName که اول تعریف کرده بودم رو تغییر دادم به رشته عددی "North" (یک نام خانوادگی آمریکایی هست با اصالت British)

تا اینجا احتمالا خودتون تونستید حدس بزنید که چی شد! اگر الان مقدار lastName رو لاگ بگیریم، مقداری که برای ما بر می گردونه "North" هست که خب طبیعی هم هست، من یک متغیر تعریف کردم و چند خط بعد یا در ادامه کد هام اومدم مقدارش رو تغییر دادم، اگر الان لاگ بگیرم، قاعدتا باید مقداری که تغییر داده بودم رو مشاهده کنم؛

اما اگر displayName رو لاگ بگیرم، مقدار "Hosseini" رو برای من برمیگردونه! چرا؟ فکر میکنم واضح باشه، زمانی که من displayName رو برابر قرار دادم با lastName، مقدار lastName همچنان Hosseini بود پس قطعا همین مقدار داخل displayName هم ذخیره خواهد شد، حالا اینکه من بعدا در لاین های بعدی مقدار lastName رو تغییر دادم، دیگه هیچ ربطی به displayName نداره! و این دو تا کاملا مستقل از همدیگر هستند!

این مثال رو که برای یک نوع داده Primitive که در اینجا string بود رو با هم دیدیم (برای تمامی Primitive Data type های دیگه هم صدق میکنه) حالا بریم یک مثال دیگه در مورد Reference data type ها داشته باشیم؛

let person1 = { firstName: &quotArian&quot, lastName: &quotHosseini&quot };
let person2 = person1;

person2.lastName = &quotNorth&quot

console.log(person1.lastName); // North
console.log(person2.lastName); // North

نتیجه جالب شد!‌ اینطور نیست؟! چطور هر دو پراپرتی lastName برای هر دو آبجکت برابر با یک مقدار هستند؟! اجازه بدیم از اول توضیح بدم:

اول اینجا من یک متغیر تعریف کردم با نام person1 که برابر با یک آبجکتی هست که شامل دو تا پراپرتی firstName و lastName میشه که مساوی با نام بنده هستند، در لاین بعدی من یک متغیر جدید ساختم با نام person2 که مقدارش رو برابر قرار دادم با مقداری که در متغیر person1 داشتم!

همونطور که اوایل مقاله توضیح دادم Object ها یک نوع داده reference هستند، یعنی وقتی شما یک متغیر تعریف میکنید و مقدارش رو برابر با یک Object میذارید، اون Object شما در یک نقطه از حافظه ذخیره میشه و فقط آدرسش (Memory Address) هست که داخل متغیر ذخیره میشه،

Source: Mosh Hamedani Youtube Video
Source: Mosh Hamedani Youtube Video

که در مثال بالا person1 صرفا حاوی آدرس نقطه‌ای از حافظه هست که اون آبجکت در اون خانه‌ی حافظه ذخیره شده و قرار داره، بهتر بگم person1 داره اشاره میکنه به آدرس نقطه‌ای از حافظه که آبجکت من در اونجا ذخیره شده!!!

به طور مثال برای اینکه بهتر بتونید این مورد رو به خاطر بسپارید یک چنین چیزی رو تصور کنید (اصلا درست نیست صرفا برای اینکه راحت تر بتونید تصور کنید و این رو به خاطر بسپارید)

person1 -------->>>>> 962012d09b817

(مثلا فرض کنید این عبارت 962012d09b817 آدرس اون نقطه در حافظه هست)

حالا این نقطه 962012d09b817 از حافظه رو اگر بهش رجوع کنیم، میبینیم که آبجکت ما اونجا هست‌ (مشابه تصویری که در بالا میبینید، x و y دو تا متغیر هستند که جفتشون اشاره میکنند به یک نقطه از Memory که آبجکت اونجا قرار داره و صرفا آدرس اون نقطه در حافظه هست که در این متغیر ها ذخیره شده)

خب الان پس با این اوصاف مقدار person2 که برابر قرار داده بودم با مقدار person1 چیه؟ دقیقا! آدرس اون نقطه از حافظه که حالا مثلا در مثال ما اینجا شما اینطور تصور کنید که یک همچین آدرسی هست 962012d09b817.

پس در اینجا متغیر های person1 و person2 به طور همزمان دارند به یک نقطه از حافظه اشاره میکنند! یا حاوی یک آدرس هستند که اون آدرس م ارو به اون نقطه از حافظه میبره که آبجکت ما اونجا قرار داره...

به همین راحتی! پس وقتی که من در لاین بعدی

person2.lastName = &quotNorth&quot

رو مینویسم و مقدار پراپرتی lastName متغیر person2 رو تغییر میدم، در واقع دارم مقدار پراپرتی همون آبجکتی رو تغییر میدم که person1 هم داره بهش اشاره میکنه! person1 و person2 شامل دوتا آبجکت مجزا و متفاوت نیستند! هر دو یک آبجکت هستند و این دو متغیر دارند به این تک آبجکت اشاره می‌کنند، پس فرقی نمیکنه شما از طریق person2 پراپرتی های اون آبجکت رو تغییر بدید یا از طریق person1، در هر دو حالت اون آبجکت تغییر میکنه و اگر شما person1 و person2 رو لاگ بگیرید،‌ میبینید که مقداری که جفتشون برمیگردونند، یکی هست و تفاوتی نداره. (بر خلاف چیزی که در Primitive Data Types دیدیم)

مقایسه (Comparison) Primitive Data Type ها و Reference Data Type ها

این قسمت بسیار مهم هست، حتما دقت کنید!

اگر شما بخواهید با operator یا عملگر === که از انواع Comparison Operators در JavaScript هست دو تا متغیری که حاوی یک مقدار primitive یکسان هستند رو با هم دیگه مقایسه کنید، چه نتیجه‌ای می‌گیرید؟! برای مقادیری که از یک Reference data type یا یک نوع داده Reference هستند مثل آرایه ها چطور؟

بزارید چند تا مثال بزنم!

const myName = &quotArian&quot
const yourName = &quotArian&quot

console.log(myName === yourName); // true

در این مثال من اومدم دو تا string که از انواع داده های Primitive هستند رو با هم مقایسه کردم، همونطور که میبینید به من true برگشت داده؟ چرا چون عملگر === در جاوااسکریپت primitive ها رو بر اساس value یا مقدارشون مقایسه میکنه! (Comparison by value)

یعنی میگه خب من داخل myName یک string دارم که Arian هست در yourName هم یک string دارم که Arian هست، این دو تا مقدار رو با هم مقایسه میکنه و میگه بله هر دو یکسان هستند و true بر میگردونه؛

حالا به این مثال توجه کنید:

const BMW = {
	type: &quotCompany&quot,
	location: &quotGermany&quot,
};
const MercedesBenz = {
	type: &quotCompany&quot,
        location: &quotGermany&quot,
};

console.log(BMW === MercedesBenz); // false

در Reference ها (انواع داده های Reference مثل object ها و...) این عملگر === در جاوااسکریپت مقادیر رو بر اساس رفرنسشون (reference) یا نقطه‌ای که در حافظه هستند، مکانی که در حافظه قرار دارند با هم مقایسه میکنه! (Comparison by reference)

در این مثال میتونید ببینید که من دوتا آبجکت یکسان دارم که هیچ تفاوتی باهم ندارند، تمامی پراپرتی هاشون با هم برابره اما زمانی که این دو رو با هم مقایسه میکنم، چون که جاوااسکریپت این دو رو بر اساس نقطه‌ای که در حافظه هستند با هم مقایسه میکنه، میبینه که خب آبجکت اول که در متغیر BMW هست مثلا در فلان نقطه از حافظه ذخیره شده و آدرسش یک چیزی هست اما آبجکت دوم که داخل متغیر MercedesBenz ذخیره شده در فلان نقطه دیگر از حافظه هست، پس در نتیجه با هم برابر نیستند و false.

  • Primitive Data Types -> Comparison by value
  • Reference Data Types -> Comparison by reference
دو تا آبجکت یکسان که در مکان های مختلفی از حافظه قرار دارند
دو تا آبجکت یکسان که در مکان های مختلفی از حافظه قرار دارند

در تصویر بالا مشاهده میکنید که ما دو تا آبجکت یکسان داریم، اما اگر این هارو با هم مقایسه کنیم، نتیجه false هست، دلیل رو هم که بالاتر توضیح دادم، اینکه Reference Data type ها بر اساس آدرس مکانشون در حافظه مقایسه میشن نه بر اساس مقدار یا value شون،

console.log(object1 === object2); // false 

نتیجه:

پس تنها زمانی دو تا متغیر که حاوی یک نوع مقدار Reference هستند با هم مساوی خواهند شد که هر دو به یک نقطه‌ای از حافظه اشاره کنند در غیر این صورت مساوی و برابر نیستند حتی اگر مقدارشون کاملا با هم برابر باشد. (مثال بالا در مورد پراپرتی های آبجکت ها - بر خلاف Primitive Data Types)

اما متغیر هایی که حاوی یک نوع مقدار Primitive هستند، تنها زمانی با هم مساوی و برابر خواهند شد که مقدار یا value هر دو با هم کاملا برابر باشد.


درست شد؟ امیدوارم که تونسته باشم به خوبی این مبحث رو بهتون انتقال بدم، اگر متوجه نشدید، بدونید قطعا من خیلی بد توضیح دادم و این مشکل شما نیست، از این بابت هم عذر میخوام.

بخش دوم: به صورت کلی React چطور فرآیند re-render رو تعیین میکنه؟

همونطور که میدونید، در React تنها زمانی کامپوننت رندر مجدد خواهد شد که تغییری در state یا props های اون کامپوننت به وجود بیاد، پس برای مثال اگر زمانی مقدار state یک کامپوننت تغییر کنه، React.js میاد اون کامپوننت رو re-render میکنه.

برای درک بهتر به مثال زیر توجه کنید:

const [name, setName] = useState(&quot&quot);

const handleChangeName = (newName) => {
     setName(newName);
};

در مثال بالا فرض کنید من یک Event Handler دارم که هر زمان شما اون رو call کنید و اسم جدید رو به عنوان argument بهش پاس بدید، مقدار name رو در state آپدیت میکنه و name برابر میشه با نام جدیدی (newName) که در نظر گرفتید؛

حالا React اینجا میاد مقدار نام جدید رو با مقدار name قبلی در state مقایسه میکنه (به واسطه الگوریتم خاص خودش که داره و البته به کمک متد Object.is) و در صورتی که اینها با هم تفاوت داشتند، re-render انجام میشه در غیر این صورت اگر برابر بودند که اتفاق خاصی نمی‌افته.

گفتم که React در پروسه مقایسه کردن و الگوریتم خودش برای اینکار از built-in متد () Object.is استفاده میکنه، این متد دو تا argument قبول میکنه، این دو تا رو با هم مقایسه میکنه و یک boolean بر میگردونه که آیا با هم برابر بودند یا خیر،

نکته:‌ متد Object.is با عملگر === در جاوااسکریپت یکسان نیست و یکسری تفاوت هایی با هم دارند، برای اطلاعات بیشتر میتونید این لینک رو بررسی کنید.

Syntax: Object.is(value1, value2)

متد Object.is در JavaScript تعیین می کنه که آیا دو مقدار یکسان هستند یا خیر در صورتی که:

  • هر دو مقدار undefined یا null باشند،
  • هر دو مقدار true یا false باشند،
  • هر دو مقدار یک string با length، ترتیب و کاراکتر های کاملا یکسان باشند،
  • هر دو مقدار number باشند با یک مقدار یکسان یا NaN
  • هر دو مقدار یک Object یکسان باشند که در یک نقطه از حافظه قرار دارند (بالاتر توضیح دادم)،

پس React این قوانین رو برای رندر مجدد کامپوننت ها در زمانی که تغییری در state یا props ایجاد میشه، اعمال می کنه.


خب این موارد رو گوشه‌ی ذهنتون داشته باشید حالا میریم سراغ مشاهده این مفهوم Referential Equality در کد و عمل!

بخش سوم: Referential Equality - نحوه مقایسه در عمل!

کد زیر رو در نظر بگیرید، یک کامپوننت ساده با نام App:

https://gist.github.com/aryanhosseini/b6ece338d9164c70c48248c86908db46


خب همونطور که در کد بالا مشاهده می کنید، من یک state variable ساختم با نام myself که به صورت پیش فرض با یک آبجکت شامل دو پراپرتی name و age اون رو مقدار دهی کردم، در لاین بعدی یک Event handler تعریف کردم با نام changeNameToClarian که کارش اینه بیاد و پراپرتی های مقدار myself رو در state تغییر بده (بله! میدونم نباید state و props رو به صورت مستقیم تغییر بدیم، اینجا صرفا برای آموزش هست)، یک button در صفحه قرار دادم با که هر موقع کلیک شد، این فانکشن رو call میکنه و باید نام رو از Arian به Clarian تغییر بده همینطور مقدار سن رو، اگر موافق هستید بریم نتیجه رو بررسی کنیم؛


خروجی مثال کد بالا
خروجی مثال کد بالا

در اینجا زمانی که من روی button "نام من رو تغییر بده" کلیک میکنم، طبق انتظاراتون باید مقدار state رو تغییر بده و رندر مجدد انجام بشه و در نهایت UI ما آپدیت بشه، اما این اتفاق نمی‌افته! چرا؟

حتی اگر console رو مشاهده کنید،‌ میبینید که آبجکت myself که در لاین 9 لاگ گرفتیم رو هم با مقادیر جدید نشون میده، اما چرا این آبجکت جایگزین initial value قبلی state مون نشده و مقدار همچنان Arian و 20 هست؟!

مقدار myself که در لاین 9 لاگ گرفتیم!
مقدار myself که در لاین 9 لاگ گرفتیم!

اگر متعجب شدید که چرا کامپوننت re-render نشده، باید بگم که بهتره یک نگاهی دوباره به قوانین که متد Object.is برای مقایسه کردن داشت بندازید:

5. زمانی دو Object برابر در نظر گرفته میشوند که هر دو در یک نقطه از حافظه باشند و یک Memory Address داشته باشند، در غیر اینصورت برابر نخواهند بود حتی اگر تمامی پراپرتی هاشون با هم برابر باشد.

خب حالا با دونستن این مورد، میتونیم بهتر مشکل رو درک کنیم:

const changeNameToClarian = () => { 
    myself.name = &quotClarian&quot
    myself.age = 25; 
    console.log(myself); 
    setMyself(myself);
 };

در کد بالا میبینیم که مقادیر جدید که Clarian و 25 باشند به اصطلاح assign خواهند شد به عنوان مقادیر پراپرتی های آبجکت (منظور جایگزین مقادیر پیش فرض قبلی خواهند شد)، حالا زمانی که React میاد و بر اساس الگوریتم خودش به واسطه متد Object.is میخواد این دو آبجکت تغییر یافته رو با آبجکتی که به عنوان initial value ابتدا برای state تعریف شده بود، مقایسه کنه، میبینه که خب مقدار این آبجکت همچنان برابر با اون همون initial value هست و فرقی نکرده، در واقع در پشت صحنه، هر دو مقدار دارن اشاره میکنند یا به اصطلاح پوینت (point) میکنند به یک مکان در حافظه که باعث میشه در کل یکسان و مساوی تلقی بشن!!!

به این فرآیند به اصطلاح Referential Equality گفته میشه ، به این دلیل که Object ها بر اساس مکان حافظه شان (Memory Location) برابر و مساوی در نظر گرفته می شوند و نه بر اساس مقادیرشان (پراپرتی ها)

داخل پرانتز این رو هم بگم که این کلمه در فارسی "برابری ارجاعی" ترجمه شده که به نظرم بهتره معادل انگلیسی رو یاد بگیرید و زیاد خودتون رو درگیر این ترجمه ها نکنید!

میدونم ممکنه یکم درکش در ابتدا سخت باشه، اما مطمئن باشید به مرور قطعا کامل درک خواهید کرد.

اجازه بدید یک بار دیگه خیلی ساده و راحت توضیح میدم: مشکل ما این بود که درسته که ما اومدیم myself.name و myself.age رو تغییر دادیم، اما این تغییر باعث ایجاد آبجکت جدیدی که نمیشه و این تغییرات روی همون آبجکتی اعمال میشه که در نقطه یکسان از حافظه قرار داشت، دقیقا برابر با همون مکان آبجکتی که در state ابتدا به عنوان initial value تعریف کردیم! یعنی ما صرفا پراپرتی های اون آبجکت رو درسته در واقعیت تغییر دادیم اما متد Object.is که اینو نمیفهمه!!! این متد میگه اوکی اون آبجکت اولیه بود که داخل state به عنوان initial value ست کردیم مثلا در نقطه 5sdf4 از حافظه قرار داره، و این آبجکت جدید هم که اومدیم پراپرتی هاش رو در متد changeNameToClarian ویرایش کردیم در نقطه 5sdf4 از حافظه قرار داره، خب پس این متد Object.is خیلی شیرین به این نتیجه میرسه که این دوتا آبجکت هیچ تفاوتی با هم ندارند و به React میگه که رندر مجدد بی رندر مجدد و از این خبرا اینجا نیست...! درسته شد؟!


حالا راه حلش چیه؟

خیلی ساده باید کاری کنیم که یک آبجکت جدید ساخته باشه که Memory Address‌اش برابر با Memory Address اون آبجکت اولیه در state نباشه! همین! حالا یکی از روش ها به این صورت هست:

const changeNameToClarian = () => {
    myself.name = &quotClarian&quot
    myself.age = 25;
    console.log(myself);
    setMyself({ ...myself });
};

مشاهده میکنید که ما یک آبجکت جدید به setMyself پاس دادیم که در یک نقطه دیگری از حافظه قرار داره و با spread operator اومدیم پراپرتی های آبجکت myself که تغییرش داده بودیم رو استخراج کردیم و به عنوان پراپرتی های این آبجکت جدید ست کردیم و عملا دیگه خبری از Referential Equality اینجا نیست چون ما دو تا آبجکت مستقل از هم داریم که الان React میتونه این رو متوجه بشه.

مشخصات با موفقیت تغییر کرد و رندر مجدد انجام شد!
مشخصات با موفقیت تغییر کرد و رندر مجدد انجام شد!

یکی از دلایلی که state رو نباید به صورت مستقیم ویرایش کنیم، همین هست چون React دیگه کنترل وضعیت از دستش خارج میشه و دیگه نمیتونه پیگیر تغییرات state باشه که بعد بخواد re-render انجام بده.


نتیجه گیری نهایی:

این بسیار مهم هست که ما بدونیم آبجکت ها چطور و کجا ذخیره میشن و اینکه React چطور میاد مشخص میکنه که الان باید این کامیپوننت re-render بشه یا نه، بنابراین، در مواردی که کامپوننت re-render نمیشن در زمانیکه که state شون تغییر کرده، یک راه حل این هست که مطمئن بشیم Object ها با هم برابر نیستند (یعنی هر دو آبجکت در نقاط مختلفی از حافظه قرار دارند)، این مورد از به وجود اومدن یکسری باگ های احتمالی در کد جلوگیری میکنه و در روند توسعه اپلیکیشن به ما کمک میکنه.


خب امیدوارم که این مقاله تونسته باشه کمکتون کنه، میدونم خیلی جاهاش ممکنه براتون گنگ بوده باشه یا اشکلاتی درش وجود داشته باشه چون یک مقداری توضیح دادنش توی متن برام سخت بود، مخصوصا اینکه این اولین مقاله‌ای هست که منتشر میکنم علاوه بر اینکه خیلی طولانی هم شد، حتما این مقاله رو با کمک شما عزیزان آپدیت خواهم کرد و اشکالاتش رو به مرور برطرف میکنم،

خوشحال میشم نظراتتون رو بدونم، اگر متوجه اشکالی شدید یا قسمتی که به نظرتون خوبه توضیح نداده بودم، حتما اشاره کنید که اصلاحش کنم! مرسی از اینکه تا آخر با من همراه بودید!


منابع:

منبع ۱ - منبع ۲ - منبع ۳ - منبع ۴ - منبع ۵ - منبع ۶ - منبع ۷