「從原始碼中學習」Vue原始碼中的JS騷操作

前端勸退師發表於2019-02-25

本文不準備解析Vue原始碼的執行原理,僅單純探尋vue中工具函式中那些值得學習的騷操作

終極目標:從工具函式中擴充套件知識點

1. 當前環境的一系列判斷

1.1inBrowser: 檢測當前宿主環境是否是瀏覽器

// 通過判斷 `window` 物件是否存在即可
export const inBrowser = typeof window !== 'undefined'
複製程式碼

1.2 hasProto:檢查當前環境是否可以使用物件的 __proto__ 屬性

// 一個物件的 __proto__ 屬性指向了其建構函式的原型
// 從一個空的物件字面量開始沿著原型鏈逐級檢查。
export const hasProto = '__proto__' in {}
複製程式碼

2. user Agent常量的一系列操作

2.1 獲取當瀏覽器的user Agent

// toLowerCase目的是 為了後續的各種環境檢測
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
複製程式碼

2.2 IE瀏覽器判斷

export const isIE = UA && /msie|trident/.test(UA)

解析:使用正則去匹配 UA 中是否包含'msie'或者'trident'這兩個字串即可判斷是否為 IE 瀏覽器

「從原始碼中學習」Vue原始碼中的JS騷操作

來源:Internet Explorer User Agent Strings

多關鍵詞高亮外掛:Multi-highlight

2.3 IE9| Edge | Chrome 判斷

export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isEdge = UA && UA.indexOf('edge/') > 0
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
複製程式碼

3. 字串操作

3.1 isReserved:檢測字串是否以 $ 或者 _ 開頭

// charCodeAt() 方法可返回指定位置的字元的 Unicode 編碼
export function isReserved (str: string): boolean {
  const c = (str + '').charCodeAt(0)
  return c === 0x24 || c === 0x5F
}
複製程式碼

解析: 獲得該字串第一個字元的unicode,然後與 0x240x5F 作比較。

若作為一個想進階中高階的前端,charCodeAt方法的各種妙用還是需要知道的(面試演算法題各種考)。

3.1.2 Javascript中級演算法之charCodeAt

從傳遞進來的字母序列中找到缺失的字母並返回它。 如:fearNotLetter("abce") 應該返回 "d"。

function fearNotLetter(str) {
  //將字串轉為ASCII碼,並存入陣列
  let arr=[];
  for(let i=0; i<str.length; i++){
    arr.push(str.charCodeAt(i));
  }
  for(let j=1; j<arr.length; j++){
    let num=arr[j]-arr[j-1];
    //判斷後一項減前一項是否為1,若不為1,則缺失該字元的前一項
    if(num!=1){
      //將缺失字元ASCII轉為字元並返回 
      return String.fromCharCode(arr[j]-1); 
    }
  }
  return undefined;
}
fearNotLetter("abce") // "d"
複製程式碼

3.2 camelize: 連字元轉駝峰

const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
複製程式碼

解析: 定義正規表示式:/-(\w)/g,用來全域性匹配字串中 中橫線及連字元後的一個字元。若捕獲到,則將字元以toUpperCase大寫替換,否則以''替換。 如:camelize('aa-bb') // aaBb

3.3 toString: 將給定變數的值轉換為 string 型別並返回

export function toString (val: any): string {
  return val == null
    ? ''
    : typeof val === 'object'
      ? JSON.stringify(val, null, 2)
      : String(val)
}
複製程式碼

解析:Vue中充斥著很多這類增強型的封裝,大大減少了我們程式碼的複雜性。但這裡,我們要學習的是這種多重三元運算子的用法

3.3.1 多重三元運算子

export function toString (val: any): string {
  return val == null
    ? ''
    : typeof val === 'object'
      ? JSON.stringify(val, null, 2)
      : String(val)
}
複製程式碼

解析:

export function toString (val: any): string {
  return 當變數值為 null 時
    ? 返回空字串
    : 否則,判斷當變數型別為 object時
      ? 返回 JSON.stringify(val, null, 2)
      : 否則 String(val)
}
複製程式碼

類似的操作在vue原始碼裡很多。比如mergeHook

3.3.2 mergeHook: 合併生命週期選項

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}
複製程式碼

這裡我們不關心mergeHook在原始碼中是做什麼的(其實是判斷父子元件有無對應名字的生命週期鉤子函式,然後將其通過 concat合併

3.4 capitalize:首字元大寫

// 忽略cached
export const capitalize = cached((str: string): string => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})
複製程式碼

解析: str.charAt(0)獲取str的第一項,利用toUpperCase()轉換為大寫字母,str.slice(1) 擷取除第一項的str部分。

3.5 hyphenate:駝峰轉連字元

const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})
複製程式碼

解析:camelize相反。實現方式同樣是使用正則,/\B([A-Z])/g用來全域性匹配字串中的大寫字母, 然後替換掉。

4. 型別判斷

4.1 isPrimitive: 判斷變數是否為原型型別

export function isPrimitive (value: any): boolean %checks {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}
複製程式碼

解析: 這個很簡單,但我們經常忽略掉symbol這個型別(雖然完全沒用過)。

4.2 isRegExp: 判斷變數是否為正則物件。

// 使用 Object.prototype.toString 與 '[object RegExp]' 做全等對比。

export function isRegExp (v: any): boolean {
  return _toString.call(v) === '[object RegExp]'
}
複製程式碼

這也是最準確的型別判斷方法,在Vue中其它型別也是一樣的判斷

4.3 isValidArrayIndex: 判斷變數是否含有效的陣列索引

export function isValidArrayIndex (val: any): boolean {
  const n = parseFloat(String(val))
  // n >= 0 && Math.floor(n) === n 保證了索引是一個大於等於 0 的整數
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
複製程式碼

isFinite方法檢測它引數的數值。如果引數是NaN,正無窮大或者負無窮大,會返回false,其他返回true

擴充套件:語法:isFinite()

「從原始碼中學習」Vue原始碼中的JS騷操作

4.4 isObject: 區分物件和原始值

export function isObject (obj: mixed): boolean %checks {
  return obj !== null && typeof obj === 'object'
}
複製程式碼

5.Vue中的閉包騷操作

5.1 makeMap():判斷一個變數是否包含在傳入字串裡

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}
複製程式碼
  1. 定義一個物件map
  2. str 分隔成陣列並儲存到 list 變數中
  3. 遍歷list,並以list中的元素作為 mapkey,將其設定為 true
  4. 返回一個函式,並且如果expectsLowerCasetrue的話,小寫map[key]:

我們用一個例子來說明下:

let isMyName = makeMap('前端勸退師,帥比',true); 
//設定一個檢測是否為我的名字的方法,第二個引數不區分大小寫
isMyName('前端勸退師')  // true
isMyName('帥比')  // true
isMyName('醜逼')  // false
複製程式碼

Vue中類似的判斷非常多,也很實用。

5.1.1 isHTMLTag | isSVG | isReservedAttr

這三個函式是通過 makeMap 生成的,用來檢測一個屬性(標籤)是否為保留屬性(標籤)

export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)
export const isSVG = makeMap(
  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
  true
)
// web平臺的保留屬性有 style 和 class
export const isReservedAttr = makeMap('style,class')
複製程式碼

5.2 once:只呼叫一次的函式

export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
複製程式碼

解析:called作為回撥識別符號。呼叫此函式時,called標示符改變,下次呼叫就無效了。也是典型的閉包呼叫。

5.3 cache:建立一個快取函式

/**
 * Create a cached version of a pure function.
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}
複製程式碼

解析: 這裡的註釋已把作用解釋了。

const cache = Object.create(null)建立純函式是為了防止變化(純函式的特性:輸入不變則輸出不變)

在Vue中,需要轉譯很多相同的字串,若每次都重新執行轉譯,會造成很多不必要的開銷。 cache這個函式可以讀取快取,如果快取中沒有就存放到快取中,最後再讀。

6. 多型別的全等判斷

looseEqual: 檢查兩個值是否相等

export function looseEqual (a: any, b: any): boolean {
  // 當 a === b 時,返回true
  if (a === b) return true
  // 否則進入isObject判斷
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  // 判斷是否都為Object型別
  if (isObjectA && isObjectB) {
    try {
      // 呼叫 Array.isArray() 方法,再次進行判斷
      // isObject 不能區分是真陣列還是物件(typeof)
      const isArrayA = Array.isArray(a)
      const isArrayB = Array.isArray(b)
      // 判斷是否都為陣列
      if (isArrayA && isArrayB) {
        // 對比a、bs陣列的長度
        return a.length === b.length && a.every((e, i) => {
          // 呼叫 looseEqual 進入遞迴
          return looseEqual(e, b[i])
        })
      } else if (!isArrayA && !isArrayB) {
        // 均不為陣列,獲取a、b物件的key集合
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)
        // 對比a、b物件的key集合長度
        return keysA.length === keysB.length && keysA.every(key => {
          //長度相等,則呼叫 looseEqual 進入遞迴
          return looseEqual(a[key], b[key])
        })
      } else {
        // 如果a、b中一個是陣列,一個是物件,直接返回 false
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
複製程式碼

這個函式比較長,建議配合註釋食用。 總之,就是

各種型別判斷+遞迴

「從原始碼中學習」Vue原始碼中的JS騷操作
此篇就先講講Vue中的一些工具函式類的吧,Vue原始碼很多值得挖掘的玩法。走過路過,點個贊憋老哥。

作者文章總集

相關文章