Skip to content

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 می‌سازد.

وارد کردن

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

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

نمونه

آغاز سریع

js
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

js
// فقط برچسب
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 تازه را برمی‌گرداند که آن را به‌عنوان مقدار «کهنه» برای فراخوان بعدی نگه می‌داری.

پارامترها

پارامترتوضیحنوع
oldVnodevnode پیشین، یا در نخستین سوار شدن یک عنصر DOMVNode | 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 → booleanRecord<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() همهٔ آن‌ها را ثبت می‌کند؛ ضمناً تک‌تک هم صادر شده‌اند.

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

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

صادراتنوعتوضیح
vnode(sel, data, children, text, elm) => VNodeکارخانهٔ سطح‌پایین VNode که h در درون به کار می‌برد. در کد برنامه h را ترجیح بده.
addNS(data, children, sel) => voidفضای‌نام SVG را بازگشتی بر یک زیردرخت اعمال می‌کند. برای گزینشگرهای svg… خودِ h آن را صدا می‌زند.
htmlDomApiDOMAPIمبدل پیش‌فرض DOM مرورگر که patch در درون به کار می‌برد (ساختن، درج، حذف، گره‌های متنی و…).
is{ array, isStr, primitive, isVnode }یاری‌رسان‌های کوچک پاسداری از نوع که در سراسر درون vnode به کار می‌روند.
Chainclass 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? }: فیلدها را در بالا ببین
VNodesVNode[]
VNodeChildElementVNode | string | number
VNodeChildrenVNodeChildElement | VNodeChildElement[]
ArrayOrElement<T>T | T[]
Keystring | number
Hooks{ pre?, init?, create?, insert?, prepatch?, update?, postpatch?, destroy?, remove?, post? }
DOMAPIواسطی که عملیات DOM مورد استفادهٔ patch را وصف می‌کند (htmlDomApi را ببین)
Fragmentگسترشی بر DocumentFragment برای کار با قطعه‌ها
ModulesRecord<string, Record<string, ModuleHook>>: شکل دفتر ثبت ماژول‌ها
ModuleHookیک فراخوان تنها از چرخهٔ عمر یک ماژول

یادداشت‌ها

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

منتشرشده تحت مجوز MIT.