這是一道筆試題
我們先來了解一下需求是怎樣的
首先分析需求
在基本要求中
- 基本功能同瀏覽器的下拉框組件(不就
select
標(biāo)簽嗎,實(shí)現(xiàn)起來難度好像不是很大 :)) - 兼容盡量多的瀏覽器(制作一個(gè)
vue
組件的話能兼容到ie8(還有6匆绣,7 好像不是很符合題目 T T),初步?jīng)Q定使用原生js
來寫吧) - 支持直接輸入(我參考
element
中select
組件其中也包含可以直接輸入的組件,沒有采用原生html
中的select
標(biāo)簽莽鸭,而是使用input
標(biāo)簽來寫的,我們這邊也仿照elment
的寫法) - 輸入時(shí)下拉列表的選項(xiàng)自動(dòng)前綴匹配(嗯,百度了一波前綴匹配,腦海第一印象是正則一把梭)
- 匹配到的前綴用紅色文字顯示(好像不是很難...)
在分析完基本要求之后,我決定要這樣來完成它
- 使用原生
js + html + css
來完成它 - 使用
input
標(biāo)簽作為select
下拉框的可輸入部分 - 監(jiān)聽輸入事件,進(jìn)行前綴匹配
在擴(kuò)展要求中
- 支持異步加載數(shù)據(jù)(我的理解是異步獲取數(shù)據(jù)之后顯示在
options
中,所以我需要在全局維護(hù)一個(gè)options
數(shù)組,在異步獲取數(shù)組之后戳杀,更改options
的值,顯示在頁面上, 同理茂装,也就要構(gòu)造一個(gè)方法,根據(jù)傳值的不同展示不同的options
) - 支持大量數(shù)據(jù)(腦海里首先想到的就是優(yōu)化匹配方法)
- 用測試代碼測試組件功能(之前學(xué)過的 KARMA + MOCHA 總算派上用場了)
那么,動(dòng)手開始做吧
編寫靜態(tài)頁面
并且,在js
中, 定義我們經(jīng)常使用到的公共變量
// 是否顯示option
let optionShow = false
const body = document.querySelector('body')
// 輸入框
const input = document.querySelector('.input')
// 下拉框
const select = document.querySelector('.select')
// 下拉框箭頭
const arrow = document.querySelector('.arrow')
// 選項(xiàng)
const option = document.querySelector('.option')
// 等待狀態(tài)展示
const loading = document.querySelector('.loading')
// option為空展示
const empty = document.querySelector('.empty')
// 不為空時(shí)
const notEmpty = document.querySelector('.not-empty')
// 按鈕
const asyncButton = document.querySelector('#async-button')
特別的,我們維護(hù)了兩個(gè)公共狀態(tài)
- option框顯示狀態(tài)
optionShow
- 以及option框內(nèi)數(shù)據(jù)
options
數(shù)組
放在全局變量中的目的是唯一的變量對應(yīng)唯一的狀態(tài),減少代碼的冗余程度,也方便維護(hù)
實(shí)現(xiàn)場景1
用戶點(diǎn)擊下拉框,下拉框展開,輸入框旁的小箭頭轉(zhuǎn)換方向
function selectClickHandler () {
optionShow = !optionShow
// option顯隱
optionDisplay(optionShow)
// 控制箭頭朝向
arrowDirection()
}
// 是否展示options框
function optionDisplay (optionShow) {
let show = optionShow ? 'block' : 'none'
option.style.display = show
}
function arrowDirection () {
if (arrow.classList.contains('rotate') || arrow.classList.contains('rotate1')) {
arrow.classList.toggle('rotate') // 新學(xué)到的toggle方法
arrow.classList.toggle('rotate1')
} else {
arrow.classList.toggle('rotate')
}
}
// 監(jiān)聽select點(diǎn)擊事件
select.addEventListener('click', selectClickHandler)
到這一步,我們已經(jīng)能夠簡單的實(shí)現(xiàn)點(diǎn)擊input,變彈出下拉框了
但是下拉框此時(shí)還沒有數(shù)據(jù),
我們來為它添加一些默認(rèn)數(shù)據(jù)
function initOption (options, pattern) {
// 如果傳進(jìn)來的options沒有內(nèi)容則顯示暫無數(shù)據(jù)
if (options.length > 0) {
empty.style.display = 'none'
notEmpty.style.display = 'block'
} else {
empty.style.display = 'block'
notEmpty.style.display = 'none'
}
// 初始化
while(notEmpty.hasChildNodes()) {
notEmpty.removeChild(notEmpty.firstChild);
}
// 填充i標(biāo)簽
options.forEach(item => {
let li = document.createElement('li')
li.setAttribute('data-value', item.value)
li.setAttribute('data-label', item.label)
let textNode
if (pattern) {
textNode = document.createElement('span')
let redFont = document.createElement('span')
let text = document.createTextNode(pattern)
redFont.style.color = 'red'
redFont.appendChild(text)
let restChar = item.label.replace(pattern, '')
let blackFont = document.createTextNode(restChar)
textNode.appendChild(redFont)
textNode.appendChild(blackFont)
} else {
textNode = document.createTextNode(item.label)
}
li.appendChild(textNode)
notEmpty.appendChild(li)
})
}
// option選項(xiàng)
let options = [
{label: '西', value: 1},
{label: '西瓜', value: 2},
{label: '西瓜創(chuàng)', value: 3},
{label: '西瓜創(chuàng)客', value: 4},
{label: '西西', value: 1},
{label: '瓜瓜', value: 2},
{label: '創(chuàng)創(chuàng)', value: 3},
{label: '客客', value: 4}
]
// 我們在頁面初始化時(shí),調(diào)用initOption方法,填充對象
initOption(options)
到現(xiàn)在, 頁面點(diǎn)擊之后已經(jīng)可以看到下拉框中顯示出數(shù)據(jù)了
實(shí)現(xiàn)option點(diǎn)擊之后input的value變?yōu)檫x中的值
function handleOptionClick ($event) {
let element = $event.target
if (element.nodeName === 'UL') return
if (element.nodeName !== 'LI') {
element = element.parentNode
if (element.nodeName !== 'LI') {
element = element.parentNode
}
}
let label = element.getAttribute('data-label')
selectClickHandler()
window.setTimeout(() => {
input.value = label
}, 100)
}
notEmpty.addEventListener('click', handleOptionClick)
我們監(jiān)聽option的點(diǎn)擊時(shí)間,在初始化li標(biāo)簽的時(shí)候,我們已經(jīng)將數(shù)據(jù)的值通過自定義標(biāo)簽綁定到li標(biāo)簽上。所以在這里我們可以直接通過getAttribute api獲取該值,從而傳遞到input中
現(xiàn)在
我們來實(shí)現(xiàn)前綴匹配呢
function handleValueChange () {
// 輸入框在輸入時(shí)確保展示option框
if (!optionShow) {
optionShow = !optionShow
optionDisplay(optionShow)
arrowDirection()
}
let value = input.value
let newOptions
// 如果數(shù)據(jù)為空的時(shí)候,防止報(bào)錯(cuò),直接初始化
if (value === '') {
initOption(options)
return
}
// 我們維護(hù)了一個(gè)cache對象來存儲數(shù)據(jù),為了應(yīng)對數(shù)據(jù)量大的情況
if (cache.hasOwnProperty(value.charAt(0))) {
let re = new RegExp('^' + value)
newOptions = cache[value.charAt(0)].filter(item => {
return re.test(item.label)
})
} else {
newOptions = []
}
// 過濾已匹配的
initOption(newOptions, value)
}
// 兼容ie的做法
if (input.onpropertychange) {
input.addEventListener('propertychange', handleValueChange)
} else {
input.addEventListener('input', handleValueChange)
}
我們已經(jīng)實(shí)現(xiàn)了基本功能
那么,如何來實(shí)現(xiàn)異步操作嫩舟?
其實(shí)在我們構(gòu)造了一個(gè)initOption
方法之后,我們只需要將異步操作的結(jié)果作為參數(shù)傳遞到函數(shù)中,我們的組件就可以根據(jù)異步操作結(jié)果展示不同的option
那么我們來模擬一下異步操作吧
// 點(diǎn)擊獲取網(wǎng)絡(luò)數(shù)據(jù)之后,頁面展示加載中
function showLoadingFlag (loadingFlag) {
if (loadingFlag) {
loading.style.display = 'block'
} else {
loading.style.display = 'none'
}
}
// 模擬異步操作
function asyncLoading () {
isLoading = true
showLoadingFlag(isLoading)
setTimeout(() => {
isLoading = false
showLoadingFlag(isLoading)
let value = input.value
generateRadom(value)
}, 1000)
}
// 生成隨機(jī)option
function generateRadom (pattern) {
let num = Math.ceil((Math.random() * 10))
let RandomOptions = []
for (let i = 0; i< num; i++) {
let value = pattern + Math.random().toString()
RandomOptions.push({label: value, value})
}
initOption(RandomOptions, pattern)
}
那么我們在沒有數(shù)據(jù)的時(shí)候,我們構(gòu)造的函數(shù)已經(jīng)能為我們構(gòu)造假的數(shù)據(jù)作為展示,同時(shí)也完成了模擬異步操作的效果。
為了適用于數(shù)據(jù)量大的情況
我們每次在options
加載之后對options
進(jìn)行一次處理,我們?yōu)?code>options根據(jù)首字母構(gòu)造索引,從而每次匹配時(shí)只需要匹配首字母相同的數(shù)據(jù),從而減少對數(shù)據(jù)的操作
function adjustData (options) {
cache = {}
options.forEach(item => {
let firstChar = item.label.charAt(0)
if (cache.hasOwnProperty(firstChar)) {
cache[firstChar].push({label: item.label, value: item.value})
} else {
cache[firstChar] = [{label: item.label, value: item.value}]
}
})
}
總結(jié)
我實(shí)現(xiàn)了一個(gè)可以完成前綴匹配的select
下拉框,并且可以實(shí)現(xiàn)異步操作的功能怀偷。
花費(fèi)時(shí)間: 8小時(shí)
可改進(jìn)的地方:
- 使用
trie
數(shù)據(jù)結(jié)構(gòu)進(jìn)一步提高效率 - 因?yàn)闀r(shí)間原因沒有來得及做單元測試,可以通過
KARMA + MOCHA
完成單元測試 - 增加用戶交互特效,提升用戶體驗(yàn)
- 等
源碼在github
https://github.com/hux1ao/-/tree/master/%E7%AC%94%E8%AF%95-%E4%B8%8B%E6%8B%89%E6%A1%86