Fulvaz PlayGroud

轮播图踩坑指南 (工作日志)

好吧, 我终于变回了一个普通码农, 谢天谢地.

repo: https://github.com/fulvaz/Carousel-Exercises

具体效果看淘宝首页的轮播图

总算是写完了一个轮播图, 回想上次面试被人鄙视轮播图都写不好, 我明白了是为什么 —- 真的轮播图都写不好, 基础太差了.

因为一个轮播图就能涉及到许多问题了, 下面一个个来说明

设计, 降低耦合

如果不加任何设计直接写, 结果就是全局变量满天飞, 不同的view之间糅杂到一块, 要想办法将他们分开.

ps: 虽说是设计, 但是我依旧觉得还是可以继续改进.

到我这其实只是做了个监听者模式, 将页面逻辑和业务逻辑分离, 顺便说一下这里的页面逻辑是指与页面操作相关的逻辑, 比如说div位置, 动画, 形变等等; 业务逻辑是指, 用户点击了什么, 键盘做了什么操作

那接下来实现就很简单, 先实现一个监听者, 代码见.

https://github.com/fulvaz/Carousel-Exercises/blob/master/scripts/observer.js

来来回回就那么几个, 订阅, 取消订阅, 发射事件.

然后新建imgView, buttonView, 并且继承这个监听者, view只要监听事件, 然后处理用户点击时, emmit这个事件就可以了.

相比而言, 以前需要在处理click事件时还要处理页面逻辑, 不妥.

举个例子, 我刚开始希望只是点击左右导航, 然后图片相应切换, 没问题, 很容易.

那么我加这么个需求, 图片下面需要显示一排按钮, 点那个出哪个图片, 并且这排按钮还要通过高亮某个按钮, 指示现在是第几张图片.

这就日了狗了了啊, 刚才只是两个实体的单向联系:navigator -> imgView, 现在变成了三个实体navigator, imgView, button, 而且联系有navigator->imgView, navigator->button, button->imgView

最麻烦的是navigator->button, 你要在原理navigator中修改原来的逻辑, 原来不需要知道第几张, 现在需要了, 要重新处理循环滚动的问题, blablabla

如果要再加新功能…..联系更多的话……

所以还不如让他们只是负责自己的工作, 做完传个消息出去.

话说回来, 这个监听者模式其实是同步监听模式, 即如果在监听某个时间加入耗时操作, 直接页面被艹翻, 嗯, 异步? 说起来, 我深深感觉异步是个陨石坑…和多线程一样…

异步问题

我实现循环轮播, 有动画, 并且最后一张到第一张的动画无缝(实现不好就会出现最后一张绕了一大圈回到第一张的情况)

原理很简单, 比如需要播5张图, 将他们命名为1…5, 实现无缝循环的方法就是在1前面加一张最后一张, 在5后加入第一张, 顺序就会变成[5, 1, 2, 3, 4, 5, 1], 记这个数组为a.

当处于最后一张5(a[5]), 点击下一张, 那么就是到1(a[6]), 动画是5->1, 不这样, 动画效果是5->4->3->2->1

动画解决了, 动画后, 只需要将a[6]无动画切到a[1]就可以了(直接修改left值).

然后就踩到异步的坑里面去了.

我刚开始是利用css的transition属性做的, 那简单对吧

1
2
3
4
5
container.style.left = XX; // 移动到目标位置
container.classList.remove('animiate'); // 关掉动画
if (到了a[6])
contianer.style.left = XX; // 位置改到a[1]
container.classList.add('animate'); // 开动画

问题是, 修改style在浏览器是异步的啊, 而且会打包给你做

就是说, 你以为人家会乖乖给你按顺序做代码写的, 实际浏览器给你做的是

1
2
contianer.style.left = XX; // 位置改到a[1]
container.classList.add('animate'); // 开动画

只有这两.

异步编程保证顺序的方法就是回调, css的动画的回调?

好吧, 我还是乖乖用requestAnimationFrame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
offset = parseInt(offset);
var start = null;
var from = parseInt(window.getComputedStyle(target).left);
function handler(ts) {
if (!start) start = ts;
var process = ts - start;
var distance;
if (offset > 0) {
distance = Math.min(process * speed, offset);
} else {
distance = -Math.min(process * speed, -offset);
}
target.style.left = from + distance + 'px';
if (process * speed < Math.abs(offset)) {
window.animateId = window.requestAnimationFrame(handler.bind(this));
} else if(callback) {
// 动画结束, 运行回调函数
callback();
}
}
window.requestAnimationFrame(handler.bind(this));

关键点是注释那, 其他代码都是常见的用法.

动画只有一半, 图片只动了一半

因为按太快了, 有些糟糕的教程说是内存问题, 不能按太快 (题外话, 慕课网上的前端教程糟糠多于精华, DW, 糟糕的代码风格和瞎扯, 乖乖去MDN看, 或者买书, 学习总是枯燥的)

我本来想加一个处理click事件延时, 比如说100ms才能按一次按钮, 但是因为和下面一个问题, 我最后用了另一个方案:

根本解决方法是给imgView加一个busy属性, 当他在响应某个事件时设置这个标志位, 响应结束再取消

自动播放与点击

自动播放很简单, 用setTimeout就可以了(不推荐setInterval, setInterval会堆积任务 — 或者说, 产生奇怪的结果)

但问题来了, 如果自动播放同时快速点击下一张, 那么图片会变成空白

解决方法已经说了, 就是加busy属性, 但是setTimout后, 浏览器怎么处理任务?

首先, 我推测原因是点太快了, 首先我测试了将自动播放间隔变短, 确实直接空白, 是点太快了, 而且原因应该和循环播放有关, 从结果来看是没有重置到最开始的位置, 一直这么循环了下去

调试证明, 确实有时候没有运行动画后的回调.

推测: 是动画时间长, setTimout太短打断了动画, 所以我加快的动画速度, 确实没有问题了

猜测: 动画时间长, 前一个RAF重置了图片位置, 然后就被下一个RAF给改了, 嗯, 这个解释似乎最合情合理.

根据这个原因, 修改了一下代码, 不加busy判断也可以了.

原来的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ImgView.prototype.moveHorizontal = function(target, offset, speed, callback) {
offset = parseInt(offset);
var start = null;
var from = parseInt(window.getComputedStyle(target).left);
function handler(ts) {
if (!start) start = ts;
var process = ts - start;
var distance;
if (offset > 0) {
distance = Math.min(process * speed, offset); // 已经移动的位移
} else {
distance = -Math.min(process * speed, -offset);
}
target.style.left = from + distance + 'px';
lastDistance = distance;
if (process * speed < Math.abs(offset)) {
window.animateId = window.requestAnimationFrame(handler.bind(this));
} else if(callback) {
// 动画结束, 运行回调函数
callback();
}
}
window.requestAnimationFrame(handler.bind(this));
};

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ImgView.prototype.moveHorizontal = function(target, offset, speed, callback) {
offset = parseInt(offset);
var start = null;
var lastDistance = 0;
function handler(ts) {
if (!start) start = ts;
var process = ts - start;
var currentPos = parseInt(window.getComputedStyle(target).left);
var distance;
if (offset > 0) {
distance = Math.min(process * speed, offset); // 已经移动的位移
} else {
distance = -Math.min(process * speed, -offset);
}
target.style.left = currentPos + distance - lastDistance + 'px';
lastDistance = distance;
if (process * speed < Math.abs(offset)) {
window.animateId = window.requestAnimationFrame(handler.bind(this));
} else if(callback) {
// 动画结束, 运行回调函数
target.style.left = from + offset + 'px';
callback();
}
}
window.requestAnimationFrame(handler.bind(this));
};

主要不同是, 现在每帧动画只要在现在实时位置上添加上一段距离即可

之前是先记录调用动画时的距离, 每帧根据那个初始距离算出新的图片位置.

另外callback之前还设置了一下图片的位置, 因为这么改后, 图片位置老是对不上, RAF的过程是无法debug的, 但是debug时位置是对的, 我觉得很有可能是精度被忽略的原因? 还需要研究

moveHorizontal分析 (上述解决冲突方案无效)

moveHorizontal函数根据图片当前位置和接受到的offset, 然后移动图片.

当有多个RAF同时调用moveHorizontal时…..之前之所以没出大bug是因为offset都是一样的, 如果offset不同, 比如直接点击按钮, 刚好自动播放也要用, bug就来了

好吧, 上面说的解决方案其实没用.

需要保证同时只进行一个RAF, 就是说, 给imgView加锁 — busy属性