兩萬字長文-電商sku組合查詢狀態細究與實現

最近作到一個需求,須要作一個相似於京東或者淘寶等電商的商品詳情頁,其中有一個功能就是商品SKU的選擇查詢問題html

如上圖,網絡類型、機身顏色、套餐類型、存儲容量這些每個都是一個 SKU屬性,當選擇好了全部的 SKU屬性後,會組合成一個完整的 SKU,對應一個具體的商品,而後就能夠給出這條 SKU對應的商品的庫存和價格等信息前端

而且,當點選了某些 SKU屬性後,會自動根據當前已經點選的 SKU屬性,來計算出當前條件下庫存爲 0SKU組合,給予按鈕置灰不可選的交互git

一開始接到這個需求的時候,評審需求的後端和同組合做的前端小夥伴都認爲這是一個難點,須要好好調研考量一下,而我則是邪魅一笑,以爲這東西不就是一個組合嘛,算法能寫就寫,不能寫大不了來個暴力循環查找,小場面啦github

然而,當我開始着手作的時候才發現跟日常確實不同,這些代碼好像不能閉着眼睛寫面試

實現思路

當我發現這個功能確實有點小難度的時候,我就開始在網上搜相關文章了,這才發現相關文章好像都寫的比較煞有其事,跟什麼《你絕對不知道的數組的十種用法》什麼《原型鏈全場最佳分析》什麼《個人函數式編程不可能那麼可愛》什麼《三年前端大廠面試經歷》都不太同樣,看起來竟然須要帶着腦子,並且相關文章較少,除去那些抄來抄去的以及我沒搜到的以外,幾篇真正有實用價值的:正則表達式

我看完以後,頓時陷入了沉思算法

既然塑料恐龍是塑料作的,而塑料來源於石油,石油又是遠古恐龍屍體轉化成的。 那麼是否是說,塑料恐龍就是真的恐龍?編程

這些文章描述得感受不是很清楚,主要是沒有完整可運行代碼,看的雲裏霧裏的,照着他們說的來作,還不如我本身從新寫個,不過仍是有些借鑑意義的後端

  • 首先,給定幾組 SKU的屬性,那麼根據這些屬性組合出來的 sku確定是能夠被所有列舉出來的,並且考慮到現實業務使用中,屬性並不會太多,無需關心極端狀況,因此哪怕所有窮舉,對性能也沒多大損耗;
  • 而後,既然窮舉出了全部的可能集合,那麼就能計算出每個組合對應的價格和存庫等信息,這樣一來天然就能夠造成一個字典了,不管選擇了哪些組合,都能從這個字典上快速查詢到對應的價格和庫存等信息,即以空間換時間,只要在開始時初始化好了這個包含全部可能的數據字典,後續 sku屬性的切換無非是字典 key的變化

固然了,思路是這個思路,可是真正寫代碼的時候,須要考慮的點比較多,並且都要考慮到,不管少了哪一點,數據就會都不對了數組

數據準備

假設對於手機這個品類來講,它的 sku屬性有成色、顏色、配置、版本,其中成色分爲 全新、僅拆封,顏色分爲深空灰、銀色、金色,配置分爲64G256G,版本分爲國行、港澳版、日韓、其餘版本

每一個 sku屬性確定都有本身獨一無二的標識 ID,例如,對於顏色來講,它確定有本身的 ID,稱爲 paramId,顏色下存在至少一個小屬性,例如深空灰、銀色、金色,每種顏色也都有本身的 ID,稱爲 valueId,結構以下:

interface ISkuParamItem {
  paramId: string
  paramValue: string
  valueList: Array<{
    valueId: string
    valueValue: string
  }>
}
複製代碼

請求 sku數據的時候,後端會將當前商品ID(稱爲 spuId)對應的全部sku屬性返回,稱爲 sku屬性數據集合數據格式暫定以下:

[{
    "paramId": "6977",
    "paramValue": "成色",
    "valueList": [{
        "valueId": "1081969",
        "valueValue": "全新"
    }, {
        "valueId": "1080699",
        "valueValue": "僅拆封"
    }]
}, {
    "paramId": "6975",
    "paramValue": "顏色",
    "valueList": [{
        "valueId": "730003",
        "valueValue": "深空灰色"
    }, {
        "valueId": "730004",
        "valueValue": "銀色"
    }, {
        "valueId": "730005",
        "valueValue": "金色"
    }]
}, {
    "paramId": "7335",
    "paramValue": "配置",
    "valueList": [{
        "valueId": "710004",
        "valueValue": "64G"
    }, {
        "valueId": "710006",
        "valueValue": "256G"
    }]
}, {
    "paramId": "72",
    "paramValue": "版本",
    "valueList": [{
        "valueId": "1080627",
        "valueValue": "國行"
    }, {
        "valueId": "1080628",
        "valueValue": "港澳版"
    }, {
        "valueId": "1080697",
        "valueValue": "日韓"
    }, {
        "valueId": "1080629",
        "valueValue": "其餘版本"
    }]
}]
複製代碼

當前商品ID(稱爲 spuId)對應的全部 sku數據返回,稱爲 商品全sku數據集合 數據格式暫定以下:

[
  {
      count: 98,
      paramIdJoin: "72_1080697__6975_730004__6977_1081969__7335_710006",
      priceRange: [7000, 8978],
      spuDId: "98002993445"
  }
]
複製代碼

其中,paramIdJoinsku屬性組合鏈接而成,可分爲 72_108069七、6975_73000四、 6977_108196九、 7335_710006這個四個單元,每一個單元又由 paramIdvalueId 鏈接而成,因此 72_1080697__6975_730004__6977_1081969__7335_710006的意思就是 版本爲日韓、顏色爲銀色、成色爲全新、配置爲256Gsku組合,對應的總庫存 count98,價格範圍爲 7000 ~ 8978,即最低價是 7000,最高價是 8978(若是隻有一個價格沒有高、低價格之分,那數組中的項就是那一個價格就好了),spu的標識 id spuDId98002993445

須要注意的是paramIdJoin 的值,例如72_1080697__6975_730004__6977_1081969__7335_710006,必須是按照 paramId進行升序(或者降序也能夠,這裏按照升序處理)進行鏈接的,72 < 6975 < 6977 < 7335,因此纔有 72_1080697__6975_730004__6977_1081969__7335_710006這個拼接方式

這對於後續算法的優化有着顯著的做用,不按照順序也能夠,但在數據量比較大的狀況下,計算時間會比較長,極可能出現頁面卡頓的狀況

後端可能返回的數據結構和上面不同,不過關係不大,不管後端返回的數據結構是什麼樣的,返回的信息確定須要包括上面那些,由於這些都是必要數據,若是不同,你只須要先行處理一下,處理成和上面同樣的數據結構就好了,這是確定能夠作到的 有了上述數據,就能夠開始寫核心的處理代碼了

分析思路

其實這裏的功能就兩個,那就是當選中或取消任意 sku屬性的時候:

  • 計算出此時 sku屬性組合對應的價格和庫存 例如當前選中了顏色:銀色、內存:64G、運營商:移動,這一組 sku屬性對應的商品的價格和庫存

  • 計算出在此時的 sku屬性組合之下,須要置灰哪些 sku組合 例如當前選中了顏色:銀色、內存:64G,這一組 sku屬性的時候,剩下的哪些 sku屬性與這兩個已經選中的 sku組合後的組合庫存爲 0,則說明這些 sku屬性應該要被置灰,也就是不讓用戶選中 例如,銀色 + 64G的庫存爲 0,則當選中銀色的時候,就須要將 64G這個 sku屬性置灰

對於第一點,比較簡單,只須要拿到當前選中的 sku屬性組合,而後在全部的 sku數據中去查找包含所選中的 sku屬性的數據便可,就是一個遍歷篩選操做,有難度的是第二點

不管當前選中了哪些 sku屬性,都要從整個 sku數據中找到包含任意選中的 sku屬性的數據,而後在這些數據中找出庫存爲 0的數據,再從這些數據中找到應該被置灰的 sku屬性

例如,假設 銀色-64G-國行這個 sku的庫存爲 0,則當你選中 銀色-64G的時候,應該把國行置灰,當選中 銀色-國行,應該把64G置灰,當選中 國行-64G,應該把銀色置灰

這只是最簡單的一種狀況

複雜一點,假設 銀色-64G這個 sku的庫存爲 0,則當你選擇銀色的時候,,應該把64G置灰,當你選擇64G的時候,,應該把銀色置灰,當你選擇深空灰-64G的時候,,應該把銀色置灰……等等

若是 sku屬性項有不少種,例如顏色、內存、運營商、制式、成色、套餐類型、保險,那麼這種可組合的項就更多了

隨便想一下,就感受頭好大,彷佛是好多須要計算的東西啊

不過,其實也是有據可循的

當選中 nsku屬性的時候,應該被置灰的 sku屬性實際上就是 當選中這 n箇中任一個sku屬性時應該被置灰的 sku屬性,加上當選中這 n箇中任兩個sku屬性時應該被置灰的 sku屬性,加上……加上當選中這 n箇中任 nsku屬性時應該被置灰的 sku屬性 頭好像變得更大了

例如上圖

當選中了 全新、金色、64G、國行這四個 sku屬性的時候,計算在此狀態下須要置灰的 sku屬性:

首先,先看這四個 sku屬性中,當不選中任何 sku屬性時,有哪些 sku的庫存爲 0,發現全部的商品中,都沒有 全新這個屬性,則說明 全新的庫存爲 0,將之記錄下來;

而後,再看這四個 sku屬性中,每個單獨的 sku選中的時候應該置灰的 sku屬性,例如當單獨選中 全新這個 sku屬性的時候,發現全新-銀灰色這個組合的庫存爲 0,則將 銀灰色置灰,將這個屬性記錄下來;當單獨選中 國行這個 sku屬性的時候,發現國行-僅拆封這個組合的庫存爲 0,則將 僅拆封置灰,將這個屬性記錄下來;當單獨選中 金色64G的時候,其餘任意一個sku屬性與之組合皆有庫存,則不記錄任何 sku屬性;

而後,再看這四個 sku屬性中,當選中兩個 sku的時候應該置灰的 sku屬性,例如當同時選中 金色、64G這兩個 sku屬性的時候,發現金色-64G-港行這個組合的庫存爲 0,則將 港行置灰,將這個屬性記錄下來;當同時選中 全新-金色全新-64G全新-國行金色-國行64G-國行的時候,發現其餘任一 sku與這些進行組合都有庫存,則不記錄任何 sku屬性;

而後,再看這四個 sku屬性中,當選中三個 sku的時候應該置灰的 sku屬性,例如當同時選中 金色、64G這兩個 sku屬性的時候,發現金色-64G-港行這個組合的庫存爲 0,則將 港行置灰,將這個屬性記錄下來;當同時選中 全新-金色全新-64G全新-國行金色-國行64G-國行的時候,發現其餘任一 sku與這些進行組合都有庫存,則不記錄任何 sku屬性;

應該被置灰的意思就是對於當前 sku組合來講,若是再繼續增長一個 sku屬性,這個新的 sku組合對應的庫存爲 0,則說明最後增長的那個 sku屬性應該被置灰

因此這裏須要一個集合,這個集合包含當選擇任意個數量 sku屬性的時候,應該被置灰的 sku屬性集合

想要獲得這個集合,運算量會很大,並且絕大部分都是無用計算(計算出了一堆的結果,實際上用戶只須要其中幾個),因此簡化一下,只須要獲得一個集合,這個集合中包含了全部可能選擇的 sku組合,還能夠更加有序一點,將集合換成 Map,在 js中也就是對象,這個對象存在一些屬性,這些屬性的key值是數字 一、二、3……n-1, n,表明着選中 1 到 n個任意 sku屬性,其值又是一個對象,這個小對象的 key就是 1 到 n個任意 sku屬性的可能組合,其值包含這些組合的價格範圍和庫存等信息

代碼層面的數據結構以下(部分):

上述表示,對於選中了 1(下標爲 0)個 sku屬性的狀況,共有 11sku選法,每種選法都有對應的 價格範圍和總庫存信息,例如 72_1080627表示,當選中了 版本爲國行 這個 sku屬性時,其價格範圍數組 priceArr 和總庫存 totalCount;對於選中了 2(下標爲 1)個 sku屬性的狀況,共有 44sku選法,每種選法都有對應的 價格範圍和總庫存信息,例如 72_1080627__6975_730003表示,當選中了 版本爲國行,顏色爲深空灰色 這個 sku屬性時,其價格範圍數組 priceArr 和總庫存 totalCount……等等

有了這個信息,後面就好辦了

首先,你選中任意個 sku屬性,我都能從這個集合裏找出對應的總庫存和價格範圍,無需在原數據中屢次篩選,這實現了第一個功能點; 對於第二個功能,當選中任意個 sku屬性時(設爲 n),我只須要在這個對象中選取 key爲從 0n的數據,而且找這些數據對應的 sku的總庫存爲 0的數據,這些數據中包含 sku屬性就都是須要置灰的,這就實現了第二個功能點

因此,最關鍵的就是這個Map對象了,只要計算出這個Map對象,其餘的就好辦了,固然,提及來簡單,真正用代碼實現仍是有不少須要注意的地方的

數據計算

計算單 sku屬性所屬下標集合

即從接口返回的包含了全部 sku屬性組合及其對應的數據的數據源中(也就是前面所說的 商品全sku數據集合),計算出包含了每個 sku屬性的數據項的下標的集合

好比,對於 顏色:黑色 這個 sku屬性,商品全sku數據集合數據中全部包含 顏色:黑色這個 sku屬性的數據的下標的集合,就是其所屬下標集合,以 sku屬性的 paramIdvalueIdkey,如下標集合爲值,則能夠獲得一個集合,數據結構以下:

{
    "6977_1081969": [0, 4, 5, 6, 7, 11, 14, 15, 17, 19, 20, 23, 25, 31, 32, 34, 35, 36, 38, 39, 40, 42, 44, 47],
    "6977_1080699": [1, 2, 3, 8, 9, 10, 12, 13, 16, 18, 21, 22, 24, 26, 27, 28, 29, 30, 33, 37, 41, 43, 45, 46],
    "6975_730003": [4, 14, 17, 18, 22, 23, 28, 29, 32, 33, 37, 39, 43, 44, 45, 47],
    "6975_730004": [0, 2, 5, 7, 8, 10, 12, 16, 19, 20, 24, 31, 34, 40, 41, 46],
    "6975_730005": [1, 3, 6, 9, 11, 13, 15, 21, 25, 26, 27, 30, 35, 36, 38, 42],
    "7335_710004": [1, 3, 4, 5, 8, 9, 10, 11, 19, 24, 28, 30, 31, 32, 33, 34, 35, 36, 37, 39, 42, 43, 44, 46],
    "7335_710006": [0, 2, 6, 7, 12, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 25, 26, 27, 29, 38, 40, 41, 45, 47],
    "72_1080627": [7, 12, 13, 18, 19, 23, 30, 33, 38, 42, 44, 46],
    "72_1080628": [3, 5, 6, 10, 16, 21, 36, 39, 40, 43, 45, 47],
    "72_1080697": [0, 8, 9, 11, 14, 22, 25, 26, 31, 32, 37, 41],
    "72_1080629": [1, 2, 4, 15, 17, 20, 24, 27, 28, 29, 34, 35]
}
複製代碼

例如,對於第一條數據 "6977_1081969": [0, 4, 5, 6, 7, 11, 14, 15, 17, 19, 20, 23, 25, 31, 32, 34, 35, 36, 38, 39, 40, 42, 44, 47],來講,它表示商品全sku數據集合數據中全部包含 paramId6977valueId1081969這個sku屬性,即 成色:全新的數據下標集合爲 [0, 4, 5, 6, 7, 11, 14, 15, 17, 19, 20, 23, 25, 31, 32, 34, 35, 36, 38, 39, 40, 42, 44, 47]

此集合的計算過程沒什麼好說,就是遍歷商品全sku數據集合數據建立數組而已,爲了方便敘述,這裏稱此集合爲 keyRankMap

計算 Map對象

有了上述 keyRankMap,計算咱們想要的那個 Map對象 (稱爲indexKeyInfoMap)也就有了前提條件,

indexKeyInfoMap集合的組成前面已經說過了,相似於一個緩存數據,列舉了全部任意個 sku組合的信息,因此那就涉及到一個算法了:

現有 m個數組,數組都不爲空,從這些數組中共取 n(n <= m)個數,共有多少種取法?規定,每一個數組最多隻能取一次,每次最多取一個項

這個算法實際上就是另一個更加常見的算法的加強版,即 從 m 個數中取 n 個數,共有多少種取法,這裏的 m個數能夠當作是一個數組,也就是從一個長度爲 m的數據中取 n個數,可是如今不是從一個數組中了,而是從 m個數組中取

這個算法固然有不少種寫法,只要能達到目的而且效率別太差就行:

/** * 給定 mArr長度個數組,從這些數組中取 n 個項,每一個數組最多取一項,求全部的可能集合,其中,mArr的每一個項的值表明這個數組的長度 * 例如 composeMArrN(([1, 2, 3], 2)),表示給定了 3 個數組,第一個數組長度爲 1,第二個數組長度爲 2,第二個數組長度爲 3,從這三個數組任意取兩個數 * example: composeMArrN(([1, 2, 3], 2)),返回: * [[0,0,-1],[0,1,-1],[0,-1,0],[0,-1,1],[0,-1,2],[-1,0,0],[-1,0,1],[-1,0,2],[-1,1,0],[-1,1,1],[-1,1,2]] * 返回的數組長度爲 11,表示有1 種取法,數組中每一個子數組就是一個取值組合,子數組中的數據項就表示取值的規則 * 例如,對於上述結果的第一個子數組 [0, 0, -1] 來講,表示第一種取法是 取第一個數組下標爲 0 和 第二個數組下標爲 0 的數,下標爲 2 的數組項值爲 -1 表示第三個數組不取任何數 * @param mArr 數據源信息 * @param n 取數的個數 * @param arr 遞歸使用,外部調用不須要傳此項 * @param hasSeletedArr 遞歸使用,外部調用不須要傳此項 * @param rootArr 遞歸使用,外部調用不須要傳此項 */
function composeMArrN (mArr: Array<number>, n: number, arr = [], hasSeletedArr = [], rootArr = []): Array<Array<number>> {
  if (!n || n < 1 || mArr.length < n) {
    return arr
  }
  for (let i = 0; i < mArr.length; i++) {
    // 當前層級已經存在選中項了
    if (hasSeletedArr.includes(i)) continue
    hasSeletedArr = hasSeletedArr.slice()
    hasSeletedArr.push(i)
    for (let j = 0; j < mArr[i]; j++) {
      let arr1 = completeArr(arr, i - arr.length, -1)
      arr1.push(j)
      if (n === 1) {
        arr1 = completeArr(arr1, mArr.length - arr1.length, -1)
        rootArr.push(arr1)
      } else {
        composeMArrN(mArr, n - 1, arr1, hasSeletedArr, rootArr)
      }
    }
  }
  return rootArr
}
複製代碼

其中 completeArr是一個補全數組的輔助方法,好比,對於 [1, 2]這個數組來講,我想把它的長度再增長 3個單位,用 -1來填補多出來的位置,就是調用 completeArr([1, 2], 3, -1),返回 [1, 2, -1, -1, -1]

以此類推,就能獲得當任意取 nsku屬性時,所組成的 sku組合對應的價格和庫存等信息的數據,也就是 indexKeyInfoMap

根據 indexKeyInfoMap 計算出任意狀態下 sku 組合對應的信息和置灰信息

indexKeyInfoMap只是一個緩存數據,須要實現的功能是,選中任意 sku數據時,對應的價格和庫存等信息,以及應該置灰的 sku信息

例如,對於當選中了 全新-銀色這個組合的時候

  • 計算對應的價格和庫存等信息

全新-銀色 是兩個 sku屬性的組合,則直接在 indexKeyInfoMap中找 key1的屬性,從其數組值中找匹配 全新-銀色,也就是 子key6975_730004__6977_1081969的數據項:

其中,priceArr表示全部包含 全新-銀色這個 sku組合的 sku數據的價格範圍集合,spuDIdArr表示 spuDId集合,totalCount表示總庫存

  • 計算須要置灰的 sku屬性

這纔是關鍵

第一,計算當任何屬性都不選的時候,庫存爲 0的屬性; 第二,計算當單獨選中 全新 和 單獨選中 銀色 的時候,分別與這兩個屬性組合的時候庫存爲 0的屬性; 第三,計算當同時選中 全新-銀色的時候,與 全新-銀色進行組合的時候庫存爲 0的屬性;

三種狀況下應該被置灰的屬性的並集就是 當選中 全新-銀色的時候,應該被置灰的屬性

計算的基礎就是 indexKeyInfoMap, 好比對於 第二,計算當單獨選中 全新 和 單獨選中 銀色 的時候,分別與這兩個屬性組合的時候庫存爲 0的屬性;,其計算過程:

首先,任意一個 sku屬性 與全新銀色組成的組合,就是兩個 sku屬性連接,長度爲 2,因此從 indexKeyInfoMap中選取 key1的數據,也就是 indexKeyInfoMap[1],今後數據中分別找包含 全新銀色,也就是 key中包含 6977_10819696975_730004,而且庫存爲 0的數據項:

而後再對這被篩選出來的 7條數據進行出來,獲得須要被置灰的 sku屬性的集合,也就是這些數據的 key字符串中,去掉當前已經選中的 sku屬性,即 6977_10819696975_730004後,還剩下的值的集合

例如,對於 72_1080628__6975_730004而言,其處理以後剩下 72_1080628,即 paramId72valueId1080628sku屬性所對應的按鈕應該被置灰,其餘相似

這樣就能獲得一個數組(須要對數組進行去重),此數組中的每一項,就是在同時選中 全新-銀色的時候,所須要置灰不可點的 sku屬性按鈕

歸納成通用解決方案,其實又是一個算法,上面說到過:從 m 個數據中取 n(n <= m) 個數,全部的可能取法,而後再對每種取法進行處理,計算從剩下的 sku屬性中取一個與每種取法的 sku組合進行組合時庫存爲 0的那些 sku屬性

/**
 * 從 m 個數字中取 n 個,全部可能的取法(不考慮順序)
 * @param m 數據總數
 * @param n 取數個數
 * @param arr 遞歸使用,外部調用不須要傳此項
 * @param hasSeletedArr 遞歸使用,外部調用不須要傳此項
 * @param rootArr 遞歸使用,外部調用不須要傳此項
 */
function composeMN (m: number, n: number, arr: number[] = [], hasSeletedArr: number[] = [], rootArr: number[][] = []): number[][] {
  for (let i = 0; i < m; i++) {
    if (hasSeletedArr.includes(i)) continue
    hasSeletedArr = hasSeletedArr.slice()
    hasSeletedArr.push(i)
    let arr1 = arr.slice()
    arr1.push(i)
    if (n !== 1) {
      composeMN(m, n - 1, arr1, hasSeletedArr, rootArr)
    } else {
      rootArr.push(arr1)
    }
  }
  return rootArr
}
複製代碼

固然,你也能夠用本身以爲更好的算法,總之能解決問題而且效率及格就行

至此,流程結束,功能完成

算法優化

根據上述流程已經能夠實現功能了,效率也不低,正常業務場景使用徹底沒問題,但還存在一些優化的空間

庫存爲 0 的單 sku數據緩存

對於一個正常的商品來講,特別是平臺直營的商品,在絕大部分狀況下,其每一條 sku都應該是有庫存的,那麼對於這種狀況,不管你在什麼狀況下選中了哪些 sku屬性,都不該該出現被置灰的 sku屬性,由於全部的 sku都有庫存,若是我能肯定某個商品全部 sku都有庫存,那就無需考慮按鈕置灰的事情了,計算量最大的那一部分就徹底能夠省去了,一會兒少了一大半的計算量

若是並非全部 sku庫存都爲 0,其實也有優化的空間,能夠精確到單個 sku屬性,邏輯以下: 遍歷全部商品全sku數據集合,找出全部庫存爲 0sku組合,從每一個組合中分離出單個 sku屬性,將全部的這些單個 sku屬性放入一個集合中(稱爲 emptySkuIncludeList),那麼當用戶選中任意 sku組合時,發現用戶選中的這些 sku屬性全都不在 emptySkuIncludeList這個集合中,那麼就能夠肯定,不管下一個用戶點選或取消哪一個 sku屬性,都不會有須要置灰的 sku屬性

這很好理解,好比,如今庫存爲 0sku組合只有 72_1080697__6975_730004__6977_1081969__7335_710006這一條,也就是 版本:日韓、顏色:銀色、成色:全新、配置:256G這個 sku組合庫存是 0,那麼 emptySkuIncludeList也就是 ['72_1080697', '6975_730004', '6977_1081969', '7335_710006'], 當用戶點選 sku屬性的時候,選了 版本:國行,顏色:金色,對應 id組合也就是 72_10806276975_730005,發現這兩個屬性都不在 emptySkuIncludeList中,那麼此時就無需預測用戶下一步,由於不管用戶下一步選中或是取消哪一個 sku屬性,確定都不會存在須要置灰的 sku屬性

也即,這種狀況下,也無需進行置灰 sku屬性的計算,一樣是少了一大半的計算量

求數組交集

計算 indexKeyInfoMap的時候,好比計算 72_1080627__6975_730003__6977_1080699這個 sku組合對應的 商品全sku數據集合中數據的下標集合,就是從 商品全sku數據集合 中分別查找包含 72_10806276975_7300036977_1080699的下標集合,而後三個集合求交集,就是包含 72_1080627__6975_730003__6977_1080699這個 sku組合的下標集合

一開始我求數組交集,直接就是依次兩兩比較,遍歷其中一個數組,而後查找另一個數組中有沒有包含當前遍歷的這個數組項,若是有,那說明就是交集項

const resultArr = []
arr1.forEach(item => {
    if (arr2.includes(item)) {
        // 代表是交集項
        resultArr.push(item)
    }
})
複製代碼

這種算法複雜度大概是 O(m * n),若是數據量較大,仍是比較影響效率的,因而我又想到既然 arr1arr2中數據都是升序排列,那麼徹底能夠將算法複雜度降到 O(m)啊,因而有了下面這算法

function intersectionSortArr (...params: Array<Array<number>>): Array<number> {
  if (!params || params.length === 0) return []
  if (params.length === 1) {
    return params[0]
  }
  let arr1 = params[0]
  let arr2 = params[1]
  if (params.length > 2) {
    return intersectionSortArr(arr1, intersectionSortArr(arr2, ...params.slice(2)))
  }
  let arr = []
  if (!arr1.length || !arr2.length || arr1[0] > arr2.slice(-1)[0] || arr2[0] > arr1.slice(-1)[0]) {
    return arr
  }
  let j = 0
  let k = 0
  let arr1Len = arr1.length
  let arr2Len = arr2.length
  while (j < arr1Len && k < arr2Len) {
    if (arr1[j] < arr2[k]) {
      j++
    } else if (arr1[j] > arr2[k]) {
      k++
    } else {
      arr.push(arr1[j])
      j++
      k++
    }
  }
  return arr
}
複製代碼

避免使用模糊度較高的正則表達式

當用戶選中一些 sku屬性時,須要計算當前狀態須要置灰的 sku屬性,好比用戶選中了 72_10806276975_730005這兩個 sku屬性時,按照上面說的,須要到 indexKeyInfoMap[2]裏面去找同時包含這兩個 sku屬性的 sku組合數據

也就是到這些數據裏面找:

那麼如何從 72_1080627__6975_730003__6977_1080699這種格式的字符串裏,找到同時包含 72_10806276975_730005的那些呢? 通常來講,你可能想到用正則進行匹配,好比我,一開始就拿這個正則表達式去匹配

reg = /[0-9_]*72_1080627[0-9_]*6975_730005[0-9_]*/
複製代碼

爲何這麼寫呢?由於我只知道按照 paramId進行排序的話,72_1080627是排在 6975_730005前面的,但不清楚 72_10806276975_730005都應該在第幾位,前面和後面是否還有其餘屬性相連,因此寫了個很寬泛的正則,一條正則搞定,簡單省事

原本寫完以後以爲還行,問題不大,後來後端進行極端狀況測試的時候,每種商品發了上千條 sku,發現前端會卡十幾秒才能動,我趕忙查了一下,發現這塊正則耗費了太長時間,就是由於正則太模糊了

優化的方法也不少,好比,你對用戶選擇 paramId進行位置的肯定,明確地知道 72_1080627確定在第一位,6975_730005確定在第二位, 而且確定還有第三個,這樣能夠將正則精確爲

reg = /^72_1080627__6975_730005[0-9_]+/
複製代碼

相比於開始的那種模糊匹配,這種明顯精確了不少,少了不少正則分支,可是呢這畢竟還要先肯定位置,並且仍是會有模糊匹配的狀況,最好不用正則

既然選中的一組 sku屬性確定不會有重複的,而且數據都是有序的,也就是位置固定,那麼其實就是一個字符串查找嘛,直接 indexOf或者 includes都行,不過呢,這種查找仍是會存在一些無效查找,算法複雜度差很少是 O(m * n)這個量級

既然按照 paramId對用戶選中的 sku屬性進行升序排序,按照順序組成一個數組,好比 ["72_1080627", "6975_730005"],而後取 indexKeyInfoMap[2]的每一個字符串屬性,也按照 sku屬性分隔成數組形式,好比 ["72_1080627", "6975_730003", "6977_1080699"],而後對其遍歷:

const skuJoinArr = ["72_1080627", "6975_730003", "6977_1080699"]
const currentSkuArr = ["72_1080627", "6975_730005"]
let i = 0
skuJoinArr.forEach(item => {
    if (item === currentSkuArr[i]) i++
})
if (i === skuJoinArr.length) {
    // 說明匹配
}
複製代碼

不管用戶選了哪些 sku屬性,我須要查找的 indexKeyInfoMap[index]的屬性字符串的長度都只比用戶選中的多 1個,而且兩者都按照 paramId進行了升序排序,那麼只要兩個數組按照條件同時步進,最後長度較短的 currentSkuArr可以步進到最後,就說明 skuJoinArr 包含 currentSkuArr

這樣算法複雜度就降到了 O(m) 這個量級

固然,確定還有其餘能夠優化的地方,我目前比較明顯地就作了上面幾個,優化這種事情,並非精確到每一行每一段全項目無死角的完全優化就是最好的,由於優化過的代碼,每每跟按着正常思惟寫出來的不同,業務代碼講求一個性價比,均衡發展纔是最好的

加了上面的優化以後,再對接後端極限測試,在華爲 p30上進行測試,1000多條 sku,只須要 200~300 ms便可完成初始化,600多條sku只須要 70-80ms左右,相比於開始時的十幾秒,提高了幾十倍

而在實際應用場景中,對於一個正常的商品來講,通常 sku數量不會超過 100條的,好比如今京東上的 iphone XR,也就不到 60sku,初始化用時大概 20 ~ 30ms,因此徹底夠用了

注意事項

上面說到,數據源須要按照 paramId進行排序,通常來講這個字段都會是數字類型(或者數字類型的字符串形式),這裏是根據 paramId的大小進行排序的,但這裏可能存在一個問題,那就是 js的大數運算 如下參考自 MDN - Number.MAX_SAFE_INTEGER

Javascript有個最大安全整數的概念,此值爲 Number.MAX_SAFE_INTEGER,常量值爲 9007199254740991,即 2的53次方,這個數字造成的緣由是,Javascript 使用 IEEE 754中規定的 double-precision floating-point format numbers,在這個規定中能安全地表示數字的範圍在 [-(253 - 1), 253 - 1]

這裏的安全(Safe)是指可以準確地表示整數和正確地比較整數。例如 Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 將返回 true,這在數學上並不正確。更多參見 Number.isSafeInteger()

說得明白點,那就是若是 js運算的數字大於 9007199254740991,結果可能就是不許確的,咱們這裏之後端接口返回的 paramsId 的相互大小來有序化數據,若是 paramsId值的大小超過 9007199254740991,那麼兩個 paramId之間比較的結果就多是不對的,若是你已經跟後端確認過了,此值確定不會超過 9007199254740991,那最好不過,無需任何處理,但若是不能肯定,那必然要處理一下的,根據你的實際場景來決定就行

總結

一開始在想思路的時候,我一時半會不知從何下手,由於彷佛須要考慮的東西不少,考慮到了這個就忘了那個,但實際上靜下心來仔細理清思路後,其實仍是比較清晰的,思路有了,再將思路用代碼實現出來便可,這個就比較簡單了,只要本身不把本身繞暈就行

俗話說 talk is cheap, show me the code,文中一些表達可能仍是不太清楚,仍是代碼最直接,因此我也作了一個在線 Demo(最好在移動端查看),源代碼也能夠放到 github 上了,感興趣的能夠本身試下

固然了,若是你實在想不明白,也能夠把這事丟給後端來作,你調用接口傳參數等着接數據也能夠(手動狗頭) 第一次寫那麼長的博客,累死我了

相關文章
相關標籤/搜索