จะเข้าใจและเชี่ยวชาญสาระสำคัญของ DOM เสมือนได้อย่างไร ฉันแนะนำให้ทุกคนมาเรียนรู้โครงการ Snabdom
Snabbdom เป็นไลบรารีการใช้งาน DOM เสมือน ประการแรก โค้ดมีขนาดค่อนข้างเล็ก และโค้ดหลักมีเพียงไม่กี่ร้อยบรรทัด ประการที่สอง Vue ใช้แนวคิดของโปรเจ็กต์นี้เพื่อปรับใช้ DOM เสมือน แนวคิดการออกแบบ/การนำไปใช้และการขยายโครงการนี้คุ้มค่าแก่การอ้างอิงของคุณ
snabb /snab/ ภาษาสวีเดน แปลว่า รวดเร็ว
ปรับอิริยาบถในการนั่งสบาย ๆ ของคุณแล้วเริ่มกันเลยดีกว่า หากต้องการเรียนรู้ Virtual DOM เราต้องรู้ความรู้พื้นฐานเกี่ยวกับ DOM และ Pain Point ของการใช้งาน DOM โดยตรงกับ JS ก่อน
DOM (Document Object Model) คือโมเดลออบเจ็กต์เอกสารที่ใช้โครงสร้างออบเจ็กต์เพื่อแสดงเอกสาร 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') instanceof HTMLElement;
มัน "แพง" สำหรับเบราว์เซอร์ในการสร้าง DOM มาดูตัวอย่างคลาสสิกกันดีกว่า เราสามารถสร้างองค์ประกอบ p ง่ายๆ ผ่าน document.createElement('p')
และพิมพ์แอตทริบิวต์ทั้งหมดออกมา:
คุณจะเห็นว่ามีคุณสมบัติการพิมพ์จำนวนมาก เมื่ออัปเดตแผนผัง DOM ที่ซับซ้อนบ่อยครั้ง ปัญหาด้านประสิทธิภาพจะเกิดขึ้น Virtual DOM ใช้วัตถุ JS ดั้งเดิมเพื่ออธิบายโหนด DOM ดังนั้นการสร้างวัตถุ JS จึงมีราคาถูกกว่าการสร้างวัตถุ DOM มาก
VNode เป็นโครงสร้างวัตถุที่อธิบาย DOM เสมือนใน Snabbdom เนื้อหามีดังนี้:
type Key = string number |. อินเทอร์เฟซ VNode { // ตัวเลือก CSS เช่น: 'p#container' sel: สตริง | . ไม่ได้กำหนด; // จัดการคลาส CSS คุณลักษณะ ฯลฯ ผ่านโมดูล ข้อมูล: VNodeData | . // อาร์เรย์โหนดลูกเสมือน องค์ประกอบอาร์เรย์ยังสามารถเป็นสตริงได้ ลูก ๆ : Array<VNode | .string> | . // ชี้ไปที่วัตถุ DOM จริงที่สร้างขึ้น เอล์ม: โหนด | . - * มีสองสถานการณ์สำหรับแอตทริบิวต์ข้อความ: * 1. ไม่ได้ตั้งค่าตัวเลือก sel แสดงว่าโหนดนั้นเป็นโหนดข้อความ * 2. sel ถูกตั้งค่า ระบุว่าเนื้อหาของโหนดนี้เป็นโหนดข้อความ - ข้อความ: สตริง | . ไม่ได้กำหนด; // ใช้เพื่อระบุตัวระบุสำหรับ DOM ที่มีอยู่ ซึ่งจะต้องไม่ซ้ำกันในองค์ประกอบพี่น้อง เพื่อหลีกเลี่ยงการดำเนินการสร้างใหม่โดยไม่จำเป็นอย่างมีประสิทธิภาพ คีย์: คีย์ | . ไม่ได้กำหนด; - // การตั้งค่าบางอย่างบน vnode.data, hooks ฟังก์ชันคลาสหรือวงจรชีวิต ฯลฯ อินเทอร์เฟซ VNodeData { อุปกรณ์ประกอบฉาก?: อุปกรณ์ประกอบฉาก; Attrs?: Attrs; คลาส?: คลาส; สไตล์?: VNodeStyle; ชุดข้อมูล?: ชุดข้อมูล; บน?: บน; attachmentData?: AttachData; ตะขอ?: ตะขอ; คีย์?: คีย์; ns?: string; // สำหรับ SVG fn?: () => VNode; // เสียงดังมาก args?: มี []; // สำหรับ thunks คือ?: string; // สำหรับองค์ประกอบที่กำหนดเอง v1 [คีย์: สตริง]: ใด ๆ ; // สำหรับโมดูลบุคคลที่สามอื่น ๆ }
ตัวอย่างเช่น กำหนดวัตถุ vnode ดังนี้:
const vnode = h( 'p#คอนเทนเนอร์', { คลาส: { ใช้งานอยู่: จริง } }, - h('span', { style: { fontWeight: 'bold' } }, 'นี่คือตัวหนา'), ' และนี่เป็นเพียงข้อความปกติ ' ]);
เราสร้างวัตถุ vnode ผ่านฟังก์ชัน h(sel, b, c)
การใช้งานโค้ด h()
ส่วนใหญ่จะกำหนดว่ามีพารามิเตอร์ b และ c หรือไม่ และประมวลผลเป็นข้อมูลและลูก ๆ ในที่สุดจะอยู่ในรูปแบบของอาร์เรย์ สุดท้าย รูปแบบประเภท VNode
ที่กำหนดไว้ข้างต้นจะถูกส่งกลับผ่านฟังก์ชัน vnode()
ขั้นแรกเรามาดูแผนภาพตัวอย่างง่ายๆ ของกระบวนการที่ทำงานอยู่ และอันดับแรกมีแนวคิดกระบวนการทั่วไป:
การประมวลผลส่วนต่างเป็นกระบวนการที่ใช้ในการคำนวณความแตกต่างระหว่างโหนดใหม่และโหนดเก่า
มาดูโค้ดตัวอย่างที่ดำเนินการโดย Snabbdom:
import { เริ่มต้น, คลาสโมดูล, อุปกรณ์ประกอบฉากโมดูล, สไตล์โมดูล, เหตุการณ์ผู้ฟังโมดูล, ชม, } จาก 'snabbdom'; const patch = init([ // เริ่มต้นฟังก์ชันแพทช์ classModule โดยส่งผ่านโมดูล // เปิดใช้งานฟังก์ชันคลาส propsModule // รองรับการส่งผ่านในอุปกรณ์ประกอบฉาก styleModule, // รองรับสไตล์อินไลน์และแอนิเมชั่น eventListenersModule, // เพิ่มการฟังเหตุการณ์]); // <p id="คอนเทนเนอร์"></p> const คอนเทนเนอร์ = document.getElementById('คอนเทนเนอร์'); const vnode = ชั่วโมง( 'p#container.two.classes', { เมื่อ: { คลิก: someFn } }, - h('span', { style: { fontWeight: 'bold' } }, 'นี่คือตัวหนา'), ' และนี่เป็นเพียงข้อความปกติ ' h('a', { อุปกรณ์ประกอบฉาก: { href: '/foo' } }, "ฉันจะพาคุณไป!"), - - // ส่งผ่านโหนดองค์ประกอบว่าง แพทช์ (คอนเทนเนอร์, vnode); const newVnode = h( 'p#container.two.classes', { เมื่อ: { คลิก: anotherEventHandler } }, - ชม( 'ช่วง', { สไตล์: { FontWeight: 'ปกติ', FontStyle: 'ตัวเอียง' } }, 'ตอนนี้เป็นแบบตัวเอียง' - ' และนี่ยังคงเป็นเพียงข้อความปกติ' h('a', { อุปกรณ์ประกอบฉาก: { href: ''/bar' } }, "ฉันจะพาคุณไป!"), - - // โทร patch() อีกครั้งเพื่ออัพเดตโหนดเก่าเป็นโหนดใหม่ patch(vnode, newVnode);
ดังที่เห็นได้จากแผนภาพกระบวนการและโค้ดตัวอย่าง กระบวนการทำงานของ Snabbdom อธิบายไว้ดังนี้:
การเรียกครั้งแรก init()
สำหรับการเริ่มต้น และโมดูลที่จะใช้จะต้องได้รับการกำหนดค่าระหว่างการเริ่มต้น ตัวอย่างเช่น โมดูล classModule
ใช้เพื่อกำหนดค่าแอ็ตทริบิวต์คลาสขององค์ประกอบในรูปแบบของอ็อบเจ็กต์ โมดูล eventListenersModule
ใช้เพื่อกำหนดค่าตัวฟังเหตุการณ์ เป็นต้น ฟังก์ชัน patch()
จะถูกส่งกลับหลังจากเรียกใช้ init()
สร้างอ็อบเจ็กต์ vnode ที่เตรียมใช้งานแล้วผ่านฟังก์ชัน h()
เรียกใช้ฟังก์ชัน patch()
เพื่ออัปเดต และสุดท้ายสร้างอ็อบเจ็กต์ DOM จริงผ่าน createElm()
เมื่อจำเป็นต้องอัปเดต ให้สร้างอ็อบเจ็กต์ vnode ใหม่ เรียกใช้ฟังก์ชัน patch()
เพื่ออัปเดต และดำเนินการอัปเดตส่วนต่างของโหนดนี้และโหนดย่อยให้เสร็จสิ้นผ่าน patchVnode()
และ updateChildren()
Snabbdom ใช้การออกแบบโมดูลเพื่อขยายการอัพเดตคุณสมบัติที่เกี่ยวข้อง แทนที่จะเขียนทั้งหมดลงในโค้ดหลัก แล้วสิ่งนี้ได้รับการออกแบบและนำไปใช้อย่างไร? ต่อไป เรามาพูดถึงเนื้อหาหลักของการออกแบบของ Kangkang นั่นคือ Hooks—ฟังก์ชันวงจรชีวิต
Snabbdom มีชุดฟังก์ชันวงจรชีวิตที่หลากหลาย หรือที่เรียกว่าฟังก์ชันวงจรชีวิตเหล่านี้ใช้ได้กับโมดูลหรือสามารถกำหนดได้โดยตรงบน vnode ตัวอย่างเช่น เราสามารถกำหนดการดำเนินการของ hook บน vnode ได้ดังนี้:
h('p.row', { คีย์: 'myRow', ตะขอ: { แทรก: (vnode) => { console.log(vnode.elm.offsetHeight); - - });
ฟังก์ชั่นวงจรชีวิตทั้งหมดได้รับการประกาศดังนี้:
ชื่อ | พารามิเตอร์การเรียกกลับ | โหนดทริก |
---|---|---|
pre | แพทช์ | ไม่มี |
init | vnode ถูกเพิ่ม | vnode |
create | องค์ประกอบ DOM ตาม vnode ถูกสร้างขึ้น | emptyVnode, vnode |
insert | vnode จะถูกแทรกลงในองค์ประกอบ prepatch | vnode |
prepatch | คือ กำลังจะแพตช์ | oldVnode, vnode |
update | รับการอัพเดตแล้ว | oldVnode, vnode |
postpatch | ของ vnode ได้รับการแพตช์ | oldVnode, vnode |
destroy | vnode ได้ถูกลบออกโดยตรงหรือโดย | vnode |
remove | vnode ได้ลบ vnode ออกจาก DOM | vnode, removeCallback |
post | RemoveCallback ได้เสร็จสิ้นกระบวนการแก้ไข | ไม่มีข้อใด |
ที่นำไปใช้ ไปที่โมดูล: pre
, create
, update
, destroy
, remove
, post
ใช้ได้กับการประกาศ vnode คือ: init
, create
, insert
, prepatch
, update
, postpatch
, destroy
, remove
มาดูกันว่า Kangkang ถูกนำไปใช้อย่างไร ตัวอย่างเช่น ลองใช้โมดูล classModule
เป็นตัวอย่าง คำสั่งของ Kangkang:
import { VNode, VNodeData } from "../vnode"; นำเข้า { โมดูล } จาก "./module"; ประเภทการส่งออก Classes = Record<string, boolean>; ฟังก์ชั่น updateClass (oldVnode: VNode, vnode: VNode): เป็นโมฆะ { // นี่คือรายละเอียดของการอัปเดตแอตทริบิวต์ class ไม่ต้องสนใจในตอนนี้ - - ส่งออก const classModule: Module = { create: updateClass, update: updateClass };
คุณจะเห็นว่าคำจำกัดความของ Module
ที่ส่งออกล่าสุดคือวัตถุ ดังต่อไปนี้:
นำเข้า { พรีฮุค, สร้างตะขอ อัพเดตฮุค, ทำลายตะขอ, ลบตะขอ, โพสต์ฮุค, } จาก "../hooks"; ประเภทการส่งออกโมดูล = บางส่วน<{ ก่อน: PreHook; สร้าง: CreateHook; อัปเดต: UpdateHook; ทำลาย: DestroyHook; ลบ: RemoveHook; โพสต์: PostHook; }>;
Partial
ใน TS หมายความว่าคุณลักษณะของแต่ละคีย์ในออบเจ็กต์สามารถเว้นว่างได้ กล่าวคือ เพียงกำหนดว่าฮุกใดที่คุณสนใจในคำจำกัดความของโมดูล เมื่อกำหนด hook แล้ว จะดำเนินการอย่างไรในกระบวนการ? ต่อไปเรามาดูฟังก์ชัน init()
กัน:
// อะไรคือ hooks ที่อาจกำหนดไว้ในโมดูล const hooks: Array<keyof Module> = [ "สร้าง", "อัปเดต", "ลบ", "ทำลาย", "ก่อน" "โพสต์", - ฟังก์ชั่นการส่งออก init( โมดูล: อาร์เรย์<บางส่วน<โมดูล>>, domapi?: DOMAPI, ตัวเลือก?: ตัวเลือก - //ฟังก์ชัน hook ที่กำหนดไว้ในโมดูลจะถูกเก็บไว้ที่นี่ในที่สุด const cbs: ModuleHooks = { สร้าง: [], อัปเดต: [], ลบ: [], ทำลาย: [], ก่อน: [], โพสต์: [], - - // สำรวจ hooks ที่กำหนดไว้ในโมดูลและจัดเก็บไว้ด้วยกัน สำหรับ (const hook of hooks) { สำหรับ (โมดูล const ของโมดูล) { const currentHook = โมดูล [ตะขอ]; ถ้า (currentHook !== ไม่ได้กำหนด) { (cbs[hook] อะไรก็ได้[]).push(currentHook); - - - - }
คุณจะเห็นว่า init()
แรกจะสำรวจแต่ละโมดูลระหว่างการดำเนินการ จากนั้นจึงจัดเก็บฟังก์ชัน hook ไว้ในวัตถุ cbs
เมื่อดำเนินการ คุณสามารถใช้ฟังก์ชัน patch()
:
ฟังก์ชันการส่งออก init( โมดูล: อาร์เรย์<บางส่วน<โมดูล>>, domapi?: DOMAPI, ตัวเลือก?: ตัวเลือก - - แพทช์ฟังก์ชันส่งคืน ( oldVnode: VNode | .DocumentFragment, vnode: VNode ): VNode { - // เริ่มแพตช์ ดำเนินการก่อนเบ็ด สำหรับ (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); - - }
ที่นี่เราใช้ pre
hook เป็นตัวอย่าง เวลาดำเนินการของ pre
hook คือเมื่อแพตช์เริ่มทำงาน คุณจะเห็นว่าฟังก์ชัน patch()
เรียก hooks ที่เกี่ยวข้อง pre
ที่เก็บไว้ใน cbs
ตามลำดับเมื่อเริ่มดำเนินการ การเรียกฟังก์ชันวงจรชีวิตอื่น ๆ จะคล้ายคลึงกับสิ่งนี้ คุณสามารถดูการเรียกใช้ฟังก์ชันวงจรชีวิตที่เกี่ยวข้องในที่อื่น ๆ ในซอร์สโค้ด
แนวคิดการออกแบบที่นี่คือ รูปแบบผู้สังเกตการณ์ Snabbdom ใช้งานฟังก์ชันที่ไม่ใช่แกนหลักโดยการกระจายฟังก์ชันเหล่านั้นในโมดูล เมื่อรวมกับคำจำกัดความของวงจรชีวิต โมดูลสามารถกำหนด hooks ที่สนใจได้ จากนั้นเมื่อดำเนินการ init()
ก็จะถูกประมวลผลเป็นวัตถุ cbs
เพื่อลงทะเบียน hooks เหล่านี้ เมื่อถึงเวลาดำเนินการให้โทร hooks เหล่านี้ใช้เพื่อแจ้งการประมวลผลโมดูล สิ่งนี้จะแยกโค้ดหลักและโค้ดโมดูลออกจากกัน จากจุดนี้ เราจะเห็นว่ารูปแบบผู้สังเกตการณ์เป็นรูปแบบทั่วไปสำหรับการแยกโค้ด
ต่อไปเรามาที่ฟังก์ชันหลัก Kangkang patch()
ฟังก์ชันนี้จะถูกส่งคืนหลังจากการเรียก init()
ฟังก์ชันคือเมานต์และอัปเดต VNode เป็นดังนี้:
function patch(oldVnode: VNode | Element |. DocumentFragment , vnode: VNode): VNode { // เพื่อความเรียบง่าย อย่าไปสนใจ DocumentFragment - }
พารามิเตอร์ oldVnode
คือองค์ประกอบ VNode หรือ DOM เก่าหรือส่วนของเอกสาร และพารามิเตอร์ vnode
เป็นอ็อบเจ็กต์ที่อัปเดต ที่นี่ฉันโพสต์คำอธิบายของกระบวนการโดยตรง:
การเรียก pre
hook ที่ลงทะเบียนไว้ในโมดูล
หาก oldVnode
เป็น Element
มันจะถูกแปลงเป็นอ็อบเจ็กต์ vnode
ที่ว่างเปล่า และ elm
จะถูกบันทึกในแอ็ตทริบิวต์
การตัดสินที่นี่คือไม่ว่าจะเป็น Element
(oldVnode as any).nodeType === 1
เสร็จสมบูรณ์ nodeType === 1
บ่งชี้ว่าเป็น ELEMENT_NODE ซึ่งกำหนดไว้ที่นี่
จากนั้นตรวจสอบว่า oldVnode
และ vnode
เหมือนกันหรือไม่ sameVnode()
จะถูกเรียกที่นี่เพื่อพิจารณาว่า:
ฟังก์ชัน 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; //ทั้งสามเหมือนกันเลย ส่งคืน isSameSel && isSameKey && isSameIs; }
patchVnode()
เพื่อรับการอัปเดตที่ต่างกันcreateElm()
เพื่อสร้างโหนด DOM ใหม่ หลังจากสร้างแล้ว ให้แทรกโหนด DOM และลบโหนด DOM เก่าโหนดใหม่อาจถูกแทรกโดยการเรียกคิว hook insert
ที่ลงทะเบียนในวัตถุ vnode ที่เกี่ยวข้องกับการดำเนินการข้างต้น patchVnode()
createElm()
สำหรับสาเหตุที่ทำเช่นนี้ จะมีการกล่าวถึงใน createElm()
ในที่สุด จะมีการเรียก post
hook ที่ลงทะเบียนไว้บนโมดูล
กระบวนการนี้เป็นสิ่งที่ต้องทำหาก vNodes เหมือนกันและหากพวกเขาแตกต่างกันให้สร้างใหม่และลบสิ่งเก่า ต่อไปลองมาดูกันว่า createElm()
สร้างโหนด DOM ได้อย่างไร
createElm()
สร้างโหนด DOM ตามการกำหนดค่าของ vnode กระบวนการมีดังนี้:
เรียก init
hook ที่อาจมีอยู่บนวัตถุ 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
hook บน vnode และเพิ่ม hook insert
บน vnode ลงในคิว hook insert
สถานการณ์ที่เหลืออยู่คือไม่มี vnode.sel
ซึ่งบ่งชี้ว่าโหนดนั้นเป็นข้อความ จากนั้นเรียก createTextNode
เพื่อสร้างโหนดข้อความและบันทึกลงใน vnode.elm
ในที่สุดก็กลับมา vnode.elm
จะเห็นได้จากกระบวนการทั้งหมดที่ createElm()
เลือกวิธีการสร้างโหนด DOM ตามการตั้งค่าที่แตกต่างกันของตัวเลือก sel
มีรายละเอียดที่จะเพิ่มที่นี่: insert
hook Queue ที่กล่าวถึงใน patch()
เหตุผลที่จำเป็นต้องมีคิว insert
นี้คือต้องรอจนกว่าจะแทรก DOM จริงก่อนที่จะดำเนินการ และต้องรอจนกว่าจะแทรกโหนดสืบทอดทั้งหมด เพื่อให้เราสามารถคำนวณข้อมูลขนาดและตำแหน่งของ องค์ประกอบใน insert
ให้ถูกต้อง เมื่อรวมกับกระบวนการสร้างโหนดย่อยด้านบน createElm()
เป็นการเรียกแบบเรียกซ้ำเพื่อสร้างโหนดย่อย ดังนั้นคิวจะบันทึกโหนดย่อยก่อนแล้วจึงบันทึกตัวมันเอง วิธีนี้สามารถรับประกันคำสั่งซื้อได้เมื่อดำเนินการคิวเมื่อสิ้นสุด patch()
ถัดไปลองดูว่า snabbdom ใช้ patchVnode()
ทำอย่างไรซึ่งเป็นแกนกลางของ Dom เสมือนจริง โฟลว์การประมวลผลของ patchVnode()
มีดังนี้:
ก่อนที่จะดำเนินการเบ็ด prepatch
บน vNode
หาก oldVnode และ vNode เป็นข้อมูลอ้างอิงวัตถุเดียวกันพวกเขาจะถูกส่งคืนโดยตรงโดยไม่ต้องประมวลผล
เรียก hooks update
บนโมดูลและ vnodes
หากไม่ได้กำหนด 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
hook บน vnode
จะเห็นได้จากกระบวนการที่การเปลี่ยนแปลงในคุณลักษณะที่เกี่ยวข้องของโหนดของตัวเองในความแตกต่าง เช่น class
style
ฯลฯ ได้รับการอัพเดตโดยโมดูล อย่างไรก็ตาม เราจะไม่ขยายมากเกินไปที่นี่ หากจำเป็น สามารถดูโค้ดที่เกี่ยวข้องกับโมดูลได้ การประมวลผลหลักหลักของ diff นั้นมุ่งเน้นไปที่ children
ต่อไป Kangkang diff จะประมวลผลฟังก์ชันต่าง ๆ ที่เกี่ยวข้องของ children
นั้นง่ายมาก ขั้นแรกให้เรียก createElm()
เพื่อสร้างมันขึ้นมา จากนั้นจึงแทรกมันเข้าไปในพาเรนต์ที่เกี่ยวข้อง
destory
remove
destory
เบ็ดนี้จะถูกเรียกก่อน ตรรกะคือการเรียก hook บนวัตถุ vnode ก่อน จากนั้นจึงเรียก hook บนโมดูล จากนั้นตะขอนี้เรียกว่าซ้ำบน vnode.children
ในลำดับนี้remove
ฮุกนี้จะถูกทริกเกอร์เมื่อองค์ประกอบปัจจุบันถูกลบออกจากพาเรนต์ องค์ประกอบลูกในองค์ประกอบที่ถูกลบจะไม่ถูกทริกเกอร์ และฮุกนี้จะถูกเรียกทั้งบนโมดูลและอ็อบเจ็กต์ vnode โมดูลก่อนแล้วจึงเรียกใช้ vnode สิ่งที่พิเศษกว่านั้นคือองค์ประกอบจะไม่ถูกลบออกจริง ๆ จนกว่าจะมีการเรียก remove
ทั้งหมด ซึ่งอาจบรรลุข้อกำหนดการลบล่าช้าบางประการจากข้างต้นจะเห็นได้ว่าตรรกะการเรียกของ hooks ทั้งสองนี้แตกต่างกัน โดยเฉพาะอย่างยิ่ง remove
จะถูกเรียกใช้เฉพาะในองค์ประกอบที่แยกจากพาเรนต์โดยตรง
updateChildren()
ใช้ในการประมวลผลโหนดย่อย diff และยังเป็นฟังก์ชันที่ค่อนข้างซับซ้อนใน Snabbdom แนวคิดทั่วไปคือการตั้งค่าพ newStartIdx
oldStartIdx
newEndIdx
head และ tail ทั้งหมดสี่ตัวสำหรับ oldCh
oldEndIdx
newCh
จากนั้นเปรียบเทียบอาร์เรย์ทั้งสองใน while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
เพื่อค้นหาส่วนเดียวกันเพื่อนำมาใช้ซ้ำและอัปเดต และเลื่อนตัวชี้ขึ้นไปหนึ่งคู่สำหรับการเปรียบเทียบแต่ละครั้ง กระบวนการสำรวจเส้นทางโดยละเอียดได้รับการประมวลผลตามลำดับต่อไปนี้:
หากพอยน์เตอร์ตัวใดตัวหนึ่งจากสี่ตัวชี้ไปที่ vnode == null ตัวชี้จะย้ายไปที่ตรงกลาง เช่น: start++ หรือ end-- การเกิดขึ้นของ null จะมีการอธิบายในภายหลัง
หากโหนดเริ่มต้นเก่าและใหม่เหมือนกัน นั่นคือ sameVnode(oldStartVnode, newStartVnode)
คืนค่าเป็นจริง ให้ใช้ patchVnode()
เพื่อดำเนินการต่าง และโหนดเริ่มต้นทั้งสองจะเคลื่อนไปทางตรงกลางหนึ่งก้าว
หากโหนดปลายเก่าและใหม่นั้นเหมือนกัน patchVnode()
ก็ใช้และโหนดปลายทั้งสองจะเลื่อนกลับไปที่กลาง
หากโหนดเริ่มต้นเก่าแก่เหมือนกับโหนดปลายทางใหม่ให้ใช้ patchVnode()
เพื่อประมวลผลการอัปเดตก่อน จากนั้นจำเป็นต้องย้ายโหนด DOM ที่สอดคล้องกับ oldStart กลยุทธ์การย้ายคือการย้ายก่อนโหนดพี่น้องถัดไปของโหนด DOM ที่สอดคล้องกับ oldEndVnode
ทำไมมันเคลื่อนไหวแบบนี้? ก่อนอื่น oldStart จะเหมือนกับ newEnd ซึ่งหมายความว่าในการประมวลผลลูปปัจจุบัน โหนดเริ่มต้นของอาร์เรย์เก่าจะถูกย้ายไปทางขวา เนื่องจากการประมวลผลแต่ละครั้งจะย้ายตัวชี้ส่วนหัวและส่วนท้ายไปตรงกลาง เรากำลังอัปเดต อาร์เรย์เก่าไปเป็นอาร์เรย์ใหม่ ในขณะนี้ oldEnd อาจไม่ได้รับการประมวลผล แต่ในขณะนี้ oldStart ได้รับการกำหนดให้เป็นอาร์เรย์สุดท้ายในการประมวลผลปัจจุบันของอาร์เรย์ใหม่ ดังนั้นจึงสมเหตุสมผลที่จะย้ายไปยังชุดพี่น้องถัดไป โหนดของ oldEnd หลังจากการย้ายเสร็จสิ้น oldStart++ และ newEnd-- ย้ายหนึ่งก้าวไปยังตรงกลางของอาร์เรย์ตามลำดับ
หากโหนดสิ้นสุดเก่าเหมือนกับโหนดเริ่มต้นใหม่ patchVnode()
จะถูกใช้เพื่อประมวลผลการอัปเดตก่อน จากนั้นโหนด DOM ที่สอดคล้องกับ oldEnd จะถูกย้ายไปยังโหนด DOM ที่สอดคล้องกับ oldStartVnode
เหตุผลในการย้ายคือ เช่นเดียวกับขั้นตอนก่อนหน้า หลังจากการย้ายเสร็จสิ้น oldEnd--, newStart++
หากไม่มีกรณีใดข้างต้น ให้ใช้คีย์ของ newStartVnode เพื่อค้นหา subscript idx ใน oldChildren
มีลอจิกการประมวลผลที่แตกต่างกันสองแบบ ขึ้นอยู่กับว่ามีตัวห้อยอยู่หรือไม่:
หากไม่มีตัวห้อย หมายความว่า newStartVnode ถูกสร้างขึ้นใหม่ สร้าง DOM ใหม่ผ่าน createElm()
และแทรกไว้หน้า DOM ที่สอดคล้องกับ oldStartVnode
หากมีตัวห้อยอยู่ จะถูกจัดการในสองกรณี:
หาก sel ของทั้งสอง vnodes แตกต่างกัน ก็จะยังคงถือเป็นการสร้างขึ้นใหม่ สร้าง DOM ใหม่ผ่าน createElm()
และแทรกไว้หน้า DOM ที่สอดคล้องกับ oldStartVnode
.
หาก sel เหมือนกัน การอัปเดตจะถูกประมวลผลผ่าน patchVnode()
และ vnode ที่สอดคล้องกับสคริปต์ย่อยของ oldChildren
จะถูกตั้งค่าเป็น undefinition นี่คือสาเหตุที่ == null ปรากฏในการสำรวจเส้นทางแบบ double pointer ก่อนหน้านี้ จากนั้นแทรกโหนดที่อัปเดตลงใน DOM ที่สอดคล้องกับ oldStartVnode
หลังจากการดำเนินการข้างต้นเสร็จสิ้น newStart++
หลังจากการสำรวจเสร็จสิ้น ยังมีสองสถานการณ์ที่ต้องจัดการ สิ่งหนึ่งคือ oldCh
ได้รับการประมวลผลอย่างสมบูรณ์แล้ว แต่ยังคงมีโหนดใหม่ใน newCh
และจำเป็นต้องสร้าง DOM ใหม่สำหรับแต่ละ newCh
ที่เหลือ อีกอย่างคือ newCh
ได้รับการประมวลผลอย่างสมบูรณ์แล้ว และยังคงมีโหนดเก่าใน oldCh
ต้องลบโหนดซ้ำซ้อน ทั้งสองสถานการณ์ได้รับการจัดการดังนี้:
ฟังก์ชั่น updateChildren( parentElm: โหนด oldCh: VNode[], ใหม่Ch: VNode[], InsertEdVnodeQueue: vnodequeue - // กระบวนการสำรวจตัวชี้สองครั้ง - // มีโหนดใหม่ใน newCh ที่ต้องสร้าง ถ้า (newStartIdx <= newEndIdx) { //จำเป็นต้องแทรกก่อน newEndIdx ที่ประมวลผลครั้งล่าสุด ก่อน = newCh [newEndIdx + 1] == null ? null : newCh [newEndIdx + 1] .elm; addvnodes ( Parentelm ก่อน, Newch, ใหม่StartIdx, ใหม่EndIdx, แทรกVnodeQueue - - // ยังมีโหนดเก่าใน oldCh ที่ต้องลบออก ถ้า (oldStartIdx <= oldEndIdx) { ลบVnodes (parentElm, oldCh, oldStartIdx, oldEndIdx); - }
ลองใช้ตัวอย่างเชิงปฏิบัติเพื่อดูกระบวนการประมวลผลของ updateChildren()
:
สถานะเริ่มต้นเป็นดังนี้ อาร์เรย์โหนดลูกเก่าคือ [A, B, C] และอาร์เรย์โหนดใหม่คือ [B, A, C , ง]:
ในการเปรียบเทียบรอบแรก โหนดเริ่มต้นและจุดสิ้นสุดจะแตกต่างกัน ดังนั้นเราจึงตรวจสอบว่ามี newStartVnode อยู่ในโหนดเก่าหรือไม่ และค้นหาตำแหน่งของ oldCh[1] จากนั้นให้รัน patchVnode()
เพื่ออัปเดตก่อน จากนั้นจึงตั้งค่า oldCh[1 ] = undefinition และแทรก DOM ก่อน oldStartVnode
, newStartIdx
จะเลื่อนไปข้างหลังหนึ่งขั้น และสถานะหลังการประมวลผลจะเป็นดังนี้:
ในรอบที่ oldStartIdx
ของการ newStartIdx
oldStartVnode
และ newStartVnode
เหมือน patchVnode()
ในรอบที่สามของการเปรียบเทียบ oldStartVnode == null
, oldStartIdx
ย้ายไปกลางและสถานะจะได้รับการปรับปรุงดังนี้:
ในรอบที่ oldStartIdx
ของการ newStartIdx
oldStartVnode
และ newStartVnode
เหมือน patchVnode()
ในเวลานี้ oldStartIdx
มากกว่า oldEndIdx
และลูปสิ้นสุดลง ในเวลานี้ยังมีโหนดใหม่ที่ยังไม่ได้ประมวลผลใน newCh
และคุณต้องเรียก addVnodes()
เพื่อแทรก
เนื้อหาหลักของ DOM เสมือนได้รับการจัดเรียงที่นี่ ความคิดก็คุ้มค่าที่จะเรียนรู้