JS原生事件流与事件执行顺序

绝了,框架用多了原生基本都不记得了,今天刷到了这个题顺手写篇博客

正文

事件流的阶段

DOM事件流分为三个阶段:捕获阶段处于目标阶段冒泡阶段。DOM事件流会先调用捕获阶段的处理函数,然后调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。

在这里,1-3是捕获阶段,4-5是处于目标阶段,6-8是冒泡阶段

绑定事件的方法

HTML内联绑定

<div id="box1" onclick="console.log('box1 内联绑定')"></div>

DOM属性绑定

let box1 = document.querySelector("#box1");
box1.onclick = function () {
    console.log('box1 DOM属性绑定');
}

通过addEventListener绑定

为什么不看看万能的MDN

不过方便懒得跳转的同学,我贴心的截了张图(其实主要是这个函数太常用了感觉没必要说明用法)

file

tips

addEventListener可以多次给一个元素绑定多个监听同一个事件的回调,而DOM属性绑定和HTML内联绑定都只能绑定一个事件,之后绑定的事件会覆盖之前绑定的事件,而且两者的事件也会相互覆盖

HTML内联绑定和DOM属性绑定的事件都是绑定到冒泡阶段,addEventListener绑定的事件可以选择绑定到冒泡阶段和捕获阶段

事件的触发顺序

<div id="box1" onclick="console.log('box1 内联绑定')">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>

    let box1 = document.querySelector("#box1");
    let box2 = document.querySelector("#box2");
    let box3 = document.querySelector("#box3");

    box1.addEventListener('click', function(){
        console.log('box1 冒泡阶段 这是在onclick前绑定的');
    },false);

    box2.addEventListener('click', function(){
        console.log('box2 冒泡阶段 这是在onclick前绑定的');
    },false);

    box2.onclick = function () {
        console.log('box2 onclick');
    }

    box2.addEventListener('click', function(){
        console.log('box2 冒泡阶段 这是在onclick后绑定的');
    },false);

    box3.addEventListener('click', function(){
        console.log('box3 冒泡阶段 这是在onclick前绑定的');
    },false);

    box3.onclick = function () {
        console.log('box3 onclick');
    }

    box3.addEventListener('click', function(){
        console.log('box3 冒泡阶段 这是在onclick后绑定的');
    },false);

    box1.addEventListener('click', function(){
        console.log('box1 捕获阶段1');
    },true);
    box1.addEventListener('click', function(){
        console.log('box1 捕获阶段2');
    },true);
    box2.addEventListener('click', function(){
        console.log('box2 捕获阶段1');
    },true);
    box3.addEventListener('click', function(){
        console.log('box3 捕获阶段1');
    },true);
    box2.addEventListener('click', function(){
        console.log('box2 捕获阶段2');
    },true);
    box3.addEventListener('click', function(){
        console.log('box3 捕获阶段2');
    },true);

    box1.onclick = function () {
        console.log('box1 onclick');
    }

    box1.addEventListener('click', function(){
        console.log('box1 冒泡阶段 这是在onclick后绑定的');
    },false);

页面是这样的
file

点击一下中间的box3
file

为什么是这样呢,我们分析一下

  • 首先,事件流处于捕获阶段,从外到内触发dom绑定的捕获事件,对同一个DOM,通过addEventListener绑定的捕获函数会按绑定的顺序触发。
  • 然后,事件进入到目标阶段,这个阶段不区分捕获和冒泡事件,所有事件按照绑定的顺序触发。
  • 最后,事件进入到冒泡阶段,这个阶段按照绑定的顺序触发绑定的冒泡事件,各个DOM的事件是从内到外触发的

处于捕获阶段时,从外到内触发各个DOM绑定的捕获事件,同一个DOM的捕获事件按绑定的先后顺序触发, box1,box2按下面的顺序绑定了回调

box1.addEventListener('click', function(){
    console.log('box1 捕获阶段1');
},true);
box1.addEventListener('click', function(){
    console.log('box1 捕获阶段2');
},true);
box2.addEventListener('click', function(){
    console.log('box2 捕获阶段1');
},true);
box2.addEventListener('click', function(){
    console.log('box2 捕获阶段2');
},true);

所以触发顺序是box1 捕获阶段1 -> box1 捕获阶段2 -> box2 捕获阶段1 -> box1 捕获阶段2

处于目标阶段时,不区分事件,全部按照绑定的顺序触发,box3按下面的顺序绑定了回调

box3.addEventListener('click', function(){
    console.log('box3 冒泡阶段 这是在onclick前绑定的');
},false);
box3.onclick = function () {
    console.log('box3 onclick');
}
box3.addEventListener('click', function(){
    console.log('box3 冒泡阶段 这是在onclick后绑定的');
},false);
box3.addEventListener('click', function(){
    console.log('box3 捕获阶段1');
},true);
box2.addEventListener('click', function(){
    console.log('box2 捕获阶段2');
},true);
box3.addEventListener('click', function(){
    console.log('box3 捕获阶段2');
},true);

所以触发顺序如下:
box3 冒泡阶段 这是在onclick前绑定的 -> box3 onclick -> box3 冒泡阶段 这是在onclick后绑定的 -> box3 捕获阶段1 -> box3 捕获阶段2

最后是冒泡阶段,冒泡阶段是从内到外的,所以会先触发box2的冒泡事件再触发box1的冒泡事件,对同一个DOM而言,触发顺序安装绑定的顺序触发

box2绑定的冒泡事件顺序

box2.addEventListener('click', function(){
    console.log('box2 冒泡阶段 这是在onclick前绑定的');
},false);
box2.onclick = function () {
    console.log('box2 onclick');
}
box2.addEventListener('click', function(){
    console.log('box2 冒泡阶段 这是在onclick后绑定的');
},false);

box1绑定的冒泡事件顺序

box1.addEventListener('click', function(){
    console.log('box1 冒泡阶段 这是在onclick前绑定的');
},false);
box1.onclick = function () {
    console.log('box1 onclick');
}
box1.addEventListener('click', function(){
    console.log('box1 冒泡阶段 这是在onclick后绑定的');
},false);

所以触发顺序是这样的

box2 冒泡阶段 这是在onclick前绑定的 -> box2 onclick -> box2 冒泡阶段 这是在onclick后绑定的 ->
box1 冒泡阶段 这是在onclick前绑定的 -> box1 onclick -> box1 冒泡阶段 这是在onclick后绑定的

其他小知识

阻止事件继续传递

使用e.stopPropagation(),在捕获和冒泡阶段使用都可,之后事件都继续在DOM上传递,但是在当前DOM上的事件依然会依次执行

略微修改上面的代码,我们修改box1的捕获事件

box1.addEventListener('click', function(event){
    console.log('box1 捕获阶段1');
    event.stopPropagation();
},true);

点击box3,事件不会继续向其他DOM传递了
file

如果你要阻止同一个DOM的相同事件继续被触发,可以使用e.stopImmediatePropagation()

box1.addEventListener('click', function(event){
    console.log('box1 捕获阶段1');
    event.stopImmediatePropagation();
},true);

file

不过喜欢搞事情的IE总是能搞特殊,它使用e.cancelBubble = true这样的方式来阻止事件冒泡(旧版IE不支持事件捕获)

所以完整代码应该是这样的

function stopPropagation(event) {
    event = event || window.event;
    if (event && event.stopPropagation){
        event.stopPropagation();
    }else{
        event.cancelBubble = true;
    }
}

阻止默认事件

如果你要阻止一些默认事件,比如href的链接跳转,submit的表单提交等,可以使用event.preventDefault()或者在绑定的函数里return false来搞定

a.onclick = function(){
    return false;
};

a.onclick = function(event){
    return event.preventDefault();
};

还是我们的IE喜欢搞特殊,IE使用e.returnValue = false来取消默认事件,所以完整的代码是这样的

function stopDefault(event) {
    event = event || window.event;
    if (event && event.preventDefault){
        event.preventDefault();
    }else{
        event.returnValue = false;
    }
    return false;
}

后记

困了,睡了,没有后记

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00