使用createSelectorQuery、动画和定时器实现循环滚动效果

createSelectorQuery

可以获取到元素在页面上的位置。返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用 this.createSelectorQuery() 来代替。

1
2
3
4
5
6
7
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})

res返回数据:

exec返回参数

为什么要用 setTimeout 模拟 setInterval

推入任务队列后的时间不准确

setInterval 是一个宏任务。setInterval(fn(), N);的意思是fn()将会在 N 秒之后被推入任务队列

所以,在 setInterval 被推入任务队列时,如果在它前面有很多任务或者某个任务等待时间较长比如网络请求等,那么这个定时器的执行时间和我们预定它执行的时间可能并不一致。

比如:

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
let startTime = new Date().getTime();
let count = 0;
//耗时任务
setInterval(function() {
let i = 0;
while (i++ < 1000000000);
}, 0);
setInterval(function() {
count++;
console.log(
"与原设定的间隔时差了:",
new Date().getTime() - (startTime + count * 1000),
"毫秒"
);
}, 1000);
// 输出:
// 与原设定的间隔时差了: 699 毫秒
// 与原设定的间隔时差了: 771 毫秒
// 与原设定的间隔时差了: 887 毫秒
// 与原设定的间隔时差了: 981 毫秒
// 与原设定的间隔时差了: 1142 毫秒
// 与原设定的间隔时差了: 1822 毫秒
// 与原设定的间隔时差了: 1891 毫秒
// 与原设定的间隔时差了: 2001 毫秒
// 与原设定的间隔时差了: 2748 毫秒
// ...

函数操作耗时过长导致的不准确

考虑极端情况,假如定时器里面的代码需要进行大量的计算(耗费时间较长),或者是 DOM 操作。这样一来,花的时间就比较长,有可能前一次代码还没有执行完,后一次代码就被添加到队列了。也会到时定时器变得不准确,甚至出现同一时间执行两次的情况。

最常见的出现的就是,当我们需要使用 ajax 轮询服务器是否有新数据时,必定会有一些人会使用 setInterval,然而无论网络状况如何,它都会去一遍又一遍的发送请求,最后的间隔时间可能和原定的时间有很大的出入。

setInterval 缺点 与 setTimeout 的不同

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

setInterval()执行过程

上图可见,setInterval 每隔 100ms 往队列中添加一个事件;100ms 后,添加 T1 定时器代码至队列中,主线程中还有任务在执行,所以等待,some event 执行结束后执行 T1 定时器代码;又过了 100ms,T2 定时器被添加到队列中,主线程还在执行 T1 代码,所以等待;又过了 100ms,理论上又要往队列里推一个定时器代码,但由于此时 T2 还在队列中,所以 T3 不会被添加(T3 被跳过),结果就是此时被跳过;这里我们可以看到,T1 定时器执行结束后马上执行了 T2 代码,所以并没有达到定时器的效果。

综上所述,setInterval 有两个缺点:

  • 使用 setInterval 时,某些间隔会被跳过;
  • 可能多个定时器会连续执行;

而每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务 push 到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。

所以setTimeout解决了这俩个问题

  • 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
  • 保证定时器间隔(解决缺点二)

代码实现

大致思路:

  1. 在页面渲染完成后调用initAnimation()创建动画 获取到当前滚动文字的width和外围包裹的view的width
  2. 执行回调函数startAnimation() 首先对滚动字幕的初始位置进行刷新 这样第二次进入这个方法时会回到最右边 然后才是往左滚动 setData会对页面进行一个局部刷新 所以动画效果能同步到页面
  3. 最后再添加一个定时器 用于再次执行startAnimation() 来实现循环

wxml

1
2
3
4
5
6
<view class="left-box">
<view class="left-text"></view>
<view class='content-box'>
<view class='content-text' animation="{{animationData}}"><text id="text">{{text}}</text></view>
</view>
</view>

js

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Page({

/**
* 页面的初始数据
*/
data: {
text: "不同年龄段的评分标准",
animation: null,
timer: null,
duration: 0,
textWidth: 0,
wrapWidth: 0
},
onShow() {
this.initAnimation(this.data.text)
},
onHide() {
this.destroyTimer()
this.setData({
timer: null
})
},
onUnload() {
this.destroyTimer()
this.setData({
timer: null
})
},
destroyTimer() {
if (this.data.timer) {
clearTimeout(this.data.timer);
}
},

initAnimation(text) {
console.log("执行了 initAnimation")
let that = this
this.data.duration = 5000
this.data.animation = wx.createAnimation({
duration: this.data.duration,
timingFunction: 'linear'
})
let query = wx.createSelectorQuery()
query.select('.content-box').boundingClientRect()
query.select('#text').boundingClientRect()
query.exec((rect) => {
console.log(rect)
that.setData({
wrapWidth: rect[0].width,
textWidth: rect[1].width
}, () => {
this.startAnimation()
})
})
},
// 定时器动画
startAnimation() {
console.log("执行了 startAnimation")

//reset
const resetAnimation = this.data.animation.translateX(this.data.wrapWidth).step({ duration: 0 })
this.setData({
animationData: resetAnimation.export()
})
const animationData = this.data.animation.translateX(-this.data.textWidth).step({ duration: this.data.duration })
setTimeout(() => {
this.setData({
animationData: animationData.export()
})
}, 100)
setTimeout(() => {
this.startAnimation()
}, this.data.duration)
},
})

wcss

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
32
33
34
35
36


.left {
display: flex;
align-items: center;
}


.left-box {
position: relative;
display: flex;
align-items: center;
}

.left-text {
position: absolute;
left: 0;
width: 40rpx;
height: 100%;
background: linear-gradient(to left,rgba(241,241,241,0),rgba(241,241,241,1));
z-index: 99;
}



.content-box {
overflow: hidden;
width: 350rpx;
}

.content-text {
color: #5e5e5e;
white-space: nowrap;
font-size: 28rpx;
}