Kodokon kodokon.com

เบื้องหลัง: การกระทบยอด (reconciliation), การรวมกลุ่มอัปเดต (batching), และการเรนเดอร์แบบพร้อมกัน (concurrent rendering)

ค้นพบว่า Fiber กระทบยอดต้นไม้ (tree) อย่างไร รวมกลุ่มการอัปเดตอย่างไร และทำให้งานเรนเดอร์สามารถถูกขัดจังหวะได้อย่างไร

11 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

ตั้งแต่ React 16 เป็นต้นมา เอนจินภายในถูกเรียกว่า Fiber: ทุกเอลิเมนต์ในต้นไม้ของคุณจะถูกแทนด้วยโหนด fiber ซึ่งเป็นหน่วยของงาน (unit of work) หนึ่งหน่วย จริง ๆ แล้ว React ดูแล ต้นไม้สองต้น (เทคนิค double-buffering): ต้นไม้ current ซึ่งสะท้อนสิ่งที่อยู่บนหน้าจอ และต้นไม้ workInProgress ซึ่งถูกสร้างขึ้นระหว่างการเรนเดอร์ งานเกิดขึ้นในสอง เฟส ที่ต่างกัน: เฟสเรนเดอร์ (render phase) (การเรียกคอมโพเนนต์ของคุณ การคำนวณความแตกต่าง) ซึ่งสามารถถูกขัดจังหวะและเล่นซ้ำได้ จากนั้นคือ เฟสคอมมิต (commit phase) (การนำการเปลี่ยนแปลงไปใช้กับ DOM การรันเอฟเฟกต์) ซึ่ง เป็นแบบซิงโครนัสและขัดจังหวะไม่ได้เสมอ การกระทบยอดอาศัยฮิวริสติกสองข้อที่ทำให้การเปรียบเทียบมีต้นทุนแบบเชิงเส้น: ถ้า ชนิด (type) ของเอลิเมนต์เปลี่ยนไปที่ตำแหน่งหนึ่ง React จะทำลายซับทรี (subtree) ทั้งหมดและสร้างใหม่ทั้งหมด รวมถึงสเตตด้วย; สำหรับลิสต์ พร็อพ key ช่วยให้ React จับคู่เอลิเมนต์ระหว่างการเรนเดอร์สองครั้งได้

JSX
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.
key ที่เสถียรจะรักษาสเตตและ DOM ของแต่ละแถวไว้

กลไกพื้นฐานข้อที่สอง: การรวมกลุ่มอัปเดต (batching) เมื่อมีการอัปเดตสเตตหลายครั้งถูกทริกเกอร์ภายในบริบทเดียวกัน React จะรวมพวกมันไว้ใน การเรนเดอร์เพียงครั้งเดียว ก่อน React 18 การรวมกลุ่มนี้ทำงานเฉพาะภายในตัวจัดการเหตุการณ์ (event handler) ของ React เท่านั้น; ตั้งแต่ createRoot เป็นต้นมา มันเป็นแบบ อัตโนมัติในทุกที่: ตัวจับเวลา (timer), promise, ตัวฟังเหตุการณ์ระดับเนทีฟ นี่ก็เป็นเหตุผลว่าทำไมการอ่านสเตตทันทีหลัง setState จึงคืนค่าเก่ากลับมา: การอัปเดตถูกจัดคิวไว้ ยังไม่ถูกนำไปใช้

JSX
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 แก้ปัญหานี้โดยบังคับให้เรนเดอร์แบบซิงโครนัสเมื่อสโตร์เปลี่ยนแปลง

JAVASCRIPT
import { useSyncExternalStore } from 'react';

function useWindowWidth() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('resize', notify);
      return () =>
        window.removeEventListener('resize', notify);
    },
    () => window.innerWidth
  );
}
การอ่านสโตร์ภายนอกโดยไม่เสี่ยงต่อ tearing

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ระหว่างการกระทบยอด React ทำอะไรถ้าเอลิเมนต์ <div> กลายเป็น <span> ที่ตำแหน่งเดียวกัน?
    • มันอัปเดตแท็กโดยยังคงลูก (children) ไว้
    • มันทำลายซับทรีทั้งหมด รวมถึงสเตตด้วย แล้วสร้างใหม่
    • มันเปรียบเทียบลูกแบบเรียกซ้ำก่อนตัดสินใจ
    • มันโยนข้อผิดพลาดการกระทบยอด
  2. ด้วย React 18 และ createRoot การเรียก setState สองครั้งภายใน setTimeout ทริกเกอร์การเรนเดอร์กี่ครั้ง?
    • สองครั้ง เพราะการรวมกลุ่มใช้ได้เฉพาะกับเหตุการณ์ของ React
    • เพียงครั้งเดียว ต้องขอบคุณการรวมกลุ่มอัตโนมัติ
    • ศูนย์ครั้ง การอัปเดตนอกเหตุการณ์จะถูกเพิกเฉย
  3. เฟสใดของงาน React ที่ขัดจังหวะได้ในโหมด concurrent?
    • เฟสคอมมิตเท่านั้น
    • เฟสเรนเดอร์เท่านั้น
    • ทั้งสองเฟส
    • ไม่มีเลย React ยังคงเป็นแบบซิงโครนัสเต็มรูปแบบ