# 事件机制

# 事件模型

W3C 中定义事件的发生经历三个阶段:捕获阶段(capturing)、目标阶段(targeting)、冒泡阶段(bubbling)

冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发 捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发 DOM 事件流:同时支持两种事件模型:捕获型事件和冒泡型事件 阻止冒泡:在 W3c 中,使用 stopPropagation()方法;在 IE 下设置 cancelBubble = true 阻止捕获:阻止事件的默认行为,例如 click 后的跳转。在 W3c 中,使用 preventDefault()方法,在 IE 下设置 window.event.returnValue = false

# 事件流

事件流分为两种,捕获事件流和冒泡事件流。

捕获事件流从根节点开始执行,一直往子节点查找执行,直到查找执行到目标节点。 冒泡事件流从目标节点开始执行,一直往父节点冒泡查找执行,直到查到到根节点。

DOM 事件流分为三个阶段,一个是捕获节点,一个是处于目标节点阶段,一个是冒泡阶段

阻止冒泡事件 event.stopPropagation()

function stopBubble(e) {
  if (e && e.stopPropagation) {
    // 如果提供了事件对象 event 这说明不是 IE 浏览器
    e.stopPropagation();
  } else {
    window.event.cancelBubble = true; // IE 方式阻止冒泡
  }
}

阻止默认行为 event.preventDefault()

function stopDefault(e) {
  if (e && e.preventDefault) {
    e.preventDefault();
  } else {
    // IE浏览器阻止函数器默认动作的行为
    window.event.returnValue = false;
  }
}

# 事件触发三阶段

  • document 往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 document 传播,遇到注册的冒泡事件会触发

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡');
  },
  false,
);
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ');
  },
  true,
);

# 注册事件

  • 通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:

    • capture:布尔值,和 useCapture 作用一样
    • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    • passive:布尔值,表示永远不会调用 preventDefault
  • 一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation();
    console.log('冒泡');
  },
  false,
);
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ');
  },
  true,
);

# 事件代理

事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是 DOM 元素的事件冒泡。使用事件代理的好处是可以提高性能 可以大量节省内存占用,减少事件注册,比如在 table 上代理所有 td 的 click 事件就非常棒 可以实现当新增子对象时无需再次对其绑定

举例子: 最经典的就是 ui 和 li 标签的事件监听,比如我们在添加事件的时候,采用事件委托机制,不会在 li 标签上直接添加,而是在 ul 父元素上添加 好处:可以比较合适动态元素的绑定,新添加的子元素也会监听函数,也可以有事件触发机制

事件委托是指将事件绑定目标元素的到父元素上,利用冒泡机制触发该事件

优点:

  • 可以减少事件注册,节省大量内存占用
  • 可以将事件应用于动态添加的子元素上
  • 提高运行效率(尤其对于需要循环操作的列表)
  • 动态添加后的元素,仍然可以有这些事件(对比与给自己添加的事件,后续动态添加时,新来的这些元素不会有这些事件)

但使用不当会造成事件在不应该触发时触发

ulEl.addEventListener(
  'click',
  function(e) {
    var target = event.target || event.srcElement;
    if (target && target.nodeName.toUpperCase() === 'LI') {
      console.log(target.innerHTML);
    }
  },
  false,
);

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>
<script>
  let ul = document.querySelector('##ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });
</script>

事件代理的方式相对于直接给目标注册事件来说,有以下优点

  • 节省内存
  • 不需要给子节点注销事件

# 详细

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
oUl.onmouseover = function(ev) {
  var ev = ev || window.event;
  var target = ev.target || ev.srcElement;
  // 冒泡事件中 target 指向触发事件的元素(先 li 后 ul),后面会详细说明,所以下面的判断必不可少
  if (target.nodeName.toLowerCase() == 'li')
    //DOM 节点名都是大写
    target.style.background = 'red';
  console.log(target);
};
oUl.onmouseout = function(ev) {
  var ev = ev || window.event; //做兼容
  var target = ev.target || ev.srcElement; //做兼容 srcElement 是 IE 下的
  if (target.nodeName.toLowerCase() == 'li') target.style.background = '';
};

委托事件中涉及到三个知识点:

  1. 事件冒泡(对应事件捕获);
  2. js 中的 this,event.target,event.currentTarget
  3. 衍生知识点:jquery 中的(this),(this),(event.target),$(event.currentTarget)

A:如上案例是事件冒泡(默认状态下)无论你给谁绑定事件,但触发事件是由内而外的,比较常用。与他对应的是事件捕获,由外而内的触发事件,二者怎么转换呢(注意:产生这两者的前提是,给每一个元素都要先绑定事件,而不是只绑定一个元素,它就会发生冒泡事件): addEventListener 有三个参数,最后一个默认为 false 就是冒泡事件。

document.getElementById('ul').addEventListener('click', function(e) {
  console.log('被触发的元素,' + this.nodeName);
});
document.getElementsByTagName('li')[0].addEventListener('click', function(e) {
  console.log('被触发的元素,' + this.nodeName);
});

当点击第一个 li 时,打印顺序是 LI-UL(事件冒泡有内而外) 修改为 true 后

document.getElementById('ul').addEventListener(
  'click',
  function(e) {
    console.log('被触发的元素,' + this.nodeName);
  },
  true,
);
document.getElementsByTagName('li')[0].addEventListener(
  'click',
  function(e) {
    console.log('被触发的元素,' + this.nodeName);
  },
  true,
);

当点击第一个 li 时,打印顺序是 UL-LI(事件捕获有外而内) 一般都使用默认状态下的,也就是冒泡事件。

  1. 首先 js 中的 this,event.target,event.currentTarget 和 jquery 中的(this),(this),(event.target),(前面的$不显示哎!!!!)各自所指代的都是内容不变只是获取方式不同,获取到后的命名不同,前者是 dom,后续能用 js 方法处理,后者是 jquery 对象,后续能用 jquery 方法处理

  2. 相关关键词的兼容性,在 js 中的 event 和 target 都需要做兼容

var ev = ev || window.event; //做兼容
var target = ev.target || ev.srcElement; //做兼容 srcElement 是 IE 下的
  1. this,event.target,event.currentTarget 三者个指代谁?

前提先理解:触发事件的元素和绑定事件的元素完全不是同一个元素,只有当鼠标放在绑定事件的元素身上时,他俩才指代一个东西。 超容易理解的:

这里面有一个相对论的结论: 1,当同时给 li 和 ul 都绑定事件,this 指的是各自绑定事件的元素(ul 或 li,相对来说是变化的),targrt 指的都是是触发事件的元素(相对来说是不变的),

$('li').on('click', function(e) {
  console.log('找target' + e.target);
  console.log('找currentTarget' + e.currentTarget);
  console.log('找' + this);
});
$('ul').on('click', function(e) {
  console.log('找 target' + e.target);
  console.log('找 currentTarget' + e.currentTarget);
  console.log('找' + this);
});

2,当只给 ul 添加绑定事件,(单独加事件时只能给父级,要给 li 加就必须是全加绑定事件才有效果) A:触发 li 时,this 指的还是绑定事件的 ul,target 指的是触发事件的 li,B:当触发 ul 时,this 还是指代绑定事件的 ul(想对这一层意义来说 this 不变),而 target 指的是触发事件的 ul(发生了改变) this 指的是绑定事件的对象。(绑定在 ul 上指的就是 ul,绑定在 li 上,指的就是 li)

$('ul').on('click', function(e) {
  console.log('找 target' + e.target);
  console.log('找 currentTarget' + e.currentTarget);
  console.log('找' + this);
});

event.target 指的触发该事件的元素。(鼠标放在 li 上就是 li,放在 ul 上就是 ul) 总结:this 永远指向绑定事件的元素,target 永远指向触发事件的元素。 不要再用变与不变来解释 this 和 target,都是相对的。 event.currentTarget 存在于冒泡过程中:哪个元素的监听器触发了事件 衍生一个获取函数

function getEventTrigger(event){
x=event.currentTarget;
alert("谁的监听器触发了事件"

 x.id);
}

# 实现 1

要给所有的 li 元素绑定 click 时间,在鼠标点击每个 li 的时候 alert 该 li 里面的内容;且在鼠标离开外部 ul 元素范围的时候弹出一个 alert 提示、(实现时请注意代码执行小路及浏览器兼容性,不要使用现成的框架库,用原生 js 编写完成)

<ul id="ulItem">
  <li>内容1</li>
  ......此处省略1000+个li对象(注:不要使用循环绑定,如果使用循环绑定1000+的绑定事件会很慢)......
  <li>内容n</li>
</ul>

# 实现 2

一个列表中给每项添加点击事件,如何添加?当列表有一万项的时候怎么添加?(事件委托是什么)

假如有个列表,如下:

<ul id="contaniner">
  <li id="li1">1</li>
  <li id="li2">2</li>
  <li id="li3">3</li>
</ul>

可以单个的给每个表单项添加点击事件,如<li id="li1" onclick="alert(1)">1</li>,可以看[##14]题目中的答案。

但是当列表数据过多时,则不能采用这种形式来添加点击事件。

想象以上示例中,<li>标签的数量很大时,循环为每个子元素添加事件,绝非好方法。下面给出一种优雅的方法,采用事件委托。

document.getElementById('contaniner').addEventListener(
  'click',
  function(e) {
    var target = e.target;
    if (target.tagName == 'LI') {
      alert(target.innerText);
    }
  },
  false,
);

这段代码里,使用事件委托只为<ul>元素添加一个 onclick 事件处理程序。因为有事件冒泡机制,单击每个<li>标签时,都会被这个函数处理。

# 实现 3

用 js 创建 10 个 a 标签,点击弹出对应的序号(考察作用域)

# React 中是怎么实现事件代理的?

React 并不会真正的绑定事件到每一个具体的元素上,而是采用事件代理的模式:在根节点 document 上为每种事件添加唯一的 Listener,然后通过事件的 target 找到真实的触发元素。这样从触发元素到顶层节点之间的所有节点如果有绑定这个事件,React 都会触发对应的事件处理函数。这就是所谓的 React 模拟事件系统。

尽管整个事件系统由 React 管理,但是其 API 和使用方法与原生事件一致。这种机制确保了跨浏览器的一致性:在所有浏览器(IE8 及以上)都可以使用符合 W3C 标准的 API,包括 stopPropagation(),preventDefault()等等。对于事件的冒泡(bubble)和捕获(capture)模式也都完全支持。