當時剛寫GAS專案時,依寫前端專案的習慣,把一些被重複使用的變數抽出、重構至全域,可是卻出現預期外的錯誤。繞了一圈才發現,啊,GAS環境的變數的生命週期跟瀏覽器不一樣!
其實這篇文章的重點就一句:「One execution is an isolated clean session.」,並且這篇官方文件也有提供官方解決方案。
好的,下述都可以略過了。
什麼?你竟然對於我踩雷與摸索的思路歷程感興趣?Welcome, welcome. どうぞお座りください。[^1]
這是我用瀏覽器環境的直覺在寫GAS環境的腳本時,前期踩到的首批雷之一:把一些被重複使用的變數抽出、重構至全域,可是卻出現預期外的錯誤。
那時非常之困惑,在Stack Overflow搜尋到的這篇文章提到:
You won't like this: global variables in GAS are static - you can't update them and expect them to retain their values. I, too, googled this for hours.
進一步研究後,弄明白實際上不僅僅是全域變數(global variables),而是所有變數都會在腳本執行後「被重置」。[^2]
瀏覽器環境的變數會於頁面(Document)重新載入或關閉時死去,前進、後退時則不一定,有可能死去或僅暫時被凍結;Node.js環境/Deno環境/Python環境(作為網頁後端)則於腳本執行(process)完畢後死去。[^3]
不同於JavaScript,VBA則可以透過Dim
/Static
來決定變數是否可以跨執行存續,其中Static
於Excel被關閉時才死去。
至於GAS環境的變數之生命週期最短,僅生於函式被觸發,並死於函式執行完畢。
這段是我主觀的推測性觀點,僅表達我個人的心智地圖。[^4]
現在讓我試著從雲服務的角度來看,GAS環境是建立在GCP(Google Cloud Platform)上,所以GAS環境並不是一個獨立的infra,可以把它視為Google Workspace的serverless runtime。
每次函式被觸發,GCP上會建立一個無狀態沙盒,而當函式執行完畢,沙盒就會被回收、釋出記憶體,於是就結果來看,全域變數就「被重置」了。
我們可以把GAS環境視為一種被層層包裝好的Cloud Functions,專門服務Google Workspace上的應用程式。
更具體來說,不同於VBA腳本的變數會在Excel使用期間存續,在GAS環境即使該Google Sheet仍被使用中,也不會有任何有效的變數存續著,變數僅僅在函式被觸發的那段時間暫時存在,執行完畢後就死亡。
我們來看個簡單的例子:
let globalVariables = 0;
function A() {
globalVariables++
}
function B() {
globalVariables++
}
function all() {
A();
B();
console.log(globalVariables); //2
}
當執行all()
時,GAS環境的執行步驟如下:
globalVariables
all
all
呼叫了A
和 B
,這兩個函式在同一個沙盒裡被執行,因此全域變數 globalVariables
的值不會在A
和B
之間被重置globalVariables
不僅是失去了值,連globalVariables
本身也不在GCP的記憶體裡換句話說,執行腳本時,全域變數只會在該次執行環境中初始化一次,但若重新執行腳本,全域變數將會被重置。
那麼,若需要在多次執行之間保留變數的值,官方文件推薦使用 PropertiesService
或 Google Sheets保存。我個人覺得,如果團隊成員需要自行修改該數值時,就用Google Sheets;反之就用PropertiesService
,除了邏輯寫起來更簡單,還可以避免使用者預期外的操作而導致意想不到的錯誤。
現在回顧覺得,踏入一個新的環境,了解生命週期是很基本的事項之一,但那時就憑著都是JavaScript的直覺寫。至少透過這個經驗,讓我對於腳本語言與運行環境的概念,有更清楚的區分意識。
此外,一直都知道應該先閱讀官方文件,但當時就是習慣去找社群討論,從:「全域變數為什麼被重置了?」→「難道全域變數有不同的特性?」→「GAS環境是無狀態?」→「infra的stateless是什麼意思?」→「原來官方文件早就有給針對此常見需求的API」
繞了一大圈,包含重構本身都是徒勞。
瀏覽社群討論的缺點是,如果對於GAS平台的發展歷程不清楚,可能只是跟著前人重走一段歷史。[^5]
我覺得有趣的是,從我既有的知識,stateless之於函式和之於infra,當然是完全不同的概念。不過從結果上來理解,例如helper function這類最常見的無狀態函式,也是每次執行時才代入暫時存在的變數,執行後函式本身就沒有留存的變數,我是從這個角度去類比看待GAS環境的stateless特性。
[^1]:如第一天(Day01)提到的,參賽時原本是想回顧我誤會的、我踩過的坑。今天原本打算開始回顧我以前的實作,但不知道為什麼翻閱以前的筆記在情緒上有各種預料之外的阻力,可能有種離職後還在加班的錯覺?總之我決定放棄去找以前踩坑的那段refactored snippet,在沒有實例的前提下,隨興談談。
[^2]: 此文章所謂「被重置」僅就腳本撰寫者可觀察的角度而言,實際上仍依Google官方對於記憶體的具體操作而定。
[^3]: 撰文時才意識到,其實自己對於網頁開發環境的變數之生命週期也不是真的那麼清楚,例如Realm、Worker或bfcache都是很久以前剛碰JavaScript時稍微瀏覽過而已。實作上現在都透過React的hook宣告我對於各states的需求,讓前端腳架去處理生命週期。
[^4]: 這篇文章是修改自原本打算留給前公司後輩的小筆記,但現在重寫才發現當時的自己其實也是一知半解,稍微使用過gcloud
後,才從GCP的視角進一步理解GAS環境。這一段並非基於官方contract,且自己也只使用Apache這類傳統伺服器,歡迎有AWS/GCP/Azure等實務經驗的人指正錯誤。
[^5]: 現在寫起來很單純,不過當時真的感覺在解謎,不知道到底發生什麼事😅