代碼並非從開始一直運行到結束,經歷順序、分支、循環的控制結構,經歷函數、類和對象等各類封裝就能夠了,工具腳本是這樣的,只執行一次,傳入參數或者修改配置,而後就會執行或計算出你想要的結果,客戶端和服務端的代碼都是長期運行的,有阻塞、併發等概念,涉及到多線程甚至多進程。服務端的代碼邏輯多是在接收到請求以後再執行,或者某個時間自動執行,客戶端也是同樣,有的代碼是用戶操做觸發了什麼事件纔會執行,而有的是定時的自動執行的。html
定時執行的任務根據有沒有周期概念和週期的長短複雜度也不一樣,有的任務是週期性的以天爲單位的定時執行,好比天天的數據和日誌備份,有的任務只執行一次或者只是很短的週期,好比咱們雲課堂中紀律樹的成長,下課的提醒。不少後端應用的框架都會提供定時任務的功能,好比eggjs的schedule,好比java生態的quartz。這些是週期比較長的定時任務,由於服務器是不會中止的。客戶端用到的定時任務通常週期都較短,框架或者運行平臺提供的也都比較簡單,好比瀏覽器自帶的setInterval、setTimeout,egret的Timer等。vue
咱們產品所面向的場景是教室中的一次上課過程,其中有一些須要定時任務的地方,好比下課的提醒,好比紀律樹的自動生長。這些任務能夠單獨去定時,可是這樣太過散亂,不易於管理和維護,因此咱們封裝了一個定時任務隊列的庫,叫作TimeRemider,功能是添加一個時間和該時間要執行的任務,到時間後會自動執行,這和直接用setInterval或者setTimout的區別有兩個:java
這部分功能比較獨立,所以咱們把他抽取成了一個單獨的項目,以一個node module的形式來被咱們業務的項目所使用,經過版本號的更新來迭代升級。node
最近我在讀這部分的代碼,對定時功能實現的方式、代碼職責和結構的劃分、暴露出去的api還有配置方面都有一些本身的見解。git
time-reminder的定時器是用setInterval來實現的,經過定時輪詢,每次輪詢取出任務隊列中最近的一個任務,判斷是否須要執行,若是須要執行,則通知主進程執行這個任務,若是任務過時則刪掉該任務。github
由於輪詢是有必定間隔的,因此這裏須要判斷當前時間是否在這個輪詢週期的時間段內。另外,這裏的offsetTime是和服務器時間的差值,項目中請求都會在接收到響應以後根據服務器的時間來更新這個offset,我以爲這裏只要在其中一個接口校準一次就行了,由於服務器的時間也不會手動調整。web
這裏的實現方式是基於setInterval,因此纔會有定時的輪詢和時間段的計算。其實這裏用setTimeout也能夠,惟一有問題的是offset更新的問題,setInterval在下一個輪詢的計算時就能感知到更新,而se'tTimeout感知不到,須要在offset更新後,手動的去clear掉全部的timer,而後基於新offfset算出的時間從新setTimeout。兩種方式各有優缺點。json
定時任務涉及到定時器和任務隊列兩個方面, 後端
如今的版本中定時器是基於setInterval來實現的,考慮到性能,把他放到了子進程中去(咱們是基於electron作的客戶端,可使用node),而主進程負責任務隊列的維護和任務的執行。如今的目錄劃分很簡單:api
index.js是主進程的代碼,主要是任務隊列的維護和定時器的啓動、中止等。core/adjust-timer.js是子進程的代碼,裏面是定時器的輪詢和在檢測到有任務要執行時通知主進程執行的邏輯。
代碼職責方面我以爲是有問題的,如上面的架構圖,我以爲定時器應該是純粹的,只有基於setInterval的根據設定的時間不斷輪詢,或者基於setTimeout的定時通知的邏輯,而不該該包含判斷任務隊列是否有要執行任務的邏輯。而如今這部分邏輯是直接寫在定時器裏的。這樣不但使得不能透明的從setInterval替換成setTimeout,也使得未來若是要適應更多平臺(不支持node的進程)的成本增長。
合理的架構應該是多層次多模塊的,層與層之間單項依賴,模塊與模塊之間職責明確,基於抽象的約定來通訊,這樣才能作到能夠靈活的替換實現方案,好比把setInterval替換成setTimeout,好比把進程的方式去掉。
如圖,至少應該有2個層次,定時器和任務隊列是底層實現功能的部分,主進程和子進程的代碼是node環境下的適配方式,而後再提供一個index.js暴露全局api。
這樣的架構和對應的目錄結構是易於擴展和替換實現方案的,vue在3.0中把observer獨立成頂層文件夾,就是爲了替換成proxy更方便,這裏也是同樣。
實現功能以後要暴露出一些api去,供外部使用,暴露出去的api對應着定時器和任務隊列,也有兩部分,一部分是添加、刪除、清空定時任務的,一部分是啓動、中止定時器以及修改計時offset的。
如今暴露出去的api以下:
addTimeListener
removeTimeListener
hasOwnId
clear
start
stop
getCurrentServiceTime
updateOffset
複製代碼
8個api前3個是定時任務的,後5個是定時器相關的,可是從名字上不能明確的區分出各自的功能,我以爲以下的命名會更好一些;
addTimedTask
removeTimedTask
clearTimedTask
startTimer
stopTimer
setTimeOffset
複製代碼
getCurrentServiceTime是獲取當前服務器時間的,雖然在請求響應的時候設置到了這裏,可是這並非定時任務的功能,不該該放到這裏面,能夠在響應的時候再保存一份到別的地方。
定時任務中有不少能夠配置的地方,好比擴展成多平臺以後的平臺選擇,好比定時器setInterval和setTimeout兩種實現的選擇,好比是否打印日誌等。
能夠像eslint、babel等提供一個配置文件放在項目下,支持json等配置方式,能夠叫timerTaskQueue.config.js。
module.exports = {
log: false,//是否打印日誌
platform: 'node',//使用定時任務的平臺
timer: 'interval'//定時器的實現方式
}
複製代碼
甚至能夠提供插件擴展的機制或者一系列內置的功能供用戶本身去選擇。
代碼中還有不少命名和實現的具體問題:
好比分了handlerList和taskList兩部分,本意是handler能夠複用,可是卻沒有提供複用handler的合理機制,像提供handler的name註冊機制等。
好比taskList中的task若是一個time有多個任務,會組織成以下的結構,我以爲這個也是沒有必要的,扁平化的放多份就能夠,這樣組織還有維護成本。
{
time: 2323232
ids: [id1,id2]
}
複製代碼
請求、定時任務、事件都是代碼觸發的方式,或者說執行的入口,定時任務根據週期的長短複雜度也不一樣,後端或者客戶端的框架都提供了定時任務的功能(eggjs、quartz、egret、web等)。
咱們的項目爲了集中管理定時任務,封裝了一個定時任務框架叫time-reminder,提供定時器和任務隊列兩方面的功能。由於是node平臺,考慮到性能使用了子進程的方式,而且定時器的實現是setInterval。我提出了一些重構的思路,包括代碼架構和目錄結構的調整、支持配置、改進暴露出去的api,以及一些代碼的細節問題。
真正作一個通用的東西,和作只能適應一種業務場景的東西是徹底不同的,咱們既然把他抽取了出來,就要使得它更加的通用,完善的差很少以後能夠考慮開源,到時候必定要支持多平臺、支持配置、暴露的頂層api更加優化,甚至提供插件功能。同時書寫文檔、demo和測試用例。會繼續完善下去。