創(chuàng)建一個(gè)看似簡單的select下拉框

這是一道筆試題


需求.png

我們先來了解一下需求是怎樣的

首先分析需求

在基本要求中

  • 基本功能同瀏覽器的下拉框組件(不就select標(biāo)簽嗎,實(shí)現(xiàn)起來難度好像不是很大 :))
  • 兼容盡量多的瀏覽器(制作一個(gè)vue組件的話能兼容到ie8(還有6匆绣,7 好像不是很符合題目 T T),初步?jīng)Q定使用原生js來寫吧)
  • 支持直接輸入(我參考elementselect組件其中也包含可以直接輸入的組件,沒有采用原生 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)頁面
頁面.png

并且,在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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末家厌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子椎工,更是在濱河造成了極大的恐慌饭于,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件维蒙,死亡現(xiàn)場離奇詭異掰吕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)颅痊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門殖熟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人斑响,你說我怎么就攤上這事菱属∏ィ” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵纽门,是天一觀的道長薛耻。 經(jīng)常有香客問我,道長赏陵,這世上最難降的妖魔是什么饼齿? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮蝙搔,結(jié)果婚禮上缕溉,老公的妹妹穿的比我還像新娘。我一直安慰自己杂瘸,他們只是感情好倒淫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著败玉,像睡著了一般敌土。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上运翼,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天返干,我揣著相機(jī)與錄音,去河邊找鬼血淌。 笑死矩欠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悠夯。 我是一名探鬼主播癌淮,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沦补!你這毒婦竟也來了乳蓄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤夕膀,失蹤者是張志新(化名)和其女友劉穎虚倒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體产舞,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡魂奥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了易猫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耻煤。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出违霞,到底是詐尸還是另有隱情嘴办,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布买鸽,位于F島的核電站,受9級特大地震影響贯被,放射性物質(zhì)發(fā)生泄漏眼五。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一彤灶、第九天 我趴在偏房一處隱蔽的房頂上張望看幼。 院中可真熱鬧,春花似錦幌陕、人聲如沸诵姜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棚唆。三九已至,卻和暖如春心例,著一層夾襖步出監(jiān)牢的瞬間宵凌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工止后, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞎惫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓译株,卻偏偏與公主長得像瓜喇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子歉糜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理乘寒,服務(wù)發(fā)現(xiàn),斷路器现恼,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • select 選擇器是個(gè)比較復(fù)雜的組件了肃续,通過不同的配置可以有多種用法。有必要單獨(dú)學(xué)習(xí)學(xué)習(xí)叉袍。 整體結(jié)構(gòu) 以下是 s...
    VioletJack閱讀 4,500評論 0 0
  • 【平安證券】開戶無需任何費(fèi)用始锚,app直接提交不會泄露個(gè)人隱私。 ??下載平安證券APP喳逛,長按圖一的二維碼圖片識別安...
    簡毅ASH閱讀 308評論 0 0
  • 關(guān)鍵詞:上癮 高刺激 毅力 動(dòng)力 低刺激變成高刺激 游戲目標(biāo):會寫作業(yè)瞧捌,快速完成作業(yè),懂這些知識在說什么,我可以怎...
    梧簡潔閱讀 373評論 0 0
  • 在單身的那些年里姐呐,我不知道和我相愛的那個(gè)人會是誰殿怜。也不清楚她什么時(shí)候會安然降臨。 我曾無數(shù)次幻想過她的模樣曙砂,是用我...
    暖先森閱讀 3,931評論 33 70