icodingicoding
主页
JavaScript
Vue
React
TypeScript
Node
bug
笔记
时间线
主页
JavaScript
Vue
React
TypeScript
Node
bug
笔记
时间线
  • 一、JavaScript 基础
  • 二、引用数据类型
  • 三、函数
  • 四、对象
  • 五、DOM 与事件
  • 六、AJAX
  • 七、ES6
  • Generator 函数
  • 模块化

五、DOM 与事件

1、选择器

getElementById():通过id来查找对应的元素。

getElementsByClassName():通过类名来查找对应的元素,返回的是一个HTMLCollection对象。

getElementsByName():通过元素的name属性查找对应的元素,返回的是NodeList对象,它是一个类似于数组的结构。

getElementsByTagName(): 通过标签的名称来查找对应的元素,返回的是HTMLCollection对象。

querySelector:该选择器返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素。该选择器的参数接收的是一个css选择

<body>
  <div>
    <h4>标题内容</h4>
    <span>span标签内容</span>
    <p>
      段落内容
      <span>段落中的第一个span标签</span><br />
      <span>段落中的第二个span标签</span>
    </p>
  </div>
</body>
<script>
  console.log(document.querySelector("p span").innerHTML); // 获取p标签中第一个span标签中的内容,所以输出结果为:段落中的第一个span标签
  console.log(document.querySelector("h4,span").innerHTML); //获取第一个h4或者是span元素的内容:所以输出结果为:标题内容
  var ele = document.querySelector("p");
  console.log(ele.querySelector("div span").innerHTML); //段落中的第一个span标签。
  // 首先先找到`p`元素,然后看一下p元素下面有没有div,我们发现没有,但是依然能够匹配到span元素。
  //原因是:在匹配的过程中会优先找出最外层div元素下的span元素的集合,然后在判断span元素是否属于p元素的子元素,最后返回
  //第一个匹配到的span元素的值。
</script>

下面,把HTML文档的结构修改成如下的形式:

<div>
  <h4>标题内容</h4>
  <span>span标签内容</span>
  <!--这里增加了一个p标签-->
  <p>第一个段落</p>
  <p>
    段落内容
    <span>段落中的第一个span标签</span><br />
    <span>段落中的第二个span标签</span>
  </p>
</div>

执行如下代码会出现异常:

var ele = document.querySelector("p");
console.log(ele.querySelector("div span").innerHTML); //Cannot read property 'innerHTML' of null

原因:会找到第一个p元素,然后看一下p标签中是否有div,发现没有,但是会找出最外层div下的所有span元素的集合,看一下span元素是否属于p元素,而第一个p元素中没有span元素,所以抛出异常。

querySelectorAll()选择器:

querySelectorAll选择器与querySelector选择器的区别是:querySelectAll选择器会获取到基准元素下匹配到所有子元素的集合。返回的是一个NodeList集合。

<div>
  <h4>标题内容</h4>
  <span>span标签内容</span>
  <p>
    段落内容
    <span>段落中的第一个span标签</span><br />
    <span>段落中的第二个span标签</span>
  </p>
</div>
<script>
  console.log(document.querySelectorAll("span")); //返回所有的span标签。
</script>

下面,再来看一段代码:

<div id="container">
  <div class="bar"></div>
  <div class="foo">
    <div class="inner"></div>
  </div>
</div>
<script>
  // 获取container下的所有div元素。
  var div1 = document.querySelectorAll("#container div");
  console.log(div1); // NodeList(3) [div.bar, div.foo, div.inner]
</script>

2、HTMLCollection 对象与 NodeList 对象区别

在介绍前面的选择器的时候,它们返回的值有些是HTMLCollection对象,有些是NodeList对象,它们有什么区别?

HTMLCollection对象具有1ength属性,返回集合的长度,可以通过item()和namedItem()函数来访问特定的元素。

item()函数:通过序号索引值来获取特定的某个节点,超过索引则返回null.

<div id="container">
  <div class="bar"></div>
  <div class="foo">
    <div class="inner"></div>
  </div>
</div>
<script>
  var main = document.getElementById("container").children;
  console.log(main); //HTMLCollection
  console.log(main.item(0)); //输出:<div class="bar"></div>
  console.log(main.item(1)); // 输出:foo元素
</script>

namedItem()函数:该函数用来返回一个节点,首先通过id属性去匹配,然后如果没有匹配到则使用name属性匹配,如果还没有匹配到则返回null. 当出现重复的id或者name属性时,只返回匹配到的第一个值。

<form id="form1">
  <input type="text" id="userName" />
  <input type="password" id="password" name="userPwd" />
</form>
<script>
  var form1 = document.getElementById("form1").children;
  console.log(form1.namedItem("userPwd")); //   <input type="password" id="password" name="userPwd" />
</script>

NodeList对象也具有length属性,返回集合的长度,同样也有item函数,也是通过索引定位子元素的位置。但是NodeList对象没有namedItem方法。

HTMLCollection对象与NodeList对象都是类似数组的结构,如果想调用数组中的方法,需要通过call()函数或者是apply()函数,转换为真正的数组后,可以使用数组中的函数。

同时,当我们对DOM树新增或者是删除一个节点的时候,都会立即的放映在HTMLCollection对象与NodeList对象中。

  <form id="form1">
      <input type="text" id="userName" />
      <input type="password" id="password" name="userPwd" />
    </form>
<script>
 //获取HTMLCollection集合
    var form1Children = document.getElementById("form1").children;
    // 获取form元素
    var form1 = document.getElementById("form1");
    console.log(form1Children.length); // 2 HTMLCollection中有两个子元素
    var input = document.createElement("input"); //创建input元素
    form1.appendChild(input); // 把创建的input元素添加到form元素中
    console.log(form1Children.length); // 3 可以看到HTMLCollection立即受到了影响

最后,总结一下HTMLCollection对象与NodeList对象的相同点与不同点

相同点:

第一:都是类似数组的结构,有length属性,可以通过call()函数或者是apply()函数转换成数组,使用数组中的函数。

第二:都用item函数,通过索引值获取相应的元素。

第三:都是实时的,当在DOM树上添加元素或者是删除元素,都会立即反应到HTMLCollection对象和NodeList对象上。

不同点:

第一:HTMLCollection对象中,有namedItem()函数,而NodeList对象中没有.

第二:NodeList对象中存储的是元素节点的集合,包括元素,以及节点,例如text文本节点,而HTMLCollection对象中只包含了元素的集合。

<form id="form1">
  用户名<input type="text" id="userName" /> <br />
  用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
  //获取HTMLCollection集合
  var form1Children = document.getElementById("form1").children;
  console.log(form1Children);
  //获取NodeList对象
  var formNodes = document.getElementById("form1").childNodes;
  console.log(formNodes);
</script>

通过查看浏览器控制台输出的结果,可以看出HTMLCollection对象与NodeList对象的区别。

3、常见的 DOM 操作有哪些?

添加节点

<form id="form1">
  用户名<input type="text" id="userName" /> <br />
  用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
  var form1 = document.getElementById("form1");
  //创建一个input元素
  var newInput = document.createElement("input");
  //创建属性
  var newAttr = document.createAttribute("type");
  newAttr.value = "password";
  //将属性绑定到元素上
  newInput.setAttributeNode(newAttr);

  //创建一个文本节点
  var newTextNode = document.createTextNode("用户密码");
  form1.appendChild(newTextNode); //添加文本节点
  form1.appendChild(newInput);
</script>

删除节点

<form id="form1">
  用户名<input type="text" id="userName" /> <br />
  用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
  var form1 = document.getElementById("form1");
  var nodeChilds = form1.childNodes;
  console.log(nodeChilds);
  form1.removeChild(nodeChilds[0]);
  form1.removeChild(nodeChilds[0]);
</script>

在上面的代码中,我们想将表单中的用户名这一项内容删除掉。

首先获取form表单,然后在获取对应的子元素。

通过执行removeChild方法删除第一个元素,而第一个元素是用户名这个文本字符串,

下面还要删除文本框,所以再次调用了removeChild函数,注意由于前面已经删除了用户名这个文本元素了,所以文本框成为了第一个元素,所以这里写到索引值也是 0.

删除文本框的id属性

<form id="form1">
  用户名<input type="text" id="userName" /> <br />
  用户密码<input type="password" id="password" name="userPwd" />
</form>
<script>
  var input = document.querySelector("#userName");
  input.removeAttribute("id");
</script>

修改节点

修改元素节点

修改元素的节点的操作,一般是直接用新的节点替换旧的节点。关于节点的替换可以使用,replaceChild函数来实现,该函数的调用是通过父元素来调用的,例如:把div1中的内容替换掉,这里就需要通过container.replaceChild方法来完成,replaceChild方法需要两个参数,第一个参数表示的是新元素,第二个参数表示的是旧元素。

<div id="container">
  <div id="div1">hello</div>
</div>
<script>
  var container = document.getElementById("container"); //获取父元素container
  var div1 = document.getElementById("div1"); //获取子元素
  var newDiv = document.createElement("div"); // 创建一个新的div元素
  var newText = document.createTextNode("nihao"); //创建一个文本内容
  newDiv.appendChild(newText); //把创建的文本内容添加到新的div中
  container.replaceChild(newDiv, div1); //用新的div替换旧的div,完成节点的修改操作。
</script>

修改属性节点

修改属性的节点,我们可以通过setAttribute()函数来完成,如果想获取属性节点可以通过getAttribute()函数来完成。

<div id="container">
  <div id="div1" style="color: red">hello</div>
</div>
<script>
  var div1 = document.getElementById("div1");
  div1.setAttribute("style", "color:blue"); //设置style属性
  console.log(div1.getAttribute("style")); // 获取style属性的值
</script>

修改属性节点除了通过setAttribute()方法完成以外,还可以通过属性名直接进行修改

<div id="container">
  <div id="div1" style="color: red">hello</div>
</div>
<script>
  var div1 = document.getElementById("div1");
  div1.style.color = "blue";
</script>

但是通过这种方式进行修改,还需要注意一个问题:直接修改的属性名与元素节点中的属性名不一定是一致的。例如class这个属性,在javascript中是关键字,不能直接作为属性使用,这时需要通过className来完成。

<div id="container">
  <div id="div1" style="color: red">hello</div>
  <div id="div2" class="foo">前端学习</div>
</div>
<script>
  var div2 = document.getElementById("div2");
  div2.className = "bar"; //注意这里使用的是className
</script>

通过查看浏览器控制台,可以看到对应的样式发生了修改。

修改文本节点

文本节点的修改,可以通过innerHTML属性来完成。

<div id="container">
  <div id="div1" style="color: red">hello</div>
  <div id="div2" class="foo">前端学习</div>
</div>
<script>
  var div2 = document.getElementById("div2");
  div2.innerHTML = "Vue 学习";
</script>

4、DOM 性能问题

Dom操作非常消耗性能,应该尽量避免频繁的操作DOM.

导致浏览器重绘,重新渲染,比较消耗cpu资源,比较消耗性能。

提升性能的方案:

第一:对DOM查询操作进行缓存

第二:将频繁操作修改为一次性操作

首先看第一种情况:

这里需要对页面中所有p标签内文字调整大小(单击按钮完成)

//不缓存的结果
for (let i = 0; i < document.getElementsByTagName("p").length; i++) {
  //每次循环,都会计算lenght,频繁进行DOM查询
}
const pList = document.getElementsByTagName("p");
const length = pList.length;
for (let i = 0; i < length; i++) {
  //缓存length,只进行一次DOM查询
}

下面看一下第二种情况:

需求:页面中有一个ul列表,需要单击按钮一次性插入 10 个或者 100 个li?

传统的做法:

<ul id="list"></ul>
const listNode = document.getElementById("list");
for (let i = 0; i < 10; i++) {
  const li = document.createElement("li");
  li.innerHTML = `item${i}`;
  list.appendChild(li);
}

执行上面的代码,可以实现对应的需求,但是问题是上面的操作是频繁操作dom,性能比较低。

const listNode = document.getElementById("list");
//创建一个文档片段,文档片段存在于内存中,并不在DOM树中,所以此时还没有插入到DOM中
//也就是先将dom插入到临时区域中
const frag = document.createDocumentFragment();
//执行插入

for (let i = 0; i < 10; i++) {
  const li = document.createElement("li");
  li.innerHTML = `item${i}`;
  frag.appendChild(li);
}
//都完成后,再插入到DOM树中
listNode.appendChild(frag);

5、什么是事件传播

在浏览器中,JavaScript和HTML之间的交互是通过事件实现的,常用的事件包括了鼠标点击的事件,鼠标移动事件等等。

当事件发生以后,会触发绑定在元素上的事件处理程序,执行相应的操作。

问题是当事件发生后,事件是怎样传播的呢?

事件发生后会在目标节点和根节点之间按照特定的顺序进行传播,路径经过的节点都会接收到事件。

这里的特定顺序是怎样的顺序呢?

第一种:事件传递的顺序是先触发最外层的元素,然后依次向内传播,这样的传递顺序我们称之为事件的捕获阶段。

第二种:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。

当然,一个完整的事件传播包含了三个阶段

首先就是事件的捕获阶段

然后是事件的目标阶段,目标阶段指的就是事件已经到达目标元素。

最后是事件的冒泡阶段

以上就是关于事件传播的描述

6、什么是事件的捕获

关于事件捕获,在上一小节,我们已经介绍过:事件的传递是从最外层开始,依次向内传播,在捕获阶段,事件从window开始,一直到触发事件的元素。

window----> document----> html----> body ---->目标元素

如下代码所示:

<body>
  <table border="1">
    <tbody>
      <tr>
        <td>单元格内容</td>
      </tr>
    </tbody>
  </table>
  <script>
    var table = document.querySelector("table");
    var tbody = document.querySelector("tbody");
    var tr = document.querySelector("tr");
    var td = document.querySelector("td");
    table.addEventListener(
      "click",
      function () {
        console.log("table");
      },
      true
    );
    tbody.addEventListener(
      "click",
      function () {
        console.log("tbody");
      },
      true
    );
    tr.addEventListener(
      "click",
      function () {
        console.log("tr");
      },
      true
    );
    td.addEventListener(
      "click",
      function () {
        console.log("td");
      },
      true
    );
  </script>
</body>

在上面的代码中,有一个表格,给表格中的每个元素通过addEventListener方法绑定了单击事件,同时该方法的第三个参数,设置为了true,这样就表明事件将在捕获阶段发生。

所以当我们单击td单元格的时候,事件的执行结果是:table,tbody,tr,td.也就是说事件从table开始,依次向下传播。这个传播的过程就是事件捕获。

7、什么是事件冒泡

关于事件的冒泡,在前面也已经提到过:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。

如下代码所示:

<body>
  <table border="1">
    <tbody>
      <tr>
        <td>单元格内容</td>
      </tr>
    </tbody>
  </table>
  <script>
    var table = document.querySelector("table");
    var tbody = document.querySelector("tbody");
    var tr = document.querySelector("tr");
    var td = document.querySelector("td");
    table.addEventListener("click", function () {
      console.log("table");
    });
    tbody.addEventListener("click", function () {
      console.log("tbody");
    });
    tr.addEventListener("click", function () {
      console.log("tr");
    });
    td.addEventListener("click", function () {
      console.log("td");
    });
  </script>
</body>

上面的代码,我们将addEventListener方法的第三个参数true去掉了,这时就有事件的捕获变成了事件的冒泡。默认值为(false).

但单击单元格的时候,执行的结果为:td,tr,tbody,table, 这个过程就是事件的冒泡。

8、阻止事件冒泡

现在,我们已经了解了事件冒泡的过程,但是在很多的情况下,我们需要阻止事件冒泡的发生。

例如:在上一小节的案例中,当我们单击了单元格后,不仅触发单元格元素的事件,同时也会触发其它元素的事件,而这里我们只希望触发单元格的事件。所以这里需要阻止事件的冒泡。

阻止事件的冒泡需要使用:event.stopPropagation()函数

如下案例:

  <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      table.addEventListener("click", function () {
        console.log("table");
      });
      tbody.addEventListener("click", function () {
        console.log("tbody");
      });
      tr.addEventListener("click", function () {
        console.log("tr");
      });
      td.addEventListener("click", function (event) {
        //阻止了事件的冒泡操作
        event.stopPropagation();
        console.log("td");
      });
    </script>

在单元格的事件处理函数中,通过event.stopPropagation()方法阻止了事件的冒泡。

与stopPropagation()函数相对的还有一个stopImmediatePropagation函数,它们两者之间有什么区别呢?

stopPropagation():函数会阻止事件冒泡,其它事件处理程序仍然可以调用

stopImmediatePropagation函数不仅可以阻止事件冒泡,也会阻止其它事件处理程序的调用。

如下代码所示:

<body>
  <table border="1">
    <tbody>
      <tr>
        <td>单元格内容</td>
      </tr>
    </tbody>
  </table>
  <script>
    var table = document.querySelector("table");
    var tbody = document.querySelector("tbody");
    var tr = document.querySelector("tr");
    var td = document.querySelector("td");
    table.addEventListener("click", function () {
      console.log("table");
    });
    tbody.addEventListener("click", function () {
      console.log("tbody");
    });
    tr.addEventListener("click", function () {
      console.log("tr");
    });
    //单元格第一个单击事件
    td.addEventListener("click", function (event) {
      console.log("td1");
    });
    //单元格第二个单击事件
    td.addEventListener("click", function (event) {
      //阻止了事件的冒泡操作
      // event.stopImmediatePropagation();
      event.stopPropagation();
      console.log("td2");
    });
    //   单元格第三个单击事件
    td.addEventListener("click", function (event) {
      console.log("td3");
    });
  </script>
</body>

在上面的代码中,给单元格添加了三个单击的事件,同时第二个单击事件使用了stopPropagation方法来阻止冒泡行为。

执行结果如下:td1,td2,td3

通过执行结果,可以看到单元格的三个单击事件全部触发,并且阻止了冒泡的行为。

如果使用stopImmediatePropagation方法,执行结果为:td1,td2

通过执行的结果可以看到,阻止了冒泡的行为,但是没有触发单元格的第三个单击的事件,也就是说会阻止其它事件的执行。

9、事件冒泡与事件捕获问题

下面我们来看一段代码:

<body>
  <table border="1">
    <tbody>
      <tr>
        <td>单元格内容</td>
      </tr>
    </tbody>
  </table>
  <script>
    var table = document.querySelector("table");
    var tbody = document.querySelector("tbody");
    var tr = document.querySelector("tr");
    var td = document.querySelector("td");
    //事件捕获
    table.addEventListener(
      "click",
      function () {
        console.log("table");
      },
      true
    );
    //事件冒泡
    tbody.addEventListener("click", function () {
      console.log("tbody");
    });
    //事件捕获
    tr.addEventListener(
      "click",
      function () {
        console.log("tr");
      },
      true
    );
    //事件冒泡
    td.addEventListener("click", function (event) {
      console.log("td");
    });
  </script>
</body>

在上面的代码中,既有事件捕获又有事件冒泡,那么执行的结果是怎样的呢?

当单击td单元格后

执行结果:table,tr,td,tbody

分析:前面我们讲解过,事件传播的循序是:先事件捕获阶段,然后事件的目标阶段,最后是事件冒泡阶段

所以说,在一个程序中有事件的捕获阶段,又有事件的冒泡阶段,会优先执行捕获阶段的事件。

所以上面代码整个执行的流程:

先执行table这个捕获阶段,输出table这个字符串

下面执行tbody,但是tbody绑定的是冒泡类的事件,所以不执行,跳过。

下面是tr,而tr是捕获类型的事件,所以会执行,输出字符串tr

下面是td,由于我们单击的是td元素,所以该元素就是事件目标元素,则会执行,输出td字符串。

当单击了td元素以后,就开始进入了事件冒泡阶段。这时会冒泡到tr元素,但是tr元素绑定的是捕获阶段的事件,所以不执行,直接跳过,下面继续冒泡到了tbody元素,该元素绑定的是冒泡类型的事件,所以执行,输出字符串tbody.

下面继续冒泡,执行到table元素,该原生是捕获类型的事件,所以直接跳过,没有输出。

10、Event 对象使用

在JavaScrip中,每触发一个事件,就会产生一个Event对象,在该对象中包含了所有与事件相关的内容,包括事件的元素,事件类型等。

当给某个元素绑定了事件处理程序后,就可以获取到Event对象,但是在不同的浏览器下,Event对象的实现还是有一定的差异的。

关于获取Event对象的方式有两种:

第一种:在事件的处理程序中,可以通过参数来获取Event对象。

第二种:在事件的处理程序中,可以通过window.event属性获取Event对象。

具体的示例代码如下:

 <body>
    <button id="btn">单击</button>

    <script>
      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        //通过参数获取Event对象
        console.log("event=", event);
        //通过window.event的方式来获取Event对象
        var windEvent = window.event;
        console.log("windEvent=", windEvent);
        //判断两种方式是否相等
        console.log(event === windEvent);
      });
    </script>
  </body>

在谷歌浏览器中,测试上面的代码,可以发现两种获取Event对象的方式是相等的。

但是注意,在其它的浏览中进行测试可能会出现不相等的情况,也就是有的浏览器会出现不支持window.event这种方式来获取Event对象,这里可以自行进行测试。

为了能够在获取事件对象的时候,支持不同的浏览器,我们可以做兼容性的处理。

 <body>
    <button id="btn">单击</button>

    <script>
      var eventFn = {
        event: function (e) {
          return e || window.event;
        },
      };

      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        //通过参数获取Event对象
        console.log("event=", eventFn.event(event));
        //通过window.event的方式来获取Event对象
        var windEvent = eventFn.event(window.event);
        console.log("windEvent=", windEvent);
        //判断两种方式是否相等
        console.log(event === windEvent);
      });
    </script>

在上面的代码中定义了eventFn对象,在该对象中有一个属性event,在该属性中,判断返回Eevent对象的方式。

在对应的事件处理函数中,可以调用eventFn对象中的event方法来获取Event对象。

获取事件的目标元素

在事件的处理程序中,我们可能需要获取 事件的目标元素。

在IE浏览器中,可以使用event对象中的srcElement属性来获取事件的目标元素,在非IE浏览器中可以通过event对象的target属性来获取事件的目标元素,当然在有的非IE浏览器下也支持event对象中的srcElement属性,目的是为了保持与ie保持一致,但是要注意的是并不是所有的非IE浏览器都支持srcElement属性。

<script>
     var eventFn = {
        event: function (e) {
          return e || window.event;
        },
      };

      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        var event = eventFn.event(event);
        console.log("target=", event.target);
        console.log("srcElement=", event.srcElement);
      });
 </script>

在谷歌浏览器中进行测试,都可以获取target属性和srcElement属性的值。

关于其它浏览器的情况,可以自行测试。

当然为了能够兼容其它的浏览器,可以做一下兼容的处理。

var eventFn = {
  event: function (e) {
    return e || window.event;
  },
  target: function (e) {
    return e.target || e.srcElement;
  },
};

var btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
  var event = eventFn.event(event);
  console.log("target=", eventFn.target(event));
  console.log("srcElement=", eventFn.target(event));
});

这里在eventFn对象中封装了一个target属性。

阻止默认行为

<a href="https://www.baidu.com" id="a1">链接</a>
<script>
  var a1 = document.getElementById("a1");
  a1.addEventListener("click", function (event) {
    event.preventDefault(); //阻止默认行为
    alert("你点击了链接");
  });
</script>

关于Event对象中的其他内容,可以参考文档。

11、介绍一下三种事件模型

关于JavaScript的事件模型有三类,分别是DOM0,DOM2,DOM3

DOM0 事件模型

DOM0的事件模型指的是:将一个函数赋值给一个事件处理属性。

如下代码:

var btn = document.getElementById("btn");
btn.onclick = function () {};

或者也可以采用如下的方式:

<button onclick="fn()">单击</button>
<script>
  function fn() {
    console.log("hello");
  }
</script>

需要注意的是:DOM0事件模型的处理程序只支持冒泡阶段。

DOM0事件模型的优点与缺点:

优点:实现起来非常简单,并且可以跨浏览器。

缺点:一个事件处理程序只能绑定一个函数。

<body>
    <button id="btn" onclick="btnClick()">单击按钮</button>
    <script>
      var btn = document.getElementById("btn");
      btn.onclick = function () {
        console.log("hello");
      };
      function btnClick() {
        console.log("123");
      }
    </script>
  </body>

在上面的代码中,我们给按钮使用两种方法绑定了事件处理程序,但是DOM0这种事件模型只能绑定一个函数,并且在JavaScript中绑定事件处理程序的优先级高于在HTML元素中定义的事件处理程序,所以打印结果为hello.

如果删除元素绑定的事件,只需要将对应的事件处理程序设置为null即可

btn.onclick = null;

DOM2 事件模型

针对DOM2事件模型不同的浏览器厂商制定了不同的的实现方式,主要分为IE浏览器和非IE浏览器

在IE10及以下版本中只支持事件的冒泡,在IE11中同时支持事件的捕获与事件冒泡。在IE10及以下版本中,可以通过attachEvent函数来添加事件处理程序,通过detachEvent函数删除事件处理程序。

element.attachEvent("on" + eventName, handler); // 添加事件处理程序
element.detachEvent("on" + eventName, handler); //  删除事件处理程序

在IE11和非IE浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()函数添加事件处理程序,可以通过removeEventListener() 函数删除事件处理程序。

addEventListener(eventName, handler, useCapture); //添加事件处理程序
removeEventListener(eventName, handler, useCapture); // 删除事件处理程序

其中useCapture如果为true表示支持事件捕获,为falseb 表示支持事件冒泡,默认是为false

通过上面的介绍,我们知道了DOM2的事件处理程序存在两种情况,那这两种实现的方式之间有没有相同点和不同点呢?

相同点

第一:在DOM2的事件处理中不管是IE浏览器还是非IE浏览器都支持对同一个事件绑定多个处理函数。

<body>
  <button id="btn">单击按钮</button>
  <script>
    var btn = document.getElementById("btn");
    btn.addEventListener("click", function () {
      console.log("hello");
    });
    btn.addEventListener("click", function () {
      console.log("nihao");
    });
  </script>
</body>

以上程序的输出结果为:hello,nihao

第二:在需要删除绑定的事件的时候,,不能删除匿名函数,因为添加的函数和删除的函数必须是同一个函数。

下面的代码中,同时绑定和删除了handler函数,这样做是完全可以的。

var btn = document.getElementById("btn");
var handle = function () {
  console.log("hello");
};
btn.addEventListener("click", handle, false);
btn.removeEventListener("click", handle);

但是如果采用如下的删除方式是无法取消绑定的事件的。因为它们都是匿名函数,而并不是同一个函数。

btn.addEventListener(
  "click",
  function () {
    console.log("hello");
  },
  false
);
btn.removeEventListener("click", function () {});

不同点

第一:在IE浏览器中,使用attachEvent函数为同一个事件添加多个处理程序时,会按照添加的相反顺序执行。

<script>
     var btn = document.getElementById("btn");
      btn.attachEvent("onclick", function () {
        console.log("hello");
      });
      btn.attachEvent("onclick", function () {
        console.log("nihao");
      });
</script>

当单击按钮的时候,先输出nihao,再输出hello.

第二:在IE浏览中,attachEvent函数添加的事件处理程序会在全局作用域中运行,因此this指向的是window.

在非IE浏览器中,addEventListener()函数添加的处理程序在指定的元素内部执行,this指向所绑定的元素。

既然DOM2事件的处理有浏览器的兼容性问题,那应该怎样进行处理呢?

var EventHandler = {
  addEventHandler: function (ele, type, handler) {
    if (ele.addEventListener) {
      ele.addEventListener(type, handler);
    } else if (ele.attachEvent) {
      ele.attachEvent("on" + type, handler);
    } else {
      ele["on" + type] = handler;
    }
  },
  removeEventHandler: function (ele, type, handler) {
    if (ele.addEventListener) {
      ele.removeEventHandler(type, handler);
    } else if (ele.detachEvent) {
      ele.detachEvent("on" + type, handler);
    } else {
      ele["on" + type] = null;
    }
  },
};

DOM3 事件模型

DOM3事件模型中允许自定义事件,自定义事件有createEvent("CustomEvent")函数来完成。返回的对象有一个initCustomEvent()方法接收如下四个参数。

1)type:字符串,触发的事件类型,自定义。例如 “keyDown”,“selectedChange”;
  2)bubble(布尔值):标示事件是否应该冒泡;
  3)cancelable(布尔值):标示事件是否可以取消;
  4)detail(对象):任意值,保存在event对象的detail属性中;

具体的示例代码如下

<body>
  <div id="div1">监听自定义事件</div>
  <button id="btn">单击</button>
  <script>
    var customeEvent;
    //在立即执行函数中创建自定义事件
    (function () {
      //判断浏览器是否支持DOM3事件处理程序,如果条件成立表示支持,固定写法
      if (document.implementation.hasFeature("CustomEvents", "3.0")) {
        var user = { userName: "zhangsan" };
        customeEvent = document.createEvent("CustomEvent"); //创建自定义事件
        customeEvent.initCustomEvent("myEvent", true, false, user);
      }
    })();

    //监听自定义事件
    //通过addEventListener()函数监听自定义的事件`myEvent`
    var div1 = document.getElementById("div1");
    div1.addEventListener("myEvent", function (e) {
      console.log("用户名是:", e.detail.userName);
    });
    //触发自定义事件
    var btn1 = document.getElementById("btn");
    btn.addEventListener("click", function () {
      div1.dispatchEvent(customeEvent);
    });
  </script>
</body>

以上就是DOM3事件模型的案例。

12、介绍一下事件委托

事件冒泡的一个应用就是事件代理,也叫做事件委托

事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上。

例如:

<div id="div1">
  <a href="#">a1</a>
  <a href="#">a2</a>
  <a href="#">a3</a>
  <button>增加按钮</button>
</div>

在上面的代码中有很多的a标签,如果给每个a标签添加单击事件比较麻烦,同时采用这种方式添加事件还会导致占用内存比较多,你可以想象一下,如果a标签比较多的话,是不是会占用更多的内存。

那应该怎样解决这个问题呢?

可以通过事件委托的机制。也就是将事件绑定到父元素上,然后通过事件冒泡的原理,来解决这个问题。

如下代码所示:

 <script>
      const div1 = document.getElementById("div1");
      div1.addEventListener("click", function (e) {
        var target = e.target;
        if (target.nodeName.toLowerCase() === "a") {
          console.log(target.innerHTML);
        }
      });
    </script>

在上面的代码中,我们通过事件委托的方式解决了对应的问题,当然,这里你可能问,在div标签下的a标签所做的操作都是一样的,那么能不能针对不同的元素所做的操作如果不一样,事件委托能否处理呢?

答案是可以处理的。

下面我们把上面的程序改造一下:

<body>
  <div id="div1">
    <a href="#" id="a1">a1</a>
    <a href="#" id="a2">a2</a>
    <a href="#" id="a3">a3</a>
    <button>增加按钮</button>
  </div>
  <script>
    const div1 = document.getElementById("div1");
    div1.addEventListener("click", function (e) {
      var target = e.target;
      switch (target.id) {
        case "a1":
          console.log("针对a1进行操作");
          break;
        case "a2":
          console.log("针对a2进行操作");
          break;
        case "a3":
          console.log("针对a3进行操作");
          break;
      }
    });
  </script>
</body>

在上面的代码中,给每个a标签,添加了id属性,通过switch结构进行判断,然后执行不同的操作。

下面,我们再来看一个关于事件委托的应用问题:

在一个ul标签中,包含了5个li,需要单击每个li标签,输出标签中的内容。同时在页面中添加一个按钮,单击按钮创建一个新的li元素,单击新创建的li元素也可以输出对应的内容。

我们想到的实现方式如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>a1</li>
      <li>a2</li>
      <li>a3</li>
      <li>a4</li>
      <li>a5</li>
    </ul>
    <button id="btn">添加</button>
    <script>
      //通过querySelectorAll方法获取所有的li元素
      //通过for循环遍历的方式,给每个li添加单击事件
      var children = document.querySelectorAll("li");
      for (var i = 0; i < children.length; i++) {
        children[i].addEventListener("click", function () {
          console.log(this.innerHTML);
        });
      }
      //获取按钮
      var btn = document.querySelector("#btn");
      //获取ul标签
      var ul = document.querySelector("ul");
      //给按钮添加单击事件
      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
        //创建文本节点
        var newText = document.createTextNode("a6");
        //将文本添加到新创建的li标签上
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
      });
    </script>
  </body>
</html>

执行上面的代码:打击a1--a5都可以获取内容,单击按钮也可以添加一个新的li元素,但是问题是单击新创建的li元素,并没有输出元素中的内容。

原因是:我们通过querySelectorAll方法获取所有li元素,但是通过这个方法不能实时对增加的事件进行绑定。也就是说无法完成对新元素事件的绑定。

解决办法:先创建新元素,然后在进行事件的绑定。

 <script>
      function bindEvent() {
        var children = document.querySelectorAll("li");
        for (var i = 0; i < children.length; i++) {
          children[i].addEventListener("click", function () {
            console.log(this.innerHTML);
          });
        }
      }
      var btn = document.querySelector("#btn");
      var ul = document.querySelector("ul");
      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
        var newText = document.createTextNode("a6");
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
        //重新添加事件处理程序
        bindEvent();
      });
    </script>

在上面的代码中,我们创建了一个bindEvent函数,把对li元素添加事件的操作封装到了该方法中。

然后在按钮对应的事件处理函数中,先完成新元素的创建,然后在调用bindEvent方法,完成对li元素事件的绑定操作。

虽然以上完成了我们的需求,但是还有一个问题需要解决,就是我们前面所讲的:由于给每个li元素都添加了单击事件,导致占用内存比较多,性能比较低,所以可以使用事件委托的方式来改造上面的程序。

改造后的程序如下所示:

<script>

      var ul = document.querySelector("ul");
      //   var parent = document.querySelector("ul");
      ul.addEventListener("click", function (e) {
        var target = e.target;
        if (target.nodeName.toLowerCase() === "li") {
          console.log(target.innerHTML);
        }
      });

      var btn = document.querySelector("#btn");

      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
        var newText = document.createTextNode("a6");
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
        //重新添加事件处理程序
        // bindEvent();
      });
    </script>

在上面的代码中,我们通过querySelector方法获取ul这个父元素,然后给该元素添加单击事件,在对应的事件处理程序中,判断获取到的元素是否为li,如果是打印li元素中的内容。

由于这里我们没有在使用querySelectorAll函数,所以这里我们也没有必要在封装到一个函数中了。

关于按钮的单击事件的处理,没有任何的变化。

以上我们就是通过事件委托的方式,给父元素添加了事件,对应的子元素都具有了相应的事件,这样的处理方式性能更高。

关于事件委托,在这里我们就介绍完了,通过前面的讲解,希望对事件委托有一个比较深刻的理解。

在JavaScript中还有一些其它的比较常用的事件,例如:

焦点相关的事件:focus,blur等事件
鼠标相关的事件:mouseover,mouseout等事件
键盘相关的事件:keydown,keyup,keypress等
其它事件...

关于这些事件,大家可以查看相应的文档,了解对应的基本使用。

13、介绍一下浏览器的重排与重绘

在前面的课程中,我们也提到过对DOM的操作是比较消耗性能的,这是因为它会带来浏览器的重绘与重排。

在讲解什么是重排与重绘之前,先来说一下浏览器渲染HTML的过程。

浏览器渲染HTML的过程大体上可以分为 4 步

第一:HTML代码被HTML解析器解析成对应的DOM树,CSS代码被CSS解析器解析成对应的样式规则集。

第二:DOM树与CSS解析完成后,附加在一起形成一个渲染树

第三:节点信息的计算,即根据渲染树计算出每个节点的几何信息(宽,高等信息)

第四:渲染绘制,根据计算完成的节点信息绘制整个页面。

而我们所要讲解的重排与重绘就发生在第三步和第四步中。

什么是重排

当对一个DOM节点信息进行修改的时候,需要对该DOM结构进行重新的计算。并且该DOM结构的修改会决定周边DOM结构的更改范围,主要分为全局范围和局部范围。

全局范围就是从页面的根节点html标签开始,对整个渲染树进行重新计算,例如:我们修改窗口的尺寸或者修改了根元素的字体大小的时,都会导致对整个渲染树进行重新计算。

局部范围只会对渲染树的某部分进行重新计算。例如要改变页面中某个div的宽度,只需要重新计算渲染树中与该div相关的内容就可以了。

而重排的过程发生在DOM节点信息修改的时候,重排实际是根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息,例如DOM元素的位置,尺寸,大小等。然后将其放在页面中的正确的位置上。

综上所述,我们明白了重排其实就是一种改变页面布局的操作。那么常见的引起重排的操作有哪些呢?

(1)页面首次渲染

在页面首次渲染的时候,HTML页面中的各个元素位置,尺寸,大小等信息都是未知的,需要通过与css样式规则集才能够确定出各个元素的几何信息。这个过程中会产生很多元素集合信息的计算,所以会产生重排。

(2)浏览器窗口大小的改变

页面渲染完成后,就会得到一个固定的渲染树。如果此时对浏览器窗口进行缩放或者是拉伸操作,渲染树中从根元素html标签开始的所有元素,都会重新计算其几何信息,从而产生重排的操作。

(3)元素位置改变和尺寸的改变

(4)元素内容改变,例如,文本内容被另外一个不同尺寸的图片替换。

(5)添加或者删除可见的 DOM 元素

(6)获取某些特定的属性

当我们对javascript某些操作属性的修改也会导致重排的操作,而频繁的重排操作会对浏览器引擎产生很大的消耗。所以浏览器不会对js的每个操作都进行一次重排,而是维护一个会引起重排操作的队列,等到队列中的操作达到了一定的数量或者是到了一定的时间间隔的时候,浏览器才会去刷新一次队列,进行真正的重排操作。

虽然浏览器有这样的优化,但是我们写的一些代码还会导致浏览器提取刷新队列,例如以下的操作。

offsetLeft, offsetWidth, offsetHeight, offsetTop;
scrollTop, scrollLeft, scrollWidth, scrollHeight;
clientTop, clientLeft, clientWidth, clientHeight;
widht, height;

当我们进行以上属性操作的时候,浏览器为了返回最精确的信息,需要刷新队列,因为队列中的某些操作会影响到这些属性值的获取。

以上就是浏览器重排的介绍

浏览器重绘

浏览器的重绘指的就是改变元素在页面中的展示样式,而不会引起元素在文档中位置的改变。例如:改变元素的颜色,背景色,透明度等。

常见的引起重绘的操作如下:

color:颜色
border-style:边框样式
visibility: 元素是否可见
background:背景样式,包括背景颜色,背景图片等
text-decoration:文本下划线,上划线等
border-radius:边框圆角
box-shadow:元素的阴影

以上就是浏览器的重绘的介绍。

通过对浏览器重排与重绘的介绍,相信大家已经有所了解了,那么它们两者之间有什么关系呢?

简单的说,重排一定会引起重绘,而重绘却不一定会引起重排的操作。

因为当元素在重排的过程中,元素的位置等几何信息会重新计算,并会引起元素的重新渲染,这样就会产生重绘的操作,而在重绘的时候,只是改变了元素的展示的样式,而不会引起元素在文档中位置的改变,所以一般不会引起重排的操作。

性能优化

浏览器的重排与重绘是比较消耗性能的,所以我们应该尽量减少重排与重绘的操作,这也是优化网页性能的一种方式。

常见的方法如下:

第一:将样式属性值的修改合并为一次。

例如,我们需要修改一个元素的样式,可以通过如下的代码实现:

var mainDiv = document.getElementById("div1");
mainDiv.style.width = "200px";
mainDiv.style.height = "100px";
mainDiv.style.background = "#ccc";

但是问题是,在上面的操作中多次修改了style属性,会引发多次的重排与重绘操作。

所以为了解决这个问题,可以将这些样式合并在一个class类中。

  <style>
      .changeStyle {
        width: 200px;
        height: 100px;
        background: "#ccc";
      }
    </style>

然后通过通过javascript直接修改元素的样式

document.getElementById("div1").className = "changeStyle";

这样我们可以在最后一步完成样式的修改,从而只引起一次的重排与重绘的操作。

第二:

将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

第三:在对多节点操作的时候,可以现在内测中完成,然后在添加到文档中。

如下代码所示:

function createTable(list) {
  var table = $("#table");
  var rowHtml = "";
  list.forEach(function (item) {
    rowHtml += "<tr>";
    rowHtml += "<td>" + item.userName + "</td>";
    rowHtml += "<td>" + item.userPwd + "</td>";
    rowHtml += "</tr>";
    table.append(rowHtml);
    rowHtml = "";
  });
}

当调用createTable方法的时候,会从list集合中取出一条数据,然后放在tr标签中,紧跟着添加到表格中,这样就会导致每添加一行数据,都会引发一次浏览器的重排和重绘的操作,如果数据很多,则会对渲染造成很大的影响。

修改后的代码

function createTabel(list) {
  var table = $("#table");
  var rowHtml = "";
  list.forEach(function (item) {
    rowHtml += "<tr>";
    rowHtml += "<td>" + item.userName + "</td>";
    rowHtml += "<td>" + item.userPwd + "</td>";
    rowHtml += "</tr>";
  });
  //将数据一次性追加到表格中,完成一次渲染
  table.append(rowHtml);
}

通过上面的改造以后,只会引起一次浏览器的重绘与重排的操作,从而带来很大的新能提升。

第四:由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。

第五: 尽量减少table布局,随便修改一个单元格的高度或宽度都会让整个表格进行重排,性能非常差。

第六:在对多个同级元素做事件绑定的时候,推荐使用事件委托机制来处理。

第七:文档片段DocumentFragment的使用,关于这块内容,在前面的课程中已经使用过。

使用批量插入元素,例如:向页面的ul元素中添加100个li元素,

const listNode = document.getElementById("list");
//创建一个文档片段,文档片段存在于内存中,并不在DOM树中,所以此时还没有插入到DOM中
//也就是先将dom插入到临时区域中
const frag = document.createDocumentFragment();
//执行插入

for (let i = 0; i < 10; i++) {
  const li = document.createElement("li");
  li.innerHTML = `item${i}`;
  frag.appendChild(li);
}
//都完成后,再插入到DOM树中
listNode.appendChild(frag);

以上就是关于浏览器重绘与重排的内容。

最后编辑:
上一页
四、对象
下一页
六、AJAX