DOM模型

512 阅读8分钟

概述

文档对象模型,将网页转为一个js对象,实现各种操作。

Document节点

document.open() document.close()

  • document.open 清除当前文档所有内容,使文档处于可写状态
  • document.close 关闭document.open()打开的文档

document.querySelector() document.querySelectorAll()

接受CSS选择器作为参数,返回匹配该选择器的元素节点。

document.addEventListener() document.removeEventListener() document.dispatchEvent()

// 添加事件监听函数
document.addEventListener('click', listener, false);

// 移除事件监听函数
document.removeEventListener('click', listener, false);

// 触发事件
var event = new Event('click');
document.dispatchEvent(event);

Element节点

Element.clientHeight Element.clientWidth

// 视口高度
document.documentElement.clientHeight

// 网页总高度
document.body.clientHeight

Element.clientLeft Element.clientTop

  • 元素节点左边框的宽度,不包括左侧的paddingmargin

  • 网页元素顶部边框的宽度

Element.scrollHeight Element.scrollWidth

// 网页总高度
document.documentElement.scrollHeight
document.body.scrollHeight

Element.scrollLeft Element.scrollTop

// 整张网页水平垂直的滚动距离
document.documentElement.scrollLeft
document.documentElement.scrollTop

Element.offsetHeight Element.offsetWidth

元素CSS垂直高度,包括元素本身的高度、paddingborder,以及水平滚动条的高度,如果存在的话。

元素CSS水平宽度。

Element.offsetLeft Element.offsetTop

返回当前元素左上角相对于Element.offsetParent节点的水平位移,Element.offsetTop返回垂直位移,单位为像素。

Element.focus() Element.blur()

将当前页面的焦点,转移到指定元素上。

将焦点从当前元素移除。

document.getElementById('my-span').focus();

属性操作

Element.getAttribute()

返回当前元素节点的指定属性。

Element.setAttribute()

为当前元素节点新增属性。

Element.hasAttribute()

当前元素节点是否包含指定属性。

Element.removeAttribute()

从当前元素节点移除属性。

事件模型

EventTarget.addEventListener()

EventTarget.removeEventListener()

EventTarget.dispatchEvent()

在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true

监听函数

js有三种形式为事件绑定监听函数:

  • 1
<body onload = "doSomething()">
<div onclick = "console.log('触发事件')">
  • 2
window.onload = doSomething;
div.onclick = function(event) {
  console.log('触发事件');
};
  • 3 推荐
window.addEventListener('load', doSomething, false);

事件传播

事件发生之后,会在子元素和父元素之间进行传播,共有三个阶段:

  • 第一阶段(捕获):从window对象传导到目标节点,上层传到底层
  • 第二阶段(目标):在目标节点上触发
  • 第三阶段(冒泡):从目标节点传导回window对象,从底层传回上层
<div>
  <p>点击</p>
</div>
var phases = {
  1: 'capture',
  2: 'target',
  3: 'bubble'
};

var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);

function callback(event) {
  var tag = event.currentTarget.tagName;
  var phase = phases[event.eventPhase];
  console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

// 点击结果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'

上例事件的传播顺序:

  • 捕获阶段:windowdocumenthtmlbodydivp

  • 冒泡阶段:pdivbodyhtmldocumentwindow

事件传播的最上层对象是window,接着依次是documenthtmldocument.documentElement)和bodydocument.body)。

事件代理

由于事件会在冒泡阶段向上传播到父节点,因此可把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素事件。

优点:只要定义一个监听函数,就能处理多个子节点事件。

var ul = document.querySelector('ul');
ul.addEventListener('click', function(event) {
  if(event.target.tagName.toLowerCase() === 'li') {
    // some code
  }
});

stopPropagation

阻止事件的传播,只是阻止传播,并没有彻底取消click事件。

// 事件传播到`p`元素后就不再向下传播
p.addEventListener('click', function(event) {
  event.stopPropagation();
}, true);
// 事件冒泡到`p`元素后,就不再向上冒泡
p.addEventListener('click', function(event) {
  event.stopPropagation();
}, false);

Event对象

事件发生后,会产生一个事件对象,作为参数传给监听函数。

浏览器提供了一个Event对象,所有事件都是这个对象的实例。

Event.preventDefault()

取消浏览器对当前事件的默认行为(a标签跳转事件)。

注意:

  • 并不会阻止事件的传播

  • 若要阻止传播,使用stopPropagation()stopImmediatePropagation()

Event.stopPropagation()

阻止事件在DOM中继续传播。

function stopEvent(e) {
  e.stopPropagation();
}

el.addEventListener('click', stopEvent, false);
// click事件将不会进一步冒泡到el的父节点上

Event.currentTarget Event.target

  • 返回事件当前所在的节点,即正在执行的监听函数所绑定的那个节点

  • 返回原始触发事件的那个节点,即事件最初发生的节点

<p id="para">
    Hello <em>World</em>
</p>

function hide(e) {
  console.log(this === e.currentTarget);  // 总是 true
  console.log(this === e.target);  // 有可能不是 true
  e.target.style.visibility = 'hidden';
}
para.addEventListener('click', hide, false);

事件类型

鼠标事件

  • click
  • dblclick
  • mousedown
  • mouseup
  • mousemove鼠标在一个节点内部移动时触发,当鼠标持续移动时会连续触发
  • mouseenter鼠标进入一个节点时触发,进入子节点不会触发这个事件
  • mouseover鼠标进入一个节点时触发,进入子节点会再一次触发这个事件
  • mouseout鼠标离开一个节点时触发,离开父节点也会触发这个事件
  • mouseleave鼠标离开一个节点时触发,离开父节点不会触发这个事件
  • wheel

完成mousedown动作,再完成mouseup动作, 触发顺序是: mousedownmouseupclick

  • mouseenter事件只触发一次。

  • 只要鼠标在节点内部移动,mouseover会触发多次。

  • 在父元素内部离开一个子元素时,mouseleave不会触发,mouseout事件会触发。

MouseEvent.clientX MouseEvent.clientY

返回鼠标位置相对于浏览器窗口左上角的水平坐标和垂直坐标。

MouseEvent.screenX MouseEvent.screenY

返回鼠标位置相对于屏幕左上角的水平和垂直坐标。

MouseEvent.offsetX MouseEvent.offsetY

返回鼠标位置与目标节点左侧的padding边缘的水平和垂直距离。

键盘事件

  • keydown

  • keypress

  • keyup

如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下:

  1. keydown
  2. keypress
  3. keydown
  4. keypress
  5. …(重复以上过程)
  6. keyup

进度事件

描述资源加载进度及文件上传。

  • error:由于错误导致外部资源无法加载时触发
  • load:外部资源加载成功时触发
  • loadstart:外部资源开始加载时触发
  • loadend:外部资源停止加载时触发
  • progress:外部资源加载过程中不断触发
  • timeout:加载超时时触发

拖拉事件

<div draggable="true">
  此区域可拖拉
</div>
  • drag:拖拉过程中
  • dragstart:用户开始拖拉时,在被拖拉的节点上触发
  • dragend:拖拉结束时,在被拖拉的节点上触发
  • dragenter:拖拉进入当前节点时,在当前节点上触发一次
  • dragover:拖拉到当前节点上方,只要没有离开这个节点,dragover事件会持续触发
  • dragleave:拖拉操作离开当前节点范围时
  • drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发
div.addEventListener('dragstart', function (e) {
  this.style.backgroundColor = 'red';
}, false);

div.addEventListener('dragend', function (e) {
  this.style.backgroundColor = 'green';
}, false);
// 将一个节点从当前父节点,拖拉到另一个父节点中
/* HTML 代码如下
 <div class="dropzone">
   <div id="draggable" draggable="true">
     该节点可拖拉
   </div>
 </div>
 <div class="dropzone"></div>
 <div class="dropzone"></div>
 <div class="dropzone"></div>
*/

// 被拖拉节点
var dragged;

document.addEventListener('dragstart', function (event) {
  // 保存被拖拉节点
  dragged = event.target;
  // 被拖拉节点的背景色变透明
  event.target.style.opacity = 0.5;
}, false);

document.addEventListener('dragend', function (event) {
  // 被拖拉节点的背景色恢复正常
  event.target.style.opacity = '';
}, false);

document.addEventListener('dragover', function (event) {
  // 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
  event.preventDefault();
}, false);

document.addEventListener('dragenter', function (event) {
  // 目标节点的背景色变紫色
  // 由于该事件会冒泡,所以要过滤节点
  if (event.target.className === 'dropzone') {
    event.target.style.background = 'purple';
  }
}, false);

document.addEventListener('dragleave', function( event ) {
  // 目标节点的背景色恢复原样
  if (event.target.className === 'dropzone') {
    event.target.style.background = '';
  }
}, false);

document.addEventListener('drop', function( event ) {
  // 防止事件默认行为(比如某些元素节点上可以打开链接),
  event.preventDefault();
  if (event.target.className === 'dropzone') {
    // 恢复目标节点背景色
    event.target.style.background = '';
    // 将被拖拉节点插入目标节点
    dragged.parentNode.removeChild(dragged);
    event.target.appendChild( dragged );
  }
}, false);

注意:

  • 拖拉过程只触发拖拉事件,不触发鼠标事件
  • dragenterdragover事件的监听函数,用来取出拖拉数据

DataTransfer.files

// 接收拖拉文件的例子
// HTML 代码如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
//   文件拖拉到这里
// </div>

var div = document.getElementById('output');

div.addEventListener("dragenter", function( event ) {
  div.textContent = '';
  event.stopPropagation();
  event.preventDefault();
}, false);

div.addEventListener("dragover", function( event ) {
  event.stopPropagation();
  event.preventDefault();
}, false);

div.addEventListener("drop", function( event ) {
  event.stopPropagation();
  event.preventDefault();
  var files = event.dataTransfer.files;
  for (var i = 0; i < files.length; i++) {
    div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
  }
}, false);

表单事件

input

select

<input id="test" type="text" value="Select me!" />

var elem = document.getElementById('test');
elem.addEventListener('select', function (e) {
  console.log(e.type); // "select"
}, false);

submit事件

表单数据向服务器提交时触发。

窗口事件

scroll事件

缺点:用户拖动滚动条时触发,会连续大量触发,所以它的监听函数不应该有耗费计算的操作。

推荐使用requestAnimationFramesetTimeout控制该事件的触发频率,然后结合customEvent抛出一个新事件。

  • 1
(function () {
  var throttle = function (type, name, obj) {
    var obj = obj || window;
    var running = false;
    var func = function () {
      if (running) { return; }
      running = true;
      requestAnimationFrame(function() {
        obj.dispatchEvent(new CustomEvent(name));
        running = false;
      });
    };
    obj.addEventListener(type, func);
  };

  // 将 scroll 事件重定义为 optimizedScroll 事件
  throttle('scroll', 'optimizedScroll');
})();

window.addEventListener('optimizedScroll', function() {
  console.log('Resource conscious scroll callback!');
});

上面代码中,throttle函数用于控制事件触发频率,requestAnimationFrame方法保证每次页面重绘(每秒60次),只会触发一次scroll事件的监听函数。

  • 2

改用setTimeout方法,可以放置更大的时间间隔。

(function() {
  window.addEventListener('scroll', scrollThrottler, false);

  var scrollTimeout;
  function scrollThrottler() {
    if (!scrollTimeout) {
      scrollTimeout = setTimeout(function () {
        scrollTimeout = null;
        actualScrollHandler();
      }, 66);
    }
  }

  function actualScrollHandler() {
    // ...
  }
}());

上面代码中,每次scroll事件都会执行scrollThrottler函数。

该函数里面有一个定时器setTimeout,每66毫秒触发一次(每秒15次)真正执行的任务actualScrollHandler

  • 3

下面是一个更一般的throttle函数的写法:

function throttle(fn, wait) {
  var time = Date.now();
  return function() {
    if ((time + wait - Date.now()) < 0) {
      fn();
      time = Date.now();
    }
  }
}

window.addEventListener('scroll', throttle(callback, 1000));

上面代码将scroll事件的触发频率,限制在一秒一次。

  • 4

lodash函数库提供了现成的throttle函数,可以直接使用。

window.addEventListener('scroll', _.throttle(callback, 1000));

剪贴板事件

  • cut
  • copy
  • paste

焦点事件

  • focus
  • blur

由于focusblur事件不会冒泡,只能在捕获阶段触发,所以addEventListener方法的第三个参数需要设为true

CSS操作

getAttributesetAttributeremoveAttribute

div.setAttribute(
  'style',
  'background-color:red;' + 'border:1px solid black;'
);
// 相当于
<div style="background-color:red; border:1px solid black;" />
var divStyle = document.querySelector('div').style;

divStyle.backgroundColor = 'red';
divStyle.border = '1px solid black';
divStyle.width = '100px';
divStyle.height = '100px';
divStyle.fontSize = '10em';