DOM مجازی (vnode)

یک DOM مجازی سبک به سبک Snabbdom. رابط کاربری تو را با شیءهای ساده جاوااسکریپت (همان VNodeها) نشان می‌دهد، درخت کهنه را با درخت نو می‌سنجد و فقط تفاوت‌ها را بر DOM واقعی می‌نشاند.

  • init() سازوکار هم‌سنجی را برپا می‌کند و یک تابع patch برمی‌گرداند. ماژول‌های همراه (class / props / attrs / style / events) خودبه‌خود ثبت می‌شوند.
  • patch(oldVnode, newVnode) اگر oldVnode عنصری واقعی از DOM باشد درخت را سوار می‌کند، وگرنه دو درخت vnode را می‌سنجد و DOM را همان‌جا به‌روز می‌کند.
  • h(sel, dataOrChildren?, children?) همان یاری‌رسان hyperscript است که VNode می‌سازد.

وارد کردن

import { init, h, classModule, propsModule, styleModule, eventListenersModule } from 'ranuts/vnode';

نکته: در این پیاده‌سازی init() هیچ آرگومانی نمی‌گیرد: مجموعهٔ ماژول‌ها ثابت است و در درون ثبت می‌شود، پس لازم نیست تک‌تک صادرات‌های *Module را به init بدهی. آن‌ها برای مراجعه و وارسی صادر شده‌اند.

نمونه

آغاز سریع

import { init, h } from 'ranuts/vnode';

// init() یک تابع `patch` برمی‌گرداند.
// ماژول‌های همراه (class، props، attrs، style، events) خودبه‌خود ثبت می‌شوند.
const patch = init();

const container = document.getElementById('app');

// ساختن یک درخت vnode
let vnode = h('div#app.container', { style: { color: 'red' } }, [
  h('h1', 'Hello vnode'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Click me'),
]);

// رسم نخست: به‌جای vnode کهنه یک عنصر واقعی DOM بده تا درخت درون آن سوار شود
patch(container, vnode);

// بعدها: درخت به‌روزشده را بساز و vnode پیشین را روی آن patch کن.
// تنها تفاوت‌ها (متن، سبک، شنونده‌ها) بر DOM می‌نشیند.
const newVnode = h('div#app.container', { style: { color: 'green' } }, [
  h('h1', 'Hello again'),
  h('button', { on: { click: () => console.log('clicked') } }, 'Updated'),
]);

patch(vnode, newVnode);
vnode = newVnode; // تازه‌ترین درخت را برای patch بعدی نگه دار

ساختن گره با h

// فقط برچسب
h('div');

// برچسب + data
h('div', { class: { active: true } });

// برچسب + یک فرزند متنی
h('span', 'hello');

// برچسب + آرایه‌ای از فرزندان
h('ul', [h('li', 'one'), h('li', 'two')]);

// برچسب + data + فرزندان
h('a', { attrs: { href: '/home' } }, 'Home');

// گزینشگرهای به سبک CSS، شناسه و کلاس‌ها را تعیین می‌کنند
h('div#main.card.large', 'content'); // <div id="main" class="card large">content</div>

// اگر گزینشگر با "svg" آغاز شود، فضای‌نام SVG خودبه‌خود اعمال می‌شود
h('svg', { attrs: { width: 100, height: 100 } }, [h('circle', { attrs: { cx: 50, cy: 50, r: 40 } })]);

API

init()

سازوکار هم‌سنجی را می‌سازد و یک تابع patch برمی‌گرداند. ماژول‌های همراه در درون ثبت می‌شوند و خودش آرگومانی نمی‌گیرد.

بازگشت

مقدار توضیح نوع
patch درخت‌های vnode را بر DOM واقعی سوار یا با آن هم‌سنجی می‌کند (oldVnode: VNode | Element, vnode: VNode) => VNode

patch(oldVnode, vnode)

همان چیزی است که init() برمی‌گرداند. در نخستین فراخوان، به‌جای oldVnode یک Element واقعی از DOM بده تا درخت درون آن سوار شود. در فراخوان‌های بعدی، VNode پیشین را بده تا همان‌جا سنجیده و به‌روز شود. VNode تازه را برمی‌گرداند که آن را به‌عنوان مقدار «کهنه» برای فراخوان بعدی نگه می‌داری.

پارامترها

پارامتر توضیح نوع
oldVnode vnode پیشین، یا در نخستین سوار شدن یک عنصر DOM VNode | Element
vnode درخت vnode تازه‌ای که باید رسم شود VNode

h(sel, dataOrChildren?, children?)

یاری‌رسان hyperscript که یک VNode می‌سازد. چند شکل دارد:

امضا توضیح
h(sel) عنصری که فقط از یک گزینشگر ساخته می‌شود
h(sel, data) عنصری همراه با VNodeData (که data می‌تواند null باشد)
h(sel, children) عنصری همراه با فرزندان: یک متن یا عدد، یک VNode، یا یک آرایه
h(sel, data, children) عنصری همراه با داده و فرزندان

پارامترها

پارامتر توضیح نوع
sel گزینشگر به سبک CSS: tag، tag#id، tag.class و ترکیب آن‌ها (div#id.a.b). با svg… فضای‌نام SVG خودبه‌خود افزوده می‌شود string
data دادهٔ گره: class / props / attrs / style / شنونده‌ها / key / hook. می‌تواند null باشد VNodeData | null
children یک متن یا عدد (که گرهٔ متنی می‌شود)، یک VNode تنها، یا آرایه‌ای از آن‌ها VNodeChildren

فیلدهای VNodeData

میدان توضیح نوع اعمال‌کننده
props ویژگی‌های DOM که با elm[key] = value گذاشته می‌شوند Record<string, any> propsModule
attrs صفت‌های HTML که با setAttribute گذاشته می‌شوند (true و false صفت را می‌گذارند و برمی‌دارند) Record<string, string | number | boolean> attributesModule
class کلاس‌های شرطی: نگاشتی از نوع name → boolean Record<string, boolean> classModule
style سبک‌های درون‌خطی: نگاشتی از نوع name → value (کلیدهای --var به متغیرهای CSS بدل می‌شوند) Record<string, any> styleModule
on شنونده‌های رویداد: event → handler (یا آرایه‌ای از دستگیره‌ها) Record<string, Function | Function[]> eventListenersModule
key نشانی پایدار که الگوریتم هم‌سنجی برای جفت کردن و جابه‌جا کردن فرزندان به کار می‌برد string | number (هستهٔ هم‌سنجی)
ns نشانی فضای‌نام (در زیردرخت‌های SVG خودبه‌خود گذاشته می‌شود) string (هستهٔ هم‌سنجی)
hook قلاب‌های چرخهٔ عمر برای هر vnode (Hooks) Hooks (فقط در سطح نوع: نکته را ببین)

نکته: hook و نوع Hooks بخشی از سطح عمومی نوع‌ها هستند. اما این پیاده‌سازی پیراسته، DOM را از راه چرخهٔ عمر ماژول‌ها (create / update / destroy) می‌گرداند؛ فراخوان‌های data.hook که به هر vnode بسته‌اند، در حلقهٔ کنونی patch صدا زده نمی‌شوند.

ماژول‌ها

هر ماژول متولی یک تکه از VNodeData است. init() همهٔ آن‌ها را ثبت می‌کند؛ ضمناً تک‌تک هم صادر شده‌اند.

صادرات متولی توضیح
classModule data.class بر پایهٔ نگاشت name → boolean کلاس‌ها را می‌گذارد و برمی‌دارد
propsModule data.props ویژگی‌های DOM را یکراست نسبت می‌دهد (elm[key] = value)
attributesModule data.attrs صفت‌های HTML را با setAttribute می‌گذارد و برمی‌دارد (از جمله xml و xlink)
styleModule data.style سبک‌های درون‌خطی و ویژگی‌های سفارشی CSS را می‌گذارد
eventListenersModule data.on شنونده‌های رویداد را می‌چسباند و برمی‌دارد
modules شیء ثبت پیش‌فرض که نام هر ماژول را به خودِ ماژول می‌نگارد

صادرات‌های لایهٔ پایین‌تر

صادرات نوع توضیح
vnode (sel, data, children, text, elm) => VNode کارخانهٔ سطح‌پایین VNode که h در درون به کار می‌برد. در کد برنامه h را ترجیح بده.
addNS (data, children, sel) => void فضای‌نام SVG را بازگشتی بر یک زیردرخت اعمال می‌کند. برای گزینشگرهای svg… خودِ h آن را صدا می‌زند.
htmlDomApi DOMAPI مبدل پیش‌فرض DOM مرورگر که patch در درون به کار می‌برد (ساختن، درج، حذف، گره‌های متنی و…).
is { array, isStr, primitive, isVnode } یاری‌رسان‌های کوچک پاسداری از نوع که در سراسر درون vnode به کار می‌روند.
Chain class Chain سازندهٔ دستوری و زنجیره‌پذیر DOM (setAttribute، append، setTextContent و…). به هم‌سنجی vnode ربطی ندارد.
create (tagName, options?) => Chain کارخانه‌ای برای راحتی کار که یک Chain تازه برمی‌گرداند.

نوع‌ها

نوع شکل و معنا
VNode { sel, data, children, elm, text, key, listener? }: یک گرهٔ مجازی
VNodeData { props?, attrs?, class?, style?, on?, key?, ns?, hook? }: فیلدها را در بالا ببین
VNodes VNode[]
VNodeChildElement VNode | string | number
VNodeChildren VNodeChildElement | VNodeChildElement[]
ArrayOrElement<T> T | T[]
Key string | number
Hooks { pre?, init?, create?, insert?, prepatch?, update?, postpatch?, destroy?, remove?, post? }
DOMAPI واسطی که عملیات DOM مورد استفادهٔ patch را وصف می‌کند (htmlDomApi را ببین)
Fragment گسترشی بر DocumentFragment برای کار با قطعه‌ها
Modules Record<string, Record<string, ModuleHook>>: شکل دفتر ثبت ماژول‌ها
ModuleHook یک فراخوان تنها از چرخهٔ عمر یک ماژول

یادداشت‌ها

  1. فقط مرورگر. ranuts/vnode به document و APIهای DOM دست می‌زند؛ آن را در کد مرورگر وارد کن، نه در Node.
  2. آخرین vnode را نگه دار. patch همان VNode تازه را برمی‌گرداند. آن را ذخیره کن و در به‌روزرسانی بعدی به‌عنوان oldVnode بده تا تفاوت‌ها نسبت به درخت کنونی سنجیده شود.
  3. در یک VNode، text و children با هم جمع نمی‌شوند: هر گره یا گرهٔ متنی است یا عنصری با فرزند.
  4. برای فهرست‌ها key بگذار. وقتی فهرستی پویا را می‌کشی، به هم‌نیاها مقدارهای key پایدار بده تا هم‌سنجی بتواند گره‌ها را جفت و جابه‌جا کند به‌جای آنکه از نو بسازد.