我從Vue原始碼中學到的一些JS程式設計技巧
在我們面試的過程中,經常會遇到問原始碼的環節,因為優秀的框架通常都會包含很多設計理念跟程式設計實踐。這段時間我一直在看Vue2
的原始碼,發現了很多有意思的實現。雖然現在Vue3
都已經發布了,也無法否認Vue2
是個優秀的框架這個事實,不影響我們從中學到一些最佳實踐。
對Vue
不感興趣的同學也可以看看,因為我只是談論一些我從這個框架的實現上學到的一些JavaScript
的用法,不涉及Vue
的概念。
- 獲取HTML格式的字串中非標籤文字(vue/src/compiler/parser/entity-decoder.js)
假設我們有這樣一個字串:
var html = '<span class="red">hello world</span> <span>hello xxx</span>'
'hello world hello xxx'
。這該怎麼辦?我們首先想到的肯定是正則表示式,但是這個場景下正則表示式寫起來肯定很煩,我們來看看Vue
的開發者是怎麼處理的:
- 既然這個字串是
HTML
文字格式,我們就可以把它解析成對應的HTML
元素。 HTML
元素的textContent
屬性可以用來獲取HTML
元素中的文字內容。
程式碼如下:
function decoder(html){
let decoder = document.createElement('div')
decoder.innerHTML = html
console.log(decoder.textContent)
// return decoder.textContent
}
複製程式碼
這個程式碼建立了一個div
元素作為容器,然後通過設定innerHTML
把字串轉換成對應的HTML
元素,最後就可以通過textContent
屬性來獲取文字內容了。
- 確定執行環境(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
也不同,像IE
的userAgent
總是會包含MSIE
,而Chrome
的userAgent
會包含Chrome
。類似地安卓系統的瀏覽器userAgent
就會帶Android
。那我們通過userAgent
就可以判斷當前用的是什麼瀏覽器,執行在什麼作業系統上。上面的程式碼中已經列舉出了對主流的瀏覽器跟作業系統的判斷,注意由於Edge
瀏覽器最新版本也基於Chromium
核心,所以它的userAgent
也會包含Chrome
,所以我們要寫const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
這樣的程式碼來判斷當前環境是Chrome
。
- 確定一個函式是不是使用者自定義的(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())
}
複製程式碼
- 實現只執行一次的函式(vue/src/shared/util.js)
很多時候我們需要一個函式只被執行一次,就算它被呼叫多次,也只有第一次呼叫時會被執行,所以我們可以寫出如下程式碼:
function once (fn) {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
複製程式碼
這樣後續再執行時我們會直接跳過,這裡是使用高階函式來實現的,感興趣的可以看看我之前的文章JavaScript高階技巧。我們來測試一下這個方法:
可以看到test
方法只被執行了一次。
- 快取函式執行結果(vue/src/shared/util.js)
這個我也在之前的部落格中提到過的,有時候函式執行比較耗時,我們想快取執行的結果。這樣當後續被呼叫時,如果引數相同,我們可以跳過計算直接返回結果。我們需要的就是實現一個cached
函式,這個函式接受實際被呼叫的函式作為引數,然後返回一個包裝的函式。在這個cached
函式裡,我們可以用一個物件或者Map
來快取結果。
functioncomputed(str){ console.log('計算了一分鐘'); return'計算出來了'; }
leta=cached(computed);
a('12'); //計算了一分鐘 //計算出來了
a('12'); //計算出來了
轉換命名風格(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"
複製程式碼
- 確定物件的型別(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"
複製程式碼
這樣我們就可以拿到一個變數的型別了。
- 把值轉換成字串(vue/src/shared/util.js)
我們經常需要把一個值轉換成字串,在JavaScript
裡面,我們有兩種方式來得到字串:
- String()
- JSON.stringify()
不過這兩種方式的實現機制是不同的:
我們裡看到,他們是基於完全不同的規則去轉換字串的,String(arg)
會嘗試呼叫arg.toString()
或者arg.valueOf()
,那麼那我們該用哪個比較好?
-
對於
null
跟undefined
,我們希望把它轉成空字串 -
當轉換一個數組或者我們建立的物件時,我們會使用
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~