前言#
用原生的 JavaScript 实现给定一个选择器(复合选择器和子选择器,不考虑逗号),和一个元素,判断该元素是否与该选择器匹配。
实现#
对于一个选择器比如 div #myid .class1.class2,我们应该是从后向前匹配。我们用 String.prototype.split() 方法将嵌套的选择器放入一个数组,然后从后向前依次进行匹配。最内层的选择器如果匹配失败则直接返回 false,其他父层级选择器则要依次向上遍历匹配。
复合选择器的匹配#
解决了基本的逻辑,我们需要处理的就是如何将一个复合选择器和元素进行匹配。比如 div#myid.class1.class2 这样的复合选择器。我的思路是用正则表达式将复合选择器分解为简单选择器,然后和元素的属性进行对比。
function specificity(selector) {
let reg = /(?<tagname>(\w+)?)(?<id>(#\w+)?)(?<classname>(.[\w.]+)?)/
let result = selector.match(reg)
return result.groups
}
console.log(specificity('div#myid.class1.class2'))
//[Object: null prototype] {
// tagname: 'div',
// id: '#myid',
// classname: '.class1.class2'
//}javascript我们利用正则表达式中的分组提取出 tagname,id 和 class。关于正则表达式可以看我的另一篇文章正则表达式的入门和JavaScript中的应用 ↗。
将复合选择器转化为简单选择器后,我们要做的就是取得元素的简单选择器然后进行比较。元素的几个对应属性很简单,tagName 可以用 element.tagName 直接获得;id 和 class 可以通过 element.getAttribute() 方法获得(形式略有不同,需要处理)
function compare(result, element) {
if (result.tagname !== '' && element.tagName.toLowerCase() !== result.tagname) {
return false
}
if (result.id !== '' && element.getAttribute('id') !== result.id.slice(1)) {
return false
}
if (result.classname !== '') {
let classnames = result.classname.split('.').filter((val) => !!val)
let el_classnames = element.getAttribute('class').split(' ')
let isContain = classnames.every((x) => el_classnames.includes(x))
if (!isContain) return false
}
return true
}javascript最后我们要做的就是循环选择器数组,与当前元素匹配。元素用 while 向上回溯,直到 element.parentElement 为 null。
function match(selector, element) {
let selectors = selector.split(' ')
for (let i = selectors.length - 1; i >= 0; i--) {
let result = specificity(selectors[i])
if (i === selectors.length - 1) {
if (!compare(result, element)) return false
} else {
let isMatch = false
element = element.parentElement
console.log(element)
while (element !== null && isMatch === false) {
console.log(result, element)
if (compare(result, element)) isMatch = true
element = element.parentElement
}
console.log(isMatch)
if (!isMatch) return false
}
}
return true
}javascript完整代码可以查看选择器元素匹配 ↗,打开开发者工具查看,
匹配机制#
我们上面说过,元素和选择器的匹配是从后往前(从右往左),这里来简单解释一下为什么。当我们要知道当前的元素是否和某个选择器匹配的时候,比如选择器 #id .class1 .class2。如果我们从左往右匹配,当我们遇到 #id 的元素的时候,我们需要检查所有的子元素来寻找 .class1,这在复杂的文档解构中是非常低效率的。而如果我们从后往前匹配,当我们遇到一个 .class2 的元素,我们只需要看他的父元素(有些选择器也要看兄弟元素)是否you 匹配 .class1 ,依次向上追溯就可以了。并且如果 .class2 没有匹配成功我们就可以直接确定这个元素不符合。
浏览器中的 CSS 是如何计算的可以参考这篇文章从Chrome源码看浏览器如何计算CSS ↗