引言
Web 前端在过去几年发展迅速,从最早的 jQuery 到 Angular、React、Vue 框架,我们对 DOM 的操作方式的也在变化。
jQuery 提供了一系列快速操作 DOM 的方式,同时保证了兼容性。React 等这些框架则封装了DOM,隐藏了直接的 DOM 操作,提供例如 ref 等的方式。似乎我们对 DOM 的操作变的越来越少,但是 DOM 操作对于 Web 开发来说肯定是重要的一环,掌握相关知识也是必须的。
本人之所以打算重新全面的学习下 DOM 操作,是因为工作中发现自己对 DOM 的操作不够优雅,通过和第三方优秀的库的写法对比,发现自己有待提升。
实例
让我们来看一个简单又很常见的功能:对于一个出现弹出框,实现点击其他空白地方隐藏它。
点击其他空白地方隐藏,事件就需要绑定到 document 上,接下来就是判断“其他地方”。这个逻辑也很简单,判断点击的对象是否是弹出框,如果不是,属于“其他地方”,故实现了前面的判断也就实现了这个功能。
事件是绑定在 document 上的,所以我们通过 event.target 拿到点击的对象,再做判断。由于 event.target 可能是弹出框内容各个层级的 DOM 元素,所以需要判断 event.target 本身是或者父元素链上有弹出框最外层的 DOM 元素。
// document 事件绑定最好在弹出框出现后,同时在弹出框隐藏后也解绑事件
document.addEventListener('click', (event) => {
const popWrapper = ... // 弹出框最外层 DOM
const target = event.target
const isClickAway = target !== popWrapper && !isDescendant(popWrapper, target)
if (isClickAway) {
... // close pop
}
})
const isDescendant = function(parent, child) {
let node = child.parentNode;
while (node) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
}
上面的方法是可以实现需求,代码看上去也很清晰。本人之前也都是这个实现方案,但是实际上有更优雅的实现方式,如下:
// document 事件绑定最好在弹出框出现后,同时在弹出框隐藏后也解绑事件
document.addEventListener('click', (event) => {
const popWrapper = ... // 弹出框最外层 DOM
const isClickAway = popWrapper.contains(event.target)
if (isClickAway) {
... // close pop
}
})
DOM APIs 中提供了 contains 方法可以直接判断,兼容性方面也没大问题(不兼容可以通过 polyfill 解决)。可以看到第一种实现方案中的判断其实 DOM APIs 是有提供直接方法的,我们完全可以不必重复写代码。
所以说,掌握 DOM APIs 可以提升你编码效率和代码的整洁度。
DOM APIs
接下来介绍一些你应该掌握的 HTML DOM APIs 或 相关知识。本文例子均来自 HTML DOM。这是个优秀的网站,建议收藏。
classList
新增或移除类名时可以使用,ie 部分方法有兼容性问题。
ele.classList.add('class-name')
// 添加多个类名 (Not supported in IE 11)
ele.classList.add('another', 'class', 'name')
ele.classList.remove('class-name')
// 移除多个类名 (Not supported in IE 11)
ele.classList.remove('another', 'class', 'name')
ele.classList.toggle('class-name')
ele.classList.contains('class-name')
实际项目中通常使用 classnames 库来处理管理类名操作,但是还是需要了解 classList。
querySelector, querySelectorAll
通过 CSS 选择器查询 DOM 元素
// 返回匹配到的第一个元素
document.querySelector('.demo')
// 返回所有匹配的元素
document.querySelectorAll('.demo')
这个方法目前应该是很常见又实用的方法。
matches
判断 DOM 元素是否和提供的 CSS 选择器匹配
const matches = function(ele, selector) {
return (
ele.matches ||
ele.matchesSelector ||
ele.msMatchesSelector ||
ele.mozMatchesSelector ||
ele.webkitMatchesSelector ||
ele.oMatchesSelector
).call(ele, selector);
}
这个方法比 classList 的 contains 方法适用范围更广。
判断 DOM 元素是否另外一个元素的子元素
直接使用 DOM APIs,这个方法 parent 和 child 相同也会返回 true
const isDescendant = parent.contains(child)
遍历查看父元素
// Check if `child` is a descendant of `parent`
const isDescendant = function(parent, child) {
let node = child.parentNode;
while (node) {
if (node === parent) {
return true;
}
// Traverse up to the parent
node = node.parentNode;
}
// Go up until the root but couldn't find the `parent`
return false;
};
获取 CSS 样式
const styles = window.getComputedStyle(ele, null);
const bgColor = styles.backgroundColor;
const bgColor = styles.getPropertyValue('background-color');
设置 CSS 样式
通过 style 属性
ele.style.backgroundColor = 'red';
ele.style['backgroundColor'] = 'red';
ele.style['background-color'] = 'red';
通过 cssText
el.style.cssText += 'background-color: red; color: white';
移除 CSS 样式,通过 delete 无法移除,需要使用 removeProperty
ele.style.removeProperty('background-color');
// 这个不生效
ele.style.removeProperty('backgroundColor');
DOM 元素的宽高
// 获取 styles
const styles = window.getComputedStyle(ele);
// 宽高但不包括边框和内边距
const height = ele.clientHeight - parseFloat(styles.paddingTop)
- parseFloat(styles.paddingBottom);
const width = ele.clientWidth - parseFloat(styles.paddingLeft)
- parseFloat(styles.paddingRight);
// 宽高包括内边距
const clientHeight = ele.clientHeight;
const clientWidth = ele.clientWidth;
// 宽高包括内边距和边框
const offsetHeight = ele.offsetHeight;
const offsetWidth = ele.offsetWidth;
// 宽高包括内外边距和边框
const heightWithMargin = ele.offsetHeight + parseFloat(styles.marginTop)
+ parseFloat(styles.marginBottom);
const widthWithMargin = ele.offsetWidth + parseFloat(styles.marginLeft)
+ parseFloat(styles.marginRight);
这边正好复习了各个 width 属性获取的是指什么宽度。
DOM 相邻插入元素操作
插入元素到后面,把 ele 插入到 refEle 后面
refEle.parentNode.insertBefore(ele, refEle.nextSibling);
// Or
refEle.insertAdjacentElement('afterend', ele);
插入元素到前面,把 ele 插入到 refEle 前面
refEle.parentNode.insertBefore(ele, refEle);
// Or
refEle.insertAdjacentElement('beforebegin', ele);
预览上传的图片
介绍两个 API: URL.createObjectURL(), FileReader's readAsDataURL()。可以在上传时预览图片这个场景使用
<input type="file" id="fileInput" />
<img id="preview" />
const fileEle = document.getElementById('fileInput');
const previewEle = document.getElementById('preview');
使用 URL.createObjectURL()
fileEle.addEventListener('change', function(e) {
// Get the selected file
const file = e.target.files[0];
// Create a new URL that references to the file
const url = URL.createObjectURL(file);
// Set the source for preview element
previewEle.src = url;
})
使用 FileReader's readAsDataURL()
fileEle.addEventListener('change', function(e) {
// Get the selected file
const file = e.target.files[0];
const reader = new FileReader();
reader.addEventListener('load', function() {
// Set the source for preview element
previewEle.src = reader.result;
});
reader.readAsDataURL(file);
})
选中 DOM 元素的文字内容
使用到 selection 和 range 相关的知识,这块知识可以展开深入学习
const selectText = function(ele) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
}
主动触发事件
触发事件,有些特殊事件在元素的方法属性中,故可以直接调用。
// For text box and textarea
inputEle.focus();
inputEle.blur();
// For form element
formEle.reset();
formEle.submit();
// For any element
ele.click();
触发原生事件
const trigger = function(ele, eventName) {
const e = document.createEvent('HTMLEvents');
e.initEvent(eventName, true, false);
ele.dispatchEvent(e);
};
trigger(ele, 'mousedown')
触发自定义事件
const e = document.createEvent('CustomEvent');
e.initCustomEvent('hello', true, true, { message: 'Hello World' });
// Trigger the event
ele.dispatchEvent(e);
滚动到某个元素位置
ele.scrollIntoView();
// ie 和 safari 不支持 smooth 参数
ele.scrollIntoView({ behavior: 'smooth' });
总结
每个Web开发人员都应该要掌握 HTML DOM APIs。HTML DOM 这个网站展示了很多 API,同时提供了很多优秀的实例,本文大部分例子都取自这个网站。
除了一些简单的 API,还有一些需要深入学习才能掌握的 API,如拖拽相关、文本选中的 selection 和 range、DOMParser、window.URL 等等。