از رویداد تا معماری قابل کنترل، قسمت چهارم: تشخیص خودکار وابستگیها
در قسمت سوم با Proxy تغییر داده را تشخیص دادیم. نسخه قبلی با هر تغییر یک callback عمومی را اجرا میکرد. حالا میخواهیم سیستم دقیقتر باشد و بداند هر تابع واقعا کدام ویژگیها را خوانده است.
این قسمت نقطهای است که چند ایده قبلی به هم میرسند. شبیه Observer اشتراک میسازیم، اما اشتراکها را دستی تعریف نمیکنیم. Proxy خواندن داده را میبیند و سیستم همان لحظه وابستگی را ثبت میکند.
در یک سیستم reactive ساده میتوانیم بعد از هر تغییر همه تابعها را دوباره اجرا کنیم، اما این کار به مرور گران میشود. بهتر است سیستم خودش بفهمد هر تابع چه دادهای را خوانده و فقط با تغییر همان داده دوباره اجرا شود.
به این کار Automatic Dependency Tracking میگویند. ایده آن از چیزی که به نظر میرسد سادهتر است: هنگام اجرای یک تابع، خواندن ویژگیها را ثبت میکنیم.
این سیستم دو مرحله دارد. مرحله اول track است که هنگام خواندن داده اجرا میشود و رابطه میان داده و effect را ثبت میکند. مرحله دوم trigger است که هنگام تغییر داده، effectهای ثبتشده را پیدا میکند. این دو مرحله به کمک تلههای get و set در Proxy به عملیات عادی خواندن و نوشتن وصل میشوند.
پیادهسازی کوچک
const dependencies = new Map();
let activeEffect = null;
function track(key) {
if (!activeEffect) return;
if (!dependencies.has(key)) dependencies.set(key, new Set());
dependencies.get(key).add(activeEffect);
}
function trigger(key) {
for (const effect of dependencies.get(key) ?? []) effect();
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const state = new Proxy({ price: 100, count: 2 }, {
get(target, key) {
track(key);
return Reflect.get(target, key);
},
set(target, key, value) {
if (Object.is(target[key], value)) return true;
const changed = Reflect.set(target, key, value);
if (changed) trigger(key);
return changed;
}
});
watchEffect(() => {
console.log(`مبلغ سبد: ${state.price * state.count}`);
});
state.count = 3; // مبلغ سبد: 300
وقتی watchEffect اجرا میشود، تابع موردنظر در activeEffect قرار میگیرد. خواندن price و count از تله get عبور میکند و تابع به عنوان وابستگی هر دو کلید ثبت میشود. بعد از تغییر count فقط effectهای مربوط به همان کلید اجرا میشوند.
اگر effect فقط state.price را بخواند، در Set مربوط به count ثبت نمیشود. این همان تفاوت اصلی با اعلان عمومی تغییر است. سیستم به جای حدس زدن، مسیر واقعی اجرای تابع را مشاهده میکند.
activeEffect را میتوان مثل برگهای دانست که نام تابع فعلی روی آن نوشته شده است. هر دادهای که در زمان حضور این برگه خوانده شود، نام تابع را در فهرست مشترکهای خودش یادداشت میکند. بعد از پایان اجرا، برگه کنار گذاشته میشود تا خواندنهای معمولی به اشتباه وابستگی نسازند.
به چه دردی میخورد؟
در یک فرم میتوانیم پیام اعتبارسنجی را فقط به فیلدهای مرتبط وصل کنیم. در داشبورد هم نمودار فروش لازم نیست با تغییر نام کاربر دوباره محاسبه شود. سیستم وابستگی به جای فهرست دستی، این ارتباطها را از روی خواندن واقعی داده پیدا میکند.
مزیت دیگر این است که وابستگی با خود کد هماهنگ میماند. اگر فرمول مبلغ سبد بعدا مالیات را هم بخواند، همان خواندن جدید کافی است تا مالیات به وابستگیها اضافه شود. دیگر لازم نیست فهرست جداگانهای را دستی بهروز کنیم.
نکتهای که این نمونه حل نکرده است
یک سیستم کامل باید وابستگیهای قدیمی را قبل از اجرای دوباره پاک کند. مثلا اگر تابع بر اساس یک شرط گاهی price و گاهی count را بخواند، وابستگی قبلی نباید برای همیشه باقی بماند. همچنین برای چند شیء مختلف، تنها نام کلید کافی نیست و باید وابستگیها بر اساس خود شیء هم جدا شوند.
این کد برای دیدن هسته ایده مناسب است، اما جایگزین سیستم reactivity یک فریمورک نیست. ارزش اصلی آن در فهمیدن این نکته است که وابستگیها جادویی پیدا نمیشوند؛ سیستم فقط خواندن داده را هنگام اجرای تابع زیر نظر میگیرد.
شرطها چه اثری دارند؟
وابستگیها بر اساس همان مسیر اجراشده ثبت میشوند. اگر effect در حالت مهمان user.name را نخواند، فعلا به آن وابسته نمیشود. با عوض شدن شرط و اجرای دوباره effect ممکن است مجموعه وابستگیها هم تغییر کند. به همین دلیل پاک کردن وابستگیهای قبلی در یک پیادهسازی کامل ضروری است.
اجرای تو در تو هم نیاز به stack دارد. اگر یک effect هنگام اجرا effect دیگری را راه بیندازد، متغیر تکی activeEffect اطلاعات قبلی را از دست میدهد. کتابخانههای واقعی بعد از پایان effect داخلی، effect بیرونی را از stack برمیگردانند.
ادامه دارد
تا اینجا چهار قطعه از مسیر کنار هم قرار گرفتهاند. در قسمت پنجم، چند روز دیگر این مجموعه را ادامه میدهیم.