この記事は最初に公開されました: https://github.com/bigo-frontend/blog/ フォローと再投稿を歓迎します。
I.はじめに
エントリーの最初の要件は、フロントエンドの上司とのアクティビティ プロジェクトを完了することです。
一緒に開発しているので、もちろん偉い人のコードを読む機会を逃しません。
ページ内でカウントダウン機能を使用する必要があるため、ボスが既製のカウントダウン コンポーネントを作成していることがわかったので、それを直接使用しました。
パラメータを渡して関数を実現するのはとても気持ちいいです。プロジェクトが完了した後、ボスのカウントダウンコンポーネントのコードを拝みました。本当にたくさんのことを教えてもらいました。以下に記載されています:
- タイマーが setInterval ではなく setTimeout を使用するのはなぜですか
- 残り時間を -1 すればいいのではないか。
- 必要な時間を返す方法 (分と秒だけが必要な場合、分と秒のみが返されるか、すべてが必要な可能性があります)。
- インターフェイスが残り時間と期限のどちらを返すか、両方の状況にどのように対応するかは不明です。
- インターフェイスから返される時間が秒単位なのかミリ秒単位なのかは不明です。
さて、これらの質問は理解できないかもしれませんが、それは問題ではありません。以下の説明を読むと、突然光が見えると思います。
2. 手動運転を開始する
1.まずvueコンポーネントを作成します
<template>
<div class="_base-count-down">
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
},
};
</script>
<style lang='scss' scoped>
</style>
2. 基本的なカウントダウン コンポーネントを実装する
次に、インターフェースが取得するのは残り時間であると仮定します。
残り時間をカウントダウン コンポーネントに渡します。時間は秒またはミリ秒の場合があるため、ミリ秒か秒かをカウントダウン コンポーネントに伝えるために1time
を渡す必要があります。以下のコードに示すように。time
isMilliSecond
time
props
<template>
<div class="_base-count-down">
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
time: {
type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
};
</script>
<style lang='scss' scoped>
</style>
computed
の期間は、時間を変換した結果であり、time
ミリ秒であっても秒であっても、秒に変換され
ます+this.time
。なぜ先頭に「+ 」記号を追加するのでしょうか。インターフェイスから返される数値の文字列は文字列の形式である場合もあれば、数値の形式である場合もあるため、これは学ぶ価値があります (バックエンドのクラスメートをあまり信頼することはできません。自分自身で予防策を講じる必要があります) )。したがって、数値に変換するには、その前に「+ 」記号を追加します。これでduration
変換されましたtime
!
継続時間を取得したら、カウントダウンを開始できます
<template>
<div class="_base-count-down">
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
time: {
type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
// 新增代码:
mounted() {
this.countDown();
},
methods: {
countDown() {
this.getTime(this.duration);
},
}
};
</script>
<style lang='scss' scoped>
</style>
ここではカウントダウンを開始するという意味の countDown メソッドを作成しており、ページに入った後にカウントダウンメソッドが実行されます。
countDown
このメソッドは getTime メソッドを呼び出します。getTime は、取得する残り時間である期間パラメータを渡す必要があります。
では、このメソッドを実装してみましょう。
<template>
<div class="_base-count-down">
还剩{
{day}}天{
{hours}}:{
{mins}}:{
{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
}),
props: {
time: {
type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
methods: {
countDown() {
this.getTime(this.duration);
},
// 新增代码:
getTime(duration) {
this.timer && clearTimeout(this.timer);
if (duration < 0) {
return;
}
const {
dd, hh, mm, ss } = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {
this.getTime(duration - 1);
}, 1000);
}
}
};
</script>
<style lang='scss' scoped>
</style>
getTimeの目的は、日、時、分、秒を取得して HTML 上に表示し、タイマーを通じてリアルタイムに日、時、分、秒の値を更新することであることがわかります。こうしてカウントダウンが実現。とても簡単です、何かありますか?
durationFormatter
日、時、分、秒を変換するメソッドでduration
、非常にシンプルですが、具体的な実装例をご覧ください。
durationFormatter(time) {
if (!time) return {
ss: 0 };
let t = time;
const ss = t % 60;
t = (t - ss) / 60;
if (t < 1) return {
ss };
const mm = t % 60;
t = (t - mm) / 60;
if (t < 1) return {
mm, ss };
const hh = t % 24;
t = (t - hh) / 24;
if (t < 1) return {
hh, mm, ss };
const dd = t;
return {
dd, hh, mm, ss };
},
さて、ここで問題が発生します!!
3. setInterval の動作をシミュレートするために setTimeoutを使用するの?
ここで setInerval を使用した方が便利ではないでしょうか。
setTimeout(function(){··· }, n); // n毫秒后执行function
setInterval(function(){··· }, n); // 每隔n毫秒执行一次function
setInterval の欠点は次のとおりです。
繰り返しますが、タイマーによって指定される時間間隔は、コードが実行されるときではなく、タイマー コードがメッセージ キューに追加されるときを示します。したがって、コードが実際に実行される時間は保証されず、メインスレッドのイベント ループによっていつ取得され、実行されるかに依存します。
setInterval(function, N)
//即:每隔N秒把function事件推到消息队列中
上の図からわかるように、setInterval は 100 ミリ秒ごとにキューにイベントを追加します。100 ミリ秒後、T1 タイマー コードをキューに追加します。メイン スレッドではまだタスクが実行されているため、待機してから T1 タイマーを実行します。何らかのイベントの実行が終了した後のコード。さらに 100 ミリ秒後、T2 タイマーがキューに追加され、メイン スレッドはまだ T1 コードを実行しているため、待機します。さらに 100 ミリ秒後、理論的には、別のタイマー コードをキューに追加されますが、T2 がまだキュー内にあるため、T3 は追加されず、結果はこの時点でスキップされます。ここでは、T1 タイマーの実行直後に T2 コードが実行されることがわかります。タイマーが達成されていません。
要約すると、setInterval には 2 つの欠点があります。
- setInterval を使用すると、一部の間隔がスキップされます。
- 複数のタイマーを連続的に実行できます。
これは次のように理解できます: setTimeout によって生成された各タスクはタスク キューに直接プッシュされ、setInterval はタスクをタスク キューにプッシュする前に判断を行う必要があります (最後のタスクがまだキューにあるかどうかを確認します)。
したがって、上記の欠点を回避するために、通常は setTimeout を使用して setInterval をシミュレートします。
4. なぜclearTimeout(this.timer)なのか
2番目の質問: なぜthis.timer && clearTimeout(this.timer);
そのような文があるのですか?
次のようなシナリオを想定します。
図に示すように、カウントダウンの親コンポーネントには 2 つのボタンがあり、アクティビティ 1 をクリックするとアクティビティ 1 の残り時間が経過し、アクティビティ 2 をクリックするとアクティビティ 2 の時間が経過します。
この時点でカウントダウン コンポーネントがアクティビティ 1 のカウントダウンを実行しているときにアクティビティ 2 をクリックすると、新しい時間がすぐに渡されるため、この時点でタイミングを再調整する必要があります。もちろん、マウントされたコンポーネントは 1 回だけ実行されるため、ここでは再タイミングは行われません。つまりthis.countDown();
、一度だけ実行されます。つまり、this.getTime(this.duration);
一度だけ実行されるため、期間はアクティビティ 1 の時間のままですが、どうすればよいですか? 時計が役に立ちました。
継続時間を監視してみましょう。継続時間が変化したことがわかった場合は、新しい時刻がコンポーネントに渡されたことを意味します。この時点で、this.countDown() を再度呼び出す必要があります。
コードは以下のように表示されます:
<template>
<div class="_base-count-down">
还剩{
{day}}天{
{hours}}:{
{mins}}:{
{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
}),
props: {
time: {
type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
// 新增代码:
watch: {
duration() {
this.countDown();
}
},
methods: {
countDown() {
this.getTime(this.duration);
},
durationFormatter(){
...}
getTime(duration) {
this.timer && clearTimeout(this.timer);
if (duration < 0) {
return;
}
const {
dd, hh, mm, ss } = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {
this.getTime(duration - 1);
}, 1000);
}
}
};
</script>
<style lang='scss' scoped>
</style>
わかりましたが、これでは上で提起された質問、つまりなぜthis.timer && clearTimeout(this.timer);
この文なのかが説明されていません。
このように、現在のページにアクティビティ 1 の時刻が表示されているとすると、このとき setTimeout を実行すると、setTimeout内のコールバック関数が1 秒後にタスクキューに入れられ、さらに1 秒後になります。ただし、この時点では、この 2 秒の開始時に [アクティビティ 2] ボタンをクリックすると、アクティビティ 2 の時間がカウントダウン コンポーネントに渡され、トリガーされて呼び出され、setTimeout が実行されますcountDown()
。this.getTime(this.duration);
これも 1 秒になります。その後、コールバック関数をタスクキューに入れます。
この時点で、タスク キューには 2 つの setTimeout コールバック関数が存在します。1 秒経過すると、2 つのコールバック関数が次々に実行され、ページ上の時間が一気に 2 減少することがわかります。実際、1 を減算する操作が 2 回非常に速く実行されます。
this.timer && clearTimeout(this.timer);
そのため、この文が追加されました。前回のsetTimeoutをクリアするためです。
5. diffTime の使用
これが完璧なコンポーネントだと思うとき、実際に使用することを前提としてプロジェクトでこのコンポーネントを使用したいと考えます。オンラインであると、大きな問題があることがわかります。ページを開くとカウントダウンが開始されます。 、時刻は还剩1天12:25:25
、その後、誰かがあなたに WeChat メッセージを送信し、すぐに WeChat に切り替え、メッセージに返信した後ブラウザに戻り、カウントダウン時間がまだ残っていることに気付きます还剩1天12:25:25
。あなたはパニックに陥ります。自分が書いたコードにバグがあるのです。
これはどうなっているでしょうか?
エネルギー節約のため、一部のブラウザは、バックグラウンドに移行する (またはフォーカスを失う) ときにsetTimeout などのスケジュールされたタスクを一時停止し、
ユーザーがブラウザに戻るとスケジュールされたタスクが再アクティブ化されます。
一時停止と言っていますが、遅延というべきでしょうか、1秒のタスクが2秒に遅延、2秒のタスクが5秒に遅延するなど、実際の状況はブラウザごとに異なります。
したがって、毎回 1 を引くだけのような単純なことはできないようです (結局のところ、ブラウザをバックグラウンドに切り替え、数秒待ってから切り替えてから setTimeout を実行すると、setTimeout はクールダウンします。これは単に2番目)。
したがって、getTime メソッドを書き直す必要があります。
<template>
<div class="_base-count-down">
还剩{
{day}}天{
{hours}}:{
{mins}}:{
{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0,// 新增代码:
}),
props: {
time: {
type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
watch: {
duration() {
this.countDown();
}
},
methods: {
countDown() {
// 新增代码:
this.curTime = Date.now();
this.getTime(this.duration);
},
durationFormatter(){
...}
getTime(duration) {
this.timer && clearTimeout(this.timer);
if (duration < 0) {
return;
}
const {
dd, hh, mm, ss } = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {
// 新增代码:
const now = Date.now();
const diffTime = Math.floor((now - this.curTime) / 1000);
this.curTime = now;
this.getTime(duration - diffTime);
}, 1000);
}
}
};
</script>
<style lang='scss' scoped>
</style>
ご覧のとおり、3 か所に新しいコードを追加しました。
まず、変数 curTime を data に追加し、次にcountDown が実行されたときのcurTime
値Date.now()
、つまり現在の瞬間、つまりページに表示される瞬間を割り当てます。
次に、変更された 3 番目のコードを見てください。-1
変更されることがわかります-diffTime
。
setTimeout のコールバック関数が実行される瞬間です。
したがって、diffTime は、現在の setTimeout コールバック関数の実行時間と、前のページの残り時間の最後の変更との間の時間を示します。実際には、これは現在の setTimeout コールバック関数の実行時間と前の setTimeout コールバック関数の実行時間の間の時間です。
もしかしたら、diffTime についてまだよく理解していないかもしれません。例えば:
カウントダウン ページを開いたので countDown を実行しました。つまり、getTime メソッドを実行する必要があります。つまり、次のコードはすぐに実行されます。
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
これらのコードを実行すると、残り時間がページに表示されます。
そしてthis.curTime = Date.now();
この瞬間の時間を記録してみました。
次に、1 秒後に setTimeout でコールバック関数を実行します。
const now = Date.now();
現在の setTimeout コールバック関数の実行時間を記録します。
const diffTime = Math.floor((now - this.curTime) / 1000);
現在の setTimeout コールバック関数の実行時間と、ページのレンダリング開始からの残り時間の間の時間を記録します。実際、このときの diffTime は =1 です。
次に、this.curTime = now;
curTime の値を、setTimeout のコールバック関数が実行される現在の時点に変更します。
this.getTime(duration - diffTime);
実はそうなんですthis.getTime(duration - 1);
次に getTime を再度実行すると、次のコードが再実行され、新しい残り時間が表示されます。
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
1 秒後に setTmieout のコールバック関数が再度実行され、2 秒目が終了する前にブラウザをバックグラウンドに切り替え、この時点で setTimeout がクールダウンします。元に戻す前に 5 秒待ってください。これでsetTmieoutのコールバック関数が実行できるようになります。
このとき、const now = Date.now();
現在のsetTimeoutのコールバック関数の実行時間を記録します。
curTime は、最後の setTimeout コールバック関数の実行時間です。
したがってconst diffTime = Math.floor((now - this.curTime) / 1000);
、実際には、diffTime の値は 5 秒です。
したがって、this.getTime(duration - diffTime);
それはthis.getTime(duration - 5);
これにより、ブラウザがバックグラウンドに切り替わって残り時間が変わらないという問題が完全に解決されました。
6. 新しい関数を追加します。有効期限を渡すことができます。
以前は残り時間での通過のみでしたが、今後は有効期限での通過にも対応してほしいとのこと。
期間を少し変更するだけです。
computed: {
duration() {
if (this.end) {
let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000;
end -= Date.now();
return end;
}
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
着信側の長さが 13 より大きいかどうかを判断して、秒かミリ秒かを判断します。簡単!
7. 新機能の追加:秒のみ表示、時間のみ表示など、表示内容を選択できます。
HTML を変更するだけです。
<template>
<div class="_base-count-down no-rtl">
<div class="content">
<slot v-bind="{
d: days, h: hours, m: mins, s: seconds,
hh: `00${
hours}`.slice(-2),
mm: `00${
mins}`.slice(-2),
ss: `00${
seconds}`.slice(-2),
}"></slot>
</div>
</div>
</template>
これは非常に賢いもので、スロットを使用してカウントダウン コンポーネント、つまり子コンポーネントの値を親コンポーネントに渡すだけで済みます。
親コンポーネントがこのコンポーネントをどのように使用するかを確認してください。
<base-counter v-slot="timeObj" :time="countDown">
<div class="count-down">
<div class="icon"></div>
{
{timeObj.d}}天{
{timeObj.hh}}小时{
{timeObj.mm}}分钟{
{timeObj.ss}}秒
</div>
</base-counter>
見てください、とても独創的でシンプルです。
00${hours}
.slice(-2) の書き方も勉強になることがわかりました。従来は分を取得する際に、取得した分が2桁の数字であるか1桁の数字であるかを手動で判断し、1桁の場合は手動で0を追加する必要がありました。フロント。次のコードのように:
var StartMinute = startDate.getMinutes().toString().length >= 2 ? startDate.getMinutes() : '0' + startDate.getHours();
また、00${hours}
.slice(-2) は判定する必要がなく、最初に 0 を埋めてから、後ろから前に向かって 2 ビットをインターセプトします。
ここ。
完璧なカウントダウンコンポーネントが完成しました。
3. 研究概要
- setInterval の欠点を理解し、setInterval を setTimeout に置き換えてください。
- 学習した
“+”
操作は、3 7 21 に関係なく、インターフェイスによって取得された長い数字の文字列を数値に変換して安全に保ちます。 - 影響を防ぐために、clearTimeout を使用して前のタイマーをクリアします。
- v-slot を使用して子から父親に値を渡す方法を学びます
- 今後のCV操作を便利にするためにカウントダウンコンポーネントを学習してください。コンポーネントの完全なコードを貼り付けます。
<template>
<div class="_base-count-down no-rtl">
<div class="content">
<slot v-bind="{
d: days, h: hours, m: mins, s: seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2),
}"></slot>
</div>
</div>
</template>
<script>
/* eslint-disable object-curly-newline */
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0
}),
props: {
time: {
type: [Number, String],
default: 0
},
refreshCounter: {
type: [Number, String],
default: 0
},
end: {
type: [Number, String],
default: 0
},
isMiniSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {
if (this.end) {
let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000;
end -= Date.now();
return end;
}
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
watch: {
duration() {
this.countDown();
},
refreshCounter() {
this.countDown();
}
},
methods: {
durationFormatter(time) {
if (!time) return {
ss: 0 };
let t = time;
const ss = t % 60;
t = (t - ss) / 60;
if (t < 1) return {
ss };
const mm = t % 60;
t = (t - mm) / 60;
if (t < 1) return {
mm, ss };
const hh = t % 24;
t = (t - hh) / 24;
if (t < 1) return {
hh, mm, ss };
const dd = t;
return {
dd, hh, mm, ss };
},
countDown() {
// eslint-disable-next-line no-unused-expressions
this.curTime = Date.now();
this.getTime(this.duration);
},
getTime(time) {
// eslint-disable-next-line no-unused-expressions
this.timer && clearTimeout(this.timer);
if (time < 0) {
return;
}
// eslint-disable-next-line object-curly-newline
const {
dd, hh, mm, ss } = this.durationFormatter(time);
this.days = dd || 0;
// this.hours = `00${hh || ''}`.slice(-2);
// this.mins = `00${mm || ''}`.slice(-2);
// this.seconds = `00${ss || ''}`.slice(-2);
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {
const now = Date.now();
const diffTime = Math.floor((now - this.curTime) / 1000);
const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
this.curTime = now;
this.getTime(time - step);
}, 1000);
}
}
};
</script>
<style lang='scss' scoped>
@import '~@assets/css/common.scss';
._base-count-down {
color: #fff;
text-align: left;
position: relative;
.content {
width: auto;
display: flex;
align-items: center;
}
span {
display: inline-block;
}
.section {
position: relative;
}
}
</style>
何を学んだか、ぜひ追加してください。!
議論するメッセージを残してくださる皆様を歓迎します。スムーズな仕事と幸せな生活をお祈りしています。
私は bigo のフロントエンドです。次号でお会いしましょう。