持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧55:理解DOM体系
本书大部分章节忽略你在哪种设备运行ts,但是这一章节不一样,如果你不工作在浏览器上,请跳过这个章节。
当你运行js在浏览器上,总会涉及DOM体系。你可以通过document.getElementById
获得DOM元素,或者通过 document.createElemen
创造DOM元素。了解DOM体系对你debug非常有帮助。
当用户拖动鼠标,假定你想跟踪用户的鼠标。你可能会这样写js:
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
targetEl.classList.add('dragging');
const dragStart = [eDown.clientX, eDown.clientY];
const handleUp = (eUp: Event) => {
targetEl.classList.remove('dragging');
targetEl.removeEventListener('mouseup', handleUp);
const dragEnd = [eUp.clientX, eUp.clientY];
console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
}
targetEl.addEventListener('mouseup', handleUp);
}
const div = document.getElementById('surface');
div.addEventListener('mousedown', handleDrag);
ts会报不少于11行错误:
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
targetEl.classList.add('dragging');
// ~~~~~~~ Object is possibly 'null'.
// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
const dragStart = [
eDown.clientX, eDown.clientY];
// ~~~~~~~ Property 'clientX' does not exist on 'Event'
// ~~~~~~~ Property 'clientY' does not exist on 'Event'
const handleUp = (eUp: Event) => {
targetEl.classList.remove('dragging');
// ~~~~~~~~ Object is possibly 'null'.
// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
targetEl.removeEventListener('mouseup', handleUp);
// ~~~~~~~~ Object is possibly 'null'
const dragEnd = [
eUp.clientX, eUp.clientY];
// ~~~~~~~ Property 'clientX' does not exist on 'Event'
// ~~~~~~~ Property 'clientY' does not exist on 'Event'
console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
}
targetEl.addEventListener('mouseup', handleUp);
// ~~~~~~~ Object is possibly 'null'
}
const div = document.getElementById('surface');
div.addEventListener('mousedown', handleDrag);
// ~~~ Object is possibly 'null'
需要理解这些错误,就要了解dom体系。这是一些HTML:
<p id="quote">and <i>yet</i> it moves</p>
当你打开浏览器后台,得到p元素,你会看到一个HTMLParagraphElement
:
onst p = document.getElementsByTagName('p')[0];
p instanceof HTMLParagraphElement
// True
HTMLParagraphElement的父类是HTMLElement,HTMLElement父类是Element,Element父类是Node,Node父类是EventTarget:
EventTarget是最通用的DOM元素类。可以在它上面添加,移除,派发事件。考虑到这一层,classList的错误开始变得合理:
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
targetEl.classList.add('dragging');
// ~~~~~~~ Object is possibly 'null'
// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'
// ...
}
顾名思义,Event的currentTarget
属性就是EventTarget。
EventTarget有可能为null。EventTarget有可能是HTMLElement,但是不可能为window,或者XMLHTTPRequest
我们来看Node。有一对例子说明存在元素是Node,但是不是Element:
- text fragment
- 注释(comments) 例如:
<p>
And <i>yet</i> it moves
<!-- quote from Galileo -->
</p>
最突出的元素是:HTMLParagraphElement
,如你所见,他有children和childNodes:
> p.children
HTMLCollection [i]
> p.childNodes
NodeList(5) [text, i, text, comment, text]
children 返回 HTMLCollection ,一个数组包含了 child Elements(<i>yet</i>)。ChildNodes返回一个NodeList,一个数组不仅包含了Elements(<i>yet</i>),还包含了text fragments('And','it moves')和注释('quote from Galileo').
Element 和HTMLElement有什么区别? 有非HTML的Element,例如SVG标签。另外<html> 和<svg>标签是HTMLHtmlElement
和SVGSvgElement
有的时候,特定的类有特定的属性。例如: HTMLImageElement有src属性,HTMLInputElement有value属性。当你想读特定属性那么元素类型一定要明确。
ts会尽可能获取精确类型:
document.getElementsByTagName('p')[0]; // HTMLParagraphElement
document.createElement('button'); // HTMLButtonElement
document.querySelector('div'); // HTMLDivElement
但是获取id就无法那么精确了:
document.getElementById('my-div'); // HTMLElement
通常类型断言不被推荐,但是如果你知道的比ts多,就建议用类断言,例如:
document.getElementById('my-div') as HTMLDivElement;
如果你能确定非Null,还可以加上非空断言:
const div = document.getElementById('my-div')!;
至于clientX和clientY错误:
function handleDrag(eDown: Event) {
// ...
const dragStart = [
eDown.clientX, eDown.clientY];
// ~~~~~~~ Property 'clientX' does not exist on 'Event'
// ~~~~~~~ Property 'clientY' does not exist on 'Event'
// ...
}
根据Mozilla文档,Event不少于52个type。Event是最通用的type,更具体的type:
- UIEvent :用户界面事件
- MouseEvent:鼠标事件
- TouchEvent:移动端的触屏事件。
- ...其他事件
clientX错误原因在于:clientX和clientY只存在MouseEvent类型上。 所以修复这个错误,指定type更具体:
function addDragHandler(el: HTMLElement) {
el.addEventListener('mousedown', eDown => {
const dragStart = [eDown.clientX, eDown.clientY];
const handleUp = (eUp: MouseEvent) => {
el.classList.remove('dragging');
el.removeEventListener('mouseup', handleUp);
const dragEnd = [eUp.clientX, eUp.clientY];
console.log('dx, dy = ', [0, 1].map(i => dragEnd[i] - dragStart[i]));
}
el.addEventListener('mouseup', handleUp);
});
}
const div = document.getElementById('surface');
if (div) {
addDragHandler(div);
}