:::

​如何在前端瀏覽器用JavaScript處理過長的字串變數?活用虛擬DOM作為快取 / How to use JavaScript to deal with long string variables in the front-end browser? Use Virtual DOM as cache

18-JavaScript_DOM_How_to_use_JavaScript.png

我會在網頁上使用SheetJS js-xlsx讀取ODS試算表檔案的內容,但最近發現檔案如果超過2.5MB,在載入檔案輸入到字串的時候,就會造成瀏覽器崩潰當機。

研究之後,我想到可以將字串建立成虛擬DOM上的節點,用來緩存過長的內容。在需要全部字串內容時,再一一從節點取得緩存即可。

以下我用兩個實際上的網頁作為例子,一個用來說明沒有緩存的過長字串導致瀏覽器當掉的情況,另一個是用虛擬DOM緩存,最後可以正常執行的情況。


系統環境 / Environment

2020-0428-140802-OZ-ou-caches-Manboard-Memory-PD.png 2020-0428-141024-Cou-Caches-Mainboard-Memory-Graphics.png

這個實驗用的電腦環境是Windows 7的64位元版本,CPU是Intel Core i7 3770,記憶體是DDR3 16GB。瀏覽器是Google Chrome的版本 81.0.4044.122 (正式版本) (64 位元)。

如果你的硬體設備不一樣,而你瀏覽器不會當機的話,請在下面留言跟我說,感謝。


瀏覽器當掉:沒有緩存的過長字串 / Browser crashed: Long string variables with no cache

2020-0428-142038-Save-too-long-string-to-variable.png

這個網頁裡面的JavaScript會宣告一個out字串變數,然後加入347812747次「-」,那個「10%」就是現在加到幾次。為了避免整個畫面定住不能操作,我還加入了非同步的設計,但這跟這次的主題沒有直接關係。

我刪掉不相關的程式碼之後,主要程式碼如下:

var main = async function () {
  console.log('start')
  var out = ''
        
  var limit = 347812747
  for (var i = 0; i < limit; i++) {
    out += '-'
  }
      
  document.write('Finish: ' + out.length)
}

按下「START」按鈕,網頁就會開始運作。當這個迴圈處理到一半左右,瀏覽器就會因為「Out of memory」(記憶體不足)而當機。整個流程動畫如下:

no-cache.gif

這可能是「out += '-'」導致的錯誤。我們也許不能在單一變數裡面加入太長的字串。

我試過拆開成多個變數,或是把它存成陣列,一樣是不能順利跑完迴圈,跑到一半就會當掉。後來我才想到,我們還有DOM可以拿來做快取呢。


成功處理過長字串:使用虛擬DOM緩存 / Deal with long string variables with virtual DOM cache

2020-0428-144831.png

這個網頁的JavaScript跟前面做的事情差不多,差別在於,迴圈每10000000次的時候,就會將字串內容轉存到新的網頁節點<textarea>裡面,並擺在網頁<body>的最後面隱藏起來。主要程式碼如下,我用紅字標示跟前面程式碼主要差別的語法:

var main = async function () {
  var out = ''
  var cache
  var cacheList = []

  var limit = 347812747
  for (var i = 0; i < limit; i++) {
    if (i % 10000000 === 0 && out !== '') {
      cache = document.createElement('textarea')
      cache.value = out
      cache.style.display = 'none'
      cacheList.push(cache)

      out = ''
    }

    out += '-'
  }

  var tempOut = out
  out = ''

  for (i = 0; i < cacheList.length; i++) {
    out += cacheList[i].value
    cacheList[i].remove()
  }
  out += tempOut


  document.write('Finish: ' + out.length)
}

像是<textarea>這種HTML元素是網頁上的文件物件模型(Document Object Model, DOM)。對Google Chrome來說,處理DOM的記憶體和處理JavaScript的記憶體似乎是分開處理。將JavaScript中過長的字串變數改為DOM之後,就不會發生記憶體不足的情況。除了使用<textarea>之外,也可以使用<div>來保存資料,似乎是一樣的效果。

值得注意的是,我雖然宣告了<textarea>元素,但並沒有把它寫入到網頁上,而是讓它保持虛擬DOM (Virtual DOM)的狀態。反之,如果把它寫入到網頁上,網頁效率會被大量拖慢,甚至會讓瀏覽器看起來像是要當機一樣。所以還是讓它維持虛擬DOM就好。

接下來讓我們來看看整個操作流程。跟前面的程式碼一樣,按下「START」按鈕,網頁就會開始運作。整個操作流程如下面的錄影,最後看到「Finish: 347812747」就表示程式跑完了。

with-dom.gif

為什麼最後只顯示out的字串長度呢?我試過把out的內容用document.write(out)寫在網頁上,但這會導致網頁當機。這似乎也是表示一次處理過長的字串時會出現問題的意思。不過這個out變數在其他JavaScript程式碼中是可以正常使用,像是我可以用out.length來取得它的字串長度。


結語 / Conclusion

這篇文章說明了在網頁使用JavaScript處理過長字串中,使用虛擬DOM來做快取來避免記憶體不足、瀏覽器當機的情況。經過這次經驗,我才真正感受到現在主流的JavaScript技術採用虛擬DOM (Virtual DOM)的理由。出乎意料之外的好用。

其他技巧:非同步的睡眠 / sleep()

這篇除了用虛擬DOM來做緩存之外,我還在迴圈執行過程中加入了sleep(),使網頁不會因為迴圈執行過久導致網頁畫面結凍、被使用者誤認為當機。這個技巧在「JavaScript async/await 的奇淫技巧」這篇裡面也有講到,該篇將它命名為「delay()」。

我宣告的sleep()程式碼如下:

function sleep () {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(true)
    }, 10)
  })
}

使用的語法如下:

await sleep()

因為這裡用到了async/await,你程式碼裡面的函式要記得改成非同步的async,而這個語法在Internet Explorer裡面無法運作之外,其他主流瀏覽器都能正常運作。關於瀏覽器相容性,請看MDN web docs的說明。


那麼這次關於過長字串的處理方式就到這裡了。寫到最後,我有些問題想問問大家:

  • 你有遇到過JavaScript程式碼導致瀏覽器記憶體不足的情況嗎?那是什麼情況呢?
  • 遇到上述狀況時,你都是怎麽處理的呢?

歡迎在下面的留言處跟我們分享你的想法。大家的意見是我繼續分享的動力喔!如果你覺得我這篇實用的話,請幫我在AddThis分享工具按讚、將這篇分享到Facebook等社群媒體吧!

感謝你的耐心閱讀,我是布丁,讓我們下一篇見。