这篇文章上次修改于 1509 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
昨日收到 @Dorasees 反馈,他发现我的小窝音乐播放器有个 Bug,进度条在点击播放按钮后未正常进行滚动。而通过点击页面歌曲链接和切歌(上一首、下一首)按钮则没有出现问题。这个 Bug 一出现,我就想着尽快解决修复。也好奇究竟是什么原因导致的。
既然只有点击播放按钮会出现这种问题,那问题根源也肯定和它有关系。首先我找到设置播放按钮的函数,看看它是怎么写的:
elements.toggle.onclick = function () {
elements.player.paused ? elements.player.play() : elements.player.pause();
};
可以看到,这个按钮主要的功能仅仅是切换播放器的播放状态而已。那么操作正常的方式(切歌)又是怎么写的呢?
elements.prev.onclick = actions.prev;
elements.next.onclick = actions.next;
它调用了一个名为 actions.next()
的函数,那么看看这个函数究竟干了什么?
next: function () {
status.playing < that.list.length - 1 ? status.playing++ : status.playing = 0;
that.play();
}
可以看到,切歌仅仅只是修改了定位到播放列表数组中的索引值,之后执行 that.play()
方法。这个方法用于向服务器发起 AJAX 请求,装载歌曲信息。很明显这个就是异步执行,ks.ajax()
没有使用 Promise,采用的依旧是回调函数。播放器会在装载完成后自动开始播放的操作。
这些逻辑看起来都没有问题呀,那我们再看看出事的进度条,它是怎么开始进行滚动的?我们都知道,Audio 对象有很多事件,给播放器设置 play 事件会让播放器开始播放音乐的时候,执行我们想要完成的操作。
我的播放器在 play
事件里调用 sel_int()
方法,使得播放器开始每隔 500 毫秒更新一次进度条的位置。
elements.player.addEventListener("play", function () {
if(!elements.player.src){
that.play();
}
that.sel_int();
if(status.lyric) that.sel_lyric();
elements.cover.wrap.classList.add("active");
elements.toggle.classList.add("pause");
});
this.sel_int = function () {
if(!intv){
intv = setInterval(function () {
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
}, 500);
}
};
可以看到,在 sel_int
这个函数里面主要有两个变量,分别是 elements.player.currentTime
和 elements.player.duration
,它们其实就是播放器的当前播放时间和歌曲总时长,我们在这里用 console.log()
输出一下看看什么情况?
this.sel_int = function () {
if(!intv){
console.log(elements.player.currentTime, elements.player.duration);
intv = setInterval(function () {
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
}, 500);
}
};
0 NaN
浏览器返回的是 NaN,说明歌曲还没有加载成功,就开始了循环。但是我正常切歌之后得到的结果和这个是完全一致的,即便有这种问题,也理应能正常执行 setInterval()
里面的函数吧。那么给它里面 console.log
一下呢?
intv = setInterval(function () {
console.log(elements.player.currentTime, elements.player.duration);
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
}, 500);
重新操作复现 Bug,发现浏览器并没有和我预期想法一致,没有给我输出任何内容。这就很迷了啊!我又重新开始寻找答案,发现点击切歌按钮和播放按钮的差异,主要是前者经过了 that.play()
装载歌曲信息,而后者却没有。目前点击按钮能播放的原因,其实是因为点击播放按钮触发了 Audio 对象的 play 事件,里面有一个判断语句,它调用了 actions.next()
函数,用于切到下一首歌。
elements.player.addEventListener("play", function () {
if(!elements.player.src){
actions.next();
}
难道是因为这个事件的原因?我把它对调到了点击按钮执行的函数,重新操作复现,结果依旧相同。
elements.toggle.onclick = function () {
if(!elements.player.src){
actions.next();
}
elements.player.paused ? elements.player.play() : elements.player.pause();
};
把 actions.next()
改成 that.play()
呢?貌似播放的歌变了,但是这个问题依旧复现(原来我一开始就写错了,直接开始播放第一首不就好,为什么要切换到下一首)还是很迷,难不成必须用 Promise?在 that.play()
方法执行后再调用后面的语句?
等等,我重新理一下思路!这个代码的主要用途是在没有设置音乐 src 路径的情况下使用的,既然 that.play()
已经可以让播放器开始播放了,那么把判断逻辑改一下试试?
elements.toggle.onclick = function () {
if(elements.player.src){
elements.player.paused ? elements.player.play() : elements.player.pause();
}
else{
that.play();
}
};
再一次进行复现测试,在这次测试中,播放进度条正常工作了起来!前面的 setInterval()
也正常工作和 log 了!虽然这个 Bug 已经解决,但是 setInterval()
不运行的这个情况是真的难以分析。这段代码修改后,其实就是在没有 src 的情况下不会强制让播放器开始播放。
难道可能和浏览器的处理机制有关联?只要是 setInterval
就不执行吗?我随便加了一个 setInterval
上去,它和设置进度条的函数区别在于是否进行了赋值。
var intv, intv_lyric;
this.sel_int = function () {
if(!intv){
setInterval(function () { console.log("test"); }, 1000);
intv = setInterval(function () {
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
}, 500);
}
};
测试了一波,发现该函数正常执行,控制台反复输出了 "test" 给我?我好像忘记了一点,在音乐播放暂停的时候,我们是需要让进度条停止滚动的,而我播放器肯定有地方触发了 clearInterval()
函数使得 interval
停止了。
答案已经渐渐浮现在我眼前了,我在停止进度条的函数里添加了一段 console.log()
,并重新复现该 Bug。结果是先运行了一段 setInterval()
之后在它还没有给我们输出信息之前,迅速执行了 clearInterval()
,最终呈现了刚才怎么 console.log()
都不给我们反馈的现象。
this.sel_int = function () {
if(!intv){
console.log("我执行了");
intv = setInterval(function () {
elements.bar.played.style.width = (elements.player.currentTime / elements.player.duration) * 100 + "%";
}, 500);
}
};
this.clear_int = function () {
console.log("被重置了");
intv = clearInterval(intv);
};
// 控制台输出 "我执行了" "被重置了"
进一步测试后发现,Audio 对象反复执行其play()
方法,其play
事件仅能触发一次,包括在没有 src 变为有 src 的情况下
在 Audio 对象没有设置 src
属性的情况下执行它的 play()
方法,其 paused
属性会从 true
变为 false
,当然此时是没有音乐可以被播放的。
if(!elements.player.src){
that.play();
console.log(elements.player.paused);
}
elements.player.paused ? elements.player.play() : elements.player.pause();
这段代码在页面刚刚加载完后被我点击触发,由于没有 src
,那个判断不成立,直接开始下一步操作,由于默认 paused
为 true
,强行让播放器执行了 play()
方法。执行后,这个值变为了 false
。
在强制执行 play()
的瞬间,正常触发了 play
事件,之后调用 setInterval()
控制进度条,开始循环。可播放器的 pause
事件并不和 paused
属性一般见识,它自己主张在我 console.log()
前停止了循环。此处虽然 pause
事件触发了,但是 paused
属性依旧为 false
。
接着,我们的 that.play()
成功从服务器获取到了信息,准备第二次执行 play()
方法开始播放音乐,可这次却并没有重新触发到 play
事件,它翻脸不认人了。可能因为浏览器内核是通过 paused
属性的变动来触发 play
事件的,由于该值没有变动,于是就没有再次调用起进度条,最终呈现出了我们所见的 Bug 效果。
不过,没有设置 src
的 Audio 对象为什么能正常执行 play()
还不报错呢?如果是一个页面存在的 DOM 元素,我在尝试给它反复播放的时候,浏览器会返回这样的错误:
player.play();
<- Promise {<rejected>: DOMException: The element has no supported sources.}
VM553:1 Uncaught (in promise) DOMException: The element has no supported sources.
而虚拟创建的 Audio 却没有报错。
let player = new Audio();
player.play();
<- Promise {<pending>}
player.addEventListener("play", function () {
console.log("play");
});
player.addEventListener("pause", function () {
console.log("pause");
});
player.src = "https://api.paugram.com/netease?id=22826435&play=true";
// 音乐在设置完 src 后自动开始播放,没有触发 play 事件
// 如果再次更改 src,音乐将会停止,而不会触发 pause 事件,必须重新 play() 才行
// 你想触发 pause 事件,只能通过 pause() 方法实现
真是奇怪的 JavaScript!
已有 2 条评论
完全不懂的我竟然一字不落的全部看完......
@老狮 是个狠人