Debug: setTimeout 使用做定時器時的錯誤函式傳遞方式

LinForest_zZ發表於2024-11-11

MarkTime: 2024-05-24 10:41:26

LogTime: 2024-11-10 14:55:53


首先複習

setTimeout():
語法: let timeId = setTimeout(func|code, [delay_millisecond])
說明: 延時器. 延遲delay_millisecond後, 執行引數1

setInterval(): 
語法: let timeId = setInterval(func|code, [delay_millisecond])
說明: 週期性執行器. 每隔delay_millisecond執行一次引數1

說明

問題

利用標識來判斷是否執行下一次的 setTimeout函式,
原本寫程式碼的時候想著的是:
第一次的periodicTimerFunc() 執行後,
如果1號還在錄音,
則等5s後開始下一次的 periodicTimerFunc() 執行
但直接就執行了,
並沒有等待

其他補充

  • 版本相關:

    • vue: 2.6.14
    • js-audio-recorder: 1.0.7
  • 背景

    • 需求: 一邊進行錄音, 一邊對錄音的內容轉義為中文輸出到互動皮膚上

    • 說明:

      1. 以下程式碼  是當時寫的 語音互動的第一版( js-audio-recorder1.0.7 ), 
        文件: https://recorder-api.zhuyuntao.cn/Recorder/event.html
        
        不支援 邊轉邊錄其實(沒辦法獲取到中間資料), 
        所以先自己取巧了一下, 
        大體邏輯就是 啟動完1號錄音物件 再定時啟動2號錄音物件進行轉錄.
        
        存在明顯 Bug (
          2號每次啟動時, 
          會對錄製正在進行中的1號造成干擾, 
          1號這個時候錄製不到音訊.
          => 1號 完成錄製匯出的錄音檔案: 聽感: 每隔幾秒就很有一段人體感受很不舒適的段靜默
        )
        
      2. 查閱了下, 
        找到比較新的大大也還在維護的 recorder-core1.3.24040900,
        支援了在啟動 recorder 錄音物件之後的實時回撥函式 onProcess().
        那麼之前的Bug, 最後採取的
        最佳化思路:
          在onProcess裡實現邊錄邊轉,
          並在最後匯出檔案的時候 把 段音訊陣列 合併即可.
        
        原始碼、說明文件: https://github.com/xiangyuecn/Recorder
        

原始碼

<script>
  /* 
    省略了很多, 只是留下基礎結構為說明
  */
  import Recorder from 'js-audio-recorder';

  export default {
    data(){
      return {
        recorder: new Recorder({ // 1號錄音物件
          sampleBits: 16, // 取樣位數,支援 8 或 16,預設是16
          sampleRate: 16000, // 取樣率,支援 11025、16000、22050、24000、44100、48000, 錄音一般用16000
          numChannels: 2, // 聲道,支援 1 或 2, 預設是1
        }),
        pRecorder: new Recorder({ // 2號錄音物件 - 用來在1號啟動的期間, 把定量的音訊傳輸轉義
          sampleBits: 16, // 取樣位數,支援 8 或 16,預設是16
          sampleRate: 16000, // 取樣率,支援 11025、16000、22050、24000、44100、48000, 錄音一般用16000
          numChannels: 2, // 聲道,支援 1 或 2, 預設是1
        }),
        isPeriodicTimerRun: false, // 是否啟動週期呼叫識別
      }
    }, 
    methods: {
      /**
       * 錄音週期轉義文字
       */
      toPhoTranscri(){
        this.toStopPhoTranscri(); // 停止所有錄音物件
        this.isPeriodicTimerRun = true;
        this.pRecorder.start();
        let cTime = '12345679';
        let periodicTimerFunc = (cTime) => {
          debugger
          console.log(cTime);

          let wavResource = this.pRecorder.getWAV();
          let mp3Resource = this.convertToMp3('pRecorder', wavResource);
          this.pRecorder.stop();
          this.pRecorder.start(); // TODO 重新啟動會有延遲
          let currentTime = new Date().formatStr('yyyy-MM-dd HH:mm:ss');

          let formData = this.convertBlobToMultipartFile(mp3Resource, `錄音${currentTime}`);
            
          // blahblahblah... 省略一堆把 wav轉為mp3檔案之後, 呼叫介面處理識別為中文後返回, 並後續渲染的邏輯
            
          if (this.isPeriodicTimerRun){ // isPeriodicTimerRun 是如果使用者點選了"停止錄音"的按鈕之後會=false的控制變數
            debugger
            this.periodicTimer = setTimeout(periodicTimerFunc(currentTime), 5000);
          }
        }
        this.periodicTimer = setTimeout(periodicTimerFunc(cTime), 5000);
      },
      /**
       * 停止所有錄音物件行為、一些初始化行為
       */
      toStopPhoTranscri(){ /* ... */ },
      /**
       * 將wav檔案轉換為mp3格式
       */
      convertToMp3(){ /* ... */ },
      /**
       * 將blob檔案轉換為form表單物件
       */
      convertBlobToMultipartFile(){ /* ... */ },
    }
  }
</script>


解決

嘿嘿我是笨蛋, 用來直接做引數傳遞肯定就直接一下子執行了啊, 會把執行的結果當作傳參再延遲 5s 後執行.

所以需要改變一下寫法,

讓它在5s後再執行 periodicTimerFunc().

使用箭頭函式, 去建立一個新的函式, 只是函式內部執行的是 periodicTimerFunc(),

這樣子當定時器在 5s 後被呼叫的時候才會去執行函式內部的內容.

setTimeout(periodicTimerFunc(cTime), 5000) => setTimeout(() => periodicTimerFunc(cTime), 5000)


<script>
  import Recorder from 'js-audio-recorder';

  export default {
    data(){
      return {
        recorder: new Recorder({ // 1號錄音物件
          sampleBits: 16, // 取樣位數,支援 8 或 16,預設是16
          sampleRate: 16000, // 取樣率,支援 11025、16000、22050、24000、44100、48000, 錄音一般用16000
          numChannels: 2, // 聲道,支援 1 或 2, 預設是1
        }),
        pRecorder: new Recorder({ // 2號錄音物件 - 用來在1號啟動的期間, 把定量的音訊傳輸轉義
          sampleBits: 16, // 取樣位數,支援 8 或 16,預設是16
          sampleRate: 16000, // 取樣率,支援 11025、16000、22050、24000、44100、48000, 錄音一般用16000
          numChannels: 2, // 聲道,支援 1 或 2, 預設是1
        }),
        isPeriodicTimerRun: false, // 是否啟動週期呼叫識別
      }
    }, 
    methods: {
      /**
       * 錄音週期轉義文字
       */
      toPhoTranscri(){
        this.toStopPhoTranscri();
        this.isPeriodicTimerRun = true;
        this.pRecorder.start();
        let cTime = '';
        let periodicTimerFunc = (cTime) => {
          let wavResource = this.pRecorder.getWAV();
          let mp3Resource = this.convertToMp3('pRecorder', wavResource);
          this.pRecorder.stop();
          this.pRecorder.start();
          let currentTime = new Date().formatStr('yyyy-MM-dd HH:mm:ss');

          let formData = this.convertBlobToMultipartFile(mp3Resource, `錄音${currentTime}`);
            
          // blahblahblah... 省略一堆把 wav轉為mp3檔案之後, 呼叫介面處理識別為中文後返回, 並後續渲染的邏輯

          if (this.isPeriodicTimerRun){
            this.periodicTimer = setTimeout(() => periodicTimerFunc(currentTime), 5000); // (ง •_•)ง 就是這裡用箭頭函式改一下
          }
        }
        
        this.periodicTimer = setTimeout(periodicTimerFunc(cTime), 5000); // 這裡的確需要 periodicTimerFunc() 立刻執行, 即第一次啟動入口
      },
      /**
       * 停止所有錄音物件行為、一些初始化行為
       */
      toStopPhoTranscri(){ /* ... */ },
      /**
       * 將wav檔案轉換為mp3格式
       */
      convertToMp3(){ /* ... */ },
      /**
       * 將blob檔案轉換為form表單物件
       */
      convertBlobToMultipartFile(){ /* ... */ },
    }
  }
</script>


↓ 2024-05-24 10:41:26 至 2024-05-24 10:41:37 中間的時間過長是因為打了debugger斷點被我人為中斷了

Debug: setTimeout 使用做定時器時的錯誤函式傳遞方式

總結

直接傳遞函式呼叫會導致函式立即執行, 並將結果傳遞給 setTimeout , 而不是傳遞函式本身, 所以應該傳遞的是返回函式呼叫的箭頭函式, 即傳遞一個函式引用. 這樣子 setTimeout 在延遲時間結束後才會執行被引用的函式.

相關文章