اخبار وب

SolidJS و Fine-grained Reactivity؛ مبانی Signals و وضعیت نسخه ۲

Signals، Memo، Effect و Cleanup در SolidJS را با مثال کامل سفارش یاد بگیرید؛ تفاوت مدل اجرای Component و وضعیت Release Candidate نسخه ۲ را بشناسید.

1405/01/08 4 دقیقه مطالعه 533 بازدید
SolidJS و Fine-grained Reactivity؛ مبانی Signals و وضعیت نسخه ۲

در یک فرم سفارش، تغییر تعداد کالا باید مبلغ را به‌روز کند. سؤال Solid این است که برای چنین تغییری، کدام بخش از محاسبه و رابط کاربر باید دوباره کار کند. Fine-grained Reactivity وابستگی میان داده و مصرف‌کننده آن را ثبت می‌کند تا به‌روزرسانی به بخش‌های مرتبط برسد. این رویکرد به معنی رایگان‌بودن محاسبات یا سریع‌ترین‌بودن هر برنامه Solid نیست؛ روش متفاوتی برای سازمان‌دهی تغییرات است.

JSX ممکن است کد Solid را شبیه React نشان دهد، اما مدل اجرای آن یکسان نیست. انتقال عادت‌های یک Framework بدون شناخت این تفاوت، خطاهایی ایجاد می‌کند که ظاهر کد آن‌ها را پنهان می‌کند: مقدار دیگر به‌روز نمی‌شود، Effect بی‌دلیل نوشته می‌شود یا Listener پس از حذف Component باقی می‌ماند. بهتر است ابتدا Signals و ردیابی وابستگی را با یک نمونه کوچک یاد بگیرید و سپس درباره مهاجرت محصول تصمیم بگیرید.

وضعیت Solid 2 را از مفهوم Reactivity جدا کنید

در بررسی ۱۲ اکتبر ۲۰۲۶، صفحه رسمی Releases، solid-js@2.0.0-rc.14 را با برچسب Pre-release نشان می‌دهد. این وضعیت با انتشار نهایی Stable یکی نیست. بنابراین عنوانی که نسخه ۲ را یک انتشار نهایی با عملکرد تضمینی معرفی کند، دقیق نیست. هم‌زمان، Fine-grained Reactivity از قبل در Solid وجود داشته و ویژگی تازه‌ای نیست که فقط با نسخه ۲ شروع شده باشد.

در معرفی رسمی Release Candidate نسخه ۲، طراحی Async در سیستم Reactive و Reveal از موضوع‌های اصلی است. این مقاله آن APIهای در حال تغییر را با APIهای آموزش عمومی مخلوط نمی‌کند. نمونه اجرایی پایین برای یادگیری مبانی، روی شاخه 1.9 نوشته شده است. نصب RC و مهاجرت به نسخه ۲ باید با مستندات همان نسخه و سازگاری ابزار Build، Router و کتابخانه‌های پروژه بررسی شود.

Signal یک مقدار و یک مسیر دسترسی دارد

طبق راهنمای Signals، createSignal یک Getter و یک Setter برمی‌گرداند. Getter را مانند تابع فراخوانی می‌کنید تا مقدار خوانده شود. این شکل، صرفاً انتخاب ظاهری API نیست؛ خواندن در زمینه Reactive امکان ثبت وابستگی را فراهم می‌کند. نام quantity به تابع Getter اشاره دارد و quantity() مقدار فعلی آن را می‌دهد. اشتباه‌گرفتن این دو، هم در محاسبه و هم در نمایش نتیجه مشکل ایجاد می‌کند.

اگر ابتدای Component مقدار Getter را در یک متغیر معمولی ذخیره کنید، آن متغیر یک Snapshot از همان لحظه است. خودش به یک مقدار Reactive تبدیل نمی‌شود. برای محاسبه‌ای که باید به‌روز شود، تابعی بنویسید که Getter را هنگام مصرف بخواند یا از Memo استفاده کنید. این تمایز هنگام انتقال Props و Destructuring هم مهم است؛ هر شکلی از دسترسی که ارتباط با منبع Reactive را قطع کند، باید آگاهانه انتخاب شود.

Component با هر تغییر State دوباره اجرا نمی‌شود

مستند Componentها توضیح می‌دهد که منطق راه‌اندازی هر نمونه Component هنگام ایجاد آن اجرا می‌شود و سیستم Reactive به‌روزرسانی‌های بعدی را مدیریت می‌کند. بنابراین یک console.log معمولی در بدنه Component، ابزار ثبت همه تغییرات Signal نیست. این جمله به معنی ممنوع‌بودن ساخت دوباره Component نیست؛ مثلاً مسیر شرطی می‌تواند یک نمونه را حذف کند و بعداً نمونه جدیدی بسازد.

JSX در Solid با ابزار مناسب Compile می‌شود تا نقاط اتصال داده و DOM ساخته شوند. مرورگر فایل JSX خام این مثال را مستقیماً اجرا نمی‌کند. نیاز به Build باید در آموزش روشن باشد، وگرنه خواننده با یک فایل ناقص روبه‌رو می‌شود. نبود Virtual DOM در این مدل نیز همه هزینه‌های Layout، Paint، دریافت داده یا JavaScript برنامه شما را حذف نمی‌کند. عملکرد محصول را با کار واقعی کاربر بسنجید.

Derived Value را با Effect اشتباه نگیرید

برای محاسبه مبلغ سفارش از تعداد و قیمت، معمولاً یک Derived Value لازم است. createMemo برای نگهداری نتیجه محاسبه Reactive کاربرد دارد. اگر محاسبه بسیار کوچک و فقط یک‌بار مصرف است، تابع ساده هم می‌تواند کافی باشد. Memo را به همه خطوط کد اضافه نکنید؛ دلیل استفاده باید وابستگی و نحوه مصرف نتیجه باشد. در نمونه ما total از quantity و unitPrice محاسبه می‌شود و State جداگانه‌ای برای مبلغ نداریم.

Effect برای اثر جانبی، مانند هماهنگ‌کردن عنوان صفحه با داده، مناسب است. اگر داخل Effect یک Signal را صرفاً برای نگهداری نتیجه Signal دیگر تنظیم کنید، امکان پیچیده‌شدن زنجیره تغییرات وجود دارد. وابستگی‌ها در زمان خواندن ثبت می‌شوند و مسیر شرطی می‌تواند مجموعه وابستگی‌های فعال را تغییر دهد. به همین دلیل، فهم محل خواندن داده از حفظ‌کردن چند نام API ارزش بیشتری دارد.

نمونه کامل: محاسبه مبلغ سفارش

یک پوشه تازه بسازید و چهار فایل زیر را با نام‌های مشخص ذخیره کنید؛ main.jsx داخل پوشه src قرار می‌گیرد. Node.js نسخه 22.12 یا جدیدتر، npm و دسترسی به Registry برای نصب Dependency لازم است. نمونه از solid-js شاخه 1.9، Vite شاخه 7 و vite-plugin-solid شاخه 2 استفاده می‌کند و مخصوص آموزش APIهای 1.x است. CSS داخل index.html قرار دارد و هیچ فایل تصویر یا API بیرونی برای اجرای برنامه لازم نیست.

در همان پوشه npm install و سپس npm run dev را اجرا کنید. نشانی محلی را از خروجی Vite بردارید و در مرورگر باز کنید. برای ساخت فایل‌های قابل انتشار، npm run build و برای مشاهده خروجی ساخته‌شده npm run preview را دارید. دستور آخر یک راه مشاهده محلی است، نه تنظیم میزبانی Production. نسخه‌های حل‌شده را در Lockfile نگه دارید؛ RC نسخه ۲ را بدون تغییر این قرارداد به پروژه اضافه نکنید.

فایل package.json

{
  "name": "solid-order-demo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22.12.0" },
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "dependencies": { "solid-js": "^1.9.0" },
  "devDependencies": { "vite": "^7.0.0", "vite-plugin-solid": "^2.11.0" }
}

فایل vite.config.js

import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
export default defineConfig({ plugins: [solid()] });

فایل index.html

<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>نمونه سفارش Solid</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; line-height: 1.8; }
    main { max-width: 32rem; }
    label { display: block; margin-block: 1rem; }
    input { display: block; padding: .6rem; font: inherit; max-width: 100%; }
    output { display: block; padding: 1rem; background: #f1f5f9; }
    button { font: inherit; padding: .6rem 1rem; }
  </style>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>
</html>

فایل src/main.jsx

import { createSignal, createMemo, createEffect, onCleanup } from 'solid-js';
import { render } from 'solid-js/web';

function readBoundedNumber(value, min, max, fallback) {
  const number = Number(value);
  return Number.isFinite(number)
    ? Math.min(max, Math.max(min, Math.trunc(number)))
    : fallback;
}

function App() {
  const [quantity, setQuantity] = createSignal(1);
  const [unitPrice, setUnitPrice] = createSignal(100000);
  const [seconds, setSeconds] = createSignal(0);
  const total = createMemo(() => quantity() * unitPrice());
  const format = new Intl.NumberFormat('fa-IR');
  const previousTitle = document.title;
  const timer = setInterval(() => setSeconds(value => value + 1), 1000);
  createEffect(() => { document.title = `مبلغ: ${format.format(total())} تومان`; });
  onCleanup(() => {
    clearInterval(timer);
    document.title = previousTitle;
  });
  return (
    <main>
      <h1>محاسبه مبلغ سفارش</h1>
      <label for="quantity">تعداد، از ۱ تا ۱۰۰
        <input id="quantity" type="number" min="1" max="100" step="1"
          value={quantity()} onInput={event => setQuantity(
            readBoundedNumber(event.currentTarget.value, 1, 100, 1))} />
      </label>
      <label for="price">قیمت واحد به تومان، از صفر تا یک میلیون
        <input id="price" type="number" min="0" max="1000000" step="1"
          value={unitPrice()} onInput={event => setUnitPrice(
            readBoundedNumber(event.currentTarget.value, 0, 1000000, 0))} />
      </label>
      <output aria-live="polite">مبلغ: {format.format(total())} تومان</output>
      <p>زمان بازبودن نمونه: {format.format(seconds())} ثانیه</p>
      <button type="button" onClick={() => { setQuantity(1); setUnitPrice(100000); }}>
        بازنشانی سفارش
      </button>
    </main>
  );
}
const root = document.getElementById('root');
if (!root) throw new Error('Missing #root');
render(() => <App />, root);

با تغییر هر ورودی، Getter مربوط خوانده می‌شود و Memo مبلغ جدید را محاسبه می‌کند. Effect عنوان صفحه را با مبلغ هماهنگ می‌کند. مقدار زمان سپری‌شده هم با Timer تغییر می‌کند؛ تغییر آن ورودی محاسبه مبلغ نیست. این مثال برای مشاهده ارتباط‌هاست و نتیجه آن Benchmark یا برتری نسبت به Framework دیگر محسوب نمی‌شود. داده ورودی در بازه مشخص نگه داشته شده و برای قیمت واقعی فروشگاه باید قرارداد عدد، واحد پول و محاسبه Server جدا طراحی شود.

Cleanup بخشی از طراحی Component است

Timer، Listener و Subscription ممکن است عمر بیشتری از محل استفاده خود پیدا کنند. onCleanup برای آزادکردن منابع در Scope مربوط کاربرد دارد. در نمونه، Timer هنگام Dispose شدن Scope پاک می‌شود و عنوان قبلی صفحه برمی‌گردد. Cleanup در یک Effect نیز می‌تواند قبل از اجرای دوباره آن انجام شود؛ آن را فقط با بستن کل صفحه یکی ندانید. محل ثبت Cleanup باید با مالکیت منبع هماهنگ باشد.

این موضوع در یک برنامه چندصفحه‌ای بیشتر دیده می‌شود. اگر هر بار ورود به صفحه یک Listener تازه ایجاد کنید و قبلی را حذف نکنید، پس از چند بار جابه‌جایی یک رویداد چند بار پردازش می‌شود. خطا را با افزودن Flagهای پراکنده پنهان نکنید؛ ابتدا مشخص کنید چه Scopeای منبع را ساخته و چه زمانی از بین می‌رود. همین قاعده برای کتابخانه نمودار، Observer و اتصال زنده هم کاربرد دارد.

Store و فهرست را بر اساس ساختار داده انتخاب کنید

Store برای داده‌های ساختاریافته، مانند تنظیمات یا مجموعه آیتم‌ها، ابزار دیگری در Solid است. این ابزار هر مسئله مدیریت State را خودکار حل نمی‌کند؛ شکل تغییر و مسیر دسترسی همچنان باید روشن باشد. برای نمایش مجموعه، راهنمای For و Index تفاوت رفتار آن‌ها را توضیح می‌دهد. انتخاب را با هویت آیتم و نحوه تغییر فهرست هماهنگ کنید، نه صرفاً کوتاه‌ترشدن کد.

برای یادگیری این موضوع‌ها، نمونه بعدی خود را به یک فهرست قابل حذف و جابه‌جایی تبدیل کنید و وضعیت هر آیتم را مستقل نگه دارید. اگر نیاز محصول فقط چند ورودی محلی است، افزودن Store عمومی ضرورت ندارد. همچنین اطلاعات مشتق‌شده را در چند محل کپی نکنید؛ اختلاف این نسخه‌ها Debug را دشوار می‌کند. برای مقایسه رویکردها، راهنمای Zustand در React یک موضوع جداست و API آن قابل انتقال مستقیم به Solid نیست.

انتخاب برای محصول به تیم و اکوسیستم وابسته است

Solid می‌تواند برای تیمی که مدل Signals را می‌فهمد، گزینه مناسبی برای ساخت رابط باشد. ولی وجود یک الگوی Reactivity جذاب، سازگاری کتابخانه فرم، ابزار طراحی، دسترس‌پذیری یا زیرساخت Server را تضمین نمی‌کند. نیازهای واقعی پروژه را فهرست کنید و برای هرکدام ابزار و مسئول نگهداری را مشخص کنید. JSX مشترک دلیل کافی برای سازگاری یک Package ساخته‌شده برای React نیست؛ وابستگی‌های Runtime آن را بررسی کنید.

اگر هدف رفع کندی محصول موجود است، ابتدا Bottleneck را پیدا کنید. تصویر بزرگ، درخواست تکراری یا Layout پرهزینه با تغییر Framework الزاماً اصلاح نمی‌شود. راهنمای بهینه‌سازی عملکرد React نیز بر اندازه‌گیری و علت مشخص تمرکز دارد. برای یادگیری Solid، همین نمونه کوچک را مبنا قرار دهید؛ برای مهاجرت، نسخه، سازگاری و هزینه نگهداری را در تصمیم پروژه ثبت کنید و وضعیت نسخه ۲ را از صفحه رسمی Releases دوباره بررسی کنید.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!