js定時任務佇列

孤狼醬發表於2018-05-24

 最近在一個專案中,遇到這麼一個需求:一個頁面中,大概有四五個元素需要按一定次序依次進場,setTimeout來實現吧,仔細一想,那樣的程式碼實在是寫不下去,大概是這樣的:

    setTimeout(()=>{
        this.setState({view1Visible: true})
        setTimeout(()=>{
            this.setState({view2Visible: true})
            setTimeout(()=>{
                this.setState({view3Visible: true})
                setTimeout(()=>{
                    // 沒完沒了的setTimout...
                },500)
            },500)
        },500)
    },100)
複製程式碼

 明顯的回撥地獄,對症下藥,用Promise來簡單封裝一下:

const timer=(task,ms)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            task && task()
            resolve()
        },ms)
    })
}
複製程式碼

 然後之前的程式碼大致可以寫成這樣:

timer(()=>this.setState({view1Visible: true}),100)
	.then(()=>timer(()=>this.setState({view2Visible: true}),500))
	.then(()=>timer(()=>this.setState({view3Visible: true}),500))
	.then(()=>timer(()=>this.setState({view4Visible: true}),500))
複製程式碼

 到這裡基本已經滿足我的需求了,如果對不喜歡用then,或者只是對它有意見,也可以用async/await來改寫一下:

async layout(){
    await timer(()=>this.setState({view1Visible: true}),100)
    await timer(()=>this.setState({view2Visible: true}),500)
    await timer(()=>this.setState({view3Visible: true}),500)
    await timer(()=>this.setState({view4Visible: true}),500)
}
複製程式碼

 寫到這裡,已經足夠了,不過我個人對timer的兩個引數不喜歡,而且我更喜歡寫鏈式風格的程式碼,理想的程式碼是這樣的:

new Schedule()
    .delay(100).task(()=>this.setState({view1Visible:true}))
    .task(()=>this.setState({view1Visible:true}))
    .task(()=>this.setState({view2Visible:true}))
    .task(()=>this.setState({view3Visible:true}))
複製程式碼

 首先task和delay分別用兩個方法傳參,語義化嘛,一眼就能看出這個引數指的是什麼;然後delay要能夠複用,很多情下我們任務之間的間隔是相等的,就不用每次都傳了。

 實現方法嘛,在Schedule類中,要有個promise來處理這些任務,然後需要一個變數來儲存delay,來達到複用的目的,然後就是delay和task兩個方法,都返回this來實現鏈式呼叫。最後把上面那個timer方法拿過來,解決回撥地獄。先看看最後的程式碼吧:

export default class Schedule{
  constructor(){
    this._delay=0
    this.p = null
  }
  timer(task,ms){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        task && task()
        resolve()
      },ms)
    })
  }
  task(task){
    const {_delay:delay,timer,p}=this
    this.p = p ?p.then(()=>timer(task,delay)) :timer(task,delay)
    return this
  }
  delay(_delay){
    this._delay = _delay
    return this
  }
}
複製程式碼

 也沒啥特別的,要注意的一點是,第一次呼叫task的時候,p為空,直接給他賦值即可。或者你一可以給p一個初始的promise,之後就不用考慮是否為空了,直接p.then()就可以了,而在這個時候,需要先用一個臨時變數把delay快取起來,否則最後再執行到當前task的時候,delay很有可能取到的是後面賦的值。

 對於一般的需求,現在這個Schedule應該完全能夠搞定,可能你想這樣做:先把任務佇列定義好,到了特定的時機再去觸發它執行,那我們要怎麼做呢?

 其實也不難,每次呼叫task的時候,不放到promise裡面,而是把task和當前delay先儲存到一個陣列裡面,最後再寫一個方法,在呼叫的時候遍歷這個陣列,把他們放到promise裡面去,直接上程式碼好了:

export default class Schedule{
  constructor(){
    this._delay=0
    this.tasks=[]
  }
  timer(task,ms){
    return new Promise((resolve,reject)=>{
      setTimeout(()=>{
        task && task()
        resolve()
      },ms)
    })
  }
  task(task){
    this.tasks.push({task,delay:this._delay})
    return this
  }
  delay(_delay){
    this._delay = _delay
    return this
  }
  exec(){
    this.tasks.length>0 && this.tasks.reduce(
      (p,t)=>p.then(()=>this.timer(t.task,t.delay)),
      Promise.resolve()
    )
  }
}
複製程式碼

一個小小的技巧就是用陣列的reduce方法來把這些task依次放到promise中,在reduce的第二個引數傳入一個空的Promise,就避免了判斷是否有初始Promise的問題。用的時候需要手動去呼叫exec方法,整個佇列才回開始執行:

new Schedule()
    .delay(100).task(()=>this.setState({view1Visible:true}))
    .task(()=>this.setState({view1Visible:true}))
    .task(()=>this.setState({view2Visible:true}))
    .task(()=>this.setState({view3Visible:true}))
    .exec() // 可以在任何你需要的時候呼叫
複製程式碼

需要介紹的就這些了,最後其實有不少可以改進的地方,比如上面說的兩種情況,完全可以寫在一起,構造方法中傳個引數來決定是否是需要延遲執行的佇列。又或者引入cron表示式,來決定在特定的時間點執行任務……當然這些不在本文討論的範疇,感興趣的朋友可以去試試。

相關文章