ค้นพบว่า Fiber กระทบยอดต้นไม้ (tree) อย่างไร รวมกลุ่มการอัปเดตอย่างไร และทำให้งานเรนเดอร์สามารถถูกขัดจังหวะได้อย่างไร
เปิดบทเรียนนี้ใน Kodokonตั้งแต่ React 16 เป็นต้นมา เอนจินภายในถูกเรียกว่า Fiber: ทุกเอลิเมนต์ในต้นไม้ของคุณจะถูกแทนด้วยโหนด fiber ซึ่งเป็นหน่วยของงาน (unit of work) หนึ่งหน่วย จริง ๆ แล้ว React ดูแล ต้นไม้สองต้น (เทคนิค double-buffering): ต้นไม้ current ซึ่งสะท้อนสิ่งที่อยู่บนหน้าจอ และต้นไม้ workInProgress ซึ่งถูกสร้างขึ้นระหว่างการเรนเดอร์ งานเกิดขึ้นในสอง เฟส ที่ต่างกัน: เฟสเรนเดอร์ (render phase) (การเรียกคอมโพเนนต์ของคุณ การคำนวณความแตกต่าง) ซึ่งสามารถถูกขัดจังหวะและเล่นซ้ำได้ จากนั้นคือ เฟสคอมมิต (commit phase) (การนำการเปลี่ยนแปลงไปใช้กับ DOM การรันเอฟเฟกต์) ซึ่ง เป็นแบบซิงโครนัสและขัดจังหวะไม่ได้เสมอ การกระทบยอดอาศัยฮิวริสติกสองข้อที่ทำให้การเปรียบเทียบมีต้นทุนแบบเชิงเส้น: ถ้า ชนิด (type) ของเอลิเมนต์เปลี่ยนไปที่ตำแหน่งหนึ่ง React จะทำลายซับทรี (subtree) ทั้งหมดและสร้างใหม่ทั้งหมด รวมถึงสเตตด้วย; สำหรับลิสต์ พร็อพ key ช่วยให้ React จับคู่เอลิเมนต์ระหว่างการเรนเดอร์สองครั้งได้
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<input defaultValue={item.label} />
</li>
))}
</ul>
);
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.กลไกพื้นฐานข้อที่สอง: การรวมกลุ่มอัปเดต (batching) เมื่อมีการอัปเดตสเตตหลายครั้งถูกทริกเกอร์ภายในบริบทเดียวกัน React จะรวมพวกมันไว้ใน การเรนเดอร์เพียงครั้งเดียว ก่อน React 18 การรวมกลุ่มนี้ทำงานเฉพาะภายในตัวจัดการเหตุการณ์ (event handler) ของ React เท่านั้น; ตั้งแต่ createRoot เป็นต้นมา มันเป็นแบบ อัตโนมัติในทุกที่: ตัวจับเวลา (timer), promise, ตัวฟังเหตุการณ์ระดับเนทีฟ นี่ก็เป็นเหตุผลว่าทำไมการอ่านสเตตทันทีหลัง setState จึงคืนค่าเก่ากลับมา: การอัปเดตถูกจัดคิวไว้ ยังไม่ถูกนำไปใช้
function handleTick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: a single render, even here.
}
setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.การเรนเดอร์แบบพร้อมกัน (concurrent rendering) ผลักดันตรรกะไปไกลกว่านั้น: เฟสเรนเดอร์สามารถ ถูกขัดจังหวะ กลับมาทำต่อ หรือแม้กระทั่งถูกทิ้งแล้วเริ่มใหม่ ได้ ถ้ามีการอัปเดตที่ด่วนกว่าเข้ามา React จัดลำดับความสำคัญของการอัปเดต (สิ่งที่เรียกว่า lanes): การกดปุ่มบนแป้นพิมพ์คือเรื่องด่วน ส่วนการกรองลิสต์ที่ถูกทำเครื่องหมายด้วย startTransition ไม่ใช่ ผลลัพธ์โดยตรง: ฟังก์ชันคอมโพเนนต์ของคุณต้อง บริสุทธิ์ (pure) เพราะ React อาจเรียกมันหลายครั้งโดยไม่เคยคอมมิตผลลัพธ์เลย อีกกรณีขอบ (edge case) หนึ่ง: สโตร์ภายนอก (Redux, Zustand, ซิงเกิลตันที่ทำเอง) ที่ถูกอ่านระหว่างการเรนเดอร์ที่ขัดจังหวะได้ อาจเปลี่ยนแปลง กลางคันของการเรนเดอร์ ทำให้เกิดอินเทอร์เฟซที่ไม่สอดคล้องกันซึ่งเรียกว่า tearing API useSyncExternalStore แก้ปัญหานี้โดยบังคับให้เรนเดอร์แบบซิงโครนัสเมื่อสโตร์เปลี่ยนแปลง
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(notify) => {
window.addEventListener('resize', notify);
return () =>
window.removeEventListener('resize', notify);
},
() => window.innerWidth
);
}<div> กลายเป็น <span> ที่ตำแหน่งเดียวกัน?createRoot การเรียก setState สองครั้งภายใน setTimeout ทริกเกอร์การเรนเดอร์กี่ครั้ง?