Khi cuộn theo phương thức lập trình đến một phần hoặc phần tử (đặc biệt là bằng behavior: 'smooth'), việc kích hoạt ngay một hành động tiếp theo (chẳng hạn như nhấp nháy trạng thái làm nổi bật, bắt đầu ảnh động khi vào hoặc di chuyển tiêu điểm) sẽ khiến hiệu ứng chạy trong khi mục tiêu vẫn ở ngoài màn hình hoặc đang cuộn.
Trước đây, nhà phát triển phải đoán thời lượng cuộn bằng setTimeout() (kích hoạt quá sớm khi cuộn dài và quá muộn khi cuộn ngắn) hoặc gói trình nghe sự kiện scrollend theo cách thủ công trong Promise bằng bộ hẹn giờ dự phòng (vì scrollend không kích hoạt khi mục tiêu đã xuất hiện và không có thao tác cuộn nào xảy ra).
Các phương thức cuộn có lập trình trên Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) và Window (scroll(), scrollTo(), scrollBy()) trả về một Promise phân giải khi thao tác cuộn hoàn tất. Nếu mục tiêu đã ở vị trí cuộn được yêu cầu và không cần cuộn, thì Promise được trả về sẽ phân giải ngay lập tức mà không bị treo.
Triển khai
- Đánh dấu trình xử lý là
async: Sử dụng hàmasynccho trình nghe sự kiện hoặc trình xử lý điều hướng kích hoạt thao tác cuộn. - Chờ trực tiếp phương thức cuộn: Gọi
await element.scrollIntoView({ behavior: 'smooth', ... })(hoặcawait element.scrollTo(...),await window.scrollTo(...), v.v.). Quá trình thực thi sẽ tạm dừng cho đến khi trình duyệt hoàn tất ảnh động cuộn và vị trí cuộn ổn định. - Áp dụng hiệu ứng sau khi cuộn: Chuyển đổi lớp làm nổi bật, bắt đầu ảnh động hoặc di chuyển tiêu điểm sau khi biểu thức
awaithoàn tất.
Thao tác cuộn bị gián đoạn và quản lý tiêu điểm
- Xử lý thao tác cuộn nhanh/bị gián đoạn: Nếu một thao tác cuộn mới bắt đầu trên cùng một vùng chứa cuộn trước khi thao tác cuộn mượt mà đang diễn ra kết thúc, thì trình duyệt sẽ huỷ thao tác cuộn mượt mà đang diễn ra và giải quyết (không từ chối) tất cả promise cuộn đang chờ xử lý trên vùng chứa đó. Nếu bạn cần loại bỏ hiệu ứng sau khi cuộn cho các thao tác cuộn bị thay thế khi người dùng nhấp nhanh, hãy theo dõi mã nhận dạng thao tác tăng dần và trả về sớm sau
awaitnếu thao tác cuộn mới hơn đã bắt đầu:
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');
}
- Lấy tiêu điểm vào mục tiêu sau khi cuộn: Khi di chuyển tiêu điểm bàn phím đến phần tử mục tiêu (hoặc tiêu đề bên trong phần tử đó bằng
tabindex="-1") sauawait targetElement.scrollIntoView(...), hãy truyền{ preventScroll: true }đếnfocus()để lệnh gọi lấy tiêu điểm không bắt đầu thao tác cuộn thứ hai, cạnh tranh:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Các quy tắc ràng buộc và phương pháp hay nhất
- BẮT BUỘC: Chờ
Promiseđược trả về trực tiếp bằngscrollIntoView(),scroll(),scrollTo()hoặcscrollBy()thay vì đoán thời lượng cuộn bằngsetTimeout(). - KHÔNG gói
scrollendtrongnew Promise()thủ công làm phương thức triển khai chính –scrollendkhông kích hoạt khi mục tiêu đã xuất hiện và không có khoảng cách cuộn, trong khi các lời hứa của phương thức cuộn sẽ tự động phân giải trong trường hợp đó. - NÊN: Truyền
{ preventScroll: true }khi gọi.focus()sau khi chờ phương thức cuộn để trình duyệt không nhảy hoặc cuộn lần thứ hai. - NÊN: Bảo vệ các hiệu ứng sau khi cuộn bằng mã thông báo yêu cầu nếu các hoạt động tương tác nhanh của người dùng có thể kích hoạt các thao tác cuộn mượt mà chồng chéo trên cùng một vùng chứa, vì việc làm gián đoạn một thao tác cuộn mượt mà sẽ giải quyết (thay vì từ chối) lời hứa trước đó.
- Lỗi đã biết của Chrome với trình cuộn gốc: Chrome có một lỗi trong đó
PromisedoscrollIntoView()trả về phân giải quá sớm khi trình cuộn gốc của tài liệu đang cuộn. Việc chờscrollIntoView()trên vùng chứa cuộn lồng nhau sẽ hoạt động như mong đợi. Đừng gói nội dung của bạn trong một vùng chứa cuộn, chẳng hạn nhưdivcóoverflow-y: auto, để giải quyết lỗi này.
Chiến lược dự phòng
Trong các trình duyệt không hỗ trợ lời hứa về phương thức cuộn, scrollIntoView(), scroll(), scrollTo() và scrollBy() sẽ trả về undefined thay vì Promise.
- Cải tiến tăng dần (hành vi mặc định của
await): Vìawait undefinedlà JavaScript hợp lệ và phân giải ngay lập tức trên tác vụ vi mô tiếp theo, nên việc viếtawait element.scrollIntoView({ behavior: 'smooth' })sẽ không gây ra lỗi trong các trình duyệt không được hỗ trợ. Thay vào đó, nó sẽ giảm dần một cách mượt mà bằng cách chạy mã sau khi cuộn ngay khi bắt đầu cuộn. Đối với các hiệu ứng hình ảnh không quan trọng như hiệu ứng làm nổi bật tạm thời, thì cách này thường là đủ. - Dự phòng trung thực (
scrollend+ thời gian chờ): Nếu mục tiêu Baseline của bạn không hỗ trợscroll-promisesvà hành động sau khi cuộn phải đợi cho đến khi quá trình cuộn hoàn tất trong các trình duyệt cũ, hãy kiểm tra xem giá trị trả về có phải làPromise(result instanceof Promise) hay không. Khi giá trị này làundefined, hãy đợi sự kiệnscrollendtrên vùng chứa cuộn với thời gian chờ dự phòng ngắn trong trường hợp phần tử đã xuất hiện và không có thao tác cuộn nào xảy ra:
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);
});
}