如何實現類似 lodash 的 get 與 merge 函式

shanyue發表於2019-05-13

lodash 基本上成為了寫 javascript 工具庫的標配,它廣泛應用在各種服務端以及前端應用中,但是它的包體積略大了一些。對於服務端來說,包的體積並不是十分的重要,或者換句話說,不像前端那樣對包的體積特別敏感,一分一毫都會影響頁面開啟的效能,從而影響使用者體驗。

正因為前端包體積對於使用者體驗的重要性,因此有各種各樣減小包體積的方法。針對 lodash 來說,你完全不必要引入 lodash 的所有工具函式,你只需要按需引入或者直接使用單函式包。關於按需引入你可以參考以下文章

Lessons on tree-shaking Lodash with Webpack and Babel

在針對我的個人站點中的 lodash 進行優化時,如果沒記錯的話,lodash 從以前 gzip 後的 80KB 變為了 20KB,相對來說還是比較大。而當我全域性搜尋了 lodash 的引用之後,發現 90% 的場景都是在使用 _.get

另外,隨著 ES6+ 的發展,以及瀏覽器與 Node 對它的支援,很多 lodash 的函式都很容易自己來實現或者說已被實現,如 _.assign_.trim_.startsWith 等等已被 ES6+ 實現,而 _.uniq 又很容易通過 new Set() 來解決。有人就在 github 上總結了 you-dont-need/You-Dont-Need-Lodash-Underscore,其中囊括了很多工具函式很簡易的實現。

鑑於本站點就是我作為試驗田用來實踐各種技術,於是我決定自己來實現 lodash 的一些工具函式。getmerge 兩個函式在我使用時比較多,且相對來說比較複雜一些,這裡貼一下我的實現程式碼。

本文地址: shanyue.tech/post/lodash…

get

在 js 中經常會出現巢狀呼叫這種情況,如 a.b.c.d.e,但是這麼寫很容易丟擲異常。你需要這麼寫 a && a.b && a.b.c && a.b.c.d && a.b.c.d.e,但是顯得有些囉嗦與冗長了。特別是在 graphql 中,這種巢狀呼叫更是難以避免。

這時就需要一個 get 函式,使用 get(a, 'b.c.d.e') 簡單清晰,並且容錯性提高了很多。以下是需要通過的幾個測試用例

get({ a: null }, 'a.b.c', 3)
// output: 3

get({ a: undefined }, 'a', 3)
// output: 3

get({ a: null }, 'a', 3)
// output: 3

get({ a: [{ b: 1 }]}, 'a[0].b', 3)
// output: 1
複製程式碼

path 中也可能是陣列的路徑,全部轉化成 . 運算子並組成陣列

// a[3].b -> a.3.b
const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
複製程式碼

然後層層迭代屬性即可,另外注意 nullundefined 取屬性會報錯,所以使用 Object 包裝一下。

function get (source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b
  const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
  let result = source
  for (const p of paths) {
    result = Object(result)[p]
    if (result === undefined) {
      return defaultValue
    }
  }
  return result
}
複製程式碼

merge

merge 用來遞迴合併物件,相當於深層的 Object.assign。在 graphql 中會廣泛用到 merge,如會經常使用 merge 來合併所有的 resolver,特別是 Mutation 如下示例

const rootResolver = {
  Query: {
  
  },
  Mutation: {
    login () {}
  }
}

const userResolver = {
  User: {
    createUser() {}
  }
}

const resolver = merge(rootResolver, userResolver)
// output
// {
//   Query: {},
//   Mutation: {
//     login () {},
//     createUser () {}
//   }
// }
複製程式碼

另外,在前端進行 graphql 的查詢時也經常需要使用到 merge。如在進行頁面的效能優化時,為了避免一個 Query 耗時過久,頁面渲染過於耗時,會拆成兩個 Query,先渲染響應快的資料,在慢慢等待個別響應慢的資料。

以下是一個關於個人主頁資訊的 Query,但是其中有一個欄位 dataNeedDelay3s 會在伺服器耗時許久,會因為此欄位加大了使用者的等待時間,造成不友好的使用者體驗。此時會把此欄位單獨拆掉,優先渲染其它個人資訊。

query PROFILE {
  me {
    id
    age
    name
    # 需要耗時3s的欄位
    dataNeedDelay3s
  }
}

# 拆為以下兩個茶軒
query PROFILE_ONE {
  me {
    id
    age
    name
  }
}

query PROFILE_TWO {
  me {
    dataNeedDelay3s
  }
}
複製程式碼

此時就有 merge 的需求,查詢完成後把兩次查詢結果給拼到一起。

關於拆 graphql 的 Query 的需求無處不在,如在服務端渲染時,需要把許可權資源與非許可權資源分開。

這裡講述下如何實現 merge

function isObject (value) {
  const type = typeof value
  return value !== null && (type === 'object' || type === 'function')
}

// { a: [{ b: 2 }] } { a: [{ c: 2 }]} -> { a: [{b:2}, {c:2}]}
// merge({o: {a: 3}}, {o: {b:4}}) => {o: {a:3, b:4}}
function merge (source, other) {
  if (!isObject(source) || !isObject(other)) {
    return other === undefined ? source : other
  }
  // 合併兩個物件的 key,另外要區分陣列的初始值為 []
  return Object.keys({
    ...source,
    ...other
  }).reduce((acc, key) => {
    // 遞迴合併 value
    acc[key] = merge(source[key], other[key])
    return acc
  }, Array.isArray(source) ? [] : {})
}
複製程式碼

相關文章