كيف تفهم وتتقن جوهر DOM الافتراضي؟ أنصح الجميع بتعلم مشروع Snabbdom.
Snabbdom هي مكتبة تنفيذ DOM افتراضية، وأسباب التوصية هي: أولاً، الكود صغير نسبيًا، والكود الأساسي يتكون من بضع مئات من الأسطر فقط، وثانيًا، يعتمد Vue على أفكار هذا المشروع لتنفيذ DOM الافتراضي؛ أفكار التصميم/التنفيذ والتوسع لهذا المشروع تستحق مرجعك.
سناب /snab/، سويدية، تعني سريع.
اضبط وضعية جلوسك المريحة وابتهج، فلنبدأ في تعلم DOM الافتراضي، يجب علينا أولاً معرفة المعرفة الأساسية بـ DOM ونقاط الضعف في تشغيل DOM مباشرةً باستخدام JS.
DOM (نموذج كائن المستند) هو نموذج كائن مستند يستخدم بنية شجرة الكائن لتمثيل مستند HTML/XML، ونهاية كل فرع من الشجرة عبارة عن عقدة تحتوي على كائنات. تسمح لك أساليب DOM API بمعالجة هذه الشجرة بطرق محددة. باستخدام هذه الأساليب، يمكنك تغيير بنية المستند أو نمطه أو محتواه.
جميع العقد في شجرة DOM هي Node
الأولى، Node
هي فئة أساسية. Element
Text
Comment
جميعهم يرثون منه.
بمعنى آخر، Element
Text
Comment
هي ثلاث Node
خاصة، تسمى ELEMENT_NODE
على التوالي.
يمثل TEXT_NODE
و COMMENT_NODE
عقد العناصر (علامات HTML) وعقد النص وعقد التعليق. يحتوي Element
أيضًا على فئة فرعية تسمى HTMLElement
. ما الفرق بين HTMLElement
و Element
؟ يمثل HTMLElement
العناصر في HTML، مثل: <span>
، <img>
، وما إلى ذلك، وبعض العناصر ليست معيار HTML، مثل <svg>
. يمكنك استخدام الطريقة التالية لتحديد ما إذا كان هذا العنصر هو HTMLElement
:
document.getElementById('myIMG') مثيل HTMLElement؛
يعد إنشاء DOM "مكلفًا" للمتصفح. لنأخذ مثالًا كلاسيكيًا يمكننا إنشاء عنصر p بسيط من خلال document.createElement('p')
وطباعة جميع السمات:
يمكنك أن ترى أن هناك الكثير من السمات المطبوعة عند تحديث أشجار DOM المعقدة بشكل متكرر، ستحدث مشكلات في الأداء. يستخدم Virtual DOM كائن JS أصليًا لوصف عقدة DOM، لذا فإن إنشاء كائن JS أقل تكلفة بكثير من إنشاء كائن DOM.
VNode عبارة عن بنية كائن تصف DOM الظاهري في Snabbdom، ويكون المحتوى كما يلي:
type Key = string number |. واجهة VNode { // محدد CSS، مثل: 'p#container'. سيل: سلسلة |. // التعامل مع فئات CSS وسماتها وما إلى ذلك من خلال الوحدات النمطية. البيانات: VNodeData |. // مصفوفة عقدة فرعية افتراضية، يمكن أن تكون عناصر المصفوفة أيضًا عبارة عن سلاسل. الأطفال: صفيف <VNode |. سلسلة> |. // أشر إلى كائن DOM الحقيقي الذي تم إنشاؤه. الدردار: عقدة |. /** * هناك حالتان لسمة النص: * 1. لم يتم تعيين محدد الاختيار، مما يشير إلى أن العقدة نفسها هي عقدة نصية. * 2. تم تعيين sel للإشارة إلى أن محتوى هذه العقدة عبارة عن عقدة نصية. */ النص: سلسلة |. // يستخدم لتوفير معرف لـ DOM الموجود، والذي يجب أن يكون فريدًا بين العناصر الشقيقة لتجنب عمليات إعادة البناء غير الضرورية بشكل فعال. المفتاح: مفتاح |. } // بعض الإعدادات على vnode.data، أو خطافات وظائف الفئة أو دورة الحياة، وما إلى ذلك. واجهة VNodeData { الدعائم؟: الدعائم؛ أترس؟: أترس؛ صنف؟: فصول؛ style ؟: VNodeStyle; مجموعة البيانات؟: مجموعة البيانات؛ على؟: على؛ AttachData ؟: AttachData؛ هوك ؟: السنانير. مفتاح؟: مفتاح؛ ns ؟: سلسلة؛ fn?: () => VNode; الحجج ؟: أي []؛ // للمجموعات هو ؟: سلسلة // للعناصر المخصصة v1 [مفتاح: سلسلة]: أي؛ // لأي وحدة أخرى تابعة لجهة خارجية }
على سبيل المثال، قم بتعريف كائن vnode مثل هذا:
const vnode = h( "ع # حاوية"، {الفئة: {نشط: صحيح } }، [ h('span', { style: {fontWeight: 'bold' } }, 'هذا غامق')، "وهذا مجرد نص عادي" ]);
نقوم بإنشاء كائنات vnode من خلال الدالة h(sel, b, c)
. يحدد تطبيق الكود h()
بشكل أساسي ما إذا كانت المعلمات b وc موجودة أم لا، ويعالجها في البيانات والأطفال في النهاية في شكل مصفوفة. أخيرًا، يتم إرجاع تنسيق نوع VNode
المحدد أعلاه من خلال وظيفة vnode()
.
لنأخذ أولاً مثالًا تخطيطيًا بسيطًا لعملية التشغيل، ولنحصل أولاً على مفهوم عام للعملية:
معالجة الفرق هي العملية المستخدمة لحساب الفرق بين العقد الجديدة والقديمة.
دعونا نلقي نظرة على نموذج التعليمات البرمجية الذي يتم تشغيله بواسطة Snabbdom:
import { حرف أولي, وحدة, الدعائمالوحدة النمطية, نمط الوحدة, الحدثListenerModule, ح، } من 'snabbdom'؛ تصحيح ثابت = init([ // تهيئة وظيفة التصحيح classModule عن طريق تمرير الوحدة، // تمكين وظيفة الفئات PropsModule، // دعم تمرير الدعائم styleModule, // يدعم الأنماط المضمنة والرسوم المتحركة eventsListenersModule, // يضيف الاستماع إلى الأحداث]); // <p id="container"></p> حاوية const = document.getElementById('container'); عقدة ثابتة = ح( "ص#حاوية.two.classes"، { على: { انقر: someFn } }، [ h('span', { style: {fontWeight: 'bold' } }, 'هذا غامق')، 'وهذا مجرد نص عادي'، h('a', {props: { href: '/foo' } }, "سآخذك إلى أماكن!"), ] ); // قم بتمرير عقدة عنصر فارغة. التصحيح (حاوية، vnode)؛ ثابت نيوVnode = ح ( "ص#حاوية.two.classes"، {في: {انقر: آخرEventHandler } }، [ ح( 'فترة'، { النمط: { وزن الخط: 'عادي'، نمط الخط: 'مائل' } }، "هذا الآن نوع مائل" )، "ولا يزال هذا مجرد نص عادي"، h('a', {props: { href: ''/bar' } }, "سآخذك إلى أماكن!"), ] ); // استدعاء patch() مرة أخرى لتحديث العقدة القديمة إلى العقدة الجديدة. patch(vnode, newVnode);
كما يتبين من مخطط العملية ونموذج التعليمات البرمجية، يتم وصف عملية تشغيل Snabbdom على النحو التالي:
قم أولاً باستدعاء init()
للتهيئة، ويجب تكوين الوحدات النمطية المستخدمة أثناء التهيئة. على سبيل المثال، يتم استخدام وحدة classModule
لتكوين سمة فئة العناصر في شكل كائنات؛ ويتم استخدام وحدة eventListenersModule
لتكوين مستمعي الأحداث، وما إلى ذلك. سيتم إرجاع الدالة patch()
بعد استدعاء init()
.
أنشئ كائن vnode الذي تمت تهيئته من خلال الدالة h()
، واستدعاء الدالة patch()
لتحديثه، وأخيرًا أنشئ كائن DOM الحقيقي من خلال createElm()
.
عندما يكون التحديث مطلوبًا، قم بإنشاء كائن vnode جديد، واستدعاء وظيفة patch()
للتحديث، وأكمل التحديث التفاضلي لهذه العقدة والعقد التابعة من خلال patchVnode()
و updateChildren()
.
يستخدم Snabbdom تصميم الوحدة النمطية لتوسيع تحديث الخصائص ذات الصلة بدلاً من كتابتها كلها في الكود الأساسي. إذن كيف يتم تصميم هذا وتنفيذه؟ بعد ذلك، دعونا نصل أولاً إلى المحتوى الأساسي لتصميم Kangkang، وهو وظائف دورة الحياة للخطافات.
Snabbdom سلسلة من وظائف دورة الحياة الغنية، والمعروفة أيضًا بوظائف الخطاف هذه قابلة للتطبيق في الوحدات النمطية أو يمكن تعريفها مباشرة على vnode. على سبيل المثال، يمكننا تحديد تنفيذ الخطاف على vnode مثل هذا:
h('p.row', { المفتاح: "myRow"، خطاف: { أدخل: (vnode) => { console.log(vnode.elm.offsetHeight); }, }, });
يتم الإعلان عن جميع وظائف دورة الحياة على النحو التالي:
معلمات رد اتصال | عقدة مشغل | الاسم |
---|---|---|
pre | بدء التصحيح، | لا |
init | تمت إضافة عقدة vnode | vnode |
create | عنصر DOM بناءً على عقدة vnode | emptyVnode, vnode |
وإدراج عنصر | insert | vnode vnode |
عنصر | prepatch | |
على وشك تصحيح | oldVnode, vnode | |
تم تحديث عنصر | update | vnode oldVnode, vnode |
تم تصحيح عنصر | postpatch | vnode oldVnode, vnode |
تمت إزالة عنصر | destroy | vnode بشكل مباشر أو غير vnode |
remove | element بإزالة vnode من DOM | vnode, removeCallback |
post | RemoveCallback عملية التصحيح | لا |
ينطبق إلى الوحدة: pre
، create
، update
، destroy
، remove
، post
. تنطبق على إعلانات vnode ما يلي: init
، create
، insert
، prepatch
، update
، postpatch
، destroy
، remove
.
دعونا نلقي نظرة على كيفية تنفيذ Kangkang. على سبيل المثال، لنأخذ وحدة classModule
كمثال:
import { VNode, VNodeData } from "../vnode"; استيراد {الوحدة النمطية} من "./module"؛ نوع التصدير Classes = Record<string, boolean>; وظيفة updateClass (oldVnode: VNode، vnode: VNode): void { // فيما يلي تفاصيل تحديث سمة الفصل، تجاهلها الآن. // ... } import const classModule: Module = { create: updateClass, update: updateClass };
يمكنك أن ترى أن مفتاح الكائن هو اسم Module
الخطاف على النحو التالي:
استيراد { خطاف مسبق, إنشاء خطاف, تحديثهوك, تدمير الخطاف, إزالة الخطاف, بوستهوك, } من "../السنانير"; وحدة نوع التصدير = جزئي<{ قبل: الخطاف المسبق؛ إنشاء: إنشاء خطاف؛ التحديث: UpdateHook؛ تدمير: تدمير هوك؛ إزالة: إزالة الخطاف؛ آخر: بوستهوك؛ }>;
يعني Partial
في TS أن سمات كل مفتاح في الكائن يمكن أن تكون فارغة، وهذا يعني أنه ما عليك سوى تحديد الخطاف الذي يهمك في تعريف الوحدة. الآن بعد أن تم تعريف الخطاف، كيف يتم تنفيذه في هذه العملية؟ بعد ذلك، دعونا نلقي نظرة على وظيفة init()
:
// ما هي الخطافات التي يمكن تعريفها في الوحدة. خطافات ثابتة: صفيف <keyof Module> = [ "يخلق"، "تحديث"، "يزيل"، "تدمير"، "قبل"، "بريد"، ]; دالة التصدير init( الوحدات: صفيف<جزئي<الوحدة النمطية>>، دومابي؟: دومابي، خيارات؟: خيارات ) { // سيتم أخيرًا تخزين وظيفة الخطاف المحددة في الوحدة هنا. const cbs: ModuleHooks = { يخلق: []، تحديث: []، يزيل: []، تدمير: []، قبل: []، بريد: []، }; // ... // اجتياز الخطافات المحددة في الوحدة وتخزينها معًا. لـ (خطاف ثابت للخطافات) { لـ (وحدة الوحدة النمطية للوحدات النمطية) { const currentHook = Module[hook]; إذا (الخطاف الحالي !== غير محدد) { (cbs[hook] as Any[]).push(currentHook); } } } // ... }
يمكنك أن ترى أن init()
يجتاز أولًا كل وحدة أثناء التنفيذ، ثم يخزن وظيفة الخطاف في كائن cbs
. عند التنفيذ، يمكنك استخدام وظيفة patch()
:
وظيفة التصدير init( الوحدات: صفيف<جزئي<الوحدة النمطية>>، دومابي؟: دومابي، خيارات؟: خيارات ) { // ... تصحيح وظيفة الإرجاع( oldVnode: VNode |.DocumentFragment. العقدة: VNode ): العقدة الافتراضية { // ... // يبدأ التصحيح، قم بتنفيذ الخطاف المسبق. for (i = 0; i < cbs.pre. length; ++i) cbs.pre[i](); // ... } }
هنا نأخذ الخطاف pre
كمثال. وقت تنفيذ الخطاف pre
هو عندما يبدأ تنفيذ التصحيح. يمكنك أن ترى أن وظيفة patch()
تستدعي بشكل دوري الخطافات المرتبطة pre
والمخزنة في cbs
في بداية التنفيذ. تشبه استدعاءات وظائف دورة الحياة الأخرى هذا. يمكنك رؤية استدعاءات وظائف دورة الحياة المقابلة في مكان آخر في الكود المصدري.
فكرة التصميم هنا هي نمط المراقب . ينفذ Snabbdom الوظائف غير الأساسية عن طريق توزيعها في وحدات نمطية، بالإضافة إلى تعريف دورة الحياة، يمكن للوحدة تحديد الخطافات التي تهتم بها. ثم عند تنفيذ init()
، تتم معالجتها في كائنات cbs
لتسجيل هذه الخطافات؛ عندما يحين وقت التنفيذ، يتم استخدام هذه الخطافات لإعلام معالجة الوحدة النمطية. هذا يفصل بين الكود الأساسي ورمز الوحدة، ومن هنا يمكننا أن نرى أن نمط المراقب هو نمط شائع لفصل الكود.
بعد ذلك نأتي إلى وظيفة Kangkang الأساسية patch()
. يتم إرجاع هذه الوظيفة بعد استدعاء init()
. وتتمثل وظيفتها في تثبيت VNode وتحديثه كما يلي:
function patch(oldVnode: VNode | Element |.DocumentFragment، vnode: VNode): VNode { // من أجل البساطة، لا تهتم بـ DocumentFragment. // ... }
المعلمة oldVnode
هي عنصر VNode أو DOM القديم أو جزء المستند، والمعلمة vnode
هي الكائن المحدث. أنشر هنا وصفًا للعملية مباشرةً:
استدعاء الخطاف pre
المسجل في الوحدة.
إذا كان oldVnode
Element
، فسيتم تحويله إلى كائن vnode
فارغ، ويتم تسجيل elm
في السمة.
الحكم هنا هو ما إذا كان Element
(oldVnode as any).nodeType === 1
nodeType === 1
.
ثم تحديد ما إذا كان oldVnode
و vnode
متماثلان سيتم استدعاء sameVnode()
هنا لتحديد:
function SameVnode(vnode1: VNode, vnode2: VNode): boolean { // نفس المفتاح. const isSameKey = vnode1.key === vnode2.key; // مكون الويب، اسم علامة العنصر المخصص، انظر هنا: // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement const isSameIs = vnode1.data?.is === vnode2.data?.is; // نفس المحدد. const isSameSel = vnode1.sel === vnode2.sel; // الثلاثة متماثلون. return isSameSel && isSameKey && isSameIs; }
patchVnode()
لتحديث الفرق.createElm()
لإنشاء عقدة DOM جديدة بعد الإنشاء، وأدخل عقدة DOM واحذف عقدة DOM القديمة.يمكن إدراج عقد جديدة عن طريق استدعاء قائمة انتظار insert
المسجلة في كائن vnode المتضمن في العملية المذكورة أعلاه، patchVnode()
createElm()
. أما سبب القيام بذلك فسيتم ذكره في createElm()
.
وأخيرًا، يتم استدعاء خطاف post
المسجل في الوحدة.
تهدف العملية بشكل أساسي إلى إجراء فرق إذا كانت العقد الافتراضية متماثلة، وإذا كانت مختلفة، فقم بإنشاء عقد جديدة وحذف القديمة. بعد ذلك، دعونا نلقي نظرة على كيفية إنشاء createElm()
لعقد DOM.
createElm()
ينشئ عقدة DOM بناءً على تكوين vnode. تتم العملية كما يلي:
قم باستدعاء الخطاف init
الذي قد يكون موجودًا على كائن vnode.
ثم سنتعامل مع عدة مواقف:
إذا كان vnode.sel === '!'
، فهذه هي الطريقة التي يستخدمها Snabbdom لحذف العقدة الأصلية، بحيث يتم إدراج عقدة تعليق جديدة. نظرًا لأنه سيتم حذف العقد القديمة بعد createElm()
، فإن هذا الإعداد يمكن أن يحقق غرض إلغاء التثبيت.
في حالة وجود تعريف محدد vnode.sel
:
قم بتحليل المحدد واحصل على id
tag
class
.
اتصل بـ document.createElement()
أو document.createElementNS
لإنشاء عقدة DOM، وتسجيلها في vnode.elm
، وتعيين id
tag
class
بناءً على نتائج الخطوة السابقة.
استدعاء ربط create
على الوحدة النمطية.
معالجة المصفوفة children
:
إذا كانت children
عبارة عن مصفوفة، فاستدعاء createElm()
بشكل متكرر لإنشاء العقدة الفرعية، ثم اتصل بـ appendChild
لتثبيتها ضمن vnode.elm
.
إذا لم يكن children
مصفوفة ولكن vnode.text
موجود، فهذا يعني أن محتوى هذا العنصر هو نص. في هذا الوقت، يتم استدعاء createTextNode
لإنشاء عقدة نصية وتثبيتها ضمن vnode.elm
.
قم باستدعاء خطاف create
الموجود على vnode. وأضف خطاف insert
الموجود على vnode إلى قائمة انتظار خطاف insert
.
الموقف المتبقي هو أن vnode.sel
غير موجود، مما يشير إلى أن العقدة نفسها عبارة عن نص، ثم قم باستدعاء createTextNode
لإنشاء عقدة نصية وتسجيلها على vnode.elm
.
وأخيرا العودة vnode.elm
.
يمكن أن نرى من العملية برمتها أن createElm()
تختار كيفية إنشاء عقد DOM بناءً على إعدادات مختلفة لمحدد sel
. هناك تفاصيل يجب إضافتها هنا: قائمة انتظار insert
المذكورة في patch()
. السبب وراء الحاجة إلى قائمة انتظار ربط insert
هذه هو أنها تحتاج إلى الانتظار حتى يتم إدراج DOM فعليًا قبل تنفيذها، وتحتاج أيضًا إلى الانتظار حتى يتم إدراج جميع العقد التابعة، حتى نتمكن من حساب معلومات الحجم والموضع الخاصة بـ DOM. العنصر الموجود في insert
ليكون دقيقًا. بالاشتراك مع عملية إنشاء العقد الفرعية المذكورة أعلاه، createElm()
عبارة عن استدعاء متكرر لإنشاء العقد الفرعية، وبالتالي فإن قائمة الانتظار ستسجل العقد الفرعية أولاً ثم نفسها. بهذه الطريقة يمكن ضمان الأمر عند تنفيذ قائمة الانتظار في نهاية patch()
.
بعد ذلك، دعونا نلقي نظرة على كيفية استخدام Snabbdom patchVnode()
لإجراء فرق، وهو جوهر DOM الافتراضي. يكون تدفق معالجة patchVnode()
كما يلي:
قم أولاً بتنفيذ ربط prepatch
على vnode.
إذا كان oldVnode وvnode هما نفس مرجع الكائن، فسيتم إرجاعهما مباشرة دون معالجة.
خطافات update
الاتصال على الوحدات النمطية والعقد الافتراضية.
إذا لم يتم تعريف vnode.text
، فسيتم التعامل مع العديد من حالات children
:
إذا كان كل من oldVnode.children
و vnode.children
موجودان وليسا متماثلين. ثم اتصل بـ updateChildren
للتحديث.
vnode.children
موجود لكن oldVnode.children
غير موجود. إذا كان oldVnode.text
موجودًا، فامسحه أولاً، ثم اتصل بـ addVnodes
لإضافة vnode.children
جديد.
vnode.children
غير موجود ولكن oldVnode.children
موجود. اتصل بـ removeVnodes
لإزالة oldVnode.children
.
في حالة عدم وجود oldVnode.children
أو vnode.children
. امسح oldVnode.text
إذا كان موجودًا.
إذا تم تعريف vnode.text
وكان مختلفًا عن oldVnode.text
. إذا كان oldVnode.children
موجودًا، فاتصل بـ removeVnodes
لمسحه. ثم قم بتعيين محتوى النص من خلال textContent
.
أخيرًا، قم بتنفيذ ربط postpatch
على vnode.
يمكن أن نرى من العملية أن التغييرات التي تطرأ على السمات ذات الصلة بالعقد الخاصة بها في الاختلاف، مثل class
style
وما إلى ذلك، يتم تحديثها بواسطة الوحدة، ومع ذلك، لن نتوسع كثيرًا هنا، إذا لزم الأمر يمكن إلقاء نظرة على التعليمات البرمجية المتعلقة بالوحدة. تركز المعالجة الأساسية الرئيسية للفرق على children
. بعد ذلك، يعالج Kangkang diff العديد من الوظائف ذات الصلة children
.
أمرًا بسيطًا للغاية. قم أولاً باستدعاء createElm()
لإنشائه، ثم قم بإدراجه في الأصل المقابل.
destory
إزالة remove
destory
، يتم استدعاء هذا الخطاف أولاً. المنطق هو أولاً استدعاء الخطاف على كائن vnode، ثم استدعاء الخطاف على الوحدة النمطية. ثم يتم استدعاء هذا الخطاف بشكل متكرر على vnode.children
بهذا الترتيب.remove
، لن يتم تشغيل هذا الخطاف إلا عند حذف العنصر الحالي من العنصر الأصلي، ولن يتم تشغيل العناصر الفرعية في العنصر المحذوف، وسيتم استدعاء هذا الخطاف على كل من الوحدة النمطية وكائن vnode قم بتشغيل الوحدة أولاً ثم اتصل على vnode. ما هو أكثر خصوصية هو أن العنصر لن تتم إزالته فعليًا حتى يتم استدعاء كافة remove
، وهذا يمكن أن يحقق بعض متطلبات الحذف المتأخر.يمكن أن نرى مما سبق أن منطق الاتصال لهذين الخطافين مختلف، على وجه الخصوص، سيتم استدعاء remove
فقط على العناصر المنفصلة مباشرة عن الأصل.
updateChildren()
لمعالجة فرق العقدة الفرعية، وهي أيضًا وظيفة معقدة نسبيًا في Snabbdom. الفكرة العامة هي تعيين إجمالي أربعة مؤشرات للرأس والذيل لـ oldCh
و newCh
. هذه المؤشرات الأربعة هي oldStartIdx
و oldEndIdx
و newStartIdx
و newEndIdx
على التوالي. ثم قارن بين المصفوفتين في while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
للعثور على نفس الأجزاء لإعادة استخدامها وتحديثها، وانتقل إلى زوج واحد من المؤشرات لكل مقارنة. تتم معالجة عملية الاجتياز التفصيلية بالترتيب التالي:
إذا كان أي من المؤشرات الأربعة يشير إلى vnode == null، ثم يتحرك المؤشر إلى المنتصف، مثل: start++ أو end--، فسيتم شرح حدوث null لاحقًا.
إذا كانت عقد البداية القديمة والجديدة متماثلة، أي أن sameVnode(oldStartVnode, newStartVnode)
يُرجع صحيحًا، استخدم patchVnode()
لإجراء فرق، وستتحرك كلتا عقدتي البداية خطوة واحدة نحو المنتصف.
إذا كانت العقد النهائية القديمة والجديدة هي نفسها، فسيتم استخدام patchVnode()
أيضًا، وتتحرك العقدتان النهائيتان خطوة واحدة إلى المنتصف.
إذا كانت عقدة البداية القديمة هي نفس عقدة النهاية الجديدة، فاستخدم patchVnode()
لمعالجة التحديث أولاً. ثم يجب نقل عقدة DOM المقابلة لـ oldStart. تتمثل استراتيجية النقل في التحرك قبل العقدة الشقيقة التالية لعقدة DOM المقابلة لـ oldEndVnode
. لماذا يتحرك هكذا؟ بادئ ذي بدء، oldStart هو نفس newEnd، مما يعني أنه في معالجة الحلقة الحالية، يتم نقل عقدة البداية للمصفوفة القديمة إلى اليمين لأن كل معالجة تحرك مؤشرات الرأس والذيل إلى المنتصف، ونحن نقوم بتحديث القديم إلى المصفوفة الجديدة في هذا الوقت، ربما لم تتم معالجة oldEnd بعد، ولكن في هذا الوقت تم تحديد oldStart لتكون الأخيرة في المعالجة الحالية للمصفوفة الجديدة، لذلك من المعقول الانتقال إلى الأخوة التاليين. العقدة القديمة بعد اكتمال النقل، يتحرك كل من oldStart++ وnewEnd-- خطوة واحدة إلى منتصف المصفوفات الخاصة بهما.
إذا كانت عقدة النهاية القديمة هي نفس عقدة البداية الجديدة، فسيتم استخدام patchVnode()
لمعالجة التحديث أولاً، ثم يتم نقل عقدة DOM المقابلة لـ oldEnd إلى عقدة DOM المقابلة لـ oldStartVnode
. سبب النقل هو نفس الخطوة السابقة. بعد اكتمال النقل، oldEnd--، newStart++.
إذا لم يكن الأمر كذلك، فاستخدم مفتاح newStartVnode للعثور على معرف منخفض في oldChildren
. هناك منطقان مختلفان للمعالجة اعتمادًا على ما إذا كان النص المنخفض موجودًا:
إذا لم يكن الرمز المنخفض موجودًا، فهذا يعني أنه تم إنشاء newStartVnode حديثًا. أنشئ DOM جديدًا من خلال createElm()
وأدخله قبل DOM المطابق لـ oldStartVnode
.
إذا كان النص المنخفض موجودًا، فسيتم التعامل معه في حالتين:
إذا كان sel الخاص بالعقدتين مختلفتين، فسيتم اعتباره تم إنشاؤه حديثًا، وقم بإنشاء DOM جديد من خلال createElm()
، وأدخله قبل DOM المطابق لـ oldStartVnode
.
إذا كان sel هو نفسه، فستتم معالجة التحديث من خلال patchVnode()
، ويتم تعيين vnode المقابل للنص المنخفض الخاص بـ oldChildren
على غير محدد، ولهذا السبب يظهر == null في اجتياز المؤشر المزدوج السابق. ثم أدخل العقدة المحدثة في DOM المطابق لـ oldStartVnode
.
بعد اكتمال العمليات المذكورة أعلاه، newStart++.
بعد اكتمال الاجتياز، لا يزال هناك حالتان للتعامل معهما. الأول هو أن oldCh
قد تمت معالجته بالكامل، ولكن لا تزال هناك عقد جديدة في newCh
، ويجب إنشاء DOM جديد لكل newCh
متبقي؛ والآخر هو أن newCh
قد تمت معالجته بالكامل، ولا تزال هناك عقد قديمة في oldCh
. يجب إزالة العقد الزائدة. يتم التعامل مع الحالتين على النحو التالي:
function updateChildren( ParentElm: العقدة، oldCh: VNode[]، newCh: VNode[]، تم إدراج VnodeQueue: VNodeQueue ) { // عملية اجتياز المؤشر المزدوج. // ... // هناك عقد جديدة في newCh يجب إنشاؤها. إذا (newStartIdx <= newEndIdx) { // يجب إدراجه قبل آخر معالجة newEndIdx. before = newCh[newEndIdx + 1] == null null : newCh[newEndIdx + 1].elm; addVnodes( الوالدين, قبل، جديد, نيوستارت آي دي إكس, جديدEndIdx, InsertedVnodeQueue ); } // لا تزال هناك عقد قديمة في oldCh تحتاج إلى إزالتها. إذا (oldStartIdx <= oldEndIdx) { RemoveVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
دعونا نستخدم مثالًا عمليًا لإلقاء نظرة على عملية معالجة updateChildren()
:
الحالة الأولية هي كما يلي، مصفوفة العقدة الفرعية القديمة هي [A, B, C]، ومصفوفة العقدة الجديدة هي [B, A, C ، د]:
في الجولة الأولى من المقارنة، تختلف عقدتي البداية والنهاية، لذلك نتحقق من وجود newStartVnode في العقدة القديمة ونبحث عن موضع oldCh[1]. ثم قم بتنفيذ patchVnode()
للتحديث أولاً، ثم قم بتعيين oldCh[1 ] = undef، وأدخل DOM قبل oldStartVnode
، ويتحرك newStartIdx
خطوة واحدة إلى الوراء، وتكون الحالة بعد المعالجة كما يلي:
في الجولة الثانية من المقارنة، فإن oldStartVnode
و newStartVnode
متماثلان. عند تنفيذ patchVnode()
للتحديث، ينتقل oldStartIdx
و newStartIdx
إلى المنتصف بعد المعالجة، وتكون الحالة كما يلي:
في الجولة الثالثة من المقارنة، oldStartVnode == null
، ينتقل oldStartIdx
إلى المنتصف، ويتم تحديث الحالة على النحو التالي:
في الجولة الرابعة من المقارنة، فإن oldStartVnode
و newStartVnode
متماثلان. عند تنفيذ patchVnode()
للتحديث، ينتقل oldStartIdx
و newStartIdx
إلى المنتصف بعد المعالجة، وتكون الحالة كما يلي:
في هذا الوقت، يكون oldStartIdx
أكبر من oldEndIdx
، وتنتهي الحلقة. في الوقت الحالي، لا تزال هناك عقد جديدة لم تتم معالجتها في newCh
، وتحتاج إلى استدعاء addVnodes()
لإدراجها، والحالة النهائية هي كما يلي:
، تم تصنيف المحتوى الأساسي لـ DOM الافتراضي هنا، وأعتقد أن مبادئ التصميم والتنفيذ لـ Snabbdom جيدة جدًا، إذا كان لديك الوقت، فيمكنك الانتقال إلى تفاصيل كود مصدر Kangkang لإلقاء نظرة فاحصة الأفكار تستحق التعلم.