هنگام پیمایش برنامهای به یک بخش یا عنصر (بهویژه با behavior: 'smooth')، راهاندازی فوری یک کنش پیگیری (مانند چشمک زدن حالت برجسته، شروع پویانمایی ورودی، یا جابهجایی تمرکز) باعث میشود که جلوه درحالی اجرا شود که هدف هنوز خارج از صفحه یا درحال پیمایش است.
قبلاً توسعهدهندگان باید مدت پیمایش را با setTimeout() حدس میزدند (که در پیمایشهای طولانی خیلی زود و در پیمایشهای کوتاه خیلی دیر فعال میشود) یا بهصورت دستی یک شنونده رویداد scrollend را در یک Promise با زمانسنج بازگشتی قرار میدادند (زیرا وقتی هدف ازقبل در دید باشد و پیمایشی انجام نشود، scrollend فعال نمیشود).
روشهای پیمایش برنامهای در Element (scrollIntoView()، scroll()، scrollTo()، scrollBy()) و Window (scroll()، scrollTo()، scrollBy()) یک Promise را برمیگردانند که پساز تکمیل عملیات پیمایش حل میشود. اگر هدف ازقبل در موقعیت پیمایش درخواستی باشد و نیازی به پیمایش نباشد، Promise برگرداندهشده بلافاصله بدون توقف حلوفصل میشود.
پیادهسازی
- علامتگذاری کنترلکننده بهعنوان
async: از تابعasyncبرای شنونده رویداد یا کنترلکننده پیمایش که پیمایش را راهاندازی میکند استفاده کنید. - مستقیماً منتظر روش پیمایش بمانید: با
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()منتقل کنید تا فراخوانی تمرکز پیمایش دوم و رقیب را آغاز نکند:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
محدودیتها و روالهای مطلوب
- اجباری: بهجای حدس زدن مدت پیمایش با
setTimeout()، منتظرPromiseباشید که مستقیماً توسطscrollIntoView()،scroll()،scrollTo()، یاscrollBy()برگردانده میشود. - DO NOT wrap
scrollendin a manualnew Promise()as your primary implementation—scrollenddoes not fire when the target is already in view and no scroll distance is traveled, whereas scroll method promises resolve automatically in that case. - باید:
{ preventScroll: true }را هنگام فراخوانی.focus()پساز انتظار برای روش پیمایش ارسال کنید تا مرورگر دوباره نپرد یا پیمایش نکند. - باید: اگر تعاملات سریع کاربر میتواند باعث ایجاد پیمایشهای روان همپوشان در یک محفظه شود، از جلوههای پساز پیمایش با یک توکن درخواست محافظت کنید، زیرا قطع کردن پیمایش روان، وعده قبلی را حل میکند (بهجای رد کردن).
- اشکال شناختهشده Chrome با پیمایشگر ریشه: Chrome اشکالی دارد که در آن
Promiseبرگرداندهشده توسطscrollIntoView()خیلی زود حل میشود، زمانی که پیمایشگر ریشه سند درحال پیمایش است. انتظار برایscrollIntoView()در محفظه پیمایش تودرتو طبق انتظار کار میکند. برای حل این اشکال، محتوایتان را در محفظه پیمایش، مثلdivباoverflow-y: auto، قرار ندهید.
راهبردهای جایگزین
در مرورگرهایی که از وعدههای روش پیمایش پشتیبانی نمیکنند، scrollIntoView()، scroll()، scrollTo()، و scrollBy() بهجای Promise، undefined را برمیگردانند.
- ارتقای پیشرو (رفتار پیشفرض
await): چونawait undefinedجاوا اسکریپت معتبری است که بلافاصله در ریزوظیفه بعدی حل میشود، نوشتن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);
});
}