2018年7月6日 星期五

淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異

淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異
Underscore.js是一個很精幹的庫,壓縮後只有5.2KB。它提供了幾十種函數式編程的方法,彌補了標準庫的不足,大大方便了JavaScript的編程。
本文僅探討Underscore.js的兩個函數方法 _.throttle 和 _.debounce 的原理、效果和用途。
通常的函數(或方法)調用過程分為三個部分:請求、執行和響應。(文中“請求”與“調用”同義,“響應”與“返回”同義,為了更好的表述,刻意採用請求和響應的說法。)
某些場景下,比如響應鼠標移動或者窗口大小調整的事件,觸發頻率比較高。若稍處理函數微複雜,需要較多的運算執行時間,響應速度跟不上觸發頻率,往往會出現延遲,導致假死或者卡頓感。
在運算資源不夠的時候,最直觀的解決辦法就是升級硬件,誠然通過購買更好的硬件可以解決部分問題,但是也需要為此付出高額的成本。特別是客戶端和服務器模式,要求客戶端統一升級硬件基本不可能。
在資源有限的前提下,處理函數無法即時響應高頻調用。退而求其次,只響應部分請求是否可行呢?某些場景下的密集性請求,具備很強的同質和連續性。比如說,鼠標移動的軌跡參數。響應越及時效果越平滑,但是如果響應速度跟不上時,反而會出現卡頓感,如果適當的丟棄一些請求效果更流暢。
throttle 和 debounce 是解決請求和響應速度不匹配問題的兩個方案。二者的差異在於選擇不同的策略。

電梯超時

想像每天上班大廈底下的電梯。把電梯完成一次運送,類比為一次函數的執行和響應。假設電梯有兩種運行策略 throttle和 debounce ,超時設定為15秒,不考慮容量限制。
  • throttle 策略的電梯。保證如果電梯第一個人進來後,15秒後準時運送一次,不等待。如果沒有人,則待機。
  • debounce 策略的電梯。如果電梯裡有人進來,等待15秒。如果又人進來,15秒等待重新計時,直到15秒超時,開始運送。

使用示例

_.throttle 使用示例

function log( event ) {
  console.log( $(window).scrollTop(), event.timeStamp );
};

// 控制台記錄窗口滾動事件,觸發頻率比你想像的要快
$(window).scroll( log );

// 控制台記錄窗口滾動事件,每250ms最多觸發一次
$(window).scroll( _.throttle( log, 250 ) );

_.debounce 使用示例

function ajax_lookup( event ) {
  // 對輸入的內容$(this).val()執行 Ajax 查詢
};

// 字符輸入的頻率比你預想的要快,Ajax 請求來不及回覆。
$('input:text').keyup( ajax_lookup );

// 當用戶停頓250毫秒以後才開始查找
$('input:text').keyup( _.debounce( ajax_lookup, 250 ) );

underscore源碼註解

讓我們來讀讀源碼,探其究竟。基於開發版本(1.7.0)的源碼,加上了一些註釋以幫助理解。
_.throttle方法源碼
/**
 * 頻率控制 返回函數連續調用時,func 執行頻率限定為 次 / wait
 * 
 * @param  {function}   func      傳入函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始邊界上的調用,傳入{leading: false}。
 *                                如果想忽略結尾邊界上的調用,傳入{trailing: false}
 * @return {function}             返回客戶調用函數   
 */
_.throttle = function(func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 上次執行時間點
  var previous = 0;
  if (!options) options = {};
  // 延遲執行函數
  var later = function() {
    // 若設定了開始邊界不執行選項,上次執行時間始終為0
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = _.now();
    // 首次執行時,如果設定了開始邊界不執行選項,將上次執行時間設定為當前時間。
    if (!previous && options.leading === false) previous = now;
    // 延遲執行時間間隔
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 延遲時間間隔remaining小於等於0,表示上次執行至此所間隔時間已經超過一個時間窗口
    // remaining大於時間窗口wait,表示客戶端系統時間被調整過
    if (remaining <= 0 || remaining > wait) {
      clearTimeout(timeout);
      timeout = null;
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    //如果延遲執行不存在,且沒有設定結尾邊界不執行選項
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};
_.debounce方法源碼
/**
 * 空閒控制 返回函數連續調用時,空閒時間必須大於或等於 wait,func 才會執行
 *
 * @param  {function} func        傳入函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置為ture時,調用觸發於開始邊界而不是結束邊界
 * @return {function}             返回客戶調用函數
 */
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result;

  var later = function() {
    // 據上一次觸發時間間隔
    var last = _.now() - timestamp;

    // 上次被包裝函數被調用時間間隔last小於設定時間間隔wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      // 如果設定為immediate===true,因為開始邊界已經調用過了此處無需調用
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function() {
    context = this;
    args = arguments;
    timestamp = _.now();
    var callNow = immediate && !timeout;
    // 如果延時不存在,重新設定延時
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
};

可視化演示

示例中每一行都以30ms的速度繪製時間軸,第一行Mousemove Events是參考基準,以50ms每次的響應頻率,在時間軸上輸出循環可見ASCII碼字符。
當鼠標進入左側方型區域(mouseenter 事件)所有行開始繪製時間軸, 鼠標晃動(mousemove 事件)會在時間軸上繪製字符塊,每個字符塊表示事件被觸發一次。為了展現延遲觸發效果,相鄰字符塊的演示和文字是不同的。
頂部的兩個按鈕100毫秒觸發1200毫秒觸發2演示以固定頻率勻速觸發事件的效果。

使用場景

只要牽涉到連續事件或頻率控制相關的應用都可以考慮到這兩個函數,比如:
  • 遊戲射擊,keydown 事件
  • 文本輸入、自動完成,keyup 事件
  • 鼠標移動,mousemove 事件
  • DOM 元素動態定位,window 對象的 resize 和 scroll 事件
前兩者 debounce 和 throttle 都可以按需使用;後兩者肯定是用 throttle 了。如果不做過濾處理,每秒種甚至會觸發數十次相應的事件。尤其是 mousemove 事件,每移動一像素都可能觸發一次事件。如果是在一個畫布上做一個鼠標相關的應用,過濾事件處理是必須的,否則肯定會造成糟糕的體驗。

參考閱讀

  1. UNDERSCORE.JS
  2. 高階函數 debounce 和 throttle
  3. jQuery throttle / debounce: Sometimes, less is more!
  4. Debounce and Throttle: a visual explanation
資料來源:https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs

2018年6月29日 星期五

網頁文字單位,px、em、rem、% 的差別

常常在設定文字大小的時候,最常用的就是這四個單位:px、em、rem、%。 簡單比較這四者的差異?
這裡列出幾個常見的問題:

哪一個是絕對單位?哪一個是相對單位?

px 是絕對單位;
em 是相對單位,其文字大小是相對於父元素 (parent element);
rem 是相對單位,其文字大小是相對於根元素 (root element),根元素就是 <html> 標籤,所以是相對於該標籤所指定的文字大小;
% 是相對單位。
 
文字大小該如何計算?
px 是絕對單位,指定 20px,那該文字就是 20px;
em 是相對單位,通常網頁的 <body> 都會預設文字大小為 16px,直接用舉例的方式來實際計算文字大小:
// 先看 html 結構部份
<body>
  <h1>標題<span>標題內小字</span></h1>
</body>

// css 樣式,指定文字大小
<style>
body{
  font-size: 75%; /* 將預設文字大小改成:12px。(12 / 16 * 100% = 75%) */
}
h1{
  font-size: 2em; /* 實際:12px * 2 = 24px,因為其父元素是 <body> 標籤。 */
}
span{
  font-size: 0.5em; /* 實際:24px * 0.5 = 12px,因為其父元素是 <h1> 標籤。 */
}
</style>
rem(是 root em 的縮寫):是相對於根元素的文字大小單位,什麼是根元素,也就是 <html> 標籤:
// 先看 html 結構部份
<html>
  <body>
    <h1>標題<span>標題內小字</span></h1>
  </body>
</html>

// css 樣式,指定文字大小
<style>
html{
  font-size: 62.5%; /* 將根元素的預設文字大小改為 10px。(10 / 16 * 100% = 62.5%) */
}
h1{
  font-size: 2rem; /* 實際:10px * 2 = 20px,因為 rem 是相對於根元素 */
}
span{
  font-size: 1rem; /* 實際:10px * 1 = 10px,因為 rem 是相對於根元素 */
}
</style>
資料來源:
http://carlos-studio.com/2017/12/26/design-%E7%B6%B2%E9%A0%81%E6%96%87%E5%AD%97%E5%96%AE%E4%BD%8D%EF%BC%8Cpx%E3%80%81em%E3%80%81rem%E3%80%81-%E7%9A%84%E5%B7%AE%E5%88%A5/

2017年4月6日 星期四

[NODEJS] COOKIE的使用

在網站裡常會使用一些cookie來對使用者所瀏覽的資訊做記憶,而這些資訊主要是以不會有安全疑慮的才適合儲存,因為cookie可以透過瀏覽器所觀看到,且容易進行竄改。
要在node.js(EXPRESS框架)中使用cookie,必須先透過npm將cookie模組進行安裝。
npm install --save cookie-parser
接著在程式碼中使用cookie-parser。

2017年3月25日 星期六

如何用webrtc建立一個視訊聊天室網頁應用程式

簡介

WebRTC(Web即時通信)是目前由Google,Mozilla和Opera支持的一種新的Web標準。它允許瀏覽器之間的點對點通信。其目的是為瀏覽器,移動平台和網路物件Web of Things(WoT)提供更豐富的高質量RTC應用程序,並允許他們通過一組通用的協議進行通信。

Web的最後一個主要挑戰是通過語音和視頻實現人與人通信,而無需使用特殊的插件並且無需支付任何費用使用這些服務。

第一個WebRTC實施是由愛立信於2011年5月建立的。 WebRTC定義了用於實時,無插件視頻,音頻和數據通信的開放標準。

目前已經有許多Web服務已經使用RTC,但還是需要下載應用程序或相關插件。

其中包括Skype,Facebook(使用Skype)和Google Hangouts(使用Google Talk插件)。下載、安裝和更新插件的程序可能會很複雜,容易出錯和惱人,並且通常很難說服人們首先安裝插件!

2017年3月22日 星期三

npm-check

npm-check

檢查 package 是否有沒有的新版本的小工具,同時取得的最新版會依 SemVer(語意化版本規範)規則進行。

packagelink

數位版權管理 (DRM, Digital Right Management)

數位出版時代來臨,面對傳播更為容易的數位內容,為保護智慧財產權,數位版權管理(Digital Right Management,DRM)的概念應運而生,這是一種用來保護數位內容使用的管理機制,透過加密認證等過程確認使用者是合法使用數位內容,透過在文件上加浮水印、限制使用時間、使用載具限制、透過取得授權等方式來保護數位內容。

2017年3月20日 星期一

如何在MacOSX中安裝MongoDB資料庫

這裡介紹如何在 Mac OS X 中安裝 MongoDB 資料庫。
在 Mac OS X 中安裝 MongoDB 資料庫有兩種方式:
一種是使用 Homebrew,另一種是手動安裝,以下是兩種安裝方式的步驟。