Google 試算表:立即套用腳本已排隊但尚未寫回的更動 flush()

 


在寫 Apps Script 操作 Google 試算表時,你可能也遇過這種怪事:明明剛把值寫進去,下一步讀回來卻空空的;或是狀態欄位更新了,畫面卻像在放空。

其實不一定是你程式寫壞了,而是試算表會把多個動作「先排隊、後處理」,為了省時間會延後真正寫回。這時候,SpreadsheetApp.flush() 就派上用場了。

它的工作很樸實——把所有待處理的更動立刻套用,讓你接著的讀取或視覺更新可以基於「最新」狀態。

本篇文章會用日常情境講清楚:什麼時候該用、不該用、怎麼用才不拖慢速度,還會整理常見坑(例如以為 flush 等於等公式重算)與實戰範例,讓你在「寫→讀→顯示」的節點更穩、更可預期。希望本篇文章能夠幫助到需要的您。


目錄

{tocify} $title={目錄} 


為什麼需要 flush()

Apps Script 在對試算表做多次改動時,會「聰明地」把多個操作打包、延後實際寫回,以減少往返與提升速度。多數情況你不必管它;但當你緊接著要讀回剛寫入的值、更新圖表或做權限鎖釋放前的最後提交,就需要手動呼叫 SpreadsheetApp.flush(),把所有排隊的更動立刻送進工作表。


flush() 是什麼(官方定義+工作原理)

官方定義:

    flush() 會套用所有待處理的試算表更動(Applies all pending Spreadsheet changes)。

工作原理:

    Apps Script 對試算表有「延遲寫回(write-back)」與「批次最佳化」;flush() 會強制把緩存中的更動推送到表格,讓接下來的讀取與 UI 狀態同步在最新的資料上。


小提醒:

    flush() 只保證寫入已落地;不保證公式已完成重算。若你的流程仰賴公式結果,仍可能需要等待或輪詢確認(下文有範例)。社群經驗也指出,單靠 flush() 並不足以等待複雜計算完成。


何時該用/不該用(判斷清單)

該用 flush() 的情境

    寫入後立刻讀取:例如 setValues() 之後馬上 getValues() 作邏輯判斷。

    即時顯示給使用者:

        腳本執行中更新狀態欄位(如「處理中 45%」),希望使用者當下就看到。

    釋放鎖之前:

        多執行緒(onEdit/定時觸發器)情境,釋放 LockService 的鎖前先 flush(),避免別的執行緒讀到半套資料。


不必或不該用的情境

    每次迴圈都 flush():

        會嚴重拖慢速度,增加 API 往返與配額壓力。應改用批次寫入(例如一次 setValues())。

    自訂函式(=myFunc())想改別的儲存格:

        就算 flush() 也無法繞過限制;自訂函式只能回填自身或相鄰範圍,不能隨意改表上其他地方。

    等待公式重算:    

        flush() 不是「等重算」,必要時要自行輪詢或加短暫 sleep() 再檢查條件。


基本語法與最小可用範例

function writeThenRead() {
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName('工作表1');
  sheet.getRange(1, 1).setValue('Hello');
  SpreadsheetApp.flush(); // 立刻寫回
  const val = sheet.getRange(1, 1).getValue(); // 現在能讀到最新值
  Logger.log(val);
}


註:SpreadsheetApp.flush() 無參數、無回傳值;呼叫即生效。


逐步操作教學(4 個情境實戰)

情境 A:寫入後馬上讀回做判斷

目標:匯入資料→立即讀回總筆數→若超過門檻則分頁。

步驟

    1.    在試算表新增工作表「原始資料」「拆分A」「拆分B」。

    2.    開啟 擴充功能 → Apps Script。

    3.    貼上程式並執行 importAndSplit(),第一次會要求授權。

    4.    回到表格觀察結果。

function importAndSplit() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('原始資料');
  // 模擬寫入 2,000 筆
  const values = Array.from({length: 2000}, (_, i) => [i + 1, `item-${i+1}`]);
  sheet.getRange(2, 1, values.length, values[0].length).setValues(values);

  // 關鍵:馬上讀回前先 flush
  SpreadsheetApp.flush(); // 套用所有待處理寫入 :contentReference[oaicite:12]{index=12}

  const lastRow = sheet.getLastRow();
  if (lastRow - 1 > 1000) {
    splitToSheets_('原始資料', ['拆分A', '拆分B']);
  }
}

function splitToSheets_(srcName, targets) {
  const ss = SpreadsheetApp.getActive();
  const source = ss.getSheetByName(srcName);
  const rows = source.getLastRow() - 1;
  const half = Math.ceil(rows / 2);
  const data = source.getRange(2,1,rows,2).getValues();
  ss.getSheetByName(targets[0]).getRange(2,1,half,2).setValues(data.slice(0, half));
  ss.getSheetByName(targets[1]).getRange(2,1,rows - half,2).setValues(data.slice(half));
  SpreadsheetApp.flush();
}


情境 B:寫入公式→等待重算→讀回結果

flush() 會把公式寫入,但不保證重算完成。做法是:flush() 後以短暫延遲+條件輪詢確認目標儲存格已呈現預期格式或值,再繼續流程。

function writeFormulaAndWait() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('工作表1');
  sheet.getRange('B2').setValue('=SUM(A2:A2001)');
  SpreadsheetApp.flush(); // 先確保公式已落地

  // 最多等 5 秒,直到 B2 不是空白/不是 # 開頭錯誤
  const deadline = Date.now() + 5000;
  while (Date.now() < deadline) {
    const v = sheet.getRange('B2').getDisplayValue();
    if (v && !String(v).startsWith('#')) break;
    Utilities.sleep(200);
  }
  const result = sheet.getRange('B2').getValue();
  Logger.log('總和:' + result);
}


情境 C:多執行緒(onEdit / 定時)+鎖控管

多個觸發器同時寫表,容易互相覆蓋。建議用 LockService 取得鎖 → 寫入 → flush() → 釋放鎖,確保別的執行緒看到的是一致狀態。官方也建議在處理試算表時釋放鎖前呼叫 flush()。

function onEdit(e) {
  const lock = LockService.getDocumentLock();
  lock.waitLock(10_000);
  try {
    const sheet = e.range.getSheet();
    sheet.getRange('F1').setValue(new Date()); // 更新最後修改時間
    SpreadsheetApp.flush(); // 在釋放鎖前提交所有更動 :contentReference[oaicite:15]{index=15}
  } finally {
    lock.releaseLock();
  }
}


情境 D:執行期間即時顯示進度

把進度寫到某格,flush() 讓使用者在畫面上即時看到更新(例如處理一萬筆時每 5% 更新一次)。

function progressDemo() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('工作表1');
  const total = 10000;
  for (let i = 1; i <= total; i++) {
    // ...處理第 i 筆...
    if (i % 500 === 0) {
      sheet.getRange('H1').setValue(`處理中… ${(i/total*100).toFixed(0)}%`);
      SpreadsheetApp.flush(); // 讓使用者馬上看到進度
    }
  }
  sheet.getRange('H1').setValue('完成');
  SpreadsheetApp.flush();
}


常見錯誤與雷點

1.    把 flush() 當「存檔」

    不是。Apps Script 自動保存腳本;flush() 是把對試算表的變更「立即套用」。

2.    每寫一次就 flush(),效能崩

    在迴圈內逐格寫+逐次 flush() 會很慢,也可能更容易撞到配額與超時。請改成批次收集→一次 setValues()。官方最佳實務也強調避免逐格操作。

3.    自訂函式想改別處,指望 flush() 幫忙

    無效。自訂函式不能任意改動其他儲存格(只能回填自身/相鄰範圍),這是引擎級限制。

4.    以為 flush() 等於「等公式算完」

    不是。若需要確定結果可用,請加短暫等待並檢查條件(如非空、非錯誤字串),或設計流程避免立刻依賴複雜公式結果。

5.    跨檔寫讀的順序混亂

    SpreadsheetApp.flush() 針對當前檔案的變更生效。若你同時用 openByUrl()/openById() 操作不同檔案,請分別在各自操作批次後呼叫一次,確保上下文正確。(此為實務建議;避免在多文件間交錯寫讀。)

6.    忽略鎖導致競態

    多觸發器/多人同時啟動腳本時,未用鎖就 flush(),仍可能互相踩到。請用 LockService 先鎖住、寫入、flush()、再釋放。官方文件明確建議此順序。


效能最佳化:搭配批次寫入與「少 flush、但用得準」

1.    一次取出/一次寫入:

      把要寫入的資料先放在二維陣列,用單次 setValues() 落地;盡量避免逐格 setValue()。

2.    把 flush() 放在「邏輯分段點」:

    例如每一萬筆一段、或「寫完→讀回」的切換點。

3.    需要多類更動時,分批組塊:

    格式變更、資料寫入、保護範圍等先各自批次處理,再於段落尾端 flush()。

4.    Sheets API 的批次更新:

    若你走 Advanced Sheets API,亦可用 spreadsheets.batchUpdate 合併多種請求,降低往返成本。


問題集

Q1:我什麼時候「一定」要加 flush()?

A:寫完就要讀、要給使用者立刻看到畫面變化、或釋放鎖前。其它時候盡量少用。

Q2:加很多次 flush() 不是更安全嗎?

A:安全未必增加,效能一定下降,還可能更容易撞到配額/時限。請用「少量且關鍵點」的呼叫,並配合批次寫入。

Q3:自訂函式裡 flush() 可以讓我改別的儲存格嗎?

A:不行。這是自訂函式規範,與是否 flush() 無關。

Q4:flush() 能保證公式算完嗎?

A:不能;它只保證寫入落地。需要就加短暫等待+條件檢查。


張貼留言 (0)
較新的 較舊