بهینه کردن کارایی(performance) فیلتر و مرتب کردن لیست ها با کتابخانه ی Reselect و Re-reselect در ریداکس

سلام.تو این مطلب می خوایم باهم بررسی کنیم که راه درست فیلتر کردن یا مرتب کردن یک لیست (که حجم بسیار بالایی هم داره) در react app چیه و اون رو نسبت یه راه عادی که به ذهنمون میرسه مقایسه کنیم.اگر در یک react app که در اون از redux استفاده شده، بخوایم حجم زیادی از داده رو فیلتر (یا sort ) کنیم، یقینا اولین راهی که به ذهن هممون می رسه اینه که این کار رو داخل reducer در ریداکس انجام بدیم.اما راه درست انجام این کار اینه که اصل داده ها رو داخل یه state درون store نگه داریم و فیلتر یا مرتب کردن داده ها رو داخل selector ها انجام بدیم.(نه داخل reducer ها)فواید استفاده از این روش به شرح زیره:1)نیازی به ذخیره ی موارد فیلتر شده در state کلی درون store نیست.(چون محاسبه می شه و در اختیار کامپوننت مربوطه قرار می گیره)2) اگر چند کامپوننت بخوان با معیارهای مختلفی داده ی اصلی رو فیلتر کنن، این امکان فراهمه.3) امکان ترکیب selector ها (فیلتر کننده ها) وجود داره؛ یعنی ما می تونیم تو چند مرحله فیلترکردن رو انجام بدیم.4) سلکتورها، pure function میشن ، پس تست کردنشون راحت تره.5) امکان استفاده ی دوباره ای selector ها در کامپوننت های مختلف وجود داره(reusable شدن selector ها).6) وقتی عملیات فیلتر یا مرتب کردن بسیار سنگین و هزینه بر باشه، این روش بسیار کارآمدتر هست.7) داده (state ) اصلی درون store، راحت تر خوانده میشه.(خوانایی بالاتر)8) برای محاسبه ی مقادیر فیلتر یا sort، منطق کمتری نیاز هست.9) داده اصلی(root state) دست نخورده باقی می مونه.اما فیلتر کردن یا مرتب کردن (یا هر عملیات دیگری به این شکل) داده ها در selector ها، دو نگرانی ایجاد می کنه:1) با هر بار dispatch شدن یک action در redux ، سلکتور(Selector) استفاده شده درون useSelector یا mapState دوباره اجرا میشن(re-run)، فارغ از اینکه چه قسمتی از state کلی درون store تغییر کرده و بروز شده(یعنی اون قسمت به این selector ارتباط داشته یا نه). (با استناد به documentation ریداکس). اجرای دوباره(re-run) شدن این محاسبات سنگین آن هم با عدم تغییر آن قسمت از state کلی ورودی به کامپوننت مربوطه، بیهوده هست و بار زمانی و پردازشی اضافی بر روی cpu داره.2) توابع useSelector و mapState، با استفاده از عملگر "===" ، چک می کنند که آیا خروجی selector موردنظر بعد از اجرا، تغییر کرده یا نه؟ اگر تغییر کرده باشه( یعنی بازگردانی رفرنس جدید)، کامپوننت مربوطه رو مجبور به re-render می کنند. حتی اگر مقادیر داده ی درون این شی یا آرایه تغییری نکرده باشه(و این بازهم به معنی پردازش بی مورد و اضافه هست). این عملیات معمولا در سلکتورهایی رخ میده که در اونا از تابع map یا filter جاوااسکریپت استفاده میشه. چون این توابع هربار یه رفرنس جدید بر می گردونن.برای حل نگرانی ها و مشکلات بالا، سلکتورها رو memoize می کنن. یعنی مجموعه ی ورودی ها و خروجیشون رو ذخیره می کنن؛ اگر ورودی ها تغییر کنه، سلکتور دوباره اجرا میشه و بعد دوباره این مجموعه ی ورودی و خروجی ذخیره میشه. اگر مجموعه ی ورودی تغییر نکنه، سلکتور دوباره اجرا نمیشه و همون خروجی کش(cache) شده بازگردانی میشه. اینطوری بار پردازشی اضافه حذف میشه.برای نوشتن سلکتورهای memoize شده، از کتابخونه ای به اسم Reselect استفاده میشه که این کتابخونه با ریداکس بطور خودکار نصب میشه.کتابخونه ی Reselect ، یک تابع به اسم createSelector داره که مجموعه سلکتورهای ورودی و یک سلکتور خروجی رو می گیره، بعد مقدار فیلتر یا مرتب شده رو بر می گردونه. یعنی داده های اولیه ی درون state رو می گیره، تو سلکتور خروجی فیلتر رو انجام میده و مقدار رو بر می گردونه.نوشتن memoized selector با استفاده از createSelectorمثلا در کد بالا، سلکتور selectABC با استفاده از createSelector ایجاد میشه که 4 تا ورودی می گیره. سه تای اول(selectA, selectB, selectC) سلکتورهایی هستن که داده های ورودی selectABC رو از state کلی انتخاب(select) می کنن و به selectABC میدن. ورودی آخری هم سلکتور خروجی هست، که مقادیر خروجی سلکتورهای اول(یعنی selectA, selectB, selectC) درون ورودی این سلکتور قرار می گیره؛ بعد ما می تونیم عملیات مدنظرمون رو روی این داده ها انجام بدیم. مثلا عملیات فیلترکردن یا مرتب سازی یا... . بعد مقدار نهایی رو return می کنه.نحوه ی کارکرد تابع createSelector:وقتی این تابع فراخونی میشه، کتابخونه ی reselect، سلکتورهای ورودی(selectA, selectB, selectC در مثال بالا) داده شده رو اجرا می کنه، بعد مقدار خروجی هرکدوم از اونها رو (با استفاده از عملگر ===)بررسی می کنه، اگر حتی یکی از خروجی های بررسی شده، رفرنس جدیدی نسبت به قبل باشن(برای اشیا و آرایه ها)، کتابخانه ی reselect ، سلکتور خروجی(مثلا آخرین تابع ورودی در مثال بالا) رو دوباره اجرا می کنه و مجموعه ی ورودی های سلکتور خروجی به همراه مقدار return شده (نتیجه) ی اون رو کش(cache) می کنه. اما اگر هیچ کدوم از رفرنس های سلکتورهای ورودی تغییر نکرده باشن، همون آخرین مقدار نتیجه کش شده را بازگردانی می کنه.مشکل کتابخانه ی reselect و تابع createSelector:اما مشکلی که تابع createSelector داره اینه که فقط می تونه یک مقدار رو برای هر عملیات ذخیره کنه(فقط یک ظرفیت حافظه بهش اختصاص داده شده). مشکل تابع createSelector مثلا تو مثال بالا، وقتی سلکتور memoize شده با مقدار 1 فراخوانی میشه، مقدار خروجی برای ورودی 1 ذخیره میشه، بعد که دوباره با 1 فراخونی میشه، سلکتور خروجی دوباره اجرا نمیشه و آخرین مقدار ذخیره شده تو حافظه اختصاص داده شده به این تابع بازگردانی میشه. در ادامه وقتی با مقدار ورودی 2 فراخوانی میشه، سلکتور خروجی اجرا میشه و خروجی جدید رو ایجاد می کنه و نتیجه (جایگزین مقدار خروجی آرگومان 1 میشه و) کش میشه. اما در انتها، وقتی دوباره این تابع با مقدار 1 (بعد از فراخوانی اون با مقدار 2) فراخوانی میشه، سلکتور خروجی دوباره اجرا میشه(چون در تنها ظرفیت حافظه ی این تابع خروجی آرگومان 2 وجود داره) و مقدار رو برای ورودی 1 (دوباره) محاسبه می کنه و جایگزین مقدار خروجی آرگومان 2(که قبلا ذخیره شده بود) میشه. در واقع سلکتور ایجاد شده با تابع createSelector ، چون فقط 1 ظرفیت حافظه برای ذخیره و کش کردن مقدار نتیجه ی سلکتور خروجی داره، هربار مقایسه رو نسبت به نتیجه ی عملیات ورودی قبلی خودش انجام میده. دیگه کاری به خروجی های قبل تر نداره. چون مقدار حافظه اش اجازه نمی ده و خروجی های آرگومان های قبلی رو نداره که مقایسه کنه.حل مشکل reselect با استفاده از re-reselect :re-reselectبرای حل این مشکل، از کتابخونه ی دیگه ای به نام Re-Reselect استفاده می کنیم. این کتابخونه، یک کتابخونه ی wrapper برای reselect هست که میاد چندین سلکتور ایجاد شده از تابع createSelector رو کش می کنه؛ برای هر سلکتور کش شده هم یک کلید(key) در نظر می گیره که همون آرگومان ورودیش هست. بعد طبق ورودی(یا همون key) که موقع فراخونی می گیره، تصمیم می گیره که یک سلکتور memoize شده ی جدید با createSelector ایجاد کنه و کلید و خروجی این سلکتور جدید رو کش کنه و بازگردانی کنه، یا بره سراغ سلکتورهای کش شده ی قبلی و خروجیشون رو به ما بده.تو تصویر بالا، می تونیم ببینیم که شکل سمت چپ، یک سلکتور ایجاد شده با reselect هست و شکل سمت راست، مجموعه ای از سلکتورهای کش شده که با توجه به ورودی تابع ایجاد شده با re-reselect ، تصمیم گیری میشه که اگر این کلید وجود داشت، خروجی متناظرش بازگردانی شه، اگر هم وجود نداشت، یک سلکتور براش ایجاد شه و فراخونی شه و مقدارش کش و سپس بازگردانی شه.در ادامه یک نمونه کد از این کتابخونه رو می تونیم بررسی کنیم:مثال re-reselectتو کد بالا، یک سلکتور cache شده به نام getUsersByLibrary با تابع createCachedSelector که مخصوص کتابخونه ی re-reselect هست( مثل زمانیکه با createSelector ایجاد می کردیم)، ایجاد شده با این تفاوت که یک کلید به نام libraryName هم تو ورودیش دریافت می کنه. هنگام فراخوانی getUersByLibrary ، یک کلید هم بهش داده میشه(مثلا در اینجا 'react' یا 'vue'). اگر به ترتیب فراخوانی ها دقت کنیم، می بینیم که اول سلکتور getUsersByLibrary با ورودی (یا همون کلید) 'react' فراخوانی شده و نتیجه اش ذخیره شده. بعد با ورودی 'vue' فراخوانی شده، بعد با همون 'react'. اما در فراخوانی آخر با ورودی(کلید) 'react' ، دیگه سلکتور جدیدی برای این کلید ایجاد نمیشه و سلکتور خروجی expensiveComputation دوباره (و بیهوده) اجرا نمیشه و به cache مراجعه میشه و مقدار ذخیره شده برای کلید(ورودی) 'react' به ما داده میشه.به این شکل، با استفاده از memoization و سلکتورها، عملیات فیلترکردن و مرتب سازی در سلکتورها انجام میشه و کارایی بهتری رو خواهیم داشت.این نتیجه ی تحقیقی بود که بنده انجام دادم. اگر اشکال و یا ایرادی و کمی و کاستی ای در این مطلب موجود هست، خوشحال میشم که تو کامنت ها بگین. با تشکر که تا به اینجا همراهی کردین.با آرزوی موفقیت و سلامتی.منابع:https://stackoverflow.com/questions/34003553/redux-what-is-the-correct-way-to-filter-a-data-array-in-reducerhttps://redux.js.org/usage/deriving-data-selectorshttps://blog.logrocket.com/react-re-reselect-better-memoization-cache-management/https://github.com/toomuchdesign/re-reselect