<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Scroll,你玩明白了嘛?

          共 8980字,需瀏覽 18分鐘

           ·

          2022-05-13 23:23

          術(shù)????

          1、引言

          最近在實現(xiàn)列表的滾動交互時,算是被復(fù)雜的業(yè)務(wù)場景整得懷疑人生了。今天主要聊一下關(guān)于 scroll 的應(yīng)用:

          • CSS 平滑滾動

          • JS 滾動方法

          • 區(qū)分人為滾動和腳本滾動

          2、CSS 平滑滾動

          2.1 一行樣式改善體驗

          在一些滾動交互比較頻繁的場景,我們可以通過在可滾動容器上增加一行樣式來改善用戶體驗。

          scroll-behavior: smooth;

          比如說,在文檔網(wǎng)站里,我們常使用?#?來去定位到對應(yīng)的瀏覽位置。

          像上面這個例子,我們首先通過?#?去錨定對應(yīng)內(nèi)容,實現(xiàn)了一個 tab 切換的效果:

          <div>
          ?<a href="#A">Aa>
          ?<a href="#B">Ba>
          ?<a href="#C">Ca>
          div>
          <div className="scroll-ctn">
          ?<div id="A" className="scroll-panel">
          ? ?A
          ?div>
          ?<div id="B" className="scroll-panel">
          ? ?B
          ?div>
          ?<div id="C" className="scroll-panel">
          ? ?C
          ?div>
          div>

          同時,為了實現(xiàn)平滑滾動,我們在滾動容器上設(shè)置了如下的 CSS:

          .scroll-ctn {
          ?display: block;
          ?width: 100%;
          ?height: 300px;
          ?overflow-y: scroll;
          ?scroll-behavior: smooth;
          ?border: 1px solid grey;
          }

          在?scroll-behavior: smooth?的作用下,容器內(nèi)的默認(rèn)滾動呈現(xiàn)了平滑滾動的效果。

          2.2 兼容性

          IE 和 移動端 ios 上兼容性較差,必要時需要依賴 polyfill。

          2.3 注意

          1、在可滾動的容器上設(shè)置了?scroll-behavior: smooth?之后,其優(yōu)先級是高于 JS 方法的。也就是說,在 JS 中指定?behavior: auto,想要恢復(fù)立即滾動到目標(biāo)位置的效果,將不會生效。

          2、在可滾動的容器上設(shè)置了?scroll-behavior: smooth?之后,還能夠影響到瀏覽器 Ctrl+F 的表現(xiàn),使其也呈現(xiàn)平滑滾動的效果。

          3、JS 滾動方法

          3.1 基本方法

          我們熟知的原生 scroll 方法,大概有這些:

          • scrollTo:滾動到目標(biāo)位置

          • scrollBy:相對當(dāng)前位置滾動

          • scrollIntoView:讓元素滾動到視野內(nèi)

          • scrollIntoViewIfNeeded:讓元素滾動到視野內(nèi)(如果不在視野內(nèi))

          以大家用得比較多的?scrollTo?為例,它有兩種調(diào)用方式:

          // 第一種形式
          const x = 0, y = 200;
          element.scrollTo(x, y);
          // 第二種形式
          const options = {
          ?top: 200,
          ?left: 0,
          ?behavior: 'smooth'
          };
          element.scrollTo(options);

          而滾動的行為,即方法參數(shù)中的?behavior?分為兩種:

          • auto:立即滾動

          • smooth:平滑滾動

          除了上述的 3 個 api,我們還可以通過簡單粗暴的?scrollTop、?scrollLeft?去設(shè)置滾動位置:

          // 設(shè)置 container 上滾動距離 200
          container.scrollTop = 200;
          // 設(shè)置 container 左滾動距離 200
          container.scrollLeft = 200;

          值得一提的是,?scrollTop、?scrollLeft?的兼容性很好。而且相較于其他的方法,一般不會出什么幺蛾子(后文會講到)。

          3.2 應(yīng)用

          自己以往需要用到滾動的場景有:

          • 組件初始化,定位到目標(biāo)位置

          • 點擊當(dāng)前頁靠底部的某個元素,觸發(fā)滾動翻頁

          • ......

          舉個例子,現(xiàn)在我希望在列表組件加載完成后,列表能夠自動滾動到第三個元素。

          根據(jù)上面提到的我們可以用很多種方式去實現(xiàn),假設(shè)我們已經(jīng)為列表容器增加了?scroll-behavior: smooth?的樣式,然后在 useEffect hook 中去調(diào)用滾動方法:

          import React, { useEffect, useRef } from "react";
          import "./styles.css";

          export default function App() {
          ?const listRef = useRef({ cnt: undefined, items: [] });
          ?const listItems = ["A", "B", "C", "D"];
          ?useEffect(() => {
          ? ?// 定位到第三個
          ? ?const { cnt, items } = listRef.current;
          ? ?// 第一種
          ? ?// cnt.scrollTop = items[2].offsetTop;
          ? ?// 第二種
          ? ?// cnt.scrollTo(0, items[2].offsetTop);
          ? ?// 第三種
          ? ?// cnt.scrollTo({ top: items[2].offsetTop, left: 0, behavior: "smooth" });
          ? ?// 第四種
          ? ?items[2].scrollIntoView();
          ? ?// items[2].scrollIntoViewIfNeeded();?
          ?}, []);
          ?return (
          ? ?<div className="App">
          ? ? ?<ul className="list-ctn" ref={(ref) => (listRef.current.cnt = ref)}>
          ? ? ? ?{listItems.map((item, index) => {
          ? ? ? ? ?return (
          ? ? ? ? ? ?<li
          ? ? ? ? ? ? ?className="list-item"
          ? ? ? ? ? ? ?ref={(ref) =>
          (listRef.current.items[index] = ref)}
          ? ? ? ? ? ? ?key={item}
          ? ? ? ? ? ?>
          ? ? ? ? ? ? ?{item}
          ? ? ? ? ? ?li>

          ? ? ? ? ?);
          ? ? ? ?})}
          ? ? ?ul>
          ? ?div>
          ?);
          }

          上述代碼中,提到了四種方式:

          • 容器的 scrollTop 賦值

          • 容器的 scrollTo 方法,傳入橫縱滾動位置

          • 容器的 scrollTo 方法,傳入滾動配置

          • 元素的 scrollIntoView / scrollIntoViewIfNeeded 方法

          雖然最后效果都是一樣的,但這幾種方法實際上還是有些許差異的。

          3.3 scrollIntoView 的奇怪現(xiàn)象

          3.3.1 頁面整體偏移

          最近在過一些歷史用例的時候,遇到了這種情況:

          現(xiàn)象大概就是,當(dāng)我通過按鈕,滾動定位到聊天區(qū)域的某條消息時,頁面整體發(fā)生了偏移(向上移動)。再看一眼代碼,發(fā)現(xiàn)使用的是 scrollIntoView:

          因為是第一次遇到,所以上萬能的 stack overflow 上逛了一圈,看到了類似的問題:scrollIntoView 導(dǎo)致頁面整體移動?。

          這個問題常常發(fā)生在哪些情況下呢?

          1、頁面有 iframe 的情況下,比如說這個例子。

          表現(xiàn)是當(dāng) iframe 內(nèi)的內(nèi)容發(fā)生滾動時,主頁面也發(fā)生了滾動。這顯然和 MDN 上的描述不一致:

          Element 接口的 scrollIntoView () 方法會滾動元素的父容器,使被調(diào)用 scrollIntoView () 的元素對用戶可見。

          2、直接使用?scrollIntoView()?的默認(rèn)參數(shù)

          先說說?scrollIntoView()?支持什么參數(shù):

          element.scrollIntoView(alignToTop); // Boolean 型參數(shù)
          element.scrollIntoView(scrollIntoViewOptions); // Object 型參數(shù)

          (1)alignToTop

          • 如果為?true,元素的頂端將和其所在滾動區(qū)的可視區(qū)域的頂端對齊。相應(yīng)的?scrollIntoViewOptions: {block: "start", inline: "nearest"}。這是這個參數(shù)的默認(rèn)值。

          • 如果為?false,元素的底端將和其所在滾動區(qū)的可視區(qū)域的底端對齊。相應(yīng)的?scrollIntoViewOptions: {block: "end", inline: "nearest"}。

          (2)scrollIntoViewOptions

          包含下列屬性:

          • behavior?可選

            定義動畫過渡效果,?"auto"?或?"smooth"?之一。默認(rèn)為?"auto"。

          • block?可選

            定義垂直方向的對齊,?"start",?"center",?"end", 或?"nearest"?之一。默認(rèn)為?"start"。

          • inline?可選

            定義水平方向的對齊,?"start",?"center",?"end", 或?"nearest"?之一。默認(rèn)為?"nearest"

          回到我們的問題,為什么使用默認(rèn)參數(shù),即?element.scrollIntoView(),會引發(fā)頁面偏移的問題呢?

          關(guān)鍵在于?block: "start",從上面的參數(shù)說明我們了解到,默認(rèn)不傳參數(shù)的情況下,取的是?block: start,它表示 “元素頂端與所在滾動區(qū)的可視區(qū)域頂端對齊”。但從現(xiàn)象上看,影響的不只是 “所在滾動區(qū)” 或者 “父容器”,祖先 DOM 元素也被影響了。

          由于尋覓不到?scrollIntoView?的源碼,暫時只能定位到是?start?這個默認(rèn)值在做妖。既然原生的方法有問題,我們需要采取一些別的方式來代替。

          3.3.2 解決方式

          1、更換參數(shù)

          既然是?block: start?有問題,那咱們換一個效果就好了,這里建議使用?nearest

          element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });

          可能也有好奇的朋友想問,這些對齊的選項具體代表了什么含義?在 MDN 里面好像都沒有做特別的解釋。這里引用 stackoverflow 上的一個高贊解答,可以幫助你更好的理解。

          1. 使用?{block: "start"},元素在其祖先的頂部對齊。

          2. 使用?{block: "center"},元素在其祖先的中間對齊。

          3. 使用?{block: "end"},元素在其祖先的底部對齊。

          4. 使用?{block: "nearest"}

            • 如果您當(dāng)前位于其祖先的下方,則元素在其祖先的頂部對齊。

            • 如果您當(dāng)前位于其祖先之上,則元素在其祖先的底部對齊。

            • 如果它已經(jīng)在視圖中,保持原樣。

          2、scrollTop/scrollLeft

          上文也提到 scrollTop/scrollLeft 賦值是兼容性最好的滾動方式,我們可以利用它來代替默認(rèn)的 scrollIntoView () 的表現(xiàn)。

          比如說置頂某個元素,可以定義可滾動容器的 scrollTop 為該元素的 offsetTop:

          container.scrollTop = element.offsetTop;

          值得一提的是,結(jié)合 CSS 的 scroll-behavior,這種賦值方式也可以實現(xiàn)平滑滾動效果。

          4、如何區(qū)分人為滾動和腳本滾動

          4.1 背景

          最近遇到這么一個需求,做一個實時高亮當(dāng)前播放內(nèi)容的字幕文稿。核心的交互是:

          1、當(dāng)用戶沒有人為滾動文稿時,會保持自動翻頁的功能

          2、當(dāng)用戶人為滾動文稿時,后續(xù)將不會自動翻頁,并出現(xiàn) “回到當(dāng)前播放位置” 的按鈕

          3、假如點擊了 “回到當(dāng)前播放位置” 的按鈕,會回到目標(biāo)位置,并恢復(fù)自動翻頁的功能。

          像上面的演示中,用戶觸發(fā)了人為滾動,之后點擊 “回到當(dāng)前播放位置”,觸發(fā)了腳本滾動。

          4.2 人為滾動

          怎么定義 “人為滾動” 呢?我們所了解的人為滾動,包含:

          • 鼠標(biāo)滾動

          • 鍵盤方向鍵滾動

          • 縮進鍵滾動

          • 翻頁鍵滾動

          • ......

          假如說,我們通過 onWheel、onKeyDown 等事件,去監(jiān)聽人為滾動,定是不能盡善盡美的。那么我們換個思路,能否去對 “腳本滾動” 下功夫?

          4.3 腳本滾動

          怎么定義 “腳本滾動”?我們將由代碼觸發(fā)的滾動,定義為 “腳本滾動”。

          我們需要用一種方式描述 “腳本滾動”,來和 “人為滾動” 做區(qū)分。由于它們是非此即彼的關(guān)系,那實際上我們只需要在?onScroll?這個事件上,通過一個 flag 去區(qū)分即可。

          流程圖如下:

          而這其中唯一需要關(guān)注的點在于,需要通過什么方式知道,腳本滾動結(jié)束了?

          scrollTo 等原生方式,顯然沒有給我們提供回調(diào)方法,來告訴我們滾動在什么時候結(jié)束。所以我們還是需要依賴 onScroll 去監(jiān)聽當(dāng)前的滾動位置,來得知滾動什么時候達(dá)到目標(biāo)位置。

          所以上面的流程還要再加一步:

          接下來看看代碼要怎么組織。

          4.4 代碼實現(xiàn)

          首先看一下我們想要實現(xiàn)的?demo

          接下來先實現(xiàn)基本的頁面結(jié)構(gòu)。

          1、定義一個長列表,并通過?useRef?記錄:

          • 滾動容器的?ref

          • 腳本滾動的判斷變量?isScriptScroll

          • 當(dāng)前的滾動位置?scrollTop

          2、接著,為滾動容器綁定一個?onScroll?方法,在其中分別編寫人為滾動和腳本滾動的邏輯,并使用節(jié)流來避免頻繁觸發(fā)。

          在人為滾動和腳本滾動的邏輯中,我們通過更新 wording 這個狀態(tài),來區(qū)分當(dāng)前處于人為滾動還是腳本滾動。

          3、用一個 button 來觸發(fā)腳本滾動,調(diào)用?listScroll?方法,傳入容器?ref,想要滾動到的?scrollTop?以及滾動結(jié)束后的?callback?方法。

          如下:

          import throttle from "lodash.throttle";
          import React, { useRef, useState } from "react";
          import { listScroll } from "./utils";
          import "./styles.css";

          const scrollItems = new Array(1000).fill(0).map((item, index) => {
          ?return index + 1;
          });

          export default function App() {
          ?const [wording, setWording] = useState("等待中");
          ?const cacheRef = useRef({
          ? ?isScriptScroll: false,
          ? ?cnt: null,
          ? ?scrollTop: 0
          ?});

          ?const onScroll = throttle(() => {
          ? ?if (cacheRef.current.isScriptScroll) {
          ? ? ?setWording("腳本滾動中");
          ? ?} else {
          ? ? ?cacheRef.current.scrollTop = cacheRef.current.cnt.scrollTop;
          ? ? ?setWording("人為滾動中");
          ? ?}
          ?}, 200);

          ?const scriptScroll = () => {
          ? ?cacheRef.current.scrollTop += 600;
          ? ?cacheRef.current.isScriptScroll = true;
          ? ?listScroll(cacheRef.current.cnt, cacheRef.current.scrollTop, () => {
          ? ? ?setWording("腳本滾動結(jié)束");
          ? ? ?cacheRef.current.isScriptScroll = false;
          ? ?});
          ?};

          ?return (
          ? ?<div className="App">
          ? ? ?<button
          ? ? ? ?className="btn"
          ? ? ? ?onClick={() =>
          {
          ? ? ? ? ?scriptScroll();
          ? ? ? ?}}
          ? ? ?>
          ? ? ? ?觸發(fā)一次腳本滾動
          ? ? ?button>

          ? ? ?<p className="wording">當(dāng)前狀態(tài):{wording}p>
          ? ? ?<ul
          ? ? ? ?className="list-ctn"
          ? ? ? ?onScroll={onScroll}
          ? ? ? ?ref={(ref) =>
          (cacheRef.current.cnt = ref)}
          ? ? ?>
          ? ? ? ?{scrollItems.map((item) => {
          ? ? ? ? ?return (
          ? ? ? ? ? ?<li className="list-item" key={item}>
          ? ? ? ? ? ? ?{item}
          ? ? ? ? ? ?li>
          ? ? ? ? ?);
          ? ? ? ?})}
          ? ? ?ul>
          ? ?div>
          ?);
          }

          接下來重點就在于?listScroll?怎么實現(xiàn)了。我們需要再去綁定一個 scroll 事件,不斷去監(jiān)聽容器的 scrollTop 是否已經(jīng)達(dá)到目標(biāo)值,所以可以這么組織:

          import debounce from "lodash.debounce";

          /** 誤差范圍內(nèi) */
          export const withErrorRange = (
          ?val: number,
          ?target: number,
          ?errorRange: number
          ) =>
          {
          ?return val <= target + errorRange && val >= target - errorRange;
          };

          /** 列表滾動封裝 */
          export const listScroll = (
          ?element: HTMLElement,
          ?targetPos: number,
          ?callback?: () => void
          ) =>
          {
          ?// 是否已成功卸載
          ?let unMountFlag = false;
          ?const { scrollHeight: listHeight } = element;

          ?// 避免一些邊界情況
          ?if (targetPos < 0 || targetPos > listHeight) {
          ? ?return callback?.();
          ?}

          ?// 調(diào)用滾動方法
          ?element.scrollTo({
          ? ?top: targetPos,
          ? ?left: 0,
          ? ?behavior: "smooth"
          ?});

          ?// 沒有回調(diào)就直接返回
          ?if (!callback) return;
          ?// 如果已經(jīng)到達(dá)目標(biāo)位置了,可以先行返回
          ?if (withErrorRange(targetPos, element.scrollTop, 10)) return callback();
          ?// 防抖處理
          ?const cb = debounce(() => {
          ? ?// 到達(dá)目標(biāo)位置了,可以返回
          ? ?if (withErrorRange(targetPos, element.scrollTop, 10)) {
          ? ? ?element.removeEventListener("scroll", cb);
          ? ? ?unMountFlag = true;
          ? ? ?return callback();
          ? ?}
          ?}, 200);

          ?element.addEventListener("scroll", cb, false);

          ?// 兜底:卸載滾動回調(diào),避免對之后的操作產(chǎn)生影響
          ?setTimeout(() => {
          ? ?if (!unMountFlag) {
          ? ? ?element.removeEventListener("scroll", cb);
          ? ? ?callback();
          ? ?}
          ?}, 1000);
          };

          按嚴(yán)謹(jǐn)?shù)牧鞒虂韺懙脑?,我們需要依?scroll 事件去不斷判斷 scrollTop,直至在誤差范圍內(nèi)相等。

          但實際上滾動是一個很快的過程,跟我們兜底的定時器邏輯,也就是前后腳的事情,是不是可以只保留兜底的邏輯?

          而且,考慮到那些異常情況:

          • 腳本滾動發(fā)生異常

          • 腳本滾動被人為滾動打斷

          我們都得保證執(zhí)行了一次回調(diào),確保外部狀態(tài)被釋放,下一次滾動的邏輯正常。

          所以在不那么嚴(yán)格的場景下,上述的代碼其實可以拋棄 eventListener 的部分,只保留兜底的邏輯,進一步簡化:

          /** 列表滾動封裝 */
          export const listScroll = (
          ?element: HTMLElement,
          ?targetPos: number,
          ?callback?: () => void
          ) =>
          {
          ?const { scrollHeight: listHeight } = element;

          ?// 避免一些邊界情況
          ?if (targetPos < 0 || targetPos > listHeight) {
          ? ?return callback?.();
          ?}

          ?// 調(diào)用滾動方法
          ?element.scrollTo({
          ? ?top: targetPos,
          ? ?left: 0,
          ? ?behavior: "smooth"
          ?});

          ?// 沒有回調(diào)就直接返回
          ?if (!callback) return;
          ?// 如果已經(jīng)到達(dá)目標(biāo)位置了,可以先行返回
          ?if (withErrorRange(targetPos, element.scrollTop, 10)) return callback();
          ?
          ?// 兜底:卸載滾動回調(diào),避免對之后的操作產(chǎn)生影響
          ?setTimeout(() => {
          ? ?callback();
          ?}, 1000);
          };

          當(dāng)然,這個實現(xiàn)只是一種參考,相信大家也有別的更好的思路。

          5、小結(jié)

          回顧整篇文章,簡單介紹了關(guān)于 scroll 的一些 api 使用,原生?scrollIntoView?的坑以及區(qū)分人為滾動和腳本滾動的實現(xiàn)參考。

          滾動,這一個看似微小的交互點,實際上可能隱藏著不少的工作量,在往后的評估或者實踐中,需要多加重視和思考,隱藏在交互體驗之下的復(fù)雜邏輯。

          ?? 謝謝支持

          以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^

          喜歡的話別忘了?分享、點贊、收藏?三連哦~。

          歡迎關(guān)注公眾號?趣談前端?收獲大廠一手好文章~





          LowCode可視化低代碼社區(qū)介紹

          LowCode低代碼社區(qū)(http://lowcode.dooring.cn)是由在一線互聯(lián)網(wǎng)公司深耕技術(shù)多年的技術(shù)專家創(chuàng)辦,意在為企業(yè)技術(shù)人員提供低代碼可視化相關(guān)的技術(shù)交流和分享,并且鼓勵國內(nèi)擁有相關(guān)業(yè)務(wù)的企業(yè)積極推薦自身產(chǎn)品,為國內(nèi)B端技術(shù)領(lǐng)域積累知識資產(chǎn)。同時我們還歡迎開源大牛們分享自己的開源項目和技術(shù)視頻。

          如需入駐請加下方小編微信:?lowcode-dooring

          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色电影网站在线免费观看 | 日日躁夜夜躁夜夜揉人人视频 | www.aaa国产 | 中文字幕无码毛片免费看 | 日韩一级高清在线 |