JavaScript事件

  最近看JavaScript忍者书,看到一个可以记录一下的事件的特性,还有就是碰到有人问我了一个事件的问题,我这里记录一下关于事件添加、事件触发的一点内容。

事件添加

  事件添加一般是两种方式。建议只使用addEventListener这个函数添加
  一种是直接给元素添加属性:

1
<button onclick="(function(e){console.log(e)})(event)">adf</button>

  另外一种是通过eventListener添加事件:

1
2
3
4
5
6
7
8
9
10
<button id="btn">adf</button>
<script>
const element = document.getElementById('btn');
element.addEventListener('click',() => {
console.log('object');
})
element.addEventListener('click',() => {
console.log('object');
})
</script>

吐槽一下两年前的我,当时写页面还特么疯狂的纠结为啥我 <button onclick="function(e){console.log(e)}">adf</button>没有效果。
  这两种方式,其实没有特别大的,通过添加onclick属性最主要问题是, 它无法添加多个listener,虽然里面本质上是一个代码块,你可以在代码块里面尽可能地多调用function,但是代码块意味着你无法重复的添加listener,因为你后面重新指定的listener会将前面的覆盖。
  addEventListener 就没这个问题,它会按照代码中添加的listener先后顺序,顺序执行所有的listener。

事件触发(派发)

  正常来说,我一般都是直接获得element,然后调用element中的click之类的方法

1
2
3
4
5
<button id="btn" onclick="(function(e){console.log(e)})(event)">adf</button>
<script>
const element = document.getElementById('btn');
element.click();
</script>

  但是这样会有个问题,那就是调用click()的时候无法传递参数(至少我现在一眼看不出来),如果需要传递参数的话需要用到dispatchEvent方法
  会需要你传入一个类型为Event的参数,我们可以通过定制event的值来进行参数传入,刚好有个customEvent它里面有个属性detail没什么其他用处,就是用来给人存储数据的,如下所示

1
2
3
4
5
6
<button id="btn" onclick="(function(e){console.log(e)})(event)">adf</button>
<script>
const element = document.getElementById('btn');
const event = new CustomEvent('click', { detail: 'elem.dataset.time' });
element.dispatchEvent(event);
</script>

  打印出的event,可以看到,detail被更改并传入listener了。

  说实话,人家问我事件派发的时候我是蒙蔽的,派发?消息派发?观察者模式?发布订阅模式?我事件需要有这种设计模式设计的时候?不是一般都是数据接收转发时候用到的?
  后来才知道,问的是事件触发,淦,我特么怀疑有人翻译书的时候出问题了,就嗯把dispatch翻译成派发,其实就是代码中事件的触发= =。

事件循环

  插一个阮一峰的博客,比较建议看这个,写的很清楚。JavaScript 运行机制详解:再谈Event Loop
  JavaScript本身是单线程的,但是为了事件响应,或者网络请求不阻塞本身的线程,JavaScript设计了任务队列来放置所有的异步任务。当主线程已有任务执行完成后会去任务队列读取所有的异步任务进行执行。
  这里要注意,任务队列也是分类的,分为微任务和宏任务,两者之间的执行顺序也有先后之分,其中宏任务,是宿主环境发起的任务,比如node,比如浏览器,微任务是js脚本引擎发起的任务貌似面视常见欸具体顺序是,执行完单个宏任务后执行所有微任务,然后再执行下个宏任务,推荐一下一个掘金老哥的文章一次弄懂Event Loop
  在这里需要知道的是script,settimeout,setinterval的callback是添加到宏任务,然后promise.then catch是微任务。以此为例,可以看一下以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
  1. 整个脚本是宏任务,执行脚本所有的同步代码,所以打印start end
  2. 同步代码是宏任务,然后执行宏任务中添加的微任务,所以打印 promise3
  3. 微任务结束后执行下一个宏任务,这里能看到下一个被添加到宏任务队列的是第一个setTimeout,所以打印 timer1,然后执行附带的微任务,打印 promise1.
  4. 再执行下一个宏任务,即第二个settimeout,所以最终打印timer2 promise2
      所以这里有个注意点,那就是所有的回调函数都是在同步代码空闲后执行的,也就是说settimeout可能会因为同步代码以及微任务执行时间过长,出现等待时间比预期时间长的问题。但是反过来可以利用以下这个特性来写测试。绝了,我是这么写之后隔了好久才终于搞明白为什么套一个setTimeout就他妈能用了。场景是,使用thunk在action中发送网络请求的时候,用正常的同步代码会发现props或者说action的内容是不变的,具体代码情况后面会有博客post出来。