在小程序开发中,优美流畅的动画可以显著提升用户体验,尤其是互动式动画。通过联动页面元素的变化,可以实现一种动态的、反馈式的视觉效果。为了满足这些需求,小程序提供了多种实现动画的方式。下面我们将逐一探讨,并重点讲解如何实现高性能倒计时功能。
四种实现动画的方式
-
CSS Animation(使用
Animation对象实现)
这是小程序最早支持的动画方式。它通过Animation对象创建 CSS 动画,但性能效率较低,在高频次渲染需求的场景下表现欠佳。 -
关键帧动画(使用
animate方法实现)
关键帧动画是通过页面或组件对象的animate方法实现的。这种方式性能相对较好,且接口简单易用,目前是官方推荐的方式之一。然而,尽管它的性能有所提升,但并非最高效的选择。 -
响应式动画(基于滚动事件驱动)
这种动画通过滚动事件触发,能够根据页面滚动位置实时调整动画进度。它的用户体验非常好,且实现成本低,适合创建响应式的页面交互效果。 -
高性能动画(通过 WXS 脚本实现)
WXS 脚本方式是目前效率最高的动画实现手段,因为它能够在视图层直接操作组件样式,无需经过逻辑层的数据传递。其关键优势在于避免了逻辑层与视图层之间的通信延迟,实现了更流畅的动画效果。
高性能倒计时功能的实现
一个典型的动画性能问题是付款倒计时功能。在订单列表中,每个待付款订单需要显示倒计时。当订单数量较多时,性能问题尤为明显,特别是当逻辑层依赖 setTimeout 或 setInterval 实现计时时,性能开销和时间不准确性问题会显著增加。
问题1:计时不准确性
传统的倒计时实现方式是通过逻辑层的 setTimeout 实现,由于 setData 处理的延迟以及两层通信的延迟,导致计时误差累积。随着倒计时时间的增加,误差会变得越来越大,甚至使倒计时功能失去实际意义。
优化思路:引入 WXS 脚本处理倒计时
为了改善这个问题,可以将倒计时的计算交由 WXS 脚本来处理。WXS 脚本运行在视图层内,能够减少逻辑层与视图层之间的通信延迟,确保计时的精确性。
使用 WXS 实现高性能倒计时
尽管 WXS 没有原生的定时器功能,但我们可以通过组件对象传递的 ownerInstance 访问定时器方法,如 setTimeout 和 setInterval。然而,考虑到性能,requestAnimationFrame 是更为推荐的方式。这个方法由视图层的帧率驱动,能够确保动画与页面的渲染频率保持一致,从而避免传统定时器带来的不流畅问题。相比定时器,它更适合频繁更新界面内容的场景,如倒计时功能。
性能测试与优化结果
通过在 WXS 脚本中使用 requestAnimationFrame 实现倒计时功能,性能测试显示,单次视图更新的时间消耗在 20 毫秒左右,较传统的逻辑层倒计时实现方案有所提升。特别是在高并发场景中,这种优化能够显著减少性能开销,并且计时的精度也得到了有效的保证。
页面切换与后台处理
在页面切换到后台时,可以通过监听页面的 onHide 事件,停止 requestAnimationFrame 的调用,以减少不必要的性能消耗。在用户返回页面时,再次启动倒计时的更新。
逐行解释代码实现
组件的JS代码
Component({
data: {
timeRemaining: -1, // 剩余时间,初始值为-1
countdownInfo: {
expiredTime: 0, // 倒计时结束的时间戳
status: false, // 倒计时状态,是否在进行中
},
},
properties: {
expiredTime: {
type: Number,
value: -1, // 过期时间,默认值为-1
},
extText: {
type: String,
value: '', // 额外显示的文本
},
},
lifetimes: {
attached() {
this.startCountDown(); // 组件挂载时启动倒计时
},
detached() {
this.setData({
countdownInfo: {
expiredTime: 0,
status: false,
isHide: true // 组件卸载时停止倒计时
},
});
},
},
pageLifetimes: {
show() {
this.setData({
"countdownInfo.isHide": false, // 页面显示时继续倒计时
});
},
hide() {
this.setData({
"countdownInfo.isHide": true, // 页面隐藏时暂停倒计时
});
}
},
methods: {
startCountDown() {
if (this.data.countdownInfo.status || this.data.expiredTime === -1) return;
const timeRemaining = this.data.expiredTime - Math.floor(new Date().getTime() / 1000);
if (timeRemaining <= 0) {
this.stopCountDown(); // 剩余时间小于等于0时停止倒计时
return;
}
this.setData({
countdownInfo: {
expiredTime: this.data.expiredTime,
status: true,
},
});
},
stopCountDown() {
this.setData({
timeRemaining: 0, // 停止倒计时,剩余时间设为0
});
this.triggerEvent('refreshOrder'); // 触发刷新订单事件
},
},
});
WXML代码
<view>{{extText}} <text class="time_remaining_text" wx:if="{{timeRemaining!==0 && expiredTime!==-1}}" change:prop="{{countdown.propObserver}}" prop="{{countdownInfo}}">{{countdown.formatCountDownTime(timeRemaining,'second')}}</text></view>
<wxs module="countdown">
var isHide = false;
function startCountdown(page, instance, _expiredTime, _status, _lastTs) {
var expiredTime = _expiredTime;
var status = _status;
var lastTs = _lastTs;
function calculateTime(){
var timeRemaining = expiredTime - Math.floor(Date.now() / 1000);
if (lastTs > timeRemaining) {
lastTs = timeRemaining;
page.callMethod("setData", {
timeRemaining: timeRemaining // 更新剩余时间
});
}
console.log("isHide", isHide);
if (isHide) {
return;
} else if (timeRemaining > 0) {
page.requestAnimationFrame(calculateTime); // 使用requestAnimationFrame实现高性能倒计时
} else {
page.callMethod("stopCountDown"); // 剩余时间为0时停止倒计时
}
}
calculateTime();
}
function propObserver(newVal, oldVal, ownerInstance, instance) {
if (!oldVal) return;
var countdownInfo = newVal;
var expiredTime = countdownInfo.expiredTime || 0;
var status = countdownInfo.status || false;
isHide = countdownInfo.isHide || false;
var lastTs = expiredTime;
if (status) {
startCountdown(ownerInstance, instance, expiredTime, status, lastTs);
}
}
module.exports = {
propObserver: propObserver,
formatCountDownTime: function (seconds, accuracy) {
var second = seconds;
if (!second || second < 0) {
return '';
}
var day = Math.floor(second / (60 * 60 * 24));
second -= day * 60 * 60 * 24;
var hour = Math.floor(second / 3600);
second -= hour * 3600;
var minute = Math.floor(second / 60);
if (minute === 60) {
hour++;
minute = 0;
}
second -= Math.floor(second / 60) * 60;
var ret = '';
if (hour > 0) {
ret += hour + ':';
}
if (minute >= 0) {
ret += minute < 10 ? '0' + minute + ':' : minute + ':';
}
if (accuracy === 'second') {
second = Math.floor(second);
ret += second < 10 ? '0' + second : second;
}
return ret;
}
};
</wxs>
WXSS代码
/* 可以使得字体等宽,倒计时不会出现抖动 */
.time_remaining_text {
display: inline-block;
vertical-align: baseline !important;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
font-size: 28rpx;
text-align: right;
}
代码解析
-
组件的JS代码:定义了倒计时的逻辑,包括启动和停止倒计时的方法。
startCountDown方法会根据传入的expiredTime计算剩余时间,并每秒更新一次。stopCountDown方法会在倒计时结束时触发一个事件通知外部。 -
WXML代码:定义了倒计时的视图结构,并引用了WXS脚本。倒计时的文字会根据
timeRemaining的变化自动更新。 -
WXS脚本:实现了高性能的倒计时逻辑。通过
requestAnimationFrame方法,确保倒计时的更新与视图层的渲染同步。propObserver方法会监听countdownInfo的变化,触发倒计时的启动。 -
WXSS代码:通过CSS设置确保倒计时文字的等宽显示,避免抖动。
总结
通过将倒计时的动画实现交由 WXS 脚本处理,并使用 requestAnimationFrame 来替代传统的定时器方法,能够有效改善动画的流畅性和计时精度。WXS 脚本与视图层的高效协同使其成为高性能场景下的理想选择。最终的优化结果表明,即使在大量订单倒计时的场景下,页面性能也能保持稳定。