我從Vue源碼中學到的一些JS編程技巧

在咱們面試的過程當中,常常會遇到問源碼的環節,由於優秀的框架一般都會包含不少設計理念跟編程實踐。這段時間我一直在看Vue2的源碼,發現了不少有意思的實現。雖然如今Vue3都已經發布了,也沒法否定Vue2是個優秀的框架這個事實,不影響咱們從中學到一些最佳實踐。javascript

Vue不感興趣的同窗也能夠看看,由於我只是談論一些我從這個框架的實現上學到的一些JavaScript的用法,不涉及Vue的概念。html

  1. 獲取HTML格式的字符串中非標籤文本(vue/src/compiler/parser/entity-decoder.js)

假設咱們有這樣一個字符串:前端

var html = '<span class="red">hello world</span> <span>hello xxx</span>'。咱們如今想要提取其中非標籤的文本,拿到以下結果:vue

'hello world hello xxx'。這該怎麼辦?咱們首先想到的確定是正則表達式,可是這個場景下正則表達式寫起來確定很煩,咱們來看看Vue的開發者是怎麼處理的:java

  • 既然這個字符串是HTML文本格式,咱們就能夠把它解析成對應的HTML元素。
  • HTML元素的textContent屬性能夠用來獲取HTML元素中的文本內容。

代碼以下:android

function decoder(html){
  let decoder = document.createElement('div')
  decoder.innerHTML = html
  console.log(decoder.textContent)
  // return decoder.textContent
}
複製代碼

這個代碼建立了一個div元素做爲容器,而後經過設置innerHTML把字符串轉換成對應的HTML元素,最後就能夠經過textContent屬性來獲取文本內容了。ios

  1. 肯定運行環境(vue/src/core/util/env.js)

隨着前端的高速發展,咱們已經能夠在多個環境中運行JavaScript代碼,爲了針對不一樣的運行環境做出調整,咱們須要知道咱們的代碼跑在哪一個環境下,咱們來看看Vue是怎麼肯定運行時環境的:面試

const inBrowser = typeof window !== 'undefined'
const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform
const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
const UA = inBrowser && window.navigator.userAgent.toLowerCase()
const isIE = UA && /msie|trident/.test(UA)
const isIE9 = UA && UA.indexOf('msie 9.0') > 0
const isEdge = UA && UA.indexOf('edge/') > 0
const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
const isPhantomJS = UA && /phantomjs/.test(UA)
const isFF = UA && UA.match(/firefox\/(\d+)/)
複製代碼

若是咱們的代碼是運行在瀏覽器中,那咱們確定會拿到一個window對象,因此咱們能夠經過const inBrowser = typeof window !== 'undefined'這種方式來判斷環境。 正則表達式

並且在瀏覽器中,咱們能夠經過window對象拿到瀏覽器的userAgent 不一樣的瀏覽器對應的userAgent也不一樣,像IEuserAgent老是會包含MSIE,而ChromeuserAgent會包含Chrome。相似地安卓系統的瀏覽器userAgent就會帶Android。那咱們經過userAgent就能夠判斷當前用的是什麼瀏覽器,運行在什麼操做系統上。上面的代碼中已經列舉出了對主流的瀏覽器跟操做系統的判斷,注意因爲Edge瀏覽器最新版本也基於Chromium內核,因此它的userAgent也會包含Chrome,因此咱們要寫const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge這樣的代碼來判斷當前環境是Chromechrome

  1. 肯定一個函數是否是用戶自定義的(vue/src/core/util/env.js)

通常咱們使用的就兩種函數,環境提供給咱們的跟咱們用戶本身定義的,這兩種函數在轉換成字符串時表現形式是不一樣的:

Array.isArray.toString() // "function isArray() { [native code] }"
function fn(){} 
fn.toString() // "function fn(){}"
複製代碼

環境自帶函數調用toString方法後老是會返回相似function fnName() { [native code] }格式的字符串,咱們能夠利用這一點來區分函數類型:

function isNative (Ctor){
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
複製代碼
  1. 實現只執行一次的函數(vue/src/shared/util.js)

不少時候咱們須要一個函數只被執行一次,就算它被調用屢次,也只有第一次調用時會被執行,因此咱們能夠寫出以下代碼:

function once (fn) {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
複製代碼

這樣後續再執行時咱們會直接跳過,這裏是使用高階函數來實現的,感興趣的能夠看看我以前的文章JavaScript高級技巧。咱們來測試一下這個方法:

能夠看到test方法只被執行了一次。

  1. 緩存函數執行結果(vue/src/shared/util.js)

這個我也在以前的博客中提到過的,有時候函數執行比較耗時,咱們想緩存執行的結果。這樣當後續被調用時,若是參數相同,咱們能夠跳過計算直接返回結果。咱們須要的就是實現一個cached函數,這個函數接受實際被調用的函數做爲參數,而後返回一個包裝的函數。在這個cached函數裏,咱們能夠用一個對象或者Map來緩存結果。

function cached(fn){
  const cache = Object.create(null);
  return function cachedFn (str) {
    if ( !cache[str] ) {
        let result = fn(str);
        cache[str] = result;
    }
    return cache[str]
  }
}
複製代碼

  1. 轉換命名風格(vue/src/shared/util.js)

咱們每一個人使用的編程風格可能都不同,有人喜歡駝峯寫法,有人喜歡小橫槓鏈接符,爲了解決這個問題,咱們能夠寫一個函數去作統一的轉換。(好比把a-b-c轉換成aBC)

const camelizeRE = /-(\w)/g
const camelize = cached((str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
camelize('a-b-c')
// "aBC"
複製代碼
  1. 肯定對象的類型(vue/src/shared/util.js)

在JavaScript中,有六種基本類型(Boolean, Number, String, Null, Undefined, Symbol)跟一個對象類型,但其實對象類型是能夠細分到許多類型的,一個對象能夠是數組,也能夠是函數等等。咱們有沒有辦法得到它確切的類型呢?

咱們能夠利用Object.prototype.toString把一個對象轉換成一個字符串,若是是咱們用{}建立的對象,這個方法老是返回[object Object]

而對於數組,正則表達式等環境自帶的對象類型,它們會返回不一樣的結果。

基於這個特性咱們能夠判斷一個對象是否是咱們用{}建立的對象了:

function isPlainObject (obj){
  return Object.prototype.toString.call(obj) === '[object Object]'
}
複製代碼

並且咱們注意到,Object.prototype.toString()的返回值老是以[object tag]的形式出現,若是咱們只想要這個tag,咱們能夠把其餘東西剔除掉,這邊比較簡單用正則或者String.prototype.slice()均可以。

function toRawType (value) {
    const _toString = Object.prototype.toString
    return _toString.call(value).slice(8, -1)
}
toRawType(null) // "Null"
toRawType(/sdfsd/) //"RegExp"
複製代碼

這樣咱們就能夠拿到一個變量的類型了。

  1. 把值轉換成字符串(vue/src/shared/util.js)

咱們常常須要把一個值轉換成字符串,在JavaScript裏面,咱們有兩種方式來獲得字符串:

  • String()
  • JSON.stringify()

不過這兩種方式的實現機制是不一樣的:

咱們裏看到,他們是基於徹底不一樣的規則去轉換字符串的,String(arg)會嘗試調用arg.toString()或者arg.valueOf(),那麼那咱們該用哪一個比較好?

  • 對於nullundefined,咱們但願把它轉成空字符串

  • 當轉換一個數組或者咱們建立的對象時,咱們會使用JSON.stringify

  • 若是對象的toString方法被重寫了,那咱們會偏向使用String()

  • 其它狀況下,通常都用String()

    爲了匹配上面的需求,Vue開發者是這麼實現的:

    function isPlainObject (obj){
      return Object.prototype.toString.call(obj) === '[object Object]'
    }
    function toString (val) {
      if(val === null || val === undefined) return ''
    if (Array.isArray(val)) return JSON.stringify(val)
    if (isPlainObject(val) && val.toString === Object.prototype.toString)
        return JSON.stringify(val)
      return String(val)
    }
    複製代碼

    又是收穫滿滿的一天,經過閱讀優秀框架的代碼實現能夠快速地提升咱們對語言的運用,增強咱們對於一些特性的理解,總結出一些編程實踐,咱們的編程能力也在無形中獲得質的飛躍,很是建議你們深刻學習一門語言時就去閱讀用那個語言實現的優秀代碼。對於JavaScript而言咱們光討論了Vue的這三個源碼文件就學到這麼多東西,還有比這更開心的事嗎?但願本文也能給你們帶來一些幫助,happy coding~

相關文章
相關標籤/搜索