-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmock_scroll_bar.html
1 lines (1 loc) · 23.5 KB
/
mock_scroll_bar.html
1
<!doctype html><html lang="zh-CN" class="night"><head><meta charset="utf-8"><meta content="width=device-width,initial-scale=1,maximum-scale=4,user-scalable=0" name="viewport"><title>Ede's Blog</title><meta name="description" content="Try to be a qualified programmer"><meta property="og:type" content="website"><meta property="og:description" content="Try to be a qualified programmer"><meta property="og:title" content="Ede's Blog"><meta property="og:site_name" content="Ede's Blog"><meta property="og:url" content="https://ede.ink"><meta property="og:image" content="https://edeity.oss-cn-shenzhen.aliyuncs.com/public/edeity_o.png"><link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"><link rel="mainfest" href="/mainfest.json"><link rel="stylesheet" href="/public/css/common.css"><link rel="stylesheet" href="//at.alicdn.com/t/font_707055_4b9og9sc5lx.css"><script>!function(){var e=-1!==window.location.search.indexOf("theme=night")||"night"===window.localStorage.getItem("edeity-theme_theme"),t=-1!==window.location.search.indexOf("theme=light")||"light"===window.localStorage.getItem("edeity-theme_theme");(new Date).getHours();var n=document.querySelector("html");e?n.classList.add("night"):t?n.classList.remove("night"):n.classList.add("night")}(),document.addEventListener("DOMContentLoaded",function(){null!==document.querySelector("ol.toc")&&(document.querySelector("#nav-bar").style.cssText="display: block")})</script><script async src="https://www.googletagmanager.com/gtag/js?id=G-M3J9QSEE2Z"></script><script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-M3J9QSEE2Z")</script><meta name="generator" content="Hexo 5.0.0"></head><body><div class="loading"></div><div id="switch" data-switch="{"toc":true,"use_pwa":false}"></div><header class="fullscreen"><div class="toolbar"><i class="iconfont icon-menu"></i></div><h1><a href="/">Ede's Blog</a></h1><div class="head-link"><a class="btn waves" href="/"><span><i class="iconfont icon-home">Home </i></span></a><a class="btn waves" href="/about/index.html"><span><i class="iconfont icon-me">About </i></span></a><a class="btn waves" target="_blank" rel="noopener" href="https://github.com/edeink"><span><i class="iconfont icon-github">Github</i></span></a></div></header><div class="some-link"><a class="btn" id="light-or-not"><i class="iconfont icon-light"></i> </a><a style="display:none" class="btn" id="up-to-top"><i class="iconfont icon-up"></i></a></div><div id="nav-bar" style="display:none"><div class="toc"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%8E%9F%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F"><span class="toc-number">1.</span> <span class="toc-text">原实现方式</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%AE%A1%E5%88%92%E9%80%9A"><span class="toc-number">2.</span> <span class="toc-text">计划通</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#Plan-1"><span class="toc-number">2.1.</span> <span class="toc-text">Plan 1</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Plan-2"><span class="toc-number">2.2.</span> <span class="toc-text">Plan 2</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Plan-2-1"><span class="toc-number">2.3.</span> <span class="toc-text">Plan 2.1</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Plan-3"><span class="toc-number">2.4.</span> <span class="toc-text">Plan 3</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E6%9C%AC%E8%AE%BE%E8%AE%A1"><span class="toc-number">3.</span> <span class="toc-text">基本设计</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%90%84%E7%A7%8D%E4%BC%98%E5%8C%96"><span class="toc-number">4.</span> <span class="toc-text">各种优化</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%98%E5%8C%961%EF%BC%9A%E9%87%87%E7%94%A8%E5%8E%9F%E7%94%9F%E5%86%99%E6%B3%95"><span class="toc-number">4.1.</span> <span class="toc-text">优化1:采用原生写法</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%98%E5%8C%962%EF%BC%9A%E5%BC%80%E5%90%AF%E7%BC%93%E5%AD%98"><span class="toc-number">4.2.</span> <span class="toc-text">优化2:开启缓存</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%98%E5%8C%963%EF%BC%9A%E5%B0%8F%E7%A2%8E%E6%AD%A5-%E5%A4%A7%E8%B7%A8%E6%AD%A5"><span class="toc-number">4.3.</span> <span class="toc-text">优化3:小碎步 + 大跨步</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%98%E5%8C%964%EF%BC%9A%E5%86%85%E5%AD%98%E5%9B%9E%E6%94%B6"><span class="toc-number">4.4.</span> <span class="toc-text">优化4:内存回收</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%98%E5%8C%965%EF%BC%9A%E7%BB%88%E6%9E%81%E5%A4%A7%E6%8B%9B%EF%BC%8C%E5%A4%A7%E9%81%93%E8%8B%A5%E7%AE%80"><span class="toc-number">4.5.</span> <span class="toc-text">优化5:终极大招,大道若简</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-number">5.</span> <span class="toc-text">总结</span></a></li></ol></div></div><main id="content-main" class="section"><div class="list-item"><h1 class="post-title"><a id="滑动优化填坑记" class="article-link" href="">滑动优化填坑记</a></h1><div class="post-meta"><time class="meta published">Sep 2, 2020</time></div><div class="article"><div class="post-excerpt markdown-body"><p>为了迎接新学期,<a href="www.kdocs.cn">金山文档</a>换上了新皮肤。但在滑动到顶部时,<strong>顶部工具栏</strong>总会唰地跳出来,如同梦寐女神脱袜漏腿毛,带有某种不可描述的视觉冲击。效果如下:</p><img class="gif" width="394" height="392" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/slide_before.gif"><p>为此,产品强烈要求优化滑动,让滑动能如丝般光滑的体验,效果如下:</p><img class="gif" width="394" height="392" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/slide_after.gif"><p>那么如何实现呢?</p><h2 id="原实现方式"><a href="#原实现方式" class="headerlink" title="原实现方式"></a>原实现方式</h2><p>假设我们的文档结构如下:</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">body</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">header</span>></span>顶部工具栏<span class="tag"></<span class="name">header</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">main</span>></span>中间内容<span class="tag"></<span class="name">main</span>></span></span><br><span class="line"><span class="tag"></<span class="name">body</span>></span></span><br></pre></td></tr></table></figure><p>原效果实现非常简单:</p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"> <span class="comment">// 后续代码 $main $body即代表对应的DOM节点</span></span><br><span class="line"><span class="keyword">const</span> $header = <span class="built_in">document</span>.querySelector(<span class="string">'header'</span>);</span><br><span class="line"><span class="keyword">const</span> $main = <span class="built_in">document</span>.querySelector(<span class="string">'main'</span>);</span><br><span class="line"></span><br><span class="line">$main.addEventListener(<span class="string">'scroll'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> { scrollTop } = $main;</span><br><span class="line"> <span class="keyword">if</span> (scrollTop > <span class="number">20</span>) {</span><br><span class="line"> $header.style.setProperty(<span class="string">'top'</span>, <span class="string">'50px'</span>);</span><br><span class="line"> $main.style.setProperty(<span class="string">'top'</span>, <span class="string">'0px'</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (scrollTop <= <span class="number">0</span>) {</span><br><span class="line"> $header.style.setProperty(<span class="string">'top'</span>, <span class="string">'0px'</span>);</span><br><span class="line"> $main.style.setProperty(<span class="string">'top'</span>, <span class="string">'50px'</span>);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h2 id="计划通"><a href="#计划通" class="headerlink" title="计划通"></a>计划通</h2><h3 id="Plan-1"><a href="#Plan-1" class="headerlink" title="Plan 1"></a>Plan 1</h3><p>通过缓动减少突兀感:<code>transition: top .2s;</code></p><p>这种方法的优点是:简单,代码都不用写!</p><p>缺点是:无论何种缓动动效,只能减少而无法消除变化的突兀感。其必然存在<code>50</code>-><code>0</code>的“突然”变化。</p><img class="gif" width="400" height="423" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/scroll_css.gif"><h3 id="Plan-2"><a href="#Plan-2" class="headerlink" title="Plan 2"></a>Plan 2</h3><p>通过监听<code>touchMove</code>的偏移量,同步更改<code>$main</code>和<code>$header</code>的top。比如,手指向上move<code>1px</code>,同时更改<code>$main</code>、<code>$header</code>为<code>49px</code>。<small>(众所周知,更改top会触发<code>重排</code>,对此可将<code>top</code>替换成<code>transformY</code>进行优化,但非关键,不在此展开。)</small></p><figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> startY = <span class="number">0</span>;</span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'touchstart'</span>, <span class="function"><span class="keyword">function</span> (<span class="params">event</span>) </span>{</span><br><span class="line"> startY = event.touches[<span class="number">0</span>].clientY;</span><br><span class="line">});</span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'touchmove'</span>, <span class="function"><span class="keyword">function</span> (<span class="params">event</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> offsetY = startY - event.touches[<span class="number">0</span>].clientY;</span><br><span class="line"> offsetY = <span class="built_in">Math</span>.min(<span class="built_in">Math</span>.round(offsetY * <span class="number">10</span>) / <span class="number">10</span>, <span class="number">50</span>);</span><br><span class="line"> <span class="keyword">if</span> (offsetY > <span class="number">0</span>) {</span><br><span class="line"> $header.style.setProperty(<span class="string">'top'</span>, <span class="number">0</span> - offsetY + <span class="string">'px'</span>);</span><br><span class="line"> $main.style.setProperty(<span class="string">'top'</span>, <span class="number">50</span> - offsetY + <span class="string">'px'</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> $header.style.setProperty(<span class="string">'top'</span>, <span class="string">'0px'</span>);</span><br><span class="line"> $main.style.setProperty(<span class="string">'top'</span>, <span class="string">'50px'</span>);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>这种方法的优点:DOM的偏移量和手指的偏移量同步。</p><p>缺点:大家别忘了<code>$main</code>本身是可以滚动的。所以会出现,<code>touchMove</code>时,<code>$main</code>同时向上滚动了若干像素,而导致内容被顶部栏覆盖的情况(覆盖的高度刚好是向上滚动的高度)。</p><p><small>或许可以考察下<code>overscroll-behavior</code>,但此css属性在safari下未被支持</small></p><img class="gif" width="400" height="423" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/scroll_touchmove.gif"><h3 id="Plan-2-1"><a href="#Plan-2-1" class="headerlink" title="Plan 2.1"></a>Plan 2.1</h3><p>虽然<code>$main</code>是可以滚动的,但工具栏只有在最开始的时候,才需要触发同步收起效果,所以能否在开始时,将<code>$main</code>设置为<code>overflow: hidden</code>,当<code>$header</code>收起时,再将<code>$main</code>设置为<code>overflow: auto</code>。</p><p>理想很丰满,现实却很骨感。这个实现方法的问题在于,滑动过程中更改<code>overflow</code>属性,并不能立即生效,即使touchMove的偏移量已经大于<code>50px</code>,<code>$main</code>也被设置成<code>overflow: auto</code>,此时<code>$main</code>仍然是不可滑动的(需要touchend后才生效)。导致上滑时,需要两次滑动(一次用于伸缩顶部工具栏,一次用于滑动内容)。</p><p>假如两次滑动还能接受,那么更糟糕的是其在ios下的表现。因为<code>弹簧效果</code>的存在,ios会出现短暂的不可滑动,或出现抖动的情况。</p><img class="gif" width="400" height="423" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/scroll_touchmove_overflow.gif"><h3 id="Plan-3"><a href="#Plan-3" class="headerlink" title="Plan 3"></a>Plan 3</h3><p>有没有可能保持<code>$main</code>的位置,而让超出<code>$main</code>的内容可见?即<code>$main</code>的top一直都是<code>50px</code>,而向上滑动时,内容在<code>0-50px</code>部分的内容仍然可见?</p><p>我们需要一种类似于<code>overflow:auto-but-visible</code>,但很遗憾,单纯通过CSS无法实现这种效果。</p><img class="gif" width="400" height="423" data-src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/scroll_overflow.gif"><h2 id="基本设计"><a href="#基本设计" class="headerlink" title="基本设计"></a>基本设计</h2><p>柯南·道尔曾经说过:当排除一切不可能,剩下的,不管多难以置信,那都是事实。所以,只剩下一种方案可供选择:<strong>模拟滚动</strong>!</p><p>布局很简单,但和常见的模拟滚动稍有不同:</p><p>除了基本的<code>top</code>/<code>bottom</code>外,多一个<code>axisTop</code>/<code>axisBottom</code>,这是为了让模拟的Y轴滚动条距离滚动区域能有一定的偏移,即<strong>滚动条区域</strong> = <strong>可视区域</strong>- <code>axisTop</code>/<code>axisBottom</code>,以实现<code>overflow: visible-hidden</code>;</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/layout.png" alt="layout"></p><p>DOM大致如下:</p><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"> <span class="comment"><!-- 滑动区 --></span></span><br><span class="line"><span class="tag"><<span class="name">OuterWrapper</span>></span></span><br><span class="line"> <span class="comment"><!-- 可视区 --></span></span><br><span class="line"> <span class="tag"><<span class="name">InnerWrapper</span>></span></span><br><span class="line"> <span class="comment"><!-- 正文内容 --></span></span><br><span class="line"> <span class="tag"><<span class="name">Context</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">InnerWrapper</span>></span></span><br><span class="line"><span class="tag"></<span class="name">OuterWrapper</span>></span></span><br></pre></td></tr></table></figure><p>底部弹出工具栏时,仅需要改变<code>outerWrapper</code>的尺寸,减少重排的损耗。</p><p>至于模拟滚动,则监听<code>touch/wheel</code>事件,同步更改CSS3属性<code>transform: translate(x, y, z)</code>。</p><p>看起来并不复杂嘛!恩,看起来…</p><h2 id="各种优化"><a href="#各种优化" class="headerlink" title="各种优化"></a>各种优化</h2><p>当我信心满满地花了一周写完组件以及处理各种偏移量后,提交测试。未闲一天,测试即反馈:太卡顿了。(从技术角度,即无法再16ms~32内执行一帧更新)</p><p>老衲擦指一算,我去,<code>排版</code>太卡了。</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/typo.png" alt="typo"></p><p>原C++代码中,排版是单独的线程,但迁移到JS上时,因为JS是单线程生物,导致<code>排版</code>像霸道总裁一样卡在那里,以及因为“某些原因”,排版也不能执行类似<code>requestAnimate</code>时间分片。导致更改<code>transfrom</code>触发排版后,整个页面进入了假死的状态。可怜我的模拟滚动,在漫长的执行周期中,连几ms执行权力都没有。</p><h3 id="优化1:采用原生写法"><a href="#优化1:采用原生写法" class="headerlink" title="优化1:采用原生写法"></a>优化1:采用原生写法</h3><p>虽然<code>React</code>的虚拟dom能减少我们操作原生dom的频率,但本身执行的流程还是有一定损耗的,在变化频繁,性能敏感的场景中,显得比较致命。所以需要将绝大部分的事件以及状态变更,均采用原生的写法。</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/set_state.png" alt="setState"></p><h3 id="优化2:开启缓存"><a href="#优化2:开启缓存" class="headerlink" title="优化2:开启缓存"></a>优化2:开启缓存</h3><p>模拟滚动能带天然的优化:内部的状态必须由程序自己托管,从而避免了排版高凭读取正文DOM属性的消耗,优化首屏打开速度或其他各种操作。通过暴露唯一的更改尺寸的接口,配合<code>Observe</code>(ResizeOberver & MutationObserver),可以实现这种效果。</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/cache_attr.png" alt="cache"></p><h3 id="优化3:小碎步-大跨步"><a href="#优化3:小碎步-大跨步" class="headerlink" title="优化3:小碎步 + 大跨步"></a>优化3:小碎步 + 大跨步</h3><p>小碎步:以“段”的加载方式替换以前“屏”的加载方式,减少单次排版耗时</p><p>大跨步:实现,实际的DOM高度 !== 滚动区域,能预置高度,避免每次排版更新导致的重排消耗</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/size.png" alt="size"></p><p>小碎步 + 大跨步是一套组合拳,小碎步通过减少单次排版区域,避免单次卡顿时间过长。大跨步,则是为了避免因为过频繁的小碎步导致过频触发重排消耗。</p><h3 id="优化4:内存回收"><a href="#优化4:内存回收" class="headerlink" title="优化4:内存回收"></a>优化4:内存回收</h3><p>浏览器原生不实现<code>overflow: visible-hidden</code>,我认为有一个重要的原因,就是内容过多时,不方便判断什么是可视区外的元素,从而导致渲染内容<code>paint</code>过多。</p><p>通过简单的标记法,标记可视区外的元素,在空闲时进行内存回收,可以减少单次需要渲染的元素,减少滑动时性能消耗。美中不足的是,回收后,DOM变成了<code>Fragment</code>片段从文档流中移除,后续滑动到对应区域,需要重新添加到文档流中,这也是一种消耗,所以需要权衡,不能过频回收。</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/gc.png" alt="gc"></p><h3 id="优化5:终极大招,大道若简"><a href="#优化5:终极大招,大道若简" class="headerlink" title="优化5:终极大招,大道若简"></a>优化5:终极大招,大道若简</h3><p>以上几种手段,能通过优化代码执行效率,减少卡顿的情况。但真正解决卡顿情景,还得借助“多线程”。那么Javascript能否在某种程度上的异步呢。<code>WebWorker</code>虽然能达到这种效果,但因为限制太死(比如不能读取DOM,和主线程只能过postMessage的方式进行数据交换,途中还要序列化和反序列化),暂不在考察范围。</p><p>其实很简单,基础的CSS就可以做到!在一般的CSS渲染中,需要进行<code>JavaScript -> Style -> Layout -> Paint -> Composite</code>操作,即这张被用到烂的图:</p><p><img src="https://edeity.oss-cn-shenzhen.aliyuncs.com/2020/flow.png" alt="flow"></p><p>这是在主线程进行的。但当开启3D加速后,部分渲染会提升到GPU中,在<code>Layer</code>线程中渲染。这就是某种意义的“多线程”。针对无交互场景(一般为离手后的惯性滑动),可通过计算最终的滚动位置,配合缓动函数<code>cubic-bezier</code>,通过<code>transition-timing-function</code>,可以向浏览器提交滑动到某段距离的动效,然后腾出时间给排版。一般的惯性滑动时间为2000~2500ms,所以在无用户操作的前提下,哪怕排版不要脸暂用两秒的执行时间,用户也没有明显感觉卡顿。</p><p>当然,这也是有弊端的,就是两个线程并不能及时通讯,导致在滑动过程中,需要不断<code>getComputeStyle</code>来修正DOM属性。由于存在<code>Style -> Layout -> Paint</code>这样的流程,导致触发<code>touchstart</code>停止后获取滑动位置(执行Javascript),和最终的位置(执行Paint)存在几毫秒时间差,导致两次时机得到位置不相等造成闪烁(低性能机器比较明显)。当然,采用动效后,效果提升体显著,对比小概率的闪烁还是好处大大滴。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>其实在开发的过程中还发现一些别的问题,比如之前:</p><ol><li>自己手写的<code>debounce</code>存在性能问题</li><li>部分代码混淆不够</li><li>babel编译析构操作符(…),会对Array执行<code>slice</code> + <code>concat</code>操作造成性能损耗</li><li>….</li></ol><p>以及吐槽下产品的产品,在“滑动流畅”没有任何描述,各种滚动因子,长短距离滑动,长按短触,多指操作,惯性滑动,都是全靠开发想象,和测试PK,一点点磨出来的。<small>(自己经验不足也是其中一部分,小声)</small></p><p>当然,优化后滑动文档,在加载文档的场景下,比原生滑动还要流畅,作为开发还是很自豪的。</p><p>总结一句话,就是,那些看起来很简单的东西,可能隐藏着各种大坑,还需要继续努力啊!冲啊!</p></div></div></div><div class="more section"><div class="pre"><a class="article-link" href="/2020_sim_and_normal.html"><i class="iconfont icon-right"></i> <span>2020 普通人,平常事</span></a></div><div class="next"><a class="article-link" href="/detail_of_throttle.html">throttle细节小记 <i class="iconfont icon-right"></i></a></div></div></main></body><footer class="section fullscreen"><div class="footer-desc">Edeink © 2015-2022 · Powered by Hexo</div></footer><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script><script src="/public/js/init.js"></script></html>