<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          研究大佬用 Vue 寫的倒計時組件,學(xué)到了不少東西

          共 4856字,需瀏覽 10分鐘

           ·

          2022-01-04 08:14

          點擊上方?前端瓶子君,關(guān)注公眾號

          回復(fù)算法,加入前端編程面試算法每日一題群

          一、前言

          入職的第一個需求是跟著一位前端大佬一起完成的一個活動項目。

          由于是一起開發(fā),當(dāng)然不會放過閱讀大佬的代碼的機會。

          因為我的頁面中需要使用到倒計時功能,發(fā)現(xiàn)大佬的已經(jīng)寫了個現(xiàn)成的倒計時組件,于是直接就拿過來用了。

          傳個參數(shù)就實現(xiàn)了功能的感覺真是太棒了。項目完成后,就膜拜了一下大佬的倒計時組件的代碼。真是讓我學(xué)到了不少。列舉如下:

          1. 計時器為什么要用setTimeout而不用setInterval
          2. 為什么不直接將剩余時間-1。
          3. 如何將所需要的時間返回出去(有可能我只需要分鐘和秒數(shù),那就只返回分鐘和秒數(shù),也有可能我全都要)。
          4. 不確定接口返回的是剩余時間還是截止日期,該怎么同時兼容這兩種情況。
          5. 不確定接口返回的時間是秒還是毫秒單位。

          好了,你可能不太理解這些問題,但是沒關(guān)系,看完下面的解釋,相信你會豁然開朗。

          二、開始手操

          1. 先創(chuàng)建一個vue組件

          <template>
          ??<div?class="_base-count-down">
          ??div>
          template>
          <script>

          export?default?{
          ??data:?()?=>?({
          ???
          ??}),
          ??props:?{
          ????
          ??},
          };
          script
          >
          <style?lang='scss'?scoped>

          style>
          復(fù)制代碼

          2. 實現(xiàn)基本的倒計時組件

          接下來,假設(shè)接口獲得的是一個剩余時間。

          將剩余時間time傳入這個倒計時組件,由于time可能是秒為單位的,也有可能是毫秒為單位的,所以我們需要在傳入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>
          復(fù)制代碼

          computed中的duration是將time進行轉(zhuǎn)化的結(jié)果,不管time是毫秒還是秒,都轉(zhuǎn)化為秒 不知道你注意到了沒有:+this.time。為什么要在前面加個‘+’號。這點很值得我們學(xué)習(xí),因為接口返回的一串?dāng)?shù)字有時候是字符串的形式,有時候是數(shù)字的形式(不能過分相信后端同學(xué),必須自己做好防范)。所以通過前面加個‘+’號 通通轉(zhuǎn)化為數(shù)字?,F(xiàn)在的duration就是轉(zhuǎn)化后的time啦!

          我們獲得duration之后就可以開始倒計時了

          <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>
          復(fù)制代碼

          在這里創(chuàng)建了一個countDown方法,表示開始倒計時的意思,已進入頁面就開始執(zhí)行countdown方法。

          countDown方法調(diào)用了getTime方法,getTime需要傳入duration這個參數(shù),也就是我們獲得的剩余時間。

          現(xiàn)在來實現(xiàn)一下這個方法。

          <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>
          復(fù)制代碼

          可以看到,getTime的目的就是獲得 days,hours,mins,seconds,然后顯示到html上,并且通過定時器實時來刷新days,hours,mins,seconds這個幾個值。從而實現(xiàn)了倒計時。很簡單,有木有?

          durationFormatter是一個將duration轉(zhuǎn)化成天數(shù),小時,分鐘,秒數(shù)的方法,很簡單,可以看下它的具體實現(xiàn)。

          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?};
          },
          復(fù)制代碼

          好了,問題開始來了?。?img data-ratio="1.003717472118959" src="https://filescdn.proginn.com/0a0f8b2a54d969ddf1d3b7eef24881b5/126376868ed8744310402f3b0e3fdc09.webp" data-type="other" data-w="269" style="margin-right: auto;margin-left: auto;width: 100%;border-radius: 5px;display: block;margin-bottom: 15px;">

          3. 為什么要用setTimeout來模擬setInterval的行為?

          這里用setInerval不是更方便嗎?

          setTimeout(function(){···?},?n);?//?n毫秒后執(zhí)行function
          復(fù)制代碼
          setInterval(function(){···?},?n);?//?每隔n毫秒執(zhí)行一次function
          復(fù)制代碼

          可以看看setInterval有什么缺點:

          再次強調(diào),定時器指定的時間間隔,表示的是何時將定時器的代碼添加到消息隊列,而不是何時執(zhí)行代碼。所以真正何時執(zhí)行代碼的時間是不能保證的,取決于何時被主線程的事件循環(huán)取到,并執(zhí)行。

          setInterval(function,?N)??
          //即:每隔N秒把function事件推到消息隊列中
          復(fù)制代碼

          上圖可見,setInterval每隔100ms往隊列中添加一個事件;100ms后,添加T1定時器代碼至隊列中,主線程中還有任務(wù)在執(zhí)行,所以等待,some event執(zhí)行結(jié)束后執(zhí)行T1定時器代碼;又過了100ms,T2定時器被添加到隊列中,主線程還在執(zhí)行T1代碼,所以等待;又過了100ms,理論上又要往隊列里推一個定時器代碼,但由于此時T2還在隊列中,所以T3不會被添加,結(jié)果就是此時被跳過;這里我們可以看到,T1定時器執(zhí)行結(jié)束后馬上執(zhí)行了T2代碼,所以并沒有達到定時器的效果。

          綜上所述,setInterval有兩個缺點:

          1. 使用setInterval時,某些間隔會被跳過;
          2. 可能多個定時器會連續(xù)執(zhí)行;

          可以這么理解:每個setTimeout產(chǎn)生的任務(wù)會直接push到任務(wù)隊列中;而setInterval在每次把任務(wù)push到任務(wù)隊列前,都要進行一下判斷(看上次的任務(wù)是否仍在隊列中)。

          因而我們一般用setTimeout模擬setInterval,來規(guī)避掉上面的缺點。

          4. 為什么要clearTimeout(this.timer)

          第二問:為什么要有this.timer && clearTimeout(this.timer);這一句?

          假設(shè)一個場景:

          如圖所示,在倒計時的父組件中,有兩個按鈕,點擊活動一就會傳入活動一的剩余時間,點擊活動二,就會傳入活動二的時間。

          如果此時倒計時組件正在做活動一的倒計時,然后點擊活動二,就要會馬上傳入新的time,這個時候就需要重新計時。當(dāng)然,這里并不會重新計時,因為組件的mounted只會執(zhí)行一次。也就是說this.countDown();只會執(zhí)行一次,也就是說this.getTime(this.duration);只會執(zhí)行一次,因此duration還是活動一的時間,怎么辦呢?watch派上用場了。

          我們來監(jiān)聽duration,如果發(fā)現(xiàn)duration變化,說明新的時間time傳入組件,這時就要重新調(diào)用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>
          復(fù)制代碼

          好了,但是并沒有解釋上面提出的那個問題:為什么要有this.timer && clearTimeout(this.timer);這一句?

          這樣,假設(shè)現(xiàn)在頁面顯示的是活動一的時間,這時,執(zhí)行到setTimeout,在一秒后就會把setTimeout里的回調(diào)函數(shù)放到任務(wù)隊列中,注意是一秒后哦!這時,然而,在這一秒的開頭,我們點擊了活動二按鈕,這時候的活動二的時間就會傳入倒計時組件中,然后觸發(fā)countDown(),也就調(diào)用this.getTime(this.duration);,然后執(zhí)行到setTimeout,也會一秒后把回調(diào)函數(shù)放到任務(wù)隊列中。

          這時,任務(wù)隊列中就會有兩個setTimeout的回調(diào)函數(shù)了。等待一秒過去,兩個回調(diào)函數(shù)相繼執(zhí)行,我們就會看到頁面上的時間一下子背減了2,實際上是很快速地進行了兩遍減1的操作。

          這就是為什么要添加上this.timer && clearTimeout(this.timer);這一句的原因了。就是要把上一個setTimeout清除掉。

          5. 使用 diffTime

          當(dāng)你認(rèn)為這是一個完美的組件的時候,你想把這個組件用到項目上,假設(shè)你也確實用了,而且還上線了,確發(fā)現(xiàn)出現(xiàn)了個大問題:當(dāng)頁面打開的時候,倒計時開始了,時間是 還剩1天12:25:25,然后有人給你發(fā)微信,你馬上切換到微信,回復(fù)消息后切回瀏覽器,發(fā)現(xiàn)倒計時時間卻還是還剩1天12:25:25。你慌了:你寫的代碼出現(xiàn)bug了!

          這是怎么回事?

          出于節(jié)能的考慮, 部分瀏覽器在進入后臺時(或者失去焦點時), 會將 setTimeout 等定時任務(wù)暫停 待用戶回到瀏覽器時, 才會重新激活定時任務(wù)

          說是暫停, 其實應(yīng)該說是延遲, 1s 的任務(wù)延遲到 2s, 2s 的延遲到 5s, 實際情況因瀏覽器而異。

          原來如此,看來不能每次都只是減1這么簡單了(畢竟你把瀏覽器切到后臺之后setTimeout就冷卻了,等幾秒后切回,然后執(zhí)行setTimeout,只是減了一秒而已)。

          所以我們需要改寫一下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>
          復(fù)制代碼

          可以看到,我們在三個位置添加了新的代碼。

          首先在data了添加了curTime這個變量,然后在執(zhí)行countDown的時候給curTime賦值Date.now(),也就是當(dāng)前的時刻,也就是顯示在頁面上的那個時刻。

          然后看修改的第三處代碼。可以看到是將-1改成了-diffTime。

          now 是 setTimeout的回調(diào)函數(shù)執(zhí)行的時候的那個時刻。

          因而 diffTime 則 表示 當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時刻距離上 頁面上的剩余時間上一次變化的時間段。其實也就是 當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時刻距離上 一個setTimeout的回調(diào)函數(shù)執(zhí)行的時刻時間段。

          可能你還是不太能理解diffTime。舉個例子:

          你打開了這個倒計時頁面,于是執(zhí)行了countDown,也就是說要執(zhí)行g(shù)etTime這個方法了。也就是會馬上執(zhí)行下列的代碼。

          this.days?=?dd?||?0;
          this.hours?=?hh?||?0;
          this.mins?=?mm?||?0;
          this.seconds?=?ss?||?0;
          復(fù)制代碼

          執(zhí)行完這些代碼頁面上就會出現(xiàn)剩余時間。

          this.curTime = Date.now(); 就記錄下了此刻的時間點。

          然后一秒后執(zhí)行setTimeout里的回調(diào)函數(shù):

          const now = Date.now(); 記錄當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時間點。

          const diffTime = Math.floor((now \- this.curTime) / 1000); 記錄當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時間點距離頁面上開始 渲染 剩余時間的 這一段時間。其實此時的diffTime就是=1。

          然后this.curTime = now; 將curTime的值變成當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時間點。

          this.getTime(duration \- diffTime); 其實就是this.getTime(duration \- 1);

          然后又執(zhí)行g(shù)etTime,就會重新執(zhí)行下面的代碼,有渲染了新的剩余時間。

          this.days?=?dd?||?0;
          this.hours?=?hh?||?0;
          this.mins?=?mm?||?0;
          this.seconds?=?ss?||?0;
          復(fù)制代碼

          然后一秒后又要執(zhí)行setTmieout的回調(diào)函數(shù),在這一秒還沒結(jié)束的時候,我們將瀏覽器切到后臺,此時setTimeout冷卻了。等5秒后再切回。于是setTmieout的回調(diào)函數(shù)才得以執(zhí)行。

          這時const now = Date.now(); 記錄當(dāng)前這個setTimeout的回調(diào)函數(shù)執(zhí)行的時間點。

          而curTime是上一個setTimeout的回調(diào)函數(shù)執(zhí)行的時間。

          所以const diffTime = Math.floor((now \- this.curTime) / 1000);實際上,diffTime的值就是5秒。

          因而this.getTime(duration \- diffTime); 其實就是this.getTime(duration \- 5);

          這樣就完美解決了因為瀏覽器切到后臺,導(dǎo)致剩余時間不變的問題。

          6. 添加新功能:可以傳入到期時間。

          之前是只能傳入剩余時間的,現(xiàn)在希望也支持傳入到期時間。

          只需要改動一下duration就好了。

          ??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;
          ????}
          ??},
          復(fù)制代碼

          判斷傳入的end的長度是否大于13來判斷是秒還是毫秒。輕松!

          7. 添加新功能:可以選擇要顯示的內(nèi)容,例如只顯示秒,或者只顯示小時。

          只需要改動一下html: