
@author: zhangbobell
@created: 2017.09.28

线上地址：http://fex.baidu.com/blog/2017/10/build-a-silky-smooth-slide-library

## 背景
随着近几年移动营销页的火爆，催生了一个中国式的名词「H5」。而 H5 最常见的形态就是类似幻灯片翻页效果。

我们需要制作 H5 的时候，最快的办法就是使用一些滑动插件库，如 iDangero.us 出品的 [Swiper](http://idangero.us/swiper)，百度 BE-FE 出品的 [iSlider](http://be-fe.github.io/iSlider/index.html)。通过这些翻页库提供的强大的配置功能，我们就能实现很酷炫的翻页效果。当然，这些库还支持自动播放，点击切换和当前页面指示等配置，所以还能用在网页上，实现一些 web carousel 的效果。

[百度 H5](https://h5.baidu.com) 也先后使用了 Swiper 和 iSlider 作为 H5 运行时的翻页框架，随着用户越来越多，也遇到了一些问题：  
1. H5 平台和这些库不能很好贴合，有些配置项用不上，而有些必要的功能需要「Hack」的方式实现。  
2. 一些 H5 的元素多、动画多，在低端机型上翻页时，翻页时会有「卡顿感」和「粘滞感」，用户体验不好。  

而我们希望的 H5 翻页库能和平台本身的功能完美贴合，在保持体积小的同时，在翻页的时候能做到「丝般顺滑」。于是我们就开始了研(zao)究(lun)之(zi)旅。


## 开始

H5 滑屏框架的开发，第一个问题就是：页面是否跟随手指滑动？这也是腾讯 ISUX 团队的《滑屏 H5 开发实践九问》的第一问（这篇文章原文出处现在是 404 ，大家可以在其他的转载网站看到），这里用这篇文章的图片说明一下这个问题。

<img alt="不跟随手指滑动" width="200" src="http://boscdn.bpc.baidu.com/v1/agroup/21a2882ad991306858076cf1e40bbe06f0ce9bc1"/> <img alt="跟随手指滑动" width="200" src="http://boscdn.bpc.baidu.com/v1/agroup/08e10aad2650eec26f2791f65fa2aa4d3682db16" />

左图：**不跟随手指滑动**，右图：**跟随手指滑动。**

左边的是不跟随手指滑动，只需要关注手指触碰开始和离开两个时间点，中间过程不用考虑。所以实现起来比较简单。但是用户的操作没有实时的反馈，体验不够好。因此，尽管实现起来更复杂，我们仍然决定实现前一种「跟随手指滑动」的效果。

## 起步
下图是跟随手指滑动的 H5 最直观的版本，所有的「页面」依次从上到下，首尾相接。需要说明一下，这里的「页面」打引号，是因为实际上他们都是 `div`，后文说的页面都指这些 `div`。同时，我们这里以最常见的竖直方向滑动为例，水平方向同理。

<img alt="swiper-basic.png" width="250" src="http://boscdn.bpc.baidu.com/v1/agroup/25b445947fe2058364c79f5a2970e55ae354507a" />

基本原理图

这些 div 的宽度和高度都是 100% 的容器高度，可视区域是中间的部分，我们监听 `touchstart`, `touchmove`, `touchend`  事件，跟鼠标拖拽的原理类似：  
1. `touchstart` 时，记下起点位置；  
2. `touchmove` 实时计算滑动的距离，让所有页面一起沿着 Y 轴 `translate` 这段距离。  
3. `touchend` 时，能得到最终的滑动距离，跟设定的阈值比较。进入到页面自动控制阶段：大于阈值则让页面滑动到下一页，小于阈值则恢复到起始位置。  

## 深入探究
简单的版本在上一部分很容易就实现了，如果其他需求不多，页面上元素和动画比较少，基本上就够用了。但是本文要探究的是如何能做到「丝般顺滑」，其实就是两个字：性能。

> 性能的瓶颈是什么呢？

我们的目标是：在「三多一低」（页面多、元素多、动画多，配置低）的情况下，滑动翻页时，尽可能不产生卡顿。

我们分成两部分来看这问题：手指离开屏幕前和手指离开屏幕后。

### 手指离开屏幕前
此时比较耗费性能的操作是：当 `touchmove` 触发时，计算出了要移动的距离，**所有的**页面都需要沿着 Y 轴移动相同的距离。此时必然免不了进行 DOM 操作，而 DOM 操作是非常「昂贵」的，再加上 `touchmove` 事件的频繁触发，性能处理不够好的话，很容易出现卡顿。

为了优化性能，我们很自然的想到一个策略：**减少 DOM 操作**。

这里面包含两部分：减少 DOM 操作的元素和减少 DOM 操作的属性。前者比如，看不到的页面不参与动画。后者比如，只改变元素的 css 属性的一个或几个。

#### 减少 DOM 操作的元素
最开始简易的版本的例子中，`touchmove` 触发时，**所有的**页面都沿着 Y 轴移动。其实没有必要，因为相当一部分页面是看不见的。那一般情况下，我们最少需要操作几个页面呢？答案是两个。可以回想一下，我们滑动的时候，最多能同时看到两个页面。这个方法相对于所有的页面一起移动，成倍地提升性能。

#### 减少 DOM 操作的属性
这个方法的主要意思是，只需要操作一次 DOM 能达到的效果，绝不用两次。实际上，对于 slide 动画，我们只需要改变页面的 `transform` 的值，其他的 DOM 操作（增加 class，修改元素的 innerHTML）等能不做就不做。

我们得到了一个初步的方案：初始化时，所有的页面一次性全部置入 container，除了我们用到的两页，`display` 属性都设置为 `none`。`touchmove` 的时候，只有这两页的 `transform` 属性有变化。

`touchmove` 的过程，我们可以写成数学表达式：

$$
s=f(x), x \in [0, sideLength]
$$

$$x$$ 表示手指滑动的距离，$$s$$ 表示页面滑动距离，$$sideLength$$ 是当前滑动边的长度，如果是沿 y 轴滑动，则是页面高度。写到这里，就跟时下很流行的「数据驱动」的概念很类似了。我们要实现的就只有一个 render 函数，输入是用户的交互数据，输出是页面表现。

### 手指离开屏幕后
当手指离开屏幕时，我们就已经知道了这次滑动的结果（向上还是向下？翻页还是回弹？），要实现的只是动画效果，我们有两个选择：  
方案一：复用 `touchmove` 的 render 逻辑，按照手指滑动的速度，使用 `requestAnimationFrame` 控制动画；  
方案二：使用 css3 transition 动画；  

方案一的优点在于：可以在手指滑动和动画过程使用同样的 render 函数，最大限度复用了代码，逻辑统一；同时可以精确控制动画的每一帧，动画曲线会比较流畅。
缺点就是可能存在的性能问题。方案二跟方案一刚好相反。其实说到底还是 js 动画 vs css 动画的问题。

#### 动画性能实验
为了比较两个方案在 H5 翻页动画上的性能优劣，我们取一个稍微复杂点的例子：

H5：百度无人车招聘的 H5  
动画：从第 1 页翻到第 2 页  
CPU： 6 * slowdown  
浏览器：Chrome 61.0.3163.100（64 位）  

js 动画方案：[点击这里](http://jing.baidu.com/h5/car-js-animation.html)  
css 动画方案：[点击这里](http://jing.baidu.com/h5/car-css-animation.html)  


![js 动画](http://boscdn.bpc.baidu.com/v1/agroup/90c48a375f69eea7ad6ae29d9c11ed2e13bedc42)

js 翻页动画方案，Profile 结果


![css 动画](http://boscdn.bpc.baidu.com/v1/agroup/78a683df5864b800ce145379e3e7a593410a51df)

css 翻页动画方案，Profile 结果

通过实验我们可以看到，js 的动画过程中，帧率大多维持在 30 fps 上下。而 css 动画，基本都在 60 fps 上下。而且在动画过程中，明显感觉 js 动画有卡顿。这种情况在一些 CPU 和显卡配置相对低的 Android 机型上尤为明显。对于这个问题有兴趣的同学，可以看一下 swiper 库的 raf 分支，这是本次对比测试所用到的 js 。

所以，尽管 js 的动画方案看起来比较「优雅」，能用「数据驱动」的理念，统一解决滑动过程和动画过程的问题。实际上性能有瓶颈，我们只能在手指离开屏幕后，采用 css 的动画方案以保证性能。正应了一句话「能用 css 做的，绝对不要用 js 解决」。


## 实施方案
下图形象地展示了我们实施的基本思路，只有两页：  
`currentPage` ：当前页面  
`activePage`：即将要翻到的下一页  

其余的页面都是初始化的时候加载进 DOM 结构，但是 `display` 为 `none` 并且 `z-index` 都是 `0`。这里展示「层叠」的状态是为了更形象的展示。

<img alt="swiper-basic.png" width="250" src="http://boscdn.bpc.baidu.com/v1/agroup/cb0f53440fdb49dbd76cbc1bc653234b8bdfd7b5" />

swiper 原理图

为了方便获取页面，我们采用双向链表保存页面结构。每个 `page` 具有 `prev` 和 `next` 分别指向上一个和下一个 `page`。  
我们重点要关注的是，怎么样确定 `activePage` ？即下一个要去到的页面。答案很简单，其实，当用户开始触碰屏幕，并且滑动的时候，就能确定了：  
1. 滑动距离 x < 0，表示页面向上滑动，此时 `activepage = currentPage.next`  
2. 滑动距离 x > 0，表示页面向下滑动，此时 `activepage = currentPage.prev`  

## 扩展

### 翻页效果
我们举的例子中的翻页效果是最普通的滑动效果。怎么样扩展支持立方体、翻转等效果呢？可以回头看看「手指离开屏幕前」部分，我们提出了 $$s=f(x)$$，$$x$$ 是用户滑动距离，$$s$$ 是页面滑动距离。我们把 $$s$$ 扩展一下，变成「页面翻转角度」或「页面缩放比率」，就可以支持其他的效果了。

事实上，我们在滑动的时候，本身就是使用 css3 的 `transform` 属性，将其中的 `translate`, `rotate`, `scale` 适当的组合就能做出千变万化的翻页效果了。

### 更令人愉悦的动画
这里指的是 animation-timing-function，拿最简单的滑动效果举例。如果是线性的函数，用户滑动的速度始终等于页面滑动速度。而「感觉上」更流畅、更灵敏的应该是：刚开始页面滑动速度大于用户滑动速度，随着翻页的进行，两者趋于相同，过了某个点后，单位时间内，页面滑动速度开始逐渐小于用户滑动速度，将速度表示为距离，就可以得到 $$x$$ 和 $$s$$ 之间的关系如下图：  

<img alt="swiper-basic.png" width="300" src="http://boscdn.bpc.baidu.com/v1/agroup/dcdeec11c407ebf5b4f0a3256d7f0bd2a8bdcfe2" />  
$$x$$ 和 $$s$$ 的关系图（横轴为 $$x$$，纵轴为 $$s$$）

在这里，不得不再提起两种动画方案： js 动画和 css 动画。

js 动画方案的一个优点是，可以精确控制动画的进程，而 css 无法做到。比如用户在 x = 0.8 的时候手指离开屏幕，因为采用的同一个 render，js 可以知道手指离开屏幕的瞬间 x 处于 0.8 的位置，接下来的动画由  requestAnimationFrame 完成，整个过程流畅且完整。

而 css 动画则不同，css 动画只有在动画开始之前设定 `animation-timing-function`，当用户在 x = 0.8 手指离开屏幕时，原本的 js 控制滑动过程中断，由 css 来完成剩余的动画，css 无法根据手指离开屏幕的瞬间动态计算 `animation-timing-function` ，所以在衔接的那个点，两者速度不匹配，会影响整体动画效果。

但遗憾的是，js 的动画方案有性能问题，我们在用户手指离开屏幕后的那一部分只能采取 css 动画方案。这个「更令人愉悦的动画」也只能用在手指滑动期间。

## 总结和展望
本文讲述了一个「丝般顺滑」的 H5 翻页库的开发过程中遇到的一些问题和对应的解决方法。基本的滑动翻页模型建立之后，重点关注了性能的问题，分为**手指离开屏幕前**和**手指离开屏幕后**两个阶段。前一阶段主要聚焦于减少 DOM 操作。后一阶段聚焦于动画的性能，并且对比了 js 动画和 css 动画的性能数据，最后得出了在手指离开屏幕后使用 css 动画的结论。此外，我们还基于「数据驱动」的思想，在**翻页效果**和**动画函数**两部分进行了扩展，增强了翻页库的功能，也丰富了 H5 的展现效果。

本文中尝试用「数据驱动」的思想去解释整个过程，但是因为性能问题只能暂时放弃，希望在未来能找到更好的方案。由于水平所限，文中难免会出现纰漏，欢迎大家批评指正，共同学习进步。感谢 [Swiper](http://idangero.us/swiper) 和 [islider](http://be-fe.github.io/iSlider/) 翻页库的启发，特别感谢和 [@Ronny](https://github.com/ronnyKJ) 的热烈讨论。

本文所述的 swiper 库地址：[https://github.com/fex-team/swiper](https://github.com/fex-team/swiper)。master 分支所用的代码是目前百度 H5 线上使用的。raf 分支是文中提到的使用 js 动画方案。