เมื่อเลื่อนไปยังส่วนหรือองค์ประกอบแบบเป็นโปรแกรม (โดยเฉพาะเมื่อใช้ behavior: 'smooth') การเรียกใช้การดำเนินการติดตามผลทันที เช่น การกะพริบสถานะไฮไลต์ การเริ่มภาพเคลื่อนไหวขาเข้า หรือการย้ายโฟกัส จะทำให้เอฟเฟกต์ทำงานขณะที่เป้าหมายยังอยู่นอกหน้าจอหรืออยู่ระหว่างการเลื่อน
ก่อนหน้านี้ นักพัฒนาแอปต้องเดาระยะเวลาการเลื่อนด้วย setTimeout() (ซึ่งเริ่มทำงานเร็วเกินไปในการเลื่อนแบบยาวและช้าเกินไปในการเลื่อนแบบสั้น) หรือรวม Listener เหตุการณ์ scrollend ไว้ใน Promise ด้วยตนเองพร้อมตัวจับเวลาสำรอง (เนื่องจาก scrollend จะไม่เริ่มทำงานเมื่อเป้าหมายอยู่ในมุมมองอยู่แล้วและไม่มีการเลื่อนเกิดขึ้น)
เมธอดการเลื่อนแบบเป็นโปรแกรมใน Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) และ Window (scroll(), scrollTo(), scrollBy()) จะแสดงผล Promise ซึ่งจะได้รับการแก้ไขเมื่อการดำเนินการเลื่อนเสร็จสมบูรณ์ หากเป้าหมายอยู่ที่ตำแหน่งการเลื่อนที่ขออยู่แล้วและไม่จำเป็นต้องเลื่อน Promise ที่แสดงผลจะได้รับการแก้ไขทันทีโดยไม่ค้าง
การใช้งาน
- ทำเครื่องหมายเครื่องจัดการเป็น
async: ใช้ฟังก์ชันasyncสำหรับ Listener เหตุการณ์หรือเครื่องจัดการการนำทางที่ทริกเกอร์การเลื่อน - รอเมธอดการเลื่อนโดยตรง: เรียกใช้
await element.scrollIntoView({ behavior: 'smooth', ... })(หรือawait element.scrollTo(...),await window.scrollTo(...)ฯลฯ) การดำเนินการจะหยุดชั่วคราวจนกว่าเบราว์เซอร์จะเลื่อนภาพเคลื่อนไหวเสร็จและตำแหน่งการเลื่อนคงที่ - ใช้เอฟเฟกต์หลังการเลื่อน: สลับคลาสไฮไลต์ เริ่มภาพเคลื่อนไหว หรือย้ายโฟกัสหลังจากนิพจน์
awaitทำงานเสร็จสมบูรณ์
การเลื่อนที่ถูกขัดจังหวะและการจัดการโฟกัส
- การจัดการการเลื่อนอย่างรวดเร็ว/ถูกขัดจังหวะ: หากการเลื่อนใหม่เริ่มขึ้นในคอนเทนเนอร์การเลื่อนเดียวกันก่อนที่การเลื่อนอย่างราบรื่นที่ดำเนินอยู่จะเสร็จสิ้น เบราว์เซอร์จะยกเลิกการเลื่อนอย่างราบรื่นที่ดำเนินอยู่และแก้ไข (ไม่ปฏิเสธ) สัญญาการเลื่อนที่รอดำเนินการทั้งหมดในคอนเทนเนอร์นั้น หากต้องการระงับเอฟเฟกต์หลังการเลื่อนสำหรับการเลื่อนที่ถูกแทนที่เมื่อผู้ใช้คลิกอย่างรวดเร็ว ให้ติดตามรหัสการดำเนินการที่เพิ่มขึ้นและแสดงผลก่อนกำหนดหลังจาก
awaitหากมีการเริ่มการเลื่อนใหม่
let currentScrollId = 0;
const sections = document.querySelectorAll('section');
async function scrollToAndHighlight(targetElement) {
const scrollId = ++currentScrollId;
sections.forEach((sec) => sec.classList.remove('highlight'));
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Abort follow-up effect if another scroll was triggered while awaiting
if (scrollId !== currentScrollId) return;
targetElement.classList.add('highlight');
}
- การโฟกัสเป้าหมายหลังจากเลื่อน: เมื่อย้ายโฟกัสของแป้นพิมพ์ไปยังองค์ประกอบเป้าหมาย (หรือส่วนหัวภายในองค์ประกอบที่มี
tabindex="-1") หลังจากawait targetElement.scrollIntoView(...)ให้ส่ง{ preventScroll: true }ไปยังfocus()เพื่อไม่ให้การเรียกโฟกัสเริ่มการเลื่อนที่แข่งขันกันเป็นครั้งที่ 2
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
ข้อจำกัดและแนวทางปฏิบัติแนะนำ
- บังคับ: รอให้
PromiseแสดงผลโดยตรงจากscrollIntoView(),scroll(),scrollTo()หรือscrollBy()แทนที่จะคาดเดาระยะเวลาการเลื่อนด้วยsetTimeout() - อย่ารวม
scrollendไว้ในnew Promise()ด้วยตนเองเป็นการติดตั้งใช้งานหลัก เนื่องจากscrollendจะไม่เริ่มทำงานเมื่อเป้าหมายอยู่ในมุมมองอยู่แล้วและไม่มีการเลื่อนหน้าจอ ในขณะที่เมธอดการเลื่อนจะแก้ไขโดยอัตโนมัติในกรณีดังกล่าว - สิ่งที่ควรทำ: ส่ง
{ preventScroll: true }เมื่อเรียกใช้.focus()หลังจากรอเมธอดการเลื่อน เพื่อไม่ให้เบราว์เซอร์ข้ามหรือเลื่อนเป็นครั้งที่ 2 - ควรทำ: ป้องกันเอฟเฟกต์หลังการเลื่อนด้วยโทเค็นคำขอ หากการโต้ตอบของผู้ใช้อย่างรวดเร็วสามารถทริกเกอร์การเลื่อนอย่างราบรื่นที่ทับซ้อนกันในคอนเทนเนอร์เดียวกันได้ เนื่องจากการขัดจังหวะการเลื่อนอย่างราบรื่นจะแก้ไข (แทนที่จะปฏิเสธ) สัญญา (Promise) ก่อนหน้า
- ข้อบกพร่องที่ทราบของ Chrome เกี่ยวกับ Root Scroller: Chrome มีข้อบกพร่องที่
Promiseซึ่งแสดงผลโดยscrollIntoView()จะได้รับการแก้ไขเร็วเกินไปเมื่อเป็น Root Scroller ของเอกสารที่กำลังเลื่อน การรอscrollIntoView()ในคอนเทนเนอร์การเลื่อนที่ฝังอยู่จะทำงานได้ตามที่คาดไว้ อย่ารวมเนื้อหาไว้ในคอนเทนเนอร์แบบเลื่อน เช่นdivที่มีoverflow-y: autoเพื่อแก้ปัญหาข้อบกพร่องนี้
กลยุทธ์สำรอง
ในเบราว์เซอร์ที่ไม่รองรับ Scroll Method Promise, scrollIntoView(), scroll(), scrollTo() และ scrollBy() จะแสดงผล undefined แทน Promise
- การเพิ่มประสิทธิภาพแบบต่อเนื่อง (ลักษณะการทำงานเริ่มต้นของ
await): เนื่องจากawait undefinedเป็น JavaScript ที่ถูกต้องซึ่งจะแก้ไขทันทีในไมโครทาสก์ถัดไป การเขียนawait element.scrollIntoView({ behavior: 'smooth' })จะไม่ส่งข้อผิดพลาดในเบราว์เซอร์ที่ไม่รองรับ แต่จะลดระดับลงอย่างราบรื่นโดยเรียกใช้โค้ดหลังการเลื่อนทันทีที่เริ่มเลื่อน สำหรับภาพที่สวยงามแต่ไม่สำคัญ เช่น ไฮไลต์ชั่วคราว วิธีนี้มักจะเพียงพอ - ทางเลือกที่เชื่อถือได้ (
scrollend+ หมดเวลา): หากเป้าหมาย Baseline ไม่รองรับscroll-promisesและการดำเนินการหลังการเลื่อนต้องรอจนกว่าการเลื่อนจะเสร็จสิ้นในเบราว์เซอร์รุ่นเก่า ให้ตรวจสอบว่าค่าที่แสดงผลเป็นPromise(result instanceof Promise) หรือไม่ เมื่อเป็นundefinedให้รอเหตุการณ์scrollendในคอนเทนเนอร์การเลื่อนที่มีการหมดเวลาทางเลือกสั้นๆ ในกรณีที่องค์ประกอบอยู่ในมุมมองอยู่แล้วและไม่มีการเลื่อนเกิดขึ้น
function scrollIntoViewAsync(element, options, scroller = window) {
const result = element.scrollIntoView(options);
// Feature detect: in supporting browsers, scrollIntoView() returns a Promise
if (result instanceof Promise) {
return result;
}
// Fallback for browsers where scrollIntoView() returns undefined
return new Promise((resolve) => {
let timeoutId;
const finish = () => {
clearTimeout(timeoutId);
scroller.removeEventListener('scrollend', finish);
scroller.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
// Restart the idle timer on every scroll event. If scrollend never fires
// (browsers without scrollend support), this resolves once scrolling goes idle.
clearTimeout(timeoutId);
timeoutId = setTimeout(finish, 100);
};
scroller.addEventListener('scrollend', finish, { once: true });
scroller.addEventListener('scroll', onScroll, { passive: true });
// If no scroll event fires within the idle window (e.g., already in view), resolve.
// 100ms is an example value; it only needs to exceed the gap between scroll events.
timeoutId = setTimeout(finish, 100);
});
}