盤點Vue源碼中用到的工具函數

如下摘取的函數,在 shared 目錄下公用的工具方法。文件在 util.js 中,githu地址前端

提取了一些經常使用通用的函數進行剖析,主要包含如下內容:vue

  1. 建立一個被凍結的空對象
  2. 判斷是不是 undefinednull
  3. 判斷是否不是 undefined 和 null
  4. 判斷是不是原始類型
  5. 判斷是不是對象類型
  6. 判斷有效的數組下標
  7. 判斷是不是一個 Promise 對象
  8. 刪除數組中指定元素
  9. 用作緩存的高階函數
  10. 遞歸判斷一個對象是否和另個一個對象徹底相同
  11. 函數只執行一次
  12. 自定義 bind 函數

1. 建立一個被凍結的空對象

export const emptyObject = Object.freeze({})

一旦建立不能給這個對象添加任何屬性。git

2. 判斷是不是 undefinednull

function isUndef (v) {
  return v === undefined || v === null
}

在源碼中不少地方會判斷一個值是否被定義,因此這裏直接抽象成一個公共函數。
傳入任意值,返回是一個布爾值。github

3. 判斷是否不是 undefinednull

function isDef (v) {
  return v !== undefined && v !== null
}

當傳入的值,既不是 undefined 也不是 null 返回true。segmentfault

4. 判斷是不是原始類型

function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

在js中提供了兩大類數據類型:api

  1. 原始類型(基礎類型):String、Number、Boolean、Null、Undefined、Symbol
  2. 對象類型:Object、Array、Function

5. 判斷是不是對象類型

function isObject (obj: mixed) {
  return obj !== null && typeof obj === 'object'
}

傳入的值排除掉 null,由於在js中 null 使用運算符 typeof 獲得的值是 object,這是一個 bug。由於歷史緣由放棄修復了。具體能夠參考這裏查看數組

6. 判斷有效的數組下標

function isValidArrayIndex (val) {
  const n = parseFloat(String(val)); // 轉成數字
  // 下標大於等於0,而且不是小數,而且是有限的數
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
  • 能夠傳入任意值,先調用 String 轉成字符串,目的是防止傳入的值爲 Symbol 類型,那樣直接調用 parseFloat 會報錯,例如:
let test = Symbol('test');
console.log(parseFloat(test))
控制檯捕獲錯誤:Uncaught TypeError: Cannot convert a Symbol value to a string

緣由是在調用 parseFloat 時,內部會調用內置的 ToString 方法,能夠參考這裏。而內置的 ToString 方法在遇到 Symbol 類型的值時,會拋出 TypeError 錯誤,能夠參考這裏緩存

跟使用一些隱式轉換遇到的問題同樣,例如使用 + 號:閉包

let test = '' + Symbol('text');
控制檯捕獲錯誤:Uncaught TypeError: Cannot convert a Symbol value to a string

都是由於內部會調用內置的 ToString 方法形成的。app

而若是手動調用 toString 方法或者調用 String,轉換爲字符串,則不會報錯:

let test = Symbol('test');
console.log(test.toString()); // "Symbol(test)"
console.log(String(test)) // "Symbol(test)"
  • 接下來判斷 n >= 0 ,數組的下標不能小於0,這樣就會排除掉小於0的數,以及 NaN
  • 而且 Math.floor(n) === n 一個數向下取整而且還等於本身,那隻能是正整數,排除掉小數,由於數組的下標不能是小數。
  • 而且用 isFinite 來斷定一個數字是不是有限數
console.log(isFinite(Infinity)); // false
console.log(isFinite(-Infinity)); // false
console.log(isFinite(123)); // true

7. 判斷是不是一個 Promise 對象

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

當一個對象存在 then 方法,而且也存在 catch 方法,能夠斷定爲 Promise 對象。

8. 刪除數組中指定元素

這個方法有效的避免了進行刪除數組某一項時,都要進行查找位置再刪除的重複工做。

function remove (arr, item){
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
  • 先判斷數組長度,若是數組是空的,則不必進行刪除操做
  • indexOf 方法查找到元素在數組中的位置,若是找到返回元素所在的位置下標,若是不存在,則返回-1
  • index>-1 表明存在數組中,則調用 splice 進行刪除,並返回刪除的元素組成的數組,也就是 splice 的返回值。

9. 用作緩存的高階函數

用高階函數的好處是無需暴露不一樣要求的緩存對象在外面,造成一個閉包。下面這個函數的技巧,應用在工做中,能夠提升代碼運行的效率。

function cached(fn) {
  // 建立一個緩存對象
  const cache = Object.create(null)
  return (function cachedFn (str) {
    // 先從緩存對象中找,要操做的值,是否已經有了操做結果
    const hit = cache[str]
    // 若是有,則直接返回;沒有,則調用函數對值進行操做,並把操做結果存在緩存對象中
    return hit || (cache[str] = fn(str))
  })
}
  • 調用 cached 時會傳入一個 fn 函數,這個函數對某些值進行操做,操做以後會產生返回值
  • cached 函數先定義一個沒有原型的對象,會比用 {} 高效,由於不須要繼承一大堆 Object.prototype 上的屬性。
  • 執行完 cached 會返回一個函數 cachedFn,未來接收須要操做的值。函數 cachedFn 內部調用 fn 函數獲得操做後的值,並緩存在對象 cache 中,若是再對同一個值進行操做時,則直接從緩存中取,無需再調用函數計算。

例如如下運用,函數的做用是把字符串的首字母大寫。

const capitalize = cached((str) => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})
  • 先調用 cached 傳入一個函數,這個函數是對字符串進行首字母大寫的操做,並返回首字母大寫的字符串結果,能夠說建立了一個計算函數。
  • cached 的返回值是函數,也就是上面的 cachedFn 函數。

這時咱們就能夠調用 capitalize 對字符串進行首字母大寫了。

capitalize('test');  // "Test"
capitalize('test');  // "Test"
capitalize('test');  // "Test"

第一次調用 capitalize 函數,先從緩存對象中取值,沒有,則調用計算函數進行計算結果返回,同時存入緩存對象中。這時的緩存對象爲:

{test: 'Test'}

再屢次調用 capitalize 時,從緩存對象中取值,命中,直接返回,無需再進行計算操做。

10. 遞歸判斷一個對象是否和另個一個對象徹底相同

判斷兩個對象是否相同,主要是判斷兩個對象包含的值都是同樣的,若是包含的值依然是個對象,則繼續遞歸調用判斷是否相同。

function isObject (obj){
  return obj !== null && typeof obj === 'object'
}

function looseEqual (a, b) {
  // 若是是同一個對象,則相同
  if (a === b) return true
  // 判斷是不是對象
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  // 二者都是對象
  if (isObjectA && isObjectB) {
    try {
      // 判斷是不是數組
      const isArrayA = Array.isArray(a)
      const isArrayB = Array.isArray(b)
      // 二者都是數組
      if (isArrayA && isArrayB) {
        // 長度要同樣,同時每一項都要相同,遞歸調用
        return a.length === b.length && a.every((e, i) => {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) {  // 若是都是時間對象,則須要保證時間戳相同
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) { // 二者都不是數組,則爲對象
        // 拿到二者的key值,存入數組
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)
        // 屬性的個數要同樣,遞歸的判斷每個值是否相同
        return keysA.length === keysB.length && keysA.every(key => {
          return looseEqual(a[key], b[key])
        })
      } else {  
        return false
      }
    } catch (e) {
      return false
    }
  } else if (!isObjectA && !isObjectB) {  // 二者都不是對象
    // 轉成字符串後,值是否一致
    return String(a) === String(b)
  } else {
    return false
  }
}
  • 判斷兩個值是否相同,不管是原始類型仍是對象類型,若是相同,則直接返回true。
  • 若是兩個都會對象,則分爲兩種狀況,數組和對象。

    • 都是數組,則保證長度一致,同時調用 every 函數遞歸調用函數,保證每一項都同樣
    • 是時間對象,則保證時間戳相同
    • 是對象,則先取出 key 組成的數組,二者 key 的個數要相同;再遞歸調用比較 value 值是否相同
    • 以上都不知足,直接返回false
  • 若是二者都不是對象,轉成字符串後進行比較。
  • 以上都不知足,直接返回false

例子:

let a1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
let b1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
console.log(looseEqual(a1,b1)); // true

let a2 = [1,2,3,{a:1,b:2,c:[1,2,3,4]}];
let b2 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
console.log(looseEqual(a2,b2)); // false

11. 函數只執行一次

一樣利用高階函數,在閉包內操做標識的真假,來控制執行一次。

function once (fn) {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
  • 傳入要執行一次的函數 fn
  • 設置標識爲 false
  • 返回一個函數

實際運用:

function test(){
  console.log('我只被執行一次');
}
let test2 = once(test);
test2(); // 我只被執行一次
test2();
test2();
test2();
  • 調用 once 函數後,會返回一個函數,賦值給 test2
  • 第一次調用 test2 後,在函數的尼內部,called 初次爲 false, 因此能夠執行函數 test,而後把標識 called 設置爲true,就相似關閉了大門,下次再也不執行。
  • 以後在調用 test2 , test 將再也不執行。

12. 自定義 bind 函數

function polyfillBind (fn, ctx) {
  function boundFn (a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

自定義的 bind 函數的場景,都是用來兼容不支持原生 bind 方法的環境。 在本身模擬的 bind 函數中,實際上調用的是 callapply

這個方法寫的相對簡單,若是更深刻了解,能夠戳此查看這篇文章

若有誤差歡迎指正學習,謝謝。

若是對你有幫助,請關注【前端技能解鎖】:
qrcode_for_gh_d0af9f92df46_258.jpg

相關文章
相關標籤/搜索