الگوریتم Memoization

الگوریتم Memoization

مقدمه

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

تابع پیدا کردن عدد اول

بیاید با یک مثال خیلی ساده و الگوریتم پیدا کردن عدد اول شروع کنیم. عدد اول عددیست که از ۱ بزرگتره و به جز عدد ۱ و خود اون عدد بر هیچ عدد مثبت دیگه‌ای تقسیم پذیر نیست. (یعنی اگر بخوایم ببینیم عدد ۱۷ اول هستش یا نه، کافیه به هیچ کدوم از اعداد بین ۲ تا ۱۶ تقسیم پذیر نباشه) یکی از راحت ترین روش هایی برای حل این مثال وجود داره brute-force هستش به این صورت که تمام عدد هایی بین ۲ تا جذر اون وجود دارن و تقسیم بر ۲ میکنیم، اولین جایی که باقیمانده این تقسیم ۰ شد به این نتیجه میرسیم که عدد اول نیست. اگر تا اخر الگوریتم رفتیم و برهیچ عددی تقسیم نشد پس درنتیجه اون عدد اول هستش.

function isPrime(num) {
  if (num <= 1) return false;
  for (let i = 2; i <= Math.sqrt(num); i++) {
    if (num % i === 0) return false; // ⭐️
  } 
  return true; 
} 

console.log(isPrime(12)); // false
console.log(isPrime(7)); // true 

این تابع به درستی کار میکنه و خطایی نداره، تنها مشکل این هستش که برای اعداد بزرگ یکم کند عمل میکنه. (اگر یه عدد بزرگ پاس بدیم مثلا ۹۹۹۹۷ قسمتی که ⭐️ داره به اندازه جذر ۹۹۹۹۷ بار صدا میشه) میتونیم با memoization تابع رو بهینه تر کنیم. به این صورت که به جای اینکه به ازای هر باری که تابع صدا میشه نتیجه رو از اول حساب کنیم و به عنوان جواب تابع قرار بدیم، نتیجه اجرای تابع رو بریزیم تو یه ابجکت (اسمش میزاریم cache) و درصورتی که تابع دوباره با همین مقدار ورودی صدا شد نتیجه قبلی رو از کش میخونیم و به عنوان جواب تابع میزاریم.

function memoizedIsPrime() {    // 1️⃣
  let cache = {};
  return function (num) {       // 2️⃣
    const cacheKey = num;
    if (cache[cacheKey]) return cache[cacheKey];
    cosnt result = isPrime(num);
    cache[cacheKey] = result;
    return result;
  }
}

const memoizedIsPrime = memoizedIsPrime()
console.log(memoizedIsPrime(12)); // false
console.log(memoizedIsPrime(7)); // true 

کاری که تو قطعه کد بالا کردیم این بود که یه تابع جدید ساختیم به نام memoizedIsPrime (1️⃣) که درنتیجه صدا شدنش به ما یه تابع دیگه (2️⃣) میده که این تابع به صورت غیر مستقیم تابع isPrime و صدا میکن.

احتمالا سوال اولی که برای شما پیش میاد اینکه چرا ما یه تابع ساختیم که یه تابع دیگه بسازه که داخل اون بیایم isPrime و صداکنیم؟ در یک کلمه خلاصه بخواد گفته بشه clouser، ببینید اون متغیر cache که تو خط دوم تابع ساخته شده نتیجه اجرا شدن های تابع رو ذخیره میکنه و اون تابعی که return شده (2️⃣) از این cache که داخل clousre خودش قرار داره استفاده میکنه.

سوال دومی که ممکنه پیش بیاد اینکه اون cache و cacheKey چی هستش؟ گفتیم که متغیر cache یه ابجکت ساده‌ست، و ابجکت ها تو javascript از ۲ بخش کلید و مقدار ساخته شدن { key: value } برای اینکه بتونیم نتیجه ذخیره شدن این تابع رو داخل ابجکت ذخیره کنیم نیاز داریم به یک کلید و کلید باید به صورتی باشه که اگر دفعه بعدی همین تابع رو با همین ورودی صدا کردیم همین کلید فعلی ساخته بشه و هردفعه یه مقدار رندم غیر قابل حدسی نسازه (باید predictable باشه) برای کلید میتونیم از پارامتر (های) ورودی خود تابع استفاده کنیم، هربار که تابع صدا میشه اگر با همون مقدار قبلی صدا زده بشه کلید قبلی و داره و درنتیجه میتونیم بدون صدا کردن تابع نتیجه رو از cache بخونیم و سرعت و بالا ببریم.

حالا با این روش اگر تابع رو با همون عدد ۹۹۹۹۷ صدا کنیم بار اول به اندازه جذر ۹۹۹۹۷ بار قسمت ⭐️ صدا میشه و بعد از اون اگر هزار بار هم تابع رو با همین عدد صدا کنیم قسمت ⭐️ کلا اجرا نمیشه و نتیجه از کش خونده میشه.

تابع memoizedIsPrime فقط مخص تابع isPrime هستش و اگر بخوایم برای تک تک توابعی که داریم یه تابع دوم memoize شده بنویسیم, و خب یه پترن خاصی داخل تمام توابعی که نوشته میشه وجود داره همشون یه cache دارن که داخل closure هستش و تابعی به عنوان جواب برمیگردونن داخل cache چک میکنه که ایا جوابی برای این ورودی هستش یا نه و ... باید یه راهی باشه که بتونیم جلوی اینهمه دوباره نویسی و بگیریم! و هستش

function memoize(func) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) { 
      return cache[key]; 
    } 
    const result = func.apply(this, args);
    cache[key] = result; 
    return result; 
  }; 
} 

const memoizedIsPrime = memoize(isPrime);
console.log(memoizedIsPrime(7)); // true
console.log(memoizedIsPrime(12)); // false

تو قطعه کد بالا تابع memoize هرتابعی که بهش بدید (به این نوع توابع که یک تابع رو به عنوان ورودی میگیرن و یه تابع جدید به ما میدن گفته میشه higher order function) و memozie میکنه و ورودی (های) تابع رو به عنوان کلید استفاده میکنه و برای صدا کردن تابعی که بهش پاس داده شده از apply استفاده میکنه اطلاعات بیشتر درباره apply

React.memo, useMemo, و useCallback

ری‌اکت تا نسخه ۱۸.۲ بیشتر از ۴ تابع برای momize کردن متغیرها و توابع داخل کامپوننت و کامپوننت ها دراختیار برنامه نویس ها گذاشته.

تابع React.memo یک higher order component هستش، higher order componet دقیقا مثل higher order function ها هستن ولی به جای اینکه یه تابع به عنوان ورودی بگیرن و به ما یه تابع جدید بدن، از ما یه کامپوننت به عنوان ورودی میگیرن و به ما یه کامپوننت جدید میدن.

کار این تابع جلوگیری از ری‌رندر شدن کامپوننت درصورتی که با پراپ های یکسان صدا زده شده باشه هستش، دقیقا مثل تابع memoize که بالاتر تحلیل کردیم عمل میکنه، بار اول که کامپوننت رندر میشه cache خالیه پس کامپوننت و رندر میکنه و نتیجه رندر کامپوننت و داخل cache میریزه و دفعه بعدی اگر با همون پارامتر (پراپ ها) صدا زده بشه نتیجه رو از cache میخونه و تابعی که کامپوننت ما هستش و اجرا نمیکنه.

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p>Hello, {props.name}</p>
}

// export default React.memo(Gretting); // 1️⃣
export default Gretting;  // 2️⃣

// src/App.js
import { useState } from "react";
import Gretting from "./Gretting"

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1);
  }
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting name="Nima" />
    </div>
  )
}

export default App;

در صورتی که کد بالارو اجرا کنیم میبینیم که با هربار کلیک بر روی دکمه +1 هم کامپوننت App (به علت اینکه state عوض شده) و هم کامپوننت Greeting به علت اینکه پدرش ری‌رندر شده دوباره رندر شدن.

درصورتی که خط 1️⃣ رو از کامنت دربیاریم و خط 2️⃣ رو کامنت کنیم و از اول روی دکمه +1 کلیک کنیم کامپوننت Gretting دیگه دوباره ری‌رندر نمیشه و فقط کامپوننت App ری‌رندر میشه

حالا به کامپوننت Gretting یه پراپ جدید بدید و مقدار count و داخلش بریزید و روی button کلیک و ببینید چی میشه.

قبل از اینکه ادامه بدیم بریم قسمت بعدی لازمه یک یاداوری روی عملگر مقایسه تو جاواسکریپت داشته باشیم

مقایسه براساس رفرنس

۲ تا ابجکت باهم برابرند اگر به یک ادرس تو حافظه اشاره کنند، این به این معنیه که {} === {} نتیجه‌ش میشه false چون ۲ تا ابجکت داریم تو ۲ جای متخلف تو حافظه و ادرسشون باهم متفاوته


const "nima" === "nima" // true
const 1 ==== 1 // true

{} === {} // false
[] === [] // false
{ name: "react.ir" } === { name: "react.ir" } // false

const a = {}
const b = a

a === b // true

const c = []
const b = c

b === c // true

const e = { name: "react.ir" }
const f = e

e === f // true


const g = [{ name: "nima" }]
const h = g

g === h // true


Object.is([], []) // false
Object.is({}, {}) // false
Object.is(false, false) // true
Object.is(a, b) // true

خب ممکنه بگید چه طوری ۲ تا ابجکت که ادرسشون باهم متفاوت هستش و باهم مقایسه کنیم؟ کافیه تک تک proeprty هایی که دارن و یک به یک باهم مقایسه کنیم

const a = { name: "react.ir" }
const b = { name: "react.ir" }

a === b // false

a.name === b.name // true

یه نکته خیلی مهم درباره تابع memo اینکه براساس refrence پراپ هایی که به کامپوننت پاس میدید میفهمه که میتونه از cache مقدار رندر شده رو بخونه و return بکنه. و اصلا به این صورت که تک تک پراپرتی های اون ابجکت هایی که بهش پاس دادید و مقایسه بکنه نیست.

درنتیجه کد زیر درست memo نمیکنه و کامپوننت ما با اینکه داخل memo شده هربار که پدرش ری‌رندر میشه دوباره از اول رندر میشه

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p>Hello, {props.user.name}</p>
}

export default React.memo(Gretting);

// src/App.js
import { useState } from "react";
import Gretting from "./Gretting"

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1);
  }
  // refrecnce of this object, changes each time this component gets rerendred
  const user = { user: "Nima" }
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting user={user} />
    </div>
  )
}

export default App;

useMemo

کار این هوک دقیقا همونطوری که از اسمش مشخصه، meomize کردن هستش. خب ممکنه بگید چرا همون تابع memo و چرا استفاده نمیکنیم داخل کامپوننتمون اونم memoize میکنه دیگه

مشکلی که useMemo حل میکنه اینکه مقدار memoize شده بین رندرهای کامپوننت تو حافظه میمونه، ولی اگر تابع memoize رو داخل کامپوننت صدا کنید و کامپوننت ری‌رندر بشه تو رندر بعدی کل clousure پاک میشه و cache دوباره خالی میشه درنتیجه باید از اول تمام محاسبات و انجام بده.

از کاربردهای useMemo میشه به این ۳مورد اشاره کرد:

  1. سریعتر کردن سرعت اجرا شدن توابع پیچیده
  2. ذخیره کردن رفرنس ابجکت ها بین رندرهای کامپوننت
  3. ذخیره کردن رفرنس ابجکت ها برای استفاده در هوک های دیگه!
const visibleTodos = useMemo(  

() => filterTodos(todos, tab),  

[todos, tab]  

);

کدبالا مقدار visiblesTodos و بین رندر های کامپوننت ثابت نگه میداره و همزمان باعث میشه که تابع filterTodos تو هربار رندر کامپوننت از اول صدا نشه و فقط و فقط زمانی که todos یا tab تغییر کردند دوباره از اول صدا بشه. که منطقیه، ما میخوایم دوباره از اول تابع فیلتر کردن صدا زده بشه و مقدارش داخل visibleTodos قرار بگیره اگر لیست todo هایی که داریم عوض شده بود.

فرض کنید یه لیست (ارایه) از ۱۰۰ تا محصول و از بک‌اند گرفتید و یک سری فیلتر دارید که مشخص میکنه از این ۱۰۰ تا محصول چندتاش باید به کاربر رو صفحه نمایش داده بشه.

// products = [{ id: 1, name: "A", price: 100, }, {...}]
// priceFilter = 10

function ProductList({ products, priceFilter }) {
	const filteredProducts = products.filter((product) => {
	  return product.price >= priceFilter
	})
	
	return (
	  <ul>
		{filteredProducts.map(product => (
		  <div key={product.id}>
			<span>Name: {prodcut.name}</span>
			<span>Price: {product.price}</span>
		  </div>
		))}
	  </ul>
	)
}

تو هربار رندر شدن پدر این کامپوننت عملیات فیلتر شدن روی ۱۰۰ تا خونه ارایه یکبار انجام میشه. میتونیم با useMemo جلوی این اتفاق و بگیریم

// products = [{ id: 1, name: "A", price: 100, }, {...}]
// priceFilter = 10

function ProductList({ products, priceFilter }) {
	const filteredProducts = useMemo(() => products.filter((product) => {
	  return product.price >= priceFilter
	}), [])
	
	return (
	  <ul>
		{filteredProducts.map(product => (
		  <div key={product.id}>
			<span>Name: {prodcut.name}</span>
			<span>Price: {product.price}</span>
		  </div>
		))}
	  </ul>
	)
}

با این تغییر تو هربار رندر شدن پدر این کامپوننت، عملیات filter شدن انجام نمیشه و فقط و فقط یکبار (دفعه اولی که کامپوننت رندر میشه) این تابع فیلتر صدا زده میشه. مشکلی پیش میاد اینکه اگر مقدار priceFilter یا products تغییر کنه باز هم ما همون دیتایی که بار اول کامپوننت و رندر کردیم با فیلتر های قبلی و به کاربر نشون میدیم و باید یه جوری به useMemo بگیم وقتی products یا priceFilter تغییر کرد بیاید از اول تابع filter و صدا بکنه. و اینجاست که پارامتر دوم یا همون dependency array تابع useMemo به کار میاد.

	const filteredProducts = useMemo(() => products.filter((product) => {
	  return product.price >= priceFilter
	}), 
	[products, priceFilter] // 👈 dependency array
	)

و خب کار dependency array دقیقا همینه، که به useMemo بگه این کد وابسته هستش به این متغیرها و اگر حتی یکی از مقادیر عوض شدن باید از اول تابعی که بهت به عنوان پارامتر اول دادم و صدا کنی.

و dependency array براساس refrence مقادیر dependency array جدید و با مقادیر dependency array مرحله قبلی مقایسه میکنه(دقیقتر بخوام بگم از Object.is استفاده میکنه)

پس مهمه که رفرنس products و priceFilter بین رندر های مختلف ثابت بمونه مگرنه این useMemo که استفاده کردیم کاملا بیهوده بوده و فقط باعث شده سرعت اجرای برنامه بیاد پایین (چون داره تو هر رندر یه کار اضافه میکنه - نتیجه اجرارو تو حافظه ذخیره میکنه ولی دفعه بعدی چون dependency array مقدارش عوض شده پاکش میکنه)

این به این معنی هستش که products و priceFilter که توی کامپوننت پدر هستند باید memoize بشن.

function ProductsPage() {
  const [products, setProducts] = useState([])
  const [priceFilter, setPriceFilter] = useState(0)

  useEffect(() => {
    fetch("/api/products")
      .then(res => res.json())
      .then(res => setProducts(res))
  }, [])

  return (
    <>
      <ProductList products={products} priceFilter={priceFilter} />
      <ProductFilter onFilter={setPriceFilter} />
    </>
  )
}

نکته: همیشه مقدار استیت بین رندر های کامپوننت ثابت میمونه و لازم نیست state و memoize کنیم.

با فرض اینکه کامپوننت پدر مشابه کدی که اینجا نوشته شده باشه، نیازی نیست روی products یا priceFilter ما memoization انجام بدیم. حالا اگر وسط کامپوننت ما یه فیلتر ریزی روی دیتا انجام بدیم و بعدش به ProductList پاسش بدیم چی؟ مثلا فقط ۱۰ تا محصول اول و از لیستمون برداریم و پاس بدیم به کامپوننت ProductList

function ProductsPage() {
  const [products, setProducts] = useState([])
  const [priceFilter, setPriceFilter] = useState(0)

  useEffect(() => {
    fetch("/api/products")
      .then(res => res.json())
      .then(res => setProducts(res))
  }, [])

  const firstTenProducts = products.spilit(0, 10)

  return (
    <>
      <ProductList products={firstTenProducts} priceFilter={priceFilter} />
      <ProductFilter onFilter={setPriceFilter} />
    </>
  )
}

اینجا دیگه اون useMemo داخل کامپوننت ProductList درست کار نمیکنه چون تو هربار رندر کامپوننت ProductsPage رفرنس پراپ products تغییر میکنه (با اینکه دقیقا همون ۱۰ تا محصول اول لیست برمیداره و به کاربر نشون میده) پس لازمه این قسمت رو هم memoize کنیم

  const firstTenProducts = useMemo(() => {
    return products.spilit(0, 10)
  }, [products])

میبینید چقدر سریع شروع به پیجیده شدن کردن؟ و ما باید همیشه حواسمون باشه تمامی مقادیری که به عنوان dependency array دادیم memoize باشن و کلی کد بنویسیم و کلی حافظه مصرف میشه تا این مقادیر همیشه تو حافظه ثابت بمونن و grabage collector حذفشون نکنه

مشکل memoziation دقیقا همینه، باعث میشه پیجیده بشه و کنترلش سخت میشه، برای همینه که ما نمیایم تک تک کامپوننت ها و تک تک متغیرهای برنامه رو memo کنیم.

خبر خوب اینکه تیم ری‌اکت یا یه تیمی داخل فیسبوک دارن یه کامپایلر مینویسن که اتوماتیک این کارو انجام میده برای ما، خودش کد و بررسی میکنه خودش میفهمه کجا باید memo بشه و همرو برای ما خودش انجام میده. (اماده نیست و جایی نیست که برید دانلودش کنید و به پروژه‌تون اضافه‌ش کنید)

برگردیم به مثال قبلی که کامپوننتمون و memo کردیم:

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p>Hello, {props.user.name}</p>
}

export default React.memo(Gretting)

// src/App.js
import { useState } from "react";
import Gretting from "./Gretting"

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1);
  }
  // refrecnce of this object, changes each time this component gets rerendred
  const user = { user: "Nima" }
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting user={user} />
    </div>
  )
}

export default App;

گفتیم memozation روی کامپوونت greeting دیگه کار نمیکنه چون رفرنس متغیر user تو کامپوننت بالایی تو هر ری‌رندر عوض میشه.

پس فقط کافیه اون متغیر user و بیایم memoize کنیم تا کامپوننتمون دوباره memoize بشه

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p>Hello, {props.user.name}</p>
}

export default React.memo(Gretting); 

// src/App.js
import { useState, useMemo } from "react";
import Gretting from "./Gretting"

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1)
  }

  const user = useMemo(() => ({ user: "Nima" }), [])
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting user={user} />
    </div>
  )
}

export default App;

🎉🎉 هورا بالاخره این کامپوننتمون memo شد.

ولی میخوام یه راه بهتر بهتون بگم که این memo بدون useMemo انجام بشه! اگر به کد زیر دقت کنید dependency array این useMemo خالیه، این به این معنیه که هیچ رفرنسی به هیچ چیزی داخل کامپوننت ما نداره.

const user = useMemo(() => ({ user: "Nima" }), [])

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

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p>Hello, {props.user.name}</p>
}

export default React.memo(Gretting); 

// src/App.js
import { useState } from "react";
import Gretting from "./Gretting"

const user = { user: "Nima" } // 🔥

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1)
  }
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting user={user} />
    </div>
  )
}

export default App;

useCallback

هوک useCallback یه حالت خاصی از useMemo هستش، تفاوتشون اینکه useMemo تابعی که بهش پاس میدید و اجرا میکنه و نتیجه اجرا شدنش و ذخیره میکنه. useCallback رفرنس تابع رو ذخیره میکنه. اگر بخوایم useCallback و با useMemo پیاده کنیم:

const handleOnChange = useMemo(() => {
  return function(event) {
    console.log(even.target.value)
  }
}, [])


return <input name="name" onChange={handleOnChange} />

تو کد بالا مقداری که useMemo ذخیره میکنه رفرنس به یک تابع هستش، و خب برای اینکه ما برنامه نویس ها راحت تر باشیم تیم ری‌اکت هوک useCallback و درست کردن که به این صورت هستش:

const handleOnChange = useCallback(function(event) {
    console.log(even.target.value)
}, [])

تنها تفاوتشون با useMemo اینکه تابعی که بهش میدید و اجرا میکنه و نتیجه‌ای که اون تابع return میکنه رو ذخیره میکنه، useCallback دیگه تابع رو اجرا نمیکنه و بلکه رفرنس همون تابع رو ذخیره میکنه.

دقیقا مثل متغیر‌ها که لازم بود رفرنسشون بین رندر ها ثابت باشه تا memo درست کار کنه، توابع هم لازم هستن تا memoize بشن تا memo کامپوننتی که بهشون به عنوان پراپ پاس داده میشن درست کار کنه.

// src/Gretting.js
import React from "react";

function Gretting (props) {
  console.log("Gretting rendered");
  return <p onClick={props.onClick}>Hello, {props.user.name}</p>
}

export default React.memo(Gretting); 

// src/App.js
import { useState } from "react";
import Gretting from "./Gretting"


const user = { user: "Nima" } 

function App (props) {
  const [count, setCount] = useState(1)

  function increment() {
    setCount(count + 1)
  }

  // this will cause Gretting component memoization fail
  // wrap this in a useCallback to fix the issue
  function handleClick() {
    console.log("Clicked")
  }
  
  return (
    <div>
      <span>count is: {count}</span>
      <button onClick={increment}>+1</button>
      <Gretting onClick={handleClick} user={user} />
    </div>
  )
}

export default App;

چه طور میشه که useCallback یه حالت خاصی از useMemo میشه؟

function useCallback(cb, deps) {
  return useMemo(() => cb, deps)
}

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

اگر یادتون باشه گفتیم یکی از کاربرد های useMemo و useCallback ذخیره کردن رفرنس متغیرها برای استفاده تو هوک های دیگه هستش، که مثالش با useMemo و برای کامپوننت Products بالاتر دیدم

مثال کاربردی از useCallback فرض کنید تابعی رو داریم که میره یه دیتایی رو برای ما از بک اند فچ میکنه

function fetchProduct() {
  return fetch("/api/product/" + id)
    .then(res => res.json())
    .then(res => setProducts(res))
}

ما میخوایم وقتی کامپوننتمون رندر شد بره این تابع رو صدا کنه و دیتا رو فچ کنه و به کاربر نشون بده، پس میایم داخل useEffect این تابع رو صدا میکنیم

const [products, setProducts] = useState()

function fetchProducts() {
  return fetch("/api/products/")
    .then(res => res.json())
    .then(res => setProducts(res))
}

useEffect(() => {
  fetchProducts()
}, [fetchProducts])

مشکلی که پیش میاد اینکه همینجا کامپوننتمون میوفته تو لوپ بی نهایت چون هربار که fetchProduct صدا میشه کامپوننت ری‌رندر میشه و تو رندر بعدی رفرنس fetchProducts عوض شده پس useEffect دوباره callback که بهش دادیم و صدا میکنه و دوباره fetchProducts صدا میشه و ...

خب ممکنه بگید راه حل این مسئله خیلی سادست، تابع fetchProducts و میبریم داخل useEffect میزاریم

const [products, setProducts] = useState()

useEffect(() => {
  function fetchProducts() {
    return fetch("/api/product/")
      .then(res => res.json())
      .then(res => setProduct(res))
  }
  fetchProducts()
}, [])

و کدمون هم درست هم کار میکنه، ولی اگر بخوایم یه دکمه‌ای بزاریم که وقتی روش کلیک میشه بره دیتارو دوباره بگیره چی؟

const [products, setProducts] = useState()

function fetchProducts() {
  return fetch("/api/products/")
    .then(res => res.json())
    .then(res => setProducts(res))
}

useEffect(() => {
  fetchProducts()
}, [fetchProducts])

return (
  <button
    onClick={fetchProducts}
  >
    Refetch
  </button>
)

اینجا ما میتونیم با useCallback رفرنس تابع fetchProducts و ثابت نگه داریم که useEffect نیوفته تو لوپ و رفرنس fetchProducts ثابت بمونه.

const [products, setProducts] = useState()

const fetchProducts = useCallback(function () {
  return fetch("/api/products/")
    .then(res => res.json())
    .then(res => setProducts(res))
}), [])

useEffect(() => {
  fetchProducts()
}, [fetchProducts])

return (
  <button
    onClick={fetchProducts}
  >
    Refetch
  </button>
)

نکات مهم ^important-notes

تابع dispatcher که از توابع useState و useReducer که برای مدیریت استی�� استفاده میشن همیشه رفرنس ثابت داره و لازم نیست meomize بشه.

const [state, setState] = useState()
const [state, dispatch] = useReducer()

setState and dispatch are dispatchers and their refrence is always the same

پس اگر داخل useMemo یا useCallback از اونا استفاده کردیم میتونیم داخل dependnecy array قرارشون ندیم (اگر هم قرار بدیم هیچ اتفاقی نمی‌افته و رفتار برنامه ما تغییر نمیکنه)

اگر این توابع رو داخل توابع دیگه استفاده کردیم لازم هستش که اون تابع به صورت جدا memoize بشه (اینطوری برداشت نشه که خب اگر تو یه تابعی اومدم setState استفاده دیگه اون تابع خود به خود memoize میشه)

رفرنس state همیشه بین رندر های مختلف ثابت هستش مگر اینکه تابع setState یا dispatch صدا بشه و استیت برنامه عوض بشه، پس نیازی به memoize کردن استیت نیست.

ابزار های کمکی

یکی از چالش های هوک ها درست پاس دادن مقادیر dependency array هستش، تیم ری‌اکت یک پلاگین برای eslint نوشتن که به صورت اتوماتیک مقادیر dependency array میزاره و کار و برای شما راحت میکنه. اگر از create-react-app, razzle, afterjs, remix, next.js, gatsby استفاده میکنید و eslint روی پروژه های شما کانفیگ شده باشه به صورت پیش فرض این پلاگین فعال هستش.

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

https://www.npmjs.com/package/eslint-plugin-react-hooks

خلاصه

الگوریتم memoization برای ذخیره سازی نتیجه توابع پیچیده و بهینه کردن عملکرد برنامه‌های ما کاربرد داره. وقتی از ری‌اکت استفاده میکنیم گاهی اوقات برای بهبود عملکرد برنامه و بیشتر مواقع برای حل کردن مشکلات رفرنسی بین توابع و هوک های مختلف از این الگوریتم استفاده میکنیم.

رفرنس های به دردبخور

نظر شما درباره memo کردن تک تک کامپوننت‌های داخل برنامه‌تون چیه؟ به نظرتون سرعت برنامه و بیشتر میکنه؟ و اگر سرعت و بیشتر میکنه ارزش این همه پیچیدگی و داره؟

اگر سوالی درباره مقاله داشتید یا برای سوالات بالا جوابی داشتید تو قسمت نظرات پایین همین مقاله با من به اشتراکش بزارید.

اگر خودتون این مقاله رو دوست داشتید برای دوستانتون هم بفرستید و چندتا 🔥 پایین همین مقاله بزارید.

user profile

نیما عارفی

۱۶ اردیبهشت ۰۲

نظر دهید!

برای نظر دادن ابتدا باید وارد شوید یا رایگان ثبت نام کنید.

نظرات دیگران!

  • غزاله نیازی

    ۱۶ اردیبهشت ۰۲

    عالی بوددد 🔥🔥

  • حمید شاهسونی

    ۱۶ اردیبهشت ۰۲

    🔥🔥🔥🔥🔥🔥🔥 اقا به به , خیلی خسته نباشی منتظر مقاله های خفنت هستم نیما جان

  • علی کوهی زاده

    ۱۷ اردیبهشت ۰۲

    مقاله جالبی بود.

  • محمد

    ۱۸ اردیبهشت ۰۲

    🔥🔥🔥

  • حسن مقدسی

    ۱۳ شهریور ۰۲

    عرفان جان اگه ما fetchProducts رو داخل dependency array نزاریم مشکلی به وجود میاد؟ در این صورت دیگه نیازی نیست dependency array رو memorize کنیم.

خانه
بلاگ
ویدیوکستدوره ها

درحال بارگذاری ...