از رویداد تا معماری قابل کنترل، قسمت سوم: Reactivity با Proxy
در قسمت دوم یک store ساختیم که تغییر قیمت را به observerها اعلام میکرد. برای هر تغییر مجبور بودیم متدی مثل setPrice داشته باشیم. این روش قابل استفاده است، اما با بزرگ شدن state تعداد setterها و اعلانهای دستی هم بیشتر میشود.
در قسمت سوم مجموعه یک قدم جلوتر میرویم. میخواهیم کد مصرفکننده مثل یک شیء معمولی با داده کار کند، اما سیستم همچنان از تغییرها باخبر شود. ابزار اصلی ما برای این کار Proxy است.
Reactivity یعنی با تغییر داده، بخش وابسته به آن هم بهروز شود. در یک رابط کاربری دوست نداریم بعد از هر تغییر دستی بگوییم کدام متن یا نمودار دوباره رسم شود. بهتر است تغییر داده دیده شود و واکنش لازم خودکار اتفاق بیفتد.
Proxy در JavaScript اجازه میدهد خواندن و نوشتن ویژگیهای یک شیء را کنترل کنیم. به همین دلیل ابزار مناسبی برای ساخت یک نمونه کوچک reactive است.
نکته مهم این است که Proxy خود داده اصلی نیست. یک لایه در اطراف شیء میسازد و عملیات را از تلههایی مثل get، set و deleteProperty عبور میدهد. بنابراین باید نسخه Proxy شده را در اختیار مصرفکننده قرار دهیم. اگر کسی هنوز شیء اولیه را مستقیم تغییر دهد، تلهها اجرا نمیشوند.
نمونه عملی
در مثال زیر هر بار مقداری تغییر کند، تابع render اجرا میشود:
function reactive(target, onChange) {
return new Proxy(target, {
set(object, key, value) {
if (Object.is(object[key], value)) return true;
const changed = Reflect.set(object, key, value);
if (changed) onChange(key, value);
return changed;
}
});
}
const cart = reactive(
{ count: 1, total: 250000 },
(key, value) => console.log(`${String(key)} شد ${value}`)
);
cart.count = 2; // count شد 2
cart.total = 500000; // total شد 500000
cart.count = 2; // تغییری رخ نداده، پس چیزی چاپ نمیشود
تله set قبل از نوشتن مقدار اجرا میشود. ابتدا با Object.is بررسی میکنیم مقدار واقعا تغییر کرده یا نه. سپس نوشتن اصلی را به Reflect.set میسپاریم و در صورت موفق بودن، واکنش را اجرا میکنیم.
Reflect.set همان عملیات استاندارد نوشتن را انجام میدهد و یک مقدار boolean برمیگرداند. برگرداندن این مقدار از تله set مهم است، چون قرارداد Proxy انتظار دارد مشخص کنیم نوشتن موفق بوده یا نه.
استفاده از Object.is هم جلوی واکنش اضافه را میگیرد. اگر count از قبل ۲ باشد، انتساب دوباره ۲ نباید render تازهای ایجاد کند. در رابطهای بزرگ همین بررسی ساده میتواند تعداد زیادی کار بیهوده را حذف کند.
یک کاربرد واقعیتر
در سبد خرید میتوانیم به جای console.log یک تابع render داشته باشیم که تعداد کالا و مبلغ نهایی را در صفحه بنویسد. کد مربوط به افزودن کالا فقط cart.count و cart.total را تغییر میدهد و لازم نیست چیزی درباره محل نمایش آنها بداند.
همین الگو برای تنظیمات برنامه هم مناسب است. با تغییر theme میتوان کلاس صفحه را عوض کرد و با تغییر language متنهای رابط را دوباره نمایش داد. منبع اصلی همچنان state است و رابط فقط تصویری از آن state باقی میماند.
محدودیت این نسخه
این نمونه فقط ویژگیهای سطح اول شیء را میبیند. اگر داخل شیء یک شیء دیگر داشته باشیم و ویژگی داخلی آن را تغییر دهیم، Proxy بیرونی خبردار نمیشود. کتابخانههایی مثل Vue برای دادههای تو در تو، آرایهها و حالتهای خاص کار بیشتری انجام میدهند.
همچنین reactivity به معنی اجرای بیحساب همه چیز نیست. اگر با هر تغییر کوچک کل صفحه را دوباره بسازیم، فقط کار دستی را با کار اضافه جایگزین کردهایم. پس در یک سیستم واقعی باید محدوده واکنشها را با دقت کنترل کنیم تا هر تغییر فقط کار لازم را انجام دهد.
اشتباههای رایج
اولین اشتباه تغییر دادن شیء اصلی به جای Proxy است. بهتر است بعد از ساخت Proxy دیگر reference خام را در بخشهای مختلف پخش نکنیم. اشتباه دوم برابر دانستن Proxy با clone است. Proxy کپی جداگانه نمیسازد و عملیات را روی همان target انجام میدهد.
برای اشیای تو در تو باید مقدار برگشتی از get هم در صورت نیاز Proxy شود. این کار ظرافتهایی مثل نگهداری هویت اشیا و جلوگیری از ساخت Proxy تکراری دارد. به همین دلیل برای محصول واقعی استفاده از ابزار آزمودهشده معمولا بهتر از ساختن یک سیستم reactive شخصی است.
ادامه دارد
چیزی که در این قسمت ساختیم، در ادامه مجموعه کاربرد بیشتری پیدا میکند. قسمت چهارم چند روز دیگر منتشر خواهد شد.