前景提要
event.currentTarget
MDN只说明了它表示事件的当前目标,总是指向事件绑定的元素。让我们来看下示例一:
<!-- HTML -->
<div id="outer">
<div id="inner"></div>
</div>
/* CSS */
#outer {
width: 100px;
height: 100px;
background-color: lightpink;
}
#inner {
width: 50px;
height: 50px;
background-color: lightyellow;
}
//JavaScript
document.getElementById('outer').addEventListener('click', function(event) {
console.log('event.currentTarget:', event.currentTarget);
console.log('event.target:', event.target);
console.log('this:', this);//注意:若事件处理程序为箭头函数,则this指向window(not strict)或为undefined('use strict')
});
点击inner,打印结果一切正常,不再赘述:
event.currentTarget: <div id="outer">…</div>
event.target: <div id="inner"></div>
this: <div id="outer">…</div>
问题复现
实际上,currentTarget这个属性是一个实时值而不是快照,随着事件冒泡阶段的结束,它将被解引用,这就是它为null的原因。也就是说如果我们在事件处理程序中使用异步代码访问该属性,就会发生这个问题。让我们来看下示例二(从此省略HTML、CSS):
document.getElementById('outer').addEventListener('click', function(event) {
setTimeout(() => {console.log('event.currentTarget:', event.currentTarget)}, 0);
setTimeout(() => {console.log('event.target:', event.target)}, 0);
setTimeout(() => {console.log('this:', this)}, 0);
});
点击inner,打印结果如下:
event.currentTarget: null
event.target: <div id="inner"></div>
this: <div id="outer">…</div>
注意到:就算排期为0的宏任务也会发生这个问题,文末将会演示微任务的情况。
解决方案
既然知道了问题原因,那么对症下药,在同步代码中保存该属性的指针,然后再在异步代码中调用。让我们来看下示例三:
document.getElementById('outer').addEventListener('click', function(event) {
setTimeout(() => {console.log('event.currentTarget:', event.currentTarget)}, 0);
const ct = event.currentTarget;
setTimeout(() => {console.log('ct:', ct)}, 0);
});
点击inner,打印结果如下:
event.currentTarget: null
ct: <div id="outer">…</div>
拓展探究
在“问题复现”中我们知道,该属性在排期为0的宏任务中都会发生解引用问题,微任务呢?让我们来看下示例四:
document.getElementById('outer').addEventListener('click', function(event) {
Promise.resolve(event.currentTarget).then(console.log);
});
点击inner,打印结果如下:
<div id="outer">…</div>
可以发现:在立即解决的期约回调中,该属性不会被解引用,这与宏任务的结果不一致。
但这不代表微任务不会发生解引用,在比较耗时的期约回调中调用该属性仍然会得到null,可以再参考下面的示例:
闲得蛋疼
document.getElementById('outer').addEventListener('click', function(event) {
new Promise(resolve => setTimeout(() => resolve(event.currentTarget), 0)).then(console.log);
});