事件驱动编程是一种程序流程取决于事件发生次序的编程风格,我们只需要为事件的处理程序注册回调函数,那么在事件发生时,系统就会自动调用这些处理程序,这种编程风格又叫作后继传递风格(Continue-Passing Style,CPS)。
因为事件具有不可预知的特性,再加上异步与事件的阐述的亲近性,所以很多人都想当然地认为,事件响应一定都是异步的,例如AJAX、setTimeout,如下:
// #! /usr/local/bin/node
// 计划以bash的方式直接运行
// 获取事件注册对象
const EventEmitter = require('events').EventEmitter
// 创建事件对象,可以理解为普通的JS对象
let customEvent = new EventEmitter()
// 赋初始值
Object.assign(customEvent, {
username : 'yiifaa'
})
// 设置定时任务
setTimeout(() => {
console.log(customEvent.username)
}, 0)
// 改变初始值
customEvent.username = 'yiifee'
在上面的代码中,因为javascript执行引擎会把定时任务添加到事件队列中,并不会立即执行,所以输出的结果是未改变的值yiifaa,而不是yiifee。这样的结果更加深了我们的印象,事件响应一定是异步的,真的吗?继续下面的例子,我们添加自定义事件:
const EventEmitter = require('events').EventEmitter
// 初始化对象
let customEvent = new EventEmitter()
Object.assign(customEvent, {
username : 'yiifaa'
})
// 注册事件
customEvent.on('change', function (age) {
console.log(this.username)
})
// 触发事件
customEvent.emit('change', 20)
// 改变初始值
customEvent.username = 'yiifee'
如果事件响应是异步的,那么上面的输出结果依旧是yiifee(主线程中的代码优先执行),但很遗憾,结果却是yiifaa,这充分说明,事件响应并不一定总是异步的,而且从我们实践中的效果来看,所有的事件响应都是同步的,这也是监听者模式的特性(监听者模式是单线程环境下的顺序执行模型)。
我们再看一个在DOM中注册事件的例子,确认事件响应到底是异步的还是同步的,如下:
<h1 id="output">yiifaa</h1>
<script>
var output = document.querySelector('#output')
// 注册事件
output.addEventListener('click', () => {
console.log('yiifaa')
})
// 触发事件
output.click()
console.log('yiifee')
</script>
在上面的例子中,输出结果依旧是“yiifaa yiifee”,这说明即使是在DOM编程中,事件响应也是同步执行的。
仔细观察上面的例子,我们发现,事件响应代码的执行位置,取决于我们事件的激发位置,所以我们可以大胆地推测,事件处理引擎都把事件激发的代码摆放在当前主线程之外,所以只有主线程的代码执行完成后,才会出现事件的执行结果,如代码1。
结论
在单线程的环境下,事件响应一定是同步的,之所以会出现异步的效果,这是由于单线程的执行线程无法中断,所以事件处理引擎会把事件激发的代码摆放在主线程之外。