创建一个看似简单的select下拉框

这是一道笔试题


需求.png

我们先来了解一下需求是怎样的

首先分析需求

在基本要求中

  • 基本功能同浏览器的下拉框组件(不就select标签吗,实现起来难度好像不是很大 :))
  • 兼容尽量多的浏览器(制作一个vue组件的话能兼容到ie8(还有6,7 好像不是很符合题目 T T),初步决定使用原生js来写吧)
  • 支持直接输入(我参考elementselect组件其中也包含可以直接输入的组件,没有采用原生 html中的 select标签,而是使用input标签来写的,我们这边也仿照elment的写法)
  • 输入时下拉列表的选项自动前缀匹配(嗯,百度了一波前缀匹配,脑海第一印象是正则一把梭)
  • 匹配到的前缀用红色文字显示(好像不是很难...)

在分析完基本要求之后,我决定要这样来完成它

  • 使用原生js + html + css来完成它
  • 使用input标签作为select下拉框的可输入部分
  • 监听输入事件,进行前缀匹配

在扩展要求中

  • 支持异步加载数据(我的理解是异步获取数据之后显示在options中,所以我需要在全局维护一个options数组,在异步获取数组之后,更改options的值,显示在页面上, 同理,也就要构造一个方法,根据传值的不同展示不同的options)
  • 支持大量数据(脑海里首先想到的就是优化匹配方法)
  • 用测试代码测试组件功能(之前学过的 KARMA + MOCHA 总算派上用场了)

那么,动手开始做吧

编写静态页面
页面.png

并且,在js中, 定义我们经常使用到的公共变量

    // 是否显示option
    let optionShow = false
    const body = document.querySelector('body')
    // 输入框
    const input = document.querySelector('.input')
    // 下拉框
    const select = document.querySelector('.select')
    // 下拉框箭头
    const arrow = document.querySelector('.arrow')
    // 选项
    const option = document.querySelector('.option')
    // 等待状态展示
    const loading = document.querySelector('.loading')
    // option为空展示
    const empty = document.querySelector('.empty')
    // 不为空时
    const notEmpty = document.querySelector('.not-empty')
    // 按钮
    const asyncButton = document.querySelector('#async-button')

特别的,我们维护了两个公共状态

  • option框显示状态 optionShow
  • 以及option框内数据options数组

放在全局变量中的目的是唯一的变量对应唯一的状态,减少代码的冗余程度,也方便维护

实现场景1

用户点击下拉框,下拉框展开,输入框旁的小箭头转换方向

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') // 新学到的toggle方法
    arrow.classList.toggle('rotate1')
  } else {
    arrow.classList.toggle('rotate')
  }
}
// 监听select点击事件
select.addEventListener('click', selectClickHandler)

到这一步,我们已经能够简单的实现点击input,变弹出下拉框了
但是下拉框此时还没有数据,

我们来为它添加一些默认数据
function initOption (options, pattern) {
  // 如果传进来的options没有内容则显示暂无数据
  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标签
  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选项
let options = [
  {label: '西', value: 1},
  {label: '西瓜', value: 2},
  {label: '西瓜创', value: 3},
  {label: '西瓜创客', value: 4},
  {label: '西西', value: 1},
  {label: '瓜瓜', value: 2},
  {label: '创创', value: 3},
  {label: '客客', value: 4}
]
// 我们在页面初始化时,调用initOption方法,填充对象
initOption(options)

到现在, 页面点击之后已经可以看到下拉框中显示出数据了

实现option点击之后input的value变为选中的值
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)

我们监听option的点击时间,在初始化li标签的时候,我们已经将数据的值通过自定义标签绑定到li标签上。所以在这里我们可以直接通过getAttribute api获取该值,从而传递到input中

现在

我们来实现前缀匹配呢
function handleValueChange () {
  // 输入框在输入时确保展示option框
  if (!optionShow) {
    optionShow = !optionShow
    optionDisplay(optionShow)
    arrowDirection()
  }
  let value = input.value
  let newOptions
  // 如果数据为空的时候,防止报错,直接初始化
  if (value === '') {
    initOption(options)
    return
  }
  // 我们维护了一个cache对象来存储数据,为了应对数据量大的情况
  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)
}

我们已经实现了基本功能
那么,如何来实现异步操作?

其实在我们构造了一个initOption方法之后,我们只需要将异步操作的结果作为参数传递到函数中,我们的组件就可以根据异步操作结果展示不同的option

那么我们来模拟一下异步操作吧
// 点击获取网络数据之后,页面展示加载中
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)
}
// 生成随机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)
}

那么我们在没有数据的时候,我们构造的函数已经能为我们构造假的数据作为展示,同时也完成了模拟异步操作的效果。

为了适用于数据量大的情况

我们每次在options加载之后对options进行一次处理,我们为options根据首字母构造索引,从而每次匹配时只需要匹配首字母相同的数据,从而减少对数据的操作

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}]
    }
  })
}

总结

我实现了一个可以完成前缀匹配的select下拉框,并且可以实现异步操作的功能。

花费时间: 8小时
可改进的地方:

  • 使用trie数据结构进一步提高效率
  • 因为时间原因没有来得及做单元测试,可以通过KARMA + MOCHA 完成单元测试
  • 增加用户交互特效,提升用户体验

源码在github
https://github.com/hux1ao/-/tree/master/%E7%AC%94%E8%AF%95-%E4%B8%8B%E6%8B%89%E6%A1%86

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,656评论 18 139
  • select 选择器是个比较复杂的组件了,通过不同的配置可以有多种用法。有必要单独学习学习。 整体结构 以下是 s...
    VioletJack阅读 4,505评论 0 0
  • 【平安证券】开户无需任何费用,app直接提交不会泄露个人隐私。 ➡️下载平安证券APP,长按图一的二维码图片识别安...
    简毅ASH阅读 312评论 0 0
  • 关键词:上瘾 高刺激 毅力 动力 低刺激变成高刺激 游戏目标:会写作业,快速完成作业,懂这些知识在说什么,我可以怎...
    梧简洁阅读 374评论 0 0
  • 在单身的那些年里,我不知道和我相爱的那个人会是谁。也不清楚她什么时候会安然降临。 我曾无数次幻想过她的模样,是用我...
    暖先森阅读 3,942评论 33 70