这是一道笔试题
需求.png
我们先来了解一下需求是怎样的
首先分析需求
在基本要求中
- 基本功能同浏览器的下拉框组件(不就
select
标签吗,实现起来难度好像不是很大 :)) - 兼容尽量多的浏览器(制作一个
vue
组件的话能兼容到ie8(还有6,7 好像不是很符合题目 T T),初步决定使用原生js
来写吧) - 支持直接输入(我参考
element
中select
组件其中也包含可以直接输入的组件,没有采用原生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