最近作到一個需求,須要作一個相似於京東或者淘寶等電商的商品詳情頁,其中有一個功能就是商品SKU的選擇查詢問題html
如上圖,網絡類型、機身顏色、套餐類型、存儲容量這些每個都是一個 SKU
屬性,當選擇好了全部的 SKU
屬性後,會組合成一個完整的 SKU
,對應一個具體的商品,而後就能夠給出這條 SKU
對應的商品的庫存和價格等信息前端
而且,當點選了某些 SKU
屬性後,會自動根據當前已經點選的 SKU
屬性,來計算出當前條件下庫存爲 0
的 SKU
組合,給予按鈕置灰不可選的交互git
一開始接到這個需求的時候,評審需求的後端和同組合做的前端小夥伴都認爲這是一個難點,須要好好調研考量一下,而我則是邪魅一笑,以爲這東西不就是一個組合嘛,算法能寫就寫,不能寫大不了來個暴力循環查找,小場面啦github
然而,當我開始着手作的時候才發現跟日常確實不同,這些代碼好像不能閉着眼睛寫面試
當我發現這個功能確實有點小難度的時候,我就開始在網上搜相關文章了,這才發現相關文章好像都寫的比較煞有其事,跟什麼《你絕對不知道的數組的十種用法》什麼《原型鏈全場最佳分析》什麼《個人函數式編程不可能那麼可愛》什麼《三年前端大廠面試經歷》都不太同樣,看起來竟然須要帶着腦子,並且相關文章較少,除去那些抄來抄去的以及我沒搜到的以外,幾篇真正有實用價值的:正則表達式
我看完以後,頓時陷入了沉思算法
既然塑料恐龍是塑料作的,而塑料來源於石油,石油又是遠古恐龍屍體轉化成的。 那麼是否是說,塑料恐龍就是真的恐龍?編程
這些文章描述得感受不是很清楚,主要是沒有完整可運行代碼,看的雲裏霧裏的,照着他們說的來作,還不如我本身從新寫個,不過仍是有些借鑑意義的後端
SKU
的屬性,那麼根據這些屬性組合出來的 sku
確定是能夠被所有列舉出來的,並且考慮到現實業務使用中,屬性並不會太多,無需關心極端狀況,因此哪怕所有窮舉,對性能也沒多大損耗;sku
屬性的切換無非是字典 key
的變化固然了,思路是這個思路,可是真正寫代碼的時候,須要考慮的點比較多,並且都要考慮到,不管少了哪一點,數據就會都不對了數組
假設對於手機這個品類來講,它的 sku
屬性有成色、顏色、配置、版本,其中成色分爲 全新、僅拆封,顏色分爲深空灰、銀色、金色,配置分爲64G
、256G
,版本分爲國行、港澳版、日韓、其餘版本
每一個 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"
}
]
複製代碼
其中,paramIdJoin
是 sku
屬性組合鏈接而成,可分爲 72_108069七、6975_73000四、 6977_108196九、 7335_710006
這個四個單元,每一個單元又由 paramId
和 valueId
鏈接而成,因此 72_1080697__6975_730004__6977_1081969__7335_710006
的意思就是 版本爲日韓、顏色爲銀色、成色爲全新、配置爲256G的 sku
組合,對應的總庫存 count
爲 98
,價格範圍爲 7000 ~ 8978
,即最低價是 7000
,最高價是 8978
(若是隻有一個價格沒有高、低價格之分,那數組中的項就是那一個價格就好了),spu
的標識 id
spuDId
爲 98002993445
須要注意的是,
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
屬性項有不少種,例如顏色、內存、運營商、制式、成色、套餐類型、保險,那麼這種可組合的項就更多了
隨便想一下,就感受頭好大,彷佛是好多須要計算的東西啊
不過,其實也是有據可循的
當選中 n
個 sku
屬性的時候,應該被置灰的 sku
屬性實際上就是 當選中這 n
箇中任一個sku
屬性時應該被置灰的 sku
屬性,加上當選中這 n
箇中任兩個sku
屬性時應該被置灰的 sku
屬性,加上……加上當選中這 n
箇中任 n
個sku
屬性時應該被置灰的 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
屬性的狀況,共有 11
種 sku
選法,每種選法都有對應的 價格範圍和總庫存信息,例如 72_1080627
表示,當選中了 版本爲國行
這個 sku
屬性時,其價格範圍數組 priceArr
和總庫存 totalCount
;對於選中了 2
(下標爲 1
)個 sku
屬性的狀況,共有 44
種 sku
選法,每種選法都有對應的 價格範圍和總庫存信息,例如 72_1080627__6975_730003
表示,當選中了 版本爲國行,顏色爲深空灰色
這個 sku
屬性時,其價格範圍數組 priceArr
和總庫存 totalCount
……等等
有了這個信息,後面就好辦了
首先,你選中任意個 sku
屬性,我都能從這個集合裏找出對應的總庫存和價格範圍,無需在原數據中屢次篩選,這實現了第一個功能點; 對於第二個功能,當選中任意個 sku
屬性時(設爲 n
),我只須要在這個對象中選取 key
爲從 0
到 n
的數據,而且找這些數據對應的 sku
的總庫存爲 0
的數據,這些數據中包含 sku
屬性就都是須要置灰的,這就實現了第二個功能點
因此,最關鍵的就是這個Map
對象了,只要計算出這個Map
對象,其餘的就好辦了,固然,提及來簡單,真正用代碼實現仍是有不少須要注意的地方的
sku
屬性所屬下標集合即從接口返回的包含了全部 sku
屬性組合及其對應的數據的數據源中(也就是前面所說的 商品全sku
數據集合),計算出包含了每個 sku
屬性的數據項的下標的集合
好比,對於 顏色:黑色
這個 sku
屬性,商品全sku
數據集合數據中全部包含 顏色:黑色
這個 sku
屬性的數據的下標的集合,就是其所屬下標集合,以 sku
屬性的 paramId
和 valueId
爲 key
,如下標集合爲值,則能夠獲得一個集合,數據結構以下:
{
"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
數據集合數據中全部包含 paramId
爲 6977
,valueId
爲 1081969
這個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]
以此類推,就能獲得當任意取 n
個 sku
屬性時,所組成的 sku
組合對應的價格和庫存等信息的數據,也就是 indexKeyInfoMap
indexKeyInfoMap
只是一個緩存數據,須要實現的功能是,選中任意 sku
數據時,對應的價格和庫存等信息,以及應該置灰的 sku
信息
例如,對於當選中了 全新-銀色
這個組合的時候
全新-銀色
是兩個 sku
屬性的組合,則直接在 indexKeyInfoMap
中找 key
爲 1
的屬性,從其數組值中找匹配 全新-銀色
,也就是 子key
爲 6975_730004__6977_1081969
的數據項:
其中,priceArr
表示全部包含 全新-銀色
這個 sku
組合的 sku
數據的價格範圍集合,spuDIdArr
表示 spuDId
集合,totalCount
表示總庫存
sku
屬性這纔是關鍵
第一,計算當任何屬性都不選的時候,庫存爲 0
的屬性; 第二,計算當單獨選中 全新
和 單獨選中 銀色
的時候,分別與這兩個屬性組合的時候庫存爲 0
的屬性; 第三,計算當同時選中 全新-銀色
的時候,與 全新-銀色
進行組合的時候庫存爲 0
的屬性;
三種狀況下應該被置灰的屬性的並集就是 當選中 全新-銀色
的時候,應該被置灰的屬性
計算的基礎就是 indexKeyInfoMap
, 好比對於 第二,計算當單獨選中 全新
和 單獨選中 銀色
的時候,分別與這兩個屬性組合的時候庫存爲 0
的屬性;,其計算過程:
首先,任意一個 sku
屬性 與全新
或 銀色
組成的組合,就是兩個 sku
屬性連接,長度爲 2
,因此從 indexKeyInfoMap
中選取 key
爲 1
的數據,也就是 indexKeyInfoMap[1]
,今後數據中分別找包含 全新
和 銀色
,也就是 key
中包含 6977_1081969
或 6975_730004
,而且庫存爲 0
的數據項:
而後再對這被篩選出來的 7
條數據進行出來,獲得須要被置灰的 sku
屬性的集合,也就是這些數據的 key
字符串中,去掉當前已經選中的 sku
屬性,即 6977_1081969
和 6975_730004
後,還剩下的值的集合
例如,對於 72_1080628__6975_730004
而言,其處理以後剩下 72_1080628
,即 paramId
爲 72
, valueId
爲 1080628
的 sku
屬性所對應的按鈕應該被置灰,其餘相似
這樣就能獲得一個數組(須要對數組進行去重),此數組中的每一項,就是在同時選中 全新-銀色
的時候,所須要置灰不可點的 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
}
複製代碼
固然,你也能夠用本身以爲更好的算法,總之能解決問題而且效率及格就行
至此,流程結束,功能完成
根據上述流程已經能夠實現功能了,效率也不低,正常業務場景使用徹底沒問題,但還存在一些優化的空間
對於一個正常的商品來講,特別是平臺直營的商品,在絕大部分狀況下,其每一條 sku
都應該是有庫存的,那麼對於這種狀況,不管你在什麼狀況下選中了哪些 sku
屬性,都不該該出現被置灰的 sku
屬性,由於全部的 sku
都有庫存,若是我能肯定某個商品全部 sku
都有庫存,那就無需考慮按鈕置灰的事情了,計算量最大的那一部分就徹底能夠省去了,一會兒少了一大半的計算量
若是並非全部 sku
庫存都爲 0
,其實也有優化的空間,能夠精確到單個 sku
屬性,邏輯以下: 遍歷全部商品全sku
數據集合,找出全部庫存爲 0
的 sku
組合,從每一個組合中分離出單個 sku
屬性,將全部的這些單個 sku
屬性放入一個集合中(稱爲 emptySkuIncludeList
),那麼當用戶選中任意 sku
組合時,發現用戶選中的這些 sku
屬性全都不在 emptySkuIncludeList
這個集合中,那麼就能夠肯定,不管下一個用戶點選或取消哪一個 sku
屬性,都不會有須要置灰的 sku
屬性
這很好理解,好比,如今庫存爲 0
的 sku
組合只有 72_1080697__6975_730004__6977_1081969__7335_710006
這一條,也就是 版本:日韓、顏色:銀色、成色:全新、配置:256G這個 sku
組合庫存是 0
,那麼 emptySkuIncludeList
也就是 ['72_1080697', '6975_730004', '6977_1081969', '7335_710006']
, 當用戶點選 sku
屬性的時候,選了 版本:國行,顏色:金色,對應 id
組合也就是 72_1080627
和 6975_730005
,發現這兩個屬性都不在 emptySkuIncludeList
中,那麼此時就無需預測用戶下一步,由於不管用戶下一步選中或是取消哪一個 sku
屬性,確定都不會存在須要置灰的 sku
屬性
也即,這種狀況下,也無需進行置灰 sku
屬性的計算,一樣是少了一大半的計算量
計算 indexKeyInfoMap
的時候,好比計算 72_1080627__6975_730003__6977_1080699
這個 sku
組合對應的 商品全sku數據集合
中數據的下標集合,就是從 商品全sku數據集合
中分別查找包含 72_1080627
、6975_730003
、6977_1080699
的下標集合,而後三個集合求交集,就是包含 72_1080627__6975_730003__6977_1080699
這個 sku
組合的下標集合
一開始我求數組交集,直接就是依次兩兩比較,遍歷其中一個數組,而後查找另一個數組中有沒有包含當前遍歷的這個數組項,若是有,那說明就是交集項
const resultArr = []
arr1.forEach(item => {
if (arr2.includes(item)) {
// 代表是交集項
resultArr.push(item)
}
})
複製代碼
這種算法複雜度大概是 O(m * n)
,若是數據量較大,仍是比較影響效率的,因而我又想到既然 arr1
和 arr2
中數據都是升序排列,那麼徹底能夠將算法複雜度降到 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_1080627
和 6975_730005
這兩個 sku
屬性時,按照上面說的,須要到 indexKeyInfoMap[2]
裏面去找同時包含這兩個 sku
屬性的 sku
組合數據
也就是到這些數據裏面找:
那麼如何從 72_1080627__6975_730003__6977_1080699
這種格式的字符串裏,找到同時包含 72_1080627
和 6975_730005
的那些呢? 通常來講,你可能想到用正則進行匹配,好比我,一開始就拿這個正則表達式去匹配
reg = /[0-9_]*72_1080627[0-9_]*6975_730005[0-9_]*/
複製代碼
爲何這麼寫呢?由於我只知道按照 paramId
進行排序的話,72_1080627
是排在 6975_730005
前面的,但不清楚 72_1080627
和 6975_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
,也就不到 60
條 sku
,初始化用時大概 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 上了,感興趣的能夠本身試下
固然了,若是你實在想不明白,也能夠把這事丟給後端來作,你調用接口傳參數等着接數據也能夠(手動狗頭) 第一次寫那麼長的博客,累死我了