@charset "UTF-8";
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; overflow-x: hidden; color: rgba(43, 43, 43, 1); font-family: -apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; background-image: linear-gradient(90deg, rgba(159, 219, 252, 0.15) 3%, rgba(0, 0, 0, 0) 0), linear-gradient(1turn, rgba(159, 219, 252, 0.15) 3%, rgba(0, 0, 0, 0) 0); background-size: 20px 20px; background-position: center }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { padding: 30px 0; margin-top: 35px; margin-bottom: 10px; color: rgba(77, 208, 225, 1) }
.markdown-body h1 { font-size: 30px; text-align: center; position: relative; width: max-content; margin: 0 auto }
.markdown-body h1:before { position: absolute; content: ""; z-index: -1; top: -20px; height: 100%; width: 100px; left: 0; right: 0; margin: 0 auto; background: url("data:image/png;base64,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") center / 64px 64px no-repeat; opacity: 0.84 }
.markdown-body h1:after { position: absolute; content: ""; width: 150%; left: -25%; height: 50%; bottom: 12px; border-radius: 50%; background: linear-gradient(rgba(0, 0, 0, 0) 80%, rgba(77, 208, 225, 0.8)); opacity: 0.6; animation: 6s linear infinite h1animate }
@keyframes h1Animate { 0% { background-position: right bottom } 50% { background-position: right } 100% { background-position: right bottom } }
.markdown-body h2 { display: block; border-bottom: 4px solid rgba(77, 208, 225, 1); position: relative; font-size: 24px; padding: 12px 32px; margin: 30px 0 }
.markdown-body h2:before { width: 24px; height: 24px; left: 0; top: 0; margin: auto; background-size: 24px 24px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADGklEQVRYR81X32vTYBQ999s6mFjQgQ+DrbHiVFZYU4cDcQ/6pGhTFVYFEXGi82H+Bz448UnEF1Fx9ccEEcXpZE3d5tP2ooKiTacTHaLNpigMHDgnU9tcSbrWrkwWR0sbyEOSe885ObnfvV8IRT6oyPwoLQHBx+OVM5WJvSyEVAhnBOjt7yU/+/rr6r6l8TMO+F/EN0JQhICqQpD/xaRpcpAc9tS+M+9lBCia/oqBamK+zeDuQogQZaKJk3wcQjxSva7tGQGB2Ke1zIk3DNyMyNL+QpCnMQOaPsDAVuGAp9cjvbYc8Ec/bCYSg0zoiHilk1tHxqsqEsYlML4kjIpT/eurJxRNPweQU5VdrWaOEo1fgKAVbBgXIz73kF3R/ph+ghgdzMYWM29eAWlBJqgZaFlFYtC6nhWpaDqnSGlIlV1WjJ3DloDNgyNLncudqgX//Ucg3LxuStHGuhi8pqKCW3rqV342rwFjRznKm+/LNaN2yC237ThgF2wxcfMLeP6+ncrKzoPoKTGeLQbYbg4TNoC5iZPJY5HGVRdSNZAWYBclD3FzBQzrR8hACAKdzBzKA/4/IYioDQaOskBbpEG6PO8qKKSAEi3CnEb0Pw4oMf0OmKbTDWqh3Lw6EIiNBZi5lxh3wz4puBD5ovqAMvxhHSdFKxE1CQe3m/07TeTX4lcJdAhE+1Sv65Z5P/ByvIGTRowIZ9igbtXnmrOsbTvgj+kHBNMuBu9OdVw8EeU4nC1A0cYmAHZOTRrLhra4Z8ywnSN6vZHAFTA2WnnMfQB3qz73ddsOZM8CACFDIPSgQXqebXEgqgeZcAeEe6pXasm1f8ew3igMtAHWac0Uc/jYdyAaP0xEBwFsmgUPqbJ0NE2UKj4EGcahiOzuyhagaHpnmtgcVgTcCMuua7YdyAHbA3ArQNscVFbb4635aD6fnYaTvxxi9UNP7ddMXaRWVBdAcaLk6bDXPZCNZ9uBXEsDUX1T2Cc9yjig6Z0EHg3LK8/aqf6MwJKchkXfks1+0+JtSq3qLPa23BRR1B+T/6nkfMaW1r9hPt/MLtYfTLEpP+T9FNoAAAAASUVORK5CYII=") }
.markdown-body h2:after, .markdown-body h2:before { content: ""; display: block; position: absolute; bottom: 0 }
.markdown-body h2:after { right: 0; width: 400px; height: 10px; border-top-right-radius: 24px; background: linear-gradient(90deg, rgba(255, 255, 255, 1), rgba(77, 208, 225, 1)); max-width: 50vw }
.markdown-body h3 { margin: 30px 0; font-size: 18px; position: relative; padding: 4px 32px; width: max-content }
.markdown-body h3:before { border-bottom: 2px solid rgba(77, 208, 225, 1); width: 100%; content: ""; display: block; height: 28px; position: absolute; left: 0; top: 0; bottom: -2px; margin: auto; background-size: 28px 28px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABRklEQVRYR2NkGGDAOMD2M4w6YDQERkNg+ITAppcfY/8zMv3wF+NdTUrZQpUQ2PT6cz8Dw/8CkMWMDIwNvqK8jcQ6gmIHNN19EaXPx1XPyMCghrCUKcpPlGc5MY6gyAE+Fx52MjL8j3cU5a1UYWXtZGBkEAVb+p8hxU+Mby5NHQCxnKEMaskzJ37uFmUetkmMjAzrfUX4woixHBJlZAA0y2EmPPYU4enLkhGeQIqRJDsAh+UgO7duNpD3IcVykkOA2paT5ABaWE60A2hlOdEO8D3/4CMDIyMfWvySFefoaYSoROh74eFXBgYGLiTNVLGc+BC48PAnAwMDG9QBVLOcaAd8P5ox+x/jf5AjGLgYfnwnKqv9/8/PwPO/kFF/MSj0cAKiouD/0bgYoixFU8RovWgJIX1EOYCQIZTIjzpgNARGQ2DAQwAAvHBaIdB7zxsAAAAASUVORK5CYII="); background-repeat: no-repeat; animation: 2s infinite alternate h3animationbefore }
@keyframes h3AnimationBefore { 0% { width: 28px } 25% { width: 100% } 50% { width: 100% } 100% { width: 100% } }
.markdown-body h3:after { content: ""; display: block; width: 28px; height: 28px; position: absolute; border: 2px solid rgba(77, 208, 225, 1); border-radius: 50%; right: -15px; top: 0; bottom: 0; margin: auto; background-size: 28px 28px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABRklEQVRYR2NkGGDAOMD2M4w6YDQERkNg+ITAppcfY/8zMv3wF+NdTUrZQpUQ2PT6cz8Dw/8CkMWMDIwNvqK8jcQ6gmIHNN19EaXPx1XPyMCghrCUKcpPlGc5MY6gyAE+Fx52MjL8j3cU5a1UYWXtZGBkEAVb+p8hxU+Mby5NHQCxnKEMaskzJ37uFmUetkmMjAzrfUX4woixHBJlZAA0y2EmPPYU4enLkhGeQIqRJDsAh+UgO7duNpD3IcVykkOA2paT5ABaWE60A2hlOdEO8D3/4CMDIyMfWvySFefoaYSoROh74eFXBgYGLiTNVLGc+BC48PAnAwMDG9QBVLOcaAd8P5ox+x/jf5AjGLgYfnwnKqv9/8/PwPO/kFF/MSj0cAKiouD/0bgYoixFU8RovWgJIX1EOYCQIZTIjzpgNARGQ2DAQwAAvHBaIdB7zxsAAAAASUVORK5CYII="); animation: 2s infinite alternate h3animationafter }
@keyframes h3AnimationAfter { 0% { } 10% { } 50% { transform: rotate(-1turn) } 100% { transform: rotate(-1turn) } }
.markdown-body h4 { font-size: 16px }
.markdown-body h5 { font-size: 15px }
.markdown-body h6 { margin-top: 5px }
.markdown-body p { line-height: inherit; margin: 22px 0; letter-spacing: 2px; font-size: 14px; word-spacing: 2px }
.markdown-body img { max-width: 80%; border-radius: 6px; display: block; margin: 20px auto !important; object-fit: contain; box-shadow: 0 0 16px rgba(110, 110, 110, 0.45) }
.markdown-body figcaption { display: block; font-size: 13px; color: rgba(43, 43, 43, 1) }
.markdown-body figcaption:before { content: ""; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAGFBMVEVHcExAuPtAuPpAuPtAuPpAuPtAvPxAuPokzOX5AAAAB3RSTlMAkDLqNegkoiUM7wAAAGBJREFUKM9jYBhcgMkBTUDVBE1BeDGqEtXychNUBeXlKEqACsrLQxB8lnCQQClCiWt5OYoSiAIkJVAF5eVBqAqAShTAAs7l5ShKWMwRAmAlSArASpAVgJUkCqIAscESHwCVVjMBK9JnbQAAAABJRU5ErkJggg=="); display: inline-block; width: 18px; height: 18px; background-size: 18px; background-repeat: no-repeat; background-position: center; margin-right: 5px; margin-bottom: -5px }
.markdown-body hr { border-top: 1px solid rgba(77, 208, 225, 1); border-right: none; border-bottom: none; border-left: none; margin-top: 32px; margin-bottom: 32px }
.markdown-body del { color: rgba(77, 208, 225, 1) }
.markdown-body code { border-radius: 2px; overflow-x: auto; background-color: rgba(77, 208, 225, 0.08); color: rgba(38, 198, 218, 1); padding: 0.195em 0.4em }
.markdown-body pre { font-family: Menlo, Monaco, Consolas, Courier New, monospace; overflow: auto; position: relative; line-height: 1.75; box-shadow: 0 0 8px rgba(110, 110, 110, 0.45); border-radius: 4px; margin: 16px }
.markdown-body pre:before { content: ""; display: block; height: 30px; width: 100%; margin-bottom: -7px; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAdCAYAAABcz8ldAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAhgSURBVGhD7Zp7bBTHHcdn33t7vvOdzy+ITVKDU0xIKG2ABCPTRCCaUiEVKWoqRJuASAhCitRCVKSoalFUKZBiSmmFRRJKRUnUtIpo+aNqGgwoOCmuFUIRzxjwE4zte+97drYzztji8HPvtkit/PnH+n1397Tz+83vN/PbMZhmmmmm+d+BoX8n5diihcGqgFQf5vk6BMAskWUlw3GyFnIvtqWSf91w7mKC3npfOLX7wYeiIa6BBWCOLLFRF2NB0JvIOP/80YG+k2ev6S699b/OzOfKBW5l5KsgyC4DCFQDnpEAdE1goc/dlNPc/Up7P711UiYNSMuyxeUzZPnHgGHWh5XADEkSAcdiN+AnEXIBhBComgFU0/xQR+jnj51sOUMf9Z0NKyL8S9+JPBEN8zuCMrsqGOA5QWAAyzLAxe53HBeYFgJp1c5Cx33nyIfpV3e+22/Sx32nev/sMCgVnmM4bjOniAtZWQAsz315EfsGQQc4hgWcjHkCmOj1rheuNn95cXwmDMiVp5etC/D8m5FwUWVQUYYGPh6mZYFUOgsGVa1pXvOZzVT2jRuH54RM230jEuI3RcIiL4l4UkxAJmuD/riVsqD7ct2m9nep7BtVTbVfZ0uE/UIk+CQflAHDjf8+Lg6MldYATGpH3c/Ul7p3dWXppVGM6eElJSHmnQWPbSlRlN1lJcUBjqNRnwJZVQO3B5P/uq5rK1d90pakckFcaKp5UJHY92JR8YlwkUDVySEZfGfQdO7E7Z8s2HL9TSoXTPXRud9nA8IBqSwcZgWeqpPj6BYw7yTbXBN9q2v9lQEq5zBmWA8vWLCptCi4tzwW8RQMQlFQATPLSh6vCSh/plJBkMyQBHZfWYnkKRgEktEVpTJXERN2Xzo4ex2VC6K6qXYpF5b3ypVRT8EgcAERSJXRbwCBOTFzXblM5RxGBaRt+ZPYA+LO0mgxz5K1Ig+UgAzKIuGnz39z6S+olDeaibaXRsU1RUFvgx+GwTWgPCaDgMw2XXpr9gwq50XV0bkxJiYeEiNF5cwE5XsiOEkAUkXkUW51SSOVchjl8WKef604XFSRbzCGCYeCoESStv/p8QU1VPIM3knNDynctnBRfsEYhgSlNCIGgQv2UCkvGIHZgteMh1nBW9W4F16RAM6yDVV7amZTaYQcr59cuuhhWRTWBvAMLxQGeyFSHOLnh0MvUskz5RF+fbRYDEy0mZgqQYUHOLhr//b6rGoqeaLqQG0pw3PrBbyA+4EQUkRmhvgqNUfICUipKK4OKUqIJVPKB0jpEhjmWWp64jdbKmVZZNYogcJm493gsifOqhDyeh9GYR/FM7sW+DA5CKR0MSK3tvKZkpwB5gRE4tjFEr7RL0iWBGV51vHFCyupNGWWPqLgnoer9mtyEGSJAzwLllDTGzyznDjRN/CwOFkoFb4bm0eVIXICgpvdGoEvrF7fC89zfLkkeV5HbOhWiTwTpKYvCAJLGshRdXtKMKAWlyxq+MPQLk1h66g5RE5ABJYNFrqY3wvJklJRUKg5ZWLFXIA86yek2uDOPkBNb3CM5Pf7DL2QyIrUGiLH+xC5Bmmm/ARnHUhC6PnzxWDK0RH5HuIjZGy27erU9AZ0dTIWXyG+NpBBrSFySxZw220IqeUPFoS6jVAPNadM7yDsgNB1qOkLuAziMYIb1PQGA75wIaKGPyAb+9oF16g5RE5ALIQ+tSyLWoWDEAK6aXW3JlK9VJoyx1oyvVkNdvo5KXXDAVkdnaKmNwx0xjH98w3JNmTCm+Bc9hKVhsgJSI9pvp9Vdd++jmq6AXB2/HHrhcs5aTkVDv0DFzoHvKdq/mQsKX/4t7KJLDpOJW+IbAvMGoMkxfwAWZB8DT7W1diTE+WcgKz6pK1bs6z3daPwmJDsSKt6ZsCyjlLJMz0DsDGZ8SdlDROBjOb8YeWOjptU8kTXusuaazu7oJrfEnQvdkpVcUn6PTVHyAkIIW7br/Unklni0EJIZ1WgGsauZR+fvUglz6zY0dGfVp09ybRNlfwgi3k8YSbvJJ29VMoLt9v6rZVQL7hOYUubndHJGclBtzn1byqNMCogi09/2nFb01/oj+f/5TyjauBOKtPcZ1r7qZQ3f2lRfxZPWi2anp8TSDAGExZMa2jr8u03L1M5L7q3Xc+iAeuHRl/ScvPcjSLDBnZS/cjtNHd2v3171Ewbs9N5q7Pn4otVMx3btBsCsoRbk1FxG5dMVgMDqfTpXl1/tuFMa5zKefPROdX59qLQBwLnNog8Wy1OcjB1N+QEsW/QsFNZuO35Xb1v98QLX4/Sx+O3wqujrQ6013ABUWI8+AaqBjAH01+ghL22+5X2PirnMG7r+esbnae/V1neauvGSoHjigTcVU7UGFm2DeK4ttxKpQ+mLPvl+o/PjnkAkw9HTqSMmVHhyAMx9iFcSh/BHTfLceO/C8mKjApBf9zszGhoY92m9sN+BGOY9AeD7eGniv8OTaOB4dgyTsQd9wS+IQu4lciYdkI7CLrNH3Rvbb9FL41i0tbzVP2iWJkobpN5fmM4IJfJskTP1Bk8A9HQmbpmGDBrWqdVCN/Yd7PjxKGOXn+bmbto3feVVcVB9qehIL8EJy8nChwgr0O2xxBnhGU5eP2CfYbl/m4gBRsbtneMORP9oGpjpcCsiKzHHfdOPiQ/wMniyFEu2dbiTQCAeN/vavC466BGYLttXc9fmXBXMGlAhiHHur+sq6uPiUI9z7CVHMPwBnLSuuN8FuC48/Oaz1ylt94XfrW5ouyprwWfYRkwNyCyYYjwkBHows1fa+tV/fzGxlv39b9gqvfPmQ+i/HK8KlcBjhHwfl8HEHyOd1JnuzZd66S3TTPNNNP8/wDAfwDG7G0m9LKBpwAAAABJRU5ErkJggg==") 10px 10px / 40px no-repeat }
.markdown-body pre>code { font-size: 12px; padding: 15px 12px; margin: 0; word-break: normal; display: block; overflow-x: auto; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1) }
.markdown-body a { color: rgba(77, 208, 225, 1); border-bottom: 1px solid rgba(77, 208, 225, 1); font-weight: 400; text-decoration: none; margin: 0 4px }
.markdown-body a:active, .markdown-body a:hover { background-color: rgba(77, 208, 225, 0.1) }
.markdown-body strong { color: rgba(38, 198, 218, 1) }
.markdown-body strong:before { content: "「" }
.markdown-body strong:after { content: "」" }
.markdown-body em { font-style: normal; color: rgba(77, 208, 225, 1); font-weight: 700 }
.markdown-body table { display: inline-block !important; font-size: 12px; width: auto; max-width: 100%; overflow: auto; border: 1px solid rgba(246, 246, 246, 1) }
.markdown-body thead { background: rgba(246, 246, 246, 1); color: rgba(0, 0, 0, 1); text-align: left }
.markdown-body tr:nth-child(2n) { background-color: rgba(77, 208, 225, 0.05) }
.markdown-body td, .markdown-body th { padding: 12px 7px; line-height: 24px }
.markdown-body td { min-width: 120px }
.markdown-body blockquote { margin: 2em 0; padding: 24px 32px; border-left: 4px solid rgba(38, 198, 218, 1); background: rgba(77, 208, 225, 0.15); position: relative }
.markdown-body blockquote:before { content: "❝"; top: 8px; left: 8px; color: rgba(77, 208, 225, 1); font-size: 30px; line-height: 1; font-weight: 700; position: absolute; opacity: 0.7 }
.markdown-body blockquote:after { content: "❞"; font-size: 30px; position: absolute; right: 8px; bottom: 0; color: rgba(77, 208, 225, 1); opacity: 0.7 }
.markdown-body blockquote p { color: rgba(89, 89, 89, 1); line-height: 2 }
.markdown-body ol, .markdown-body ul { color: rgba(89, 89, 89, 1); padding-left: 28px }
.markdown-body ol li, .markdown-body ul li { margin-bottom: 0; list-style: inherit }
.markdown-body ol li .task-list-item, .markdown-body ul li .task-list-item { list-style: none }
.markdown-body ol li .task-list-item ol, .markdown-body ol li .task-list-item ul, .markdown-body ul li .task-list-item ol, .markdown-body ul li .task-list-item ul { margin-top: 0 }
.markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul { margin-top: 3px }
.markdown-body ol li { padding-left: 6px }
@media (max-width: 720px) { .markdown-body h1 { font-size: 24px } .markdown-body h2 { font-size: 20px } .markdown-body h3 { font-size: 18px } }.markdown-body pre, .markdown-body pre>code.hljs { color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1) }
.hljs-comment, .hljs-quote { color: rgba(153, 153, 136, 1); font-style: italic }
.hljs-keyword, .hljs-selector-tag, .hljs-subst { color: rgba(51, 51, 51, 1); font-weight: 700 }
.hljs-literal, .hljs-number, .hljs-tag .hljs-attr, .hljs-template-variable, .hljs-variable { color: rgba(0, 128, 128, 1) }
.hljs-doctag, .hljs-string { color: rgba(221, 17, 68, 1) }
.hljs-section, .hljs-selector-id, .hljs-title { color: rgba(153, 0, 0, 1); font-weight: 700 }
.hljs-subst { font-weight: 400 }
.hljs-class .hljs-title, .hljs-type { color: rgba(68, 85, 136, 1); font-weight: 700 }
.hljs-attribute, .hljs-name, .hljs-tag { color: rgba(0, 0, 128, 1); font-weight: 400 }
.hljs-link, .hljs-regexp { color: rgba(0, 153, 38, 1) }
.hljs-bullet, .hljs-symbol { color: rgba(153, 0, 115, 1) }
.hljs-built_in, .hljs-builtin-name { color: rgba(0, 134, 179, 1) }
.hljs-meta { color: rgba(153, 153, 153, 1); font-weight: 700 }
.hljs-deletion { background: rgba(255, 221, 221, 1) }
.hljs-addition { background: rgba(221, 255, 221, 1) }
.hljs-emphasis { font-style: italic }
.hljs-strong { font-weight: 700 }

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

介绍

最近在实现列表的滚动交互时,算是被复杂的业务场景整得怀疑人生了。今天主要聊一下关于 scroll 的应用:

  • CSS 平滑滚动
  • JS 滚动方法
  • 区分人为滚动和脚本滚动

一、CSS 平滑滚动

1、一行样式改善体验

在一些滚动交互比较频繁的场景,我们可以通过在可滚动容器上增加一行样式来改善用户体验。

scroll-behavior: smooth;

比如说,在文档网站里,我们常使用 # 来去定位到对应的浏览位置。

像上面这个例子,我们首先通过 # 去锚定对应内容,实现了一个 tab 切换的效果:

<div>
 <a href="#A">A</a>
 <a href="#B">B</a>
 <a href="#C">C</a>
</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>

同时,为了实现平滑滚动,我们在滚动容器上设置了如下的 CSS:

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

在 scroll-behavior: smooth 的作用下,容器内的默认滚动呈现了平滑滚动的效果。

2、兼容性

IE 和 移动端 ios 上兼容性较差,必要时需要依赖 polyfill。

3、注意

1、在可滚动的容器上设置了 scroll-behavior: smooth 之后,其优先级是高于 JS 方法的。也就是说,在 JS 中指定 behavior: auto,想要恢复立即滚动到目标位置的效果,将不会生效。

2、在可滚动的容器上设置了 scroll-behavior: smooth 之后,还能够影响到浏览器 Ctrl+F 的表现,使其也呈现平滑滚动的效果。

二、JS 滚动方法

1、基本方法

我们熟知的原生 scroll 方法,大概有这些:

  • scrollTo:滚动到目标位置
  • scrollBy:相对当前位置滚动
  • scrollIntoView:让元素滚动到视野内
  • scrollIntoViewIfNeeded:让元素滚动到视野内(如果不在视野内)

以大家用得比较多的 scrollTo 为例,它有两种调用方式:

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

而滚动的行为,即方法参数中的 behavior 分为两种:

  • auto:立即滚动
  • smooth:平滑滚动

除了上述的 3 个 api,我们还可以通过简单粗暴的 scrollTop、 scrollLeft 去设置滚动位置:

// 设置 container 上滚动距离 200
container.scrollTop = 200;
// 设置 container 左滚动距离 200
container.scrollLeft = 200;

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

2、应用

自己以往需要用到滚动的场景有:

  • 组件初始化,定位到目标位置
  • 点击当前页靠底部的某个元素,触发滚动翻页
  • ......

举个例子,现在我希望在列表组件加载完成后,列表能够自动滚动到第三个元素。

根据上面提到的我们可以用很多种方式去实现,假设我们已经为列表容器增加了 scroll-behavior: smooth 的样式,然后在 useEffect hook 中去调用滚动方法:

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 方法

虽然最后效果都是一样的,但这几种方法实际上还是有些许差异的。

三、scrollIntoView 的奇怪现象

1、页面整体偏移

最近在过一些历史用例的时候,遇到了这种情况:

现象大概就是,当我通过按钮,滚动定位到聊天区域的某条消息时,页面整体发生了偏移(向上移动)。再看一眼代码,发现使用的是 scrollIntoView:

因为是第一次遇到,所以上万能的 stack overflow 上逛了一圈,看到了类似的问题:scrollIntoView 导致页面整体移动。

这个问题常常发生在哪些情况下呢?

1、页面有 iframe 的情况下。

表现是当 iframe 内的内容发生滚动时,主页面也发生了滚动。这显然和 MDN 上的描述不一致:

Element 接口的 scrollIntoView () 方法会滚动元素的父容器,使被调用 scrollIntoView () 的元素对用户可见。

2、直接使用 scrollIntoView() 的默认参数

先说说 scrollIntoView() 支持什么参数:

element.scrollIntoView(alignToTop); // Boolean 型参数
element.scrollIntoView(scrollIntoViewOptions); // Object 型参数

(1)alignToTop

  • 如果为 true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 scrollIntoViewOptions: {block: "start", inline: "nearest"}。这是这个参数的默认值。
  • 如果为 false,元素的底端将和其所在滚动区的可视区域的底端对齐。相应的 scrollIntoViewOptions: {block: "end", inline: "nearest"}

(2)scrollIntoViewOptions

包含下列属性:

  • behavior 可选

    定义动画过渡效果, "auto" 或 "smooth" 之一。默认为 "auto"

  • block 可选

    定义垂直方向的对齐, "start""center""end", 或 "nearest" 之一。默认为 "start"

  • inline 可选

    定义水平方向的对齐, "start""center""end", 或 "nearest" 之一。默认为 "nearest"

回到我们的问题,为什么使用默认参数,即 element.scrollIntoView(),会引发页面偏移的问题呢?

关键在于 block: "start",从上面的参数说明我们了解到,默认不传参数的情况下,取的是 block: start,它表示 “元素顶端与所在滚动区的可视区域顶端对齐”。但从现象上看,影响的不只是 “所在滚动区” 或者 “父容器”,祖先 DOM 元素也被影响了。

由于寻觅不到 scrollIntoView 的源码,暂时只能定位到是 start 这个默认值在做妖。既然原生的方法有问题,我们需要采取一些别的方式来代替。

2、解决方式

1、更换参数

既然是 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"}
    • 如果您当前位于其祖先的下方,则元素在其祖先的顶部对齐。
    • 如果您当前位于其祖先之上,则元素在其祖先的底部对齐。
    • 如果它已经在视图中,保持原样。

2、scrollTop/scrollLeft

上文也提到 scrollTop/scrollLeft 赋值是兼容性最好的滚动方式,我们可以利用它来代替默认的 scrollIntoView () 的表现。

比如说置顶某个元素,可以定义可滚动容器的 scrollTop 为该元素的 offsetTop:

container.scrollTop = element.offsetTop;

值得一提的是,结合 CSS 的 scroll-behavior,这种赋值方式也可以实现平滑滚动效果。

四、如何区分人为滚动和脚本滚动

1、背景

最近遇到这么一个需求,做一个实时高亮当前播放内容的字幕文稿。核心的交互是:

1、当用户没有人为滚动文稿时,会保持自动翻页的功能

2、当用户人为滚动文稿时,后续将不会自动翻页,并出现 “回到当前播放位置” 的按钮

3、假如点击了 “回到当前播放位置” 的按钮,会回到目标位置,并恢复自动翻页的功能。

像上面的演示中,用户触发了人为滚动,之后点击 “回到当前播放位置”,触发了脚本滚动。

2、人为滚动

怎么定义 “人为滚动” 呢?我们所了解的人为滚动,包含:

  • 鼠标滚动
  • 键盘方向键滚动
  • 缩进键滚动
  • 翻页键滚动

假如说,我们通过 onWheel、onKeyDown 等事件,去监听人为滚动,定是不能尽善尽美的。那么我们换个思路,能否去对 “脚本滚动” 下功夫?

3、脚本滚动

怎么定义 “脚本滚动”?我们将由代码触发的滚动,定义为 “脚本滚动”。

我们需要用一种方式描述 “脚本滚动”,来和 “人为滚动” 做区分。由于它们是非此即彼的关系,那实际上我们只需要在 onScroll 这个事件上,通过一个 flag 去区分即可。

流程图如下:

而这其中唯一需要关注的点在于,需要通过什么方式知道,脚本滚动结束了?\

scrollTo 等原生方式,显然没有给我们提供回调方法,来告诉我们滚动在什么时候结束。所以我们还是需要依赖 onScroll 去监听当前的滚动位置,来得知滚动什么时候达到目标位置。

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

接下来看看代码要怎么组织。

首先看一下我们想要实现的:

接下来先实现基本的页面结构。

1、定义一个长列表,并通过 useRef 记录:

  • 滚动容器的 ref
  • 脚本滚动的判断变量 isScriptScroll
  • 当前的滚动位置 scrollTop

2、接着,为滚动容器绑定一个 onScroll 方法,在其中分别编写人为滚动和脚本滚动的逻辑,并使用节流来避免频繁触发。

在人为滚动和脚本滚动的逻辑中,我们通过更新 wording 这个状态,来区分当前处于人为滚动还是脚本滚动。

3、用一个 button 来触发脚本滚动,调用 listScroll 方法,传入容器 ref,想要滚动到的 scrollTop 以及滚动结束后的 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("脚本滚动结束");
     cacheRef.current.isScriptScroll = false;
   });
 };  return (
   <div className="App">
     <button
       className="btn"
       onClick={() => {
         scriptScroll();
       }}
     >
       触发一次脚本滚动
     </button>
     <p className="wording">当前状态:{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 怎么实现了。我们需要再去绑定一个 scroll 事件,不断去监听容器的 scrollTop 是否已经达到目标值,所以可以这么组织:

import debounce from "lodash.debounce";

/** 误差范围内 */
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?.();
 }  // 调用滚动方法
 element.scrollTo({
   top: targetPos,
   left: 0,
   behavior: "smooth"
 });  // 没有回调就直接返回
 if (!callback) return;
 // 如果已经到达目标位置了,可以先行返回
 if (withErrorRange(targetPos, element.scrollTop, 10)) return callback();
 // 防抖处理
 const cb = debounce(() => {
   // 到达目标位置了,可以返回
   if (withErrorRange(targetPos, element.scrollTop, 10)) {
     element.removeEventListener("scroll", cb);
     unMountFlag = true;
     return callback();
   }
 }, 200);  element.addEventListener("scroll", cb, false);  // 兜底:卸载滚动回调,避免对之后的操作产生影响
 setTimeout(() => {
   if (!unMountFlag) {
     element.removeEventListener("scroll", cb);
     callback();
   }
 }, 1000);
};

按严谨的流程来写的话,我们需要依靠 scroll 事件去不断判断 scrollTop,直至在误差范围内相等。

但实际上滚动是一个很快的过程,跟我们兜底的定时器逻辑,也就是前后脚的事情,是不是可以只保留兜底的逻辑?

而且,考虑到那些异常情况:

  • 脚本滚动发生异常
  • 脚本滚动被人为滚动打断

我们都得保证执行了一次回调,确保外部状态被释放,下一次滚动的逻辑正常。

所以在不那么严格的场景下,上述的代码其实可以抛弃 eventListener 的部分,只保留兜底的逻辑,进一步简化:

/** 列表滚动封装 */
export const listScroll = (
 element: HTMLElement,
 targetPos: number,
 callback?: () => void
) => {
 const { scrollHeight: listHeight } = element;  // 避免一些边界情况
 if (targetPos < 0 || targetPos > listHeight) {
   return callback?.();
 }  // 调用滚动方法
 element.scrollTo({
   top: targetPos,
   left: 0,
   behavior: "smooth"
 });  // 没有回调就直接返回
 if (!callback) return;
 // 如果已经到达目标位置了,可以先行返回
 if (withErrorRange(targetPos, element.scrollTop, 10)) return callback();
 
 // 兜底:卸载滚动回调,避免对之后的操作产生影响
 setTimeout(() => {
   callback();
 }, 1000);
};

当然,这个实现只是一种参考,相信大家也有别的更好的思路。

总结

回顾整篇文章,简单介绍了关于 scroll 的一些 api 使用,原生 scrollIntoView 的坑以及区分人为滚动和脚本滚动的实现参考。

滚动,这一个看似微小的交互点,实际上可能隐藏着不少的工作量,在往后的评估或者实践中,需要多加重视和思考,隐藏在交互体验之下的复杂逻辑。

Scroll滚动,你玩明白了嘛?的更多相关文章

  1. ios中iframe的scroll滚动事件替代方法

    在公众号的开发中,遇到ios中iframe的scroll滚动事件失效,在此做下记录. 因为接口获取的数据必须放在iframe中展示,滚动到底部按钮变亮,如图: 代码如下: <!DOCTYPE h ...

  2. js中scroll滚动相关

    js中scroll滚动相关 scroll,滚动,一般讨论的是网页整体与浏览器之间的关系. 一.元素相关 属性/方法 解释 element.scrollHeight 返回元素的整体高度. element ...

  3. scroll 滚动到指定位置触发事件 and 点击一按钮/链接让页面定位在指定的位置

    scroll 滚动到指定位置触发事件:$(function(){ $(window).scroll(function() { var s =$(window).scrollTop(); if (s&g ...

  4. 【jQuery】scroll 滚动到顶部

    Jquery 实现页面滚动到顶端 $(document).ready(function () { // 滚动窗口来判断按钮显示或隐藏 $(window).scroll(function () { // ...

  5. 【bug】—— ios scroll 滚动穿透

    BUG描述 在 ios 微信浏览器或原生浏览器中,主内容容器.content在文档流内,并且overflow-y: scroll.在其之上有一个 fixed 定位的弹出层.popUp,滚动.popUp ...

  6. scroll滚动到一定距离触发事件/返回顶部animate

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. elasticsearch 深入 —— Scroll滚动查询

    Scroll search 请求返回一个单一的结果"页",而 scroll API 可以被用来检索大量的结果(甚至所有的结果),就像在传统数据库中使用的游标 cursor. 滚动并 ...

  8. mui-选项卡+scroll滚动

    详细操作见代码: <!doctype html> <html> <head> <meta charset="UTF-8"> < ...

  9. scroll滚动动画(js/ts)

    //(蓝色this部分为dom) scrollToLeft(option?: { duration?: number, direction?: number }) { let direction = ...

  10. js处理局部scroll事件禁止外部scroll滚动解决办法,jquery.mousewheel.js处理时禁止办法说明

    js Code: <script> window.onload = function() { for (i = 0; i < 500; i++) { var x = document ...

随机推荐

  1. KUKA库卡机器人维修

    KUKA库卡机器人作为生产线上的核心设备,一旦出现KUKA机械手故障,将直接影响整个生产线的运行效率.及时的库卡机器人维修工作不仅能够迅速恢复机器人的工作状态,减少生产停滞时间,还能通过预防性维护降低 ...

  2. 移动硬盘插入win10检测到却不显示盘符解决方法

    1.开始菜单中的设置-----设备. 2.选择"蓝牙和其他设备" 3.在其他设备栏中就能看到检测到的移动硬盘,点击删除设备后重新插入移动硬盘即可在此电脑上显示盘符.

  3. 摸鱼日历,新闻简报等一些工作摸鱼日历API接口合集分享

    摸鱼人日历API接口 请求示例(图片输出): https://moyu.qqsuu.cn 请求示例(JSON输出):[推荐] https://moyu.qqsuu.cn/?type=json 调用示例 ...

  4. 火爆的 幻兽帕鲁/Palworld 单机➕联机 电脑游戏 免费畅游

    在广阔的世界中收集神奇的生物"帕鲁",派他们进行战斗.建造.做农活,工业生产等,这是一款支持多人游戏模式的全新开放世界生存制作游戏. ▼补丁主要内容 ・修复加载世界数据时,加载画面 ...

  5. JUC并发—13.Future模式和异步编程简介

    大纲 1.Runnable接口与Callable接口 (1)Runnable接口实现异步任务 (2)Callable接口实现异步任务 2.Future模式 (1)Future模式的概念 (2)Futu ...

  6. Java - 高射炮打蚊子(第二弹)

    题记部分 01 || 面试题 001 || 什么是JVM JVM(Java虚拟机)是Java程序运行的环境,它是一个抽象的计算机,包括指令集.寄存器集.堆栈.垃圾回收等.JVM屏蔽了与具体操作系统平台 ...

  7. Socket通信-Linux系统中C语言实现TCP/UDP图片和文件传输

    TCP实现 传输控制协议(TCP,Transmission Control Protocol) 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议.TCP是因特网中的传输层协议, ...

  8. Vulnhub-election靶机

    总结:本靶机给了很多目录,对于信息收集考察的比较严格,给了一个数据库,很多时候容易陷进去,拿到用户权限登录后,也需要大量的信息收集,虽然可以在数据库里找到root和密码,但是不是靶机本身的,最终利用s ...

  9. Mysql join算法深入浅出

    导语 联表查询在日常的数据库设计中非常的常见,但是联表查询可能会带来性能问题,为了调优.避免设计出有性能问题的SQL,在explain命令中,会显示用的是哪个join算法,学习一下join过程是非常有 ...

  10. Windows编程----线程管理

    系统中,进程主要有两部分组成:进程内核对象和进程地址空间.操作系统通过进程内核对象来管理进程,进程地址空间用于维护进程所需的资源:如代码.全局变量.资源文件等. 那么线程也是有两部分组成:线程内核对象 ...