スクロールの認知と探索

1. 基本のおさらい:要素のサイズと位置の測定

要素のサイズと位置の計算はスクロールに関係するため、ここではインターネット上の写真のレビューを示します。
画像クレジット: CSSOM を使用した要素のサイズと位置の測定
ここに画像の説明を挿入

クライアントの高さ


clientHeight = コンテンツの高さ + 上下のパディング - スクロール バーの太さです。
同じ要素の場合、ボックス モデルは clientHeight の値に影響します。
clientHeight は整数を返します。

#scroll-container2 {
    
    
  margin: 100px 0px 70px 50px;
  border: 7px solid black;
  box-sizing: border-box; // box-sizing的值会影响clientHeight、clientWidth的值
​
  height: 200.22px;
  width: 300.22px;
  padding: 10px;
  overflow: auto;
}
// 备注:该示例里没有水平滚动条
getClientWidth() {
    
    
  /**
   * (1)对于content-box,CSS height属性只包含内容的高度,不包含padding和border。
   *      内容高度contentHeight = 200.22
   *     clientHeight = CSS height + CSS 上下padding = 200.22+10+10=220.22 clientHeight会返回整数 220
   * 
   * (2)对于border-box,CSS height属性包含了内容的高度,padding和border。
   *     内容高度contentHeight = 200.22(CSS height)-7(上border)-7(下border)-10(上padding)-10(下padding)=166.22。
   *     clientHeight = 内容height+上下padding = 166.22+10+10=186.22 clientHeight会返回整数 186
   */
  const scrollContainer = document.getElementById('scroll-container2')!;
  console.log('clientHeight', scrollContainer?.clientHeight);
}

2. スクロールの関連概念

  1. ブロック レベル要素がoverflow: scrollまたはoverflow: auto、含まれるコンテンツが制限された表示領域を超えると、スクロール バーが表示され、コンテンツがスクロール可能になります。
  2. スクロール イベントをリッスンして、要素が特定の位置にスクロールされたときに必要なことを実行できます。
  3. ページをスクロールして指定された要素を表示領域に入れるなど、スクロール可能な要素のスクロール指定オフセットを手動で設定することもできます。
    ここに画像の説明を挿入ここに画像の説明を挿入

いくつかの無関係なサンプル コード:

<div style="display: flex">
  <div id="scroll-container1">内容很少,没有滚动条</div>
  <div id="scroll-container2">
    01. 内容很多,有滚动条<br />
    02. 内容很多,有滚动条<br />
    03. 内容很多,有滚动条<br />
    04. 内容很多,有滚动条<br />
    05. 内容很多,有滚动条<br />
    06. 内容很多,有滚动条<br />
    07. 内容很多,有滚动条<br />
    08. 内容很多,有滚动条<br />
    09. 内容很多,有滚动条<br />
    10. 内容很多,有滚动条<br />
    11. 内容很多,有滚动条<br />
    12. 内容很多,有滚动条<br />
    13. 内容很多,有滚动条<br />
    14. 内容很多,有滚动条<br />
    15. 内容很多,有滚动条<br />
    <div id="target">test scrollIntoView()</div>
    01. 内容很多,有滚动条<br />
    02. 内容很多,有滚动条<br />
    03. 内容很多,有滚动条<br />
    04. 内容很多,有滚动条<br />
    05. 内容很多,有滚动条<br />
    06. 内容很多,有滚动条<br />
    07. 内容很多,有滚动条<br />
    08. 内容很多,有滚动条<br />
    09. 内容很多,有滚动条<br />
    10. 内容很多,有滚动条<br />
    11. 内容很多,有滚动条<br />
    12. 内容很多,有滚动条<br />
    13. 内容很多,有滚动条<br />
    14. 内容很多,有滚动条<br />
    15. 内容很多,有滚动条<br />
  </div>
</div>
#scroll-container1,
#scroll-container2 {
    
    
  margin: 100px 0px 70px 50px;
  border: 7px solid black;
  box-sizing: border-box; // content-box

  height: 200.22px;
  width: 300.22px;
  padding: 10px;
  overflow: scroll; // auto
}

#target {
    
    
  height: 50px;
  padding: 10px;
  background-color: bisque;
  border: 1px solid black;
}

3. スクロール イベントをリッスンする

scroll イベントは、ユーザーが要素のコンテンツをスクロールしたときに発生します。
スクロール イベントは頻繁にトリガーされる可能性があるため、パフォーマンスの高い消費が発生しやすくなります。requestAnimationFrame()次のような、またはsetTimeout()スロットル イベントを使用することをお勧めします。CustomEvent

const scrollContainer = document.getElementById('scroll-container')!;

scrollContainer.addEventListener('scroll', () => {
    
    
  requestAnimationFrame(() => {
    
    
    // do something here
    console.log('我滚动啦');
  });
});

ディスプレイのリフレッシュ レートは固定されており、毎秒 60 回または 75 回、つまり 60Hz または 75Hz までしか再描画できません。requestAnimationFrame は、ディスプレイのリフレッシュ メカニズムをフルに活用し、このリフレッシュ レートに合わせてページを再描画します。これにより、システム リソースが節約され、システム パフォーマンスが向上し、視覚効果が向上します。

4.ネイティブ API のスクロール

1. scroll()、scrollTo()、scrollBy()

element.scroll(x-coord, y-coord) : 指定したコンテナの特定の座標までスクロール バーをスクロールさせます。
element.scroll(options): 指定したコンテナの特定の座標までスクロール バーをスクロールさせ、スクロール動作を指定できます。
element.scrollTo() element.scroll() と同じです。

const scrollContainer = document.getElementById('scroll-container')!;// 1. 内容垂直方向向上滚动50px
scrollContainer.scroll(0, 50);// 2. 内容垂直方向向上滚动50px
scrollContainer?.scroll({
    
    
  top: 50,
  left: 0,
  behavior: 'smooth', // 指定滚动行为,支持参数 smooth(平滑滚动),默认值auto(瞬间滚动)
});

scroll、scrollTo、scrollBy は同じパラメータを受け取ります。違い:
scroll と scrollTo の使い方は基本的に同じです。
scrollToスクロール距離绝对は、つまり何度実行してもスクロール位置は同じです。
scrollByスクロール距離は相对yes、実行されるたびに元のスクロール基準に相対距離が追加されます。

2. scrollIntoView()

要素の親コンテナをスクロールして、要素をユーザーに表示します。

element.scrollIntoView(alignToTop):alignToTop はオプションです。

  • デフォルト値は true です。要素の上部は、それが配置されているスクロール領域の表示領域の上部に揃えられます。(具体的には、要素がそのスクロール領域の可視領域の下部にある場合、要素はそのスクロール領域の可視領域の下部に配置されます。)
  • false の場合、要素の下部は、スクロール領域の表示可能領域の下部に揃えられます。

element.scrollIntoView(scrollIntoViewOptions) :scrollIntoViewOptions オブジェクト オプション。

  • 動作: スクロール アニメーションのトランジション効果。値は auto、smooth のいずれかです。Smooth は滑らかなスクロールを意味します。
  • block: 垂直方向の配置。値は start、center (表示領域の中央までスクロール)、end、nearest のいずれかです。デフォルトは開始です。
  • inline: 水平方向の配置。値は start、center、end、nearest のいずれかです。デフォルトは最も近いです。
// 滚动son的父容器,使son出现在可视区域。
const son = document.getElementById('son')!;
son.scrollIntoView();

3. scrollTop、scrollLeft

element.scrollTop: 要素のコンテンツの垂直方向のスクロール距離を読み取るか、設定します。この距離は、要素のコンテンツ (ラップアップ) の上部からビューポートの可視コンテンツ (その上部) までの距離です。
element.scrollLeft: 要素のスクロール バーから要素の左側までの距離を読み取るか、設定します。
ここに画像の説明を挿入


// 获取
const scrollTop = scrollContainer.scrollTop;
console.log('scrollTop', scrollTop);// 设置
scrollContainer.scrollTop = 77;

4. scrollHeight、scrollWidth

element.scrollHeight: 要素のコンテンツの高さ。オーバーフローのためにビューに表示されないコンテンツを含みます。垂直スクロールバーがない場合、scrollHeight 値は、要素のビューがすべてのコンテンツを埋めるために必要な clientHeight の最小値と同じです。scrollHeight には::before::afterや などの疑似要素も含まれます。
注: scrollHeight は値を丸めます。10 進数値が必要な場合は、Element.getBoundingClientRect() を使用します。
ここに画像の説明を挿入

5.スクロールY、スクロールX

window.scrollYドキュメントが垂直方向にスクロールされたピクセル数を返します。
window.scrollXドキュメントが水平方向にスクロールされたピクセル数を返します。

ブラウザ間の互換性を確保するには、window.pageYOffset代わりに をwindow.scrollY

window.pageYOffset == window.scrollY; // 总是返回 true

6. パフォーマンスの問題

上記のスクロール関連の API は、強制リフローをトリガーすることに注意してください。
関連概念:

  • background-color などの要素の外観を変更するときにトリガーされます重绘
  • viewport 内の要素の位置またはサイズを取得または設定するときに発生します回流(布局抖动)
  • 回流一定会引起重绘. (なぜなら、ブラウザはリフロー時にレンダリング ツリーの影響を受けた部分を無効にし、レンダリング ツリーのこの部分を再構築するためです。リフローが完了すると、ブラウザは影響を受けた部分を画面に再描画します。ペイントされます)。
  • 重绘不一定会引起回流

5.スクロールアプリ

1. 要素がターゲット可視領域に完全に表示されているかどうかを判断しますか?

解決策 1: getBoundingClientRect メソッドを使用しますが、呼び出しのたびにリフローが発生し、パフォーマンスに深刻な影響を与えます。スロットリング機能を使用して制御しても、パフォーマンスに影響します。推奨されません。

const target = document.getElementById('target')!;
const scrollContainer = document.getElementById('scroll-container')!;
​
scrollContainer.addEventListener('scroll', () => {
    
    
  const targetRect = target.getBoundingClientRect();
  const scrollContainerRect = scrollContainer.getBoundingClientRect();const isTargetAllInScrollContainer =
        targetRect.left > scrollContainerRect.left &&
        targetRect.right < scrollContainerRect.right &&
        targetRect.top > scrollContainerRect.top &&
        targetRect.bottom < scrollContainerRect.bottom;if (isTargetAllInScrollContainer) {
    
    
    console.log('目标元素完全出现在目标容器中');
  } else {
    
    
    console.log('目标元素没有完全出现在目标容器中');
  }
});

解決策 2: IntersectionObserver を使用してオブザーバーをクロスします。お勧め。

const target = document.getElementById('target')!;
const scrollContainer = document.getElementById('scroll-container')!;const callback = (entries: IntersectionObserverEntry[]) => {
    
    
  console.log('entries: ', entries);if (entries[0].intersectionRatio === 1) {
    
    
    // 目标元素有一点点在父容器的padding上都不算,必须完全出现在内容区
    console.log('目标元素完全出现在目标容器的可视区域中');
  } else {
    
    
    console.log('目标元素没有完全出现在目标容器的可视区域中');
  }
};
const options = {
    
    
  root: scrollContainer,
  rootMargin: '0px',
  threshold: 1,
};
const observer = new IntersectionObserver(callback, options);
observer.observe(target);

2. 指定した要素までスクロールするためのオプションは何ですか?

解決策 1: を使用しelement.scrollTo(ScrollToOptions)て、ターゲット要素を指定された座標点までスクロールさせます。
解決策 2:位置计算+设置scrollTopコンポーネント ライブラリの ScrollToService.scrollToElement() など。
解決策 3: ターゲット要素をターゲット ビジュアル領域の上部、中央、または下部にスクロールscrollIntoView(scrollIntoViewOptions)するために。
解決策 4: アンカー リンクをクリックして、指定したアンカー ポイントに<a href="#target">跳转到指定锚点</a>ジャンプします。

6. スクロール バーのスタイルを変更する

::-webkit-scrollbarset をoverflow:scroll;持つ。次のセレクターを使用して、スクロール バーまたはスクロール グルーブの幅、高さ、色、およびその他のスタイルを変更できます。スクロールバー セレクターには次のものがあります。

::-webkit-scrollbar高さや幅の変更など、スクロール バー全体のスタイルを設定します。

::-webkit-scrollbar-buttonスクロール バーのボタン (上矢印と下矢印)。

::-webkit-scrollbar-thumbスクロール バーのスクロール サム。

::-webkit-scrollbar-trackスクロール バー トラック。

::-webkit-scrollbar-track-pieceスライダーのないスクロール バーのトラック部分。

::-webkit-resizer一部の要素の下隅に表示されるドラッグ可能でサイズ変更可能なスライダー。

::-webkit-scrollbar-corner垂直スクロールバーと水平スクロールバーの両方がある場合に交わるセクション。通常、ブラウザ ウィンドウの右下隅。

七、スクロールとホイールの違い

scrollスクロールバーがスクロールされると、イベントが発生します。
wheelマウス ホイールがスクロールされると、イベントが発生します。

8. Angular CDK のスクロール

1.マップ「辞書」

マップの基本的なレビュー:

/**
  * new Map()
  * Map是一种叫做字典的数据结构。
  * 它的特点是键值对的形式,键可以是任意类型,值也可以是任意类型。
  */
const map = new Map();

/**
  * set()
  * 设置键名key对应的键值为value,然后返回整个 Map 结构。
  * 如果key已经有值,则键值会被更新,否则就新生成该键。
  * 同时返回的是当前Map对象,可采用链式写法。
  */
map.set('第一站', '故宫');
map
  .set('第二站', '奥森公园')
  .set('第三站', '香山公园')
  .set('第四站', '长城')
  .set('第五站', '颐和园');

/**
  *  Map(5) {'第一站' => '故宫', 
  * '第二站' => '奥森公园', '第三站' => '香山公园', 
  * '第四站' => '长城', '第五站' => '颐和园'}
  */
console.log(map); 


// get() 读取key对应的键值,如果找不到key,返回undefined。
const getValue = map.get('第三站');  // 香山公园

// size 返回Map结构的成员总数。
const size = map.size; // 5

// has() 返回一个布尔值,判断某个键是否在当前Map字典中。
const hasName = map.has('第二站');  // true

// forEach((value, key) => {}); 遍历
map.forEach((value, key) => {
    
    
  console.log('value', value);
  console.log('key', key);
});

// delete() 删除某个键,返回true。如果删除失败,返回false。
const hasDelete = map.delete('第五站');  // true

// clear() 清除所有成员,没有返回值。
map.clear();  // Map(0) {size: 0}

2. ブラウザごとの水平スクロールの処理方法

dir 属性値が 'rtl' の場合、つまり、テキストの方向は右から左です。scrollLeft の値はブラウザごとに異なります。次のように:

ここに画像の説明を挿入

例 1 : 以下のバージョンでは、異なるブラウザーが RTL レイアウトの scrollLeft 属性値を読み取ることが観察されています。

<div id="scroll-container2" [dir]="'rtl'">
  <div id="target" style="width: 500px">test 水平滚动条</div>
  01. 内容很多,有滚动条<br />
  02. 内容很多,有滚动条<br />
  03. 内容很多,有滚动条<br />
  04. 内容很多,有滚动条<br />
  05. 内容很多,有滚动条<br />
  06. 内容很多,有滚动条<br />
  07. 内容很多,有滚动条<br />
  08. 内容很多,有滚动条<br />
  09. 内容很多,有滚动条<br />
  10. 内容很多,有滚动条<br />
  11. 内容很多,有滚动条<br />
  12. 内容很多,有滚动条<br />
  13. 内容很多,有滚动条<br />
  14. 内容很多,有滚动条<br />
  15. 内容很多,有滚动条<br />
</div>
#scroll-container2 {
    
    
  margin: 100px 0px 70px 50px;
  border: 7px solid black;
  box-sizing: content-box; // box-sizing的值会影响clientHeight、clientWidth的值

  height: 200.22px;
  width: 300.22px;
  padding: 10px;
  overflow: auto;
}
#target {
    
    
  height: 50px;
  padding: 10px;
  background-color: bisque;
  border: 1px solid black;
}

ここに画像の説明を挿入

上記の対応バージョンのブラウザは、NEGATED メソッドを使用して scrollLeft 値を読み取るのでしょうか。? ?

例 2 : NEGATED ブラウザーで RTL レイアウトを確認し、コンテナーのスクロール バーを特定のオフセットだけスクロールさせ、scrollLeft の値を読み取り、コンテナーの左右のオフセットを読み取ります。

<div id="scroll-container2" [dir]="'rtl'">
  <div id="target" style="width: 500px">test 水平滚动条</div>
  01. 内容很多,有滚动条<br />
  02. 内容很多,有滚动条<br />
  03. 内容很多,有滚动条<br />
  04. 内容很多,有滚动条<br />
  05. 内容很多,有滚动条<br />
  06. 内容很多,有滚动条<br />
  07. 内容很多,有滚动条<br />
  08. 内容很多,有滚动条<br />
  09. 内容很多,有滚动条<br />
  10. 内容很多,有滚动条<br />
  11. 内容很多,有滚动条<br />
  12. 内容很多,有滚动条<br />
  13. 内容很多,有滚动条<br />
  14. 内容很多,有滚动条<br />
  15. 内容很多,有滚动条<br />
</div>
const scrollContainer = document.getElementById('scroll-container2')!;
console.log('scrollLeft: ', scrollContainer.scrollLeft);

RTL レイアウトでは、デフォルトの水平スクロール バーは右側に留まります。読み取った scrollLeft 値は 0 です。
ここに画像の説明を挿入
水平スクロールバーを左にスクロールします。

// RTL布局下,NEGATED浏览器中,scrollLeft的值是负数或0。
// scrollLeft的绝对值等于容器右侧的偏移量
scrollContainer.scrollLeft = -100;

ここに画像の説明を挿入
コンテナーの左右のオフセットを読み取ります。

console.log('容器右侧的偏移量: ', -scrollContainer.scrollLeft); // = -(-100) = 100

console.log('容器左侧的偏移量:', scrollWidth - clientWidth - Math.abs(scrollLeft));
  // 等于 scrollWidth - clientWidth - (-scrollLeft) 即是 542-271-(-(-100)) 
  // 等于 scrollWidth - clientWidth + scrollLeft 即是 542-271+(-100) = 171

3.CdkScrollable コマンド

まず、要素に cdkScrollable 命令をバインドした後、scrollDispatcher の scrollContainers "dictionary" にスクロール可能なインスタンスが登録されます。cdkScrollable コマンドが破棄されると、スクロール可能なインスタンスも scrollContainers の scrollDispatcher の「辞書」から削除され、対応するスクロール イベントのサブスクリプションも取り消されます。

ngOnInit() {
    
    
  this.scrollDispatcher.register(this);
}

ngOnDestroy() {
    
    
  this.scrollDispatcher.deregister(this);
}

次に、cdkScrollable ディレクティブの elementScrolled メソッドは、cdkScrollable ディレクティブにバインドされた要素のスクロール イベントを監視する Observable です。このメソッドを使用して、スクロール イベントをサブスクライブし、必要なことを行うことができます。

elementScrolled(): Observable<Event> {
    
    
  return new Observable((observer: Observer<Event>) =>
    this.ngZone.runOutsideAngular(() =>
      fromEvent(this.elementRef.nativeElement, 'scroll')
        .pipe(takeUntil(this._destroyed))
        .subscribe(observer),
    ),
  );
}

第 3 に、ネイティブの scrollTo を使用してスクロールしてオフセットを指定すると、上と左のオフセットしか設定できません。一部の複雑なシナリオ (さまざまなテキスト レイアウトやさまざまなブラウザーなど) では、自分で上または左の値しか計算できず、面倒で面倒です。

// 原生scrollTo
element.scrollTo({
    
    
  top: number,
  left: number,
  behavior: 'smooth'|'auto',
});

cdkScrollable コマンドは、より強力な scrollTo(ExtendedScrollToOptions) メソッドを提供します。このメソッドは、ScrollToOptions を拡張し、上、下、左、右、開始、および終了のオフセットを渡すことをサポートします。

開始と終了の意味: LTR レイアウトでは、左 (開始) と右 (終了) です。RTL レイアウトでは、右 (開始) と左 (終了) です。

ブラウザーは RTL での scrollLeft の意味に一貫性がないため、CdkScrollable の scrollTo メソッドはブラウザーのネイティブ scrollTo メソッドを正規化します。左と右は、レイアウトの方向に関係なく、スクロール コンテナーの左側と右側を常に参照します。

// CdkScrollable的scrollTo
scrollTo(options: {
    
    
  top?: number;
  bottom?: number;
  left?: number;
  right?: number;
  start?: number;
  end?: number;
  behavior?: 'auto' | 'smooth';
}): void {
    
    
  // 复杂逻辑判断......
}

その中でどのような複雑な論理判断と処理が行われているのでしょうか? ? ? コーミングの後、3 つのことを行っていることがわかりました

if (options.bottom != null) {
    
    
  options.top = el.scrollHeight - el.clientHeight - options.bottom;
}

2 番目のこと: 左の左オフセットを計算します。さまざまなテキスト方向 (ltr および rtl) と互換性があり、さまざまなブラウザーによる scrollLeft のさまざまな処理と互換性があります。

//(1)根据dir文本方向以及start和end的值,计算出left、right偏移量
if (options.left == null) {
    
    
  options.left = isRtl ? options.end : options.start;
}
if (options.right == null) {
    
    
  options.right = isRtl ? options.start : options.end;
}

// (2)兼容各个浏览器,因为浏览器对于滚动scrollLeft在 RTL 中的含义并不一致。
if (isRtl && getRtlScrollAxisType() != RtlScrollAxisType.NORMAL) {
    
    
  // 兼容dir='rtl'的情况
  if (options.left != null) {
    
    
    options.right = el.scrollWidth - el.clientWidth - options.left;
  }
  if (getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
    
    
    // 兼容 INVERTED 浏览器
    options.left = options.right;
  } else if (getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
    
    
    // 兼容 NEGATED 浏览器
    options.left = options.right ? -options.right : options.right;
  }
} else {
    
    
  // dir='ltr'的情况 或者 NORMAL 浏览器中
  if (options.right != null) {
    
    
    options.left = el.scrollWidth - el.clientWidth - options.right;
  }
}

3 つ目: ネイティブの scrollTo メソッドを呼び出すか、scrollTop、scrollLeft を設定してスクロールを実現します。

this._applyScrollToOptions(options);

private _applyScrollToOptions(options: ScrollToOptions): void {
    
    
  const el = this.elementRef.nativeElement;

  if (supportsScrollBehavior()) {
    
    
    el.scrollTo(options);
  } else {
    
    
    if (options.top != null) {
    
    
      el.scrollTop = options.top;
    }
    if (options.left != null) {
    
    
      el.scrollLeft = options.left;
    }
  }
}

4 番目に、cdkScrollable コマンドがメソッド测量相对于视口指定边缘的滚动偏移量を。このメソッドは、scrollLeft または scrollTop を直接チェックする代わりに使用できます。これは、ブラウザーがスクロールの RTL で scrollLeft が何を意味するかについて合意していないためです。このメソッドによって返される値は、レイアウトの方向に関係なく、 from の左右が常にスクロール コンテナーの左右を参照するように正規化されます。

measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number {
    
    
    // from 指定想要返回容器的哪一侧的偏移量。
    const LEFT = 'left';
    const RIGHT = 'right';
    const el = this.elementRef.nativeElement;

    if (from == 'top') {
    
    
      // 返回容器上侧的偏移量。
      return el.scrollTop;
    }
    if (from == 'bottom') {
    
    
      // 返回容器下侧的偏移量。
      return el.scrollHeight - el.clientHeight - el.scrollTop;
    }

    // ⭐️⭐️⭐️ 根据文本方向以及start和end的值来确定想要返回的是容器左侧还是右侧的偏移量。
    const isRtl = this.dir && this.dir.value == 'rtl';
    if (from == 'start') {
    
    
      // 如果是LTR布局,start表示想要返回容器左侧的偏移量,from=left。如果是RTL布局,start表示想要返回容器右侧的偏移量,from=right。
      from = isRtl ? RIGHT : LEFT;
    } else if (from == 'end') {
    
    
      // 如果是LTR布局,end表示想要返回容器右侧的偏移量,from=right。如果是RTL布局,end表示想要返回容器左侧的偏移量,from=left。
      from = isRtl ? LEFT : RIGHT;
    }

    // ⭐️⭐️⭐️ 根据不同浏览器在RTL情况下对scrollLeft的处理方式不同,来计算确定返回容器左侧或者右侧的偏移量。
    if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
    
    
      // 在RTL布局中,对于 INVERTED,当一直向左滚动时,scrollLeft 为 (scrollWidth - clientWidth),一直向右滚动时为 0。
      // 这里el.scrollLeft是正数,值等于容器的右侧偏移量。✅
      if (from == LEFT) {
    
    
        return el.scrollWidth - el.clientWidth - el.scrollLeft;
      } else {
    
    
        return el.scrollLeft;
      }
    } else if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
    
    
      // 在RTL布局中,对于 NEGATED,scrollLeft 在一直向左滚动时为 -(scrollWidth - clientWidth),在一直向右滚动时为 0。
       // 这里el.scrollLeft是负数,绝对值等于容器的右侧偏移量。✅
      if (from == LEFT) {
    
    
        return el.scrollLeft + el.scrollWidth - el.clientWidth;
      } else {
    
    
        return -el.scrollLeft;
      }
    } else {
    
    
      // 对于 NORMAL 以及非 RTL 布局,当一直向左滚动时 scrollLeft 为 0,当一直向右滚动时为 (scrollWidth - clientWidth)。
      // 这里el.scrollLeft是正数,值等于容器的左侧偏移量。✅
      if (from == LEFT) {
    
    
        return el.scrollLeft;
      } else {
    
    
        return el.scrollWidth - el.clientWidth - el.scrollLeft;
      }
    }
  }
}


5 番目に、cdkScrollable ディレクティブは、スクロール可能な要素の参照を取得するための getElementRef() メソッドを提供します。

// 获取该可滚动元素的引用。
getElementRef(): ElementRef<HTMLElement> {
    
    
  return this.elementRef;
}

一般に、CdkScrollable は、テキスト レイアウトとブラウザーの互換性の問題を考慮して処理してくれました。

  • そのスクロール可能な要素への参照を取得したいときは?
    電話するcdkScrollable.getElementRef()だけです。
  • scrollable要素のスクロールイベントを聞きたいときは?購読するには
    電話するだけです。cdkScrollable.elementScrolled()
  • 要素を特定の位置にスクロールしたいときは?
    電話するcdkScrollable.scrollTo(ExtendedScrollToOptions)だけです。
  • スクロールオフセットを測定したいときは?
    電話するcdkScrollable.measureScrollOffset(from)だけです。

4. ScrollDispatcher サービス

まず、scrollDispatcher は、[scrollable reference] とその [scroll event subscription] のマッピングを含むディクショナリを維持します。このディクショナリは scrollContainers と呼ばれます。

scrollContainers: Map<CdkScrollable, Subscription> = new Map();

2 つ目は、scrollDispatcher に "transfer station" _scrolled という通知があり、これはオブザーバブル (オブザーバブル) とオブザーバー (オブザーバー) の両方です。要素がスクロールされると「メッセージ」を発します。要素のスクロール「メッセージ」を購読するためにも使用できます。
cdkScrollable にバインドされた要素がスクロールされると、_scrolled はスクロール可能なインスタンスを発行します。また、グローバルにリッスンされた要素がスクロールするときにも放出されますが、何も放出されません。要素がスクロールしたことを世界に伝えます。

_scrolled = new Subject<CdkScrollable | void>();

第 3 に、CdkScrollable 命令が要素にバインドされると、命令は scrollDispatcher の register メソッドを呼び出し、スクロール可能な参照とそれに対応するスクロール イベント サブスクリプションをキーと値のペアとして scrollContainers ディクショナリに登録します。

register(scrollable: CdkScrollable): void {
    
    
  if (!this.scrollContainers.has(scrollable)) {
    
    
    this.scrollContainers.set(
      scrollable,
      scrollable.elementScrolled().subscribe(
        // 在scrollable元素发生滚动时,_scrolled发出该scrollable实例
        // Mark A: 这里监听的是当前的scrollable实例的滚动,_scrolled会发射该scrollable实例。
        () => this._scrolled.next(scrollable)
      ),
    );
  }
}

第 4 に、CdkScrollable 命令が破棄されると、scrollDispatcher は辞書内の対応するスクロール サブスクリプションを取得し、スクロール可能な参照に従ってそれをキャンセルし、同時に辞書からマッピングを削除します。

deregister(scrollable: CdkScrollable): void {
    
    
  const scrollableReference = this.scrollContainers.get(scrollable);
  if (scrollableReference) {
    
    
    scrollableReference.unsubscribe();
    this.scrollContainers.delete(scrollable);
  }
}

5 番目に、getAncestorScrollContainers():CdkScrollable[]メソッドは scrollContainers ディクショナリから提供された要素を含むすべてのスクロール可能オブジェクトを見つけ、それらを返します。

getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): CdkScrollable[] {
    
    
  const scrollingContainers: CdkScrollable[] = [];this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
    
    
    if (this._scrollableContainsElement(scrollable, elementOrElementRef)) {
    
    
      scrollingContainers.push(scrollable);
    }
  });return scrollingContainers;
}

第 6 に、 scrolled(): Observable<CdkScrollable | void>このメソッドは、登録された Scrollable 参照 (またはウィンドウ、ドキュメント、またはボディ) によってトリガーされるスクロール イベントをサブスクライブし、observable を返します。

scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<CdkScrollable | void> {
    
    
  return new Observable((observer: Observer<CdkScrollable | void>) => {
    
    
    if (!this._globalSubscription) {
    
    
      this._globalSubscription = this._ngZone.runOutsideAngular(() => {
    
    
        const window = this._getWindow();
        // Mark A: 这里监听的是全局元素的滚动,_scrolled发射了但没有发射任何东西。
        return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
      });
    }// 订阅全局的scroll事件,观察者observer会在这里“盯着”_scrolled发射的内容并做想做的事情。
    const subscription =
      auditTimeInMs > 0
        ? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer)
        : this._scrolled.subscribe(observer);
    this._scrolledCount++;return () => {
    
    
      subscription.unsubscribe();
      this._scrolledCount--;
      if (!this._scrolledCount) {
    
    
        if (this._globalSubscription) {
    
    
          // 取消全局监听scroll事件的订阅
          this._globalSubscription.unsubscribe();
          this._globalSubscription = null;
        }
      }
    };
  });
}

7 番目のancestorScrolled(): Observable<CdkScrollable | void>方法: 指定された要素の祖先のスクロール イベントをサブスクライブし、オブザーバブルを返します。

ancestorScrolled(
  elementOrElementRef: ElementRef | HTMLElement, // 指定要观察谁的祖先
  auditTimeInMs?: number, // 指定滚动事件节流(throttle)的时间
): Observable<CdkScrollable | void> {
    
     // 返回一个可观察者,具体内容是祖先scrollable实例
  const ancestors = this.getAncestorScrollContainers(elementOrElementRef);return this.scrolled(auditTimeInMs).pipe(
    filter(target => {
    
    
      // Mark A: target有值表示的是cdkScrollable元素滚动触发的。
      // Mark A: target为空则表示是全局监听到的元素的滚动。
      return !target || ancestors.indexOf(target) > -1;
    }),
  );
}

5.スクロールツールクラス

supportsScrollBehavior():boolean要素がスクロール動作をサポートするかどうかを決定します。
getRtlScrollAxisType(): RtlScrollAxisTypeRTL レイアウトの下で scrollLeft の現在のブラウザーの値メソッドを取得するNORMALNEGATEDまたはINVERTED.

9. 参照研究

最新のフロントエンド スクロール実装のさまざまな解釈ガイドライン
問題と解決策: 要素が最後までスクロールされているかどうかを判断する、要素がスクロールできるかどうかを判断する、ユーザーがテキストを読んだかどうかを判断する
CSSOM を使用して要素のサイズと位置を測定する
getBoundingClientRect()
Angular cdk スクロール

おすすめ

転載: blog.csdn.net/Kate_sicheng/article/details/127659037