[译]<<Effective TypeScript>> 技巧55:理解DOM体系

173 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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:

image.png

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>标签是HTMLHtmlElementSVGSvgElement

有的时候,特定的类有特定的属性。例如: 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);
}