Event Loop 也就是事件循环,可以理解为实现异步的一种方式。
它是在 HTML Standard 定义的, 规范中定义了浏览器何时进行渲染更新,了解它有助于性能优化。
macrotask 宏任务
事件、用户交互、脚本、渲染、网络这些都是由 Event Loop 协调的。 一个 Event Loop 有一个或多个 task(macrotask) 队列,当用户安排一个任务,必须将该任务增加到相应的 Event Loop 中的一个 task 队列(先进先出)中
task 任务源:
- DOM 操作任务源:被用来响应dom操作,比如一个元素以非阻塞的方式插入文档。
- 用户交互任务源:被用来对用户交互作出响应,比如键盘输入或者鼠标点击。
- 网络任务源:被用来响应网络活动
- history traversal 任务源:当调用 history.back() 等类似 api 时,将任务插进 task 队列。
- setTimeout、setInterval、setImmediate等也都是
microtask 微任务
每一个 Event Loop 都只有一个 microsoft 队列。 通常被认为是 microtask 的任务源有:
process.nextTickpromises:promise 的定义不在HTML规范中,在 ECMAScript 规范中,在 Promises/A+规范的Notes 3.1中提及:promise 的then 方法可以采用 macrotask 或者 microtask 机制来实现,所以,有些浏览器会有差异。一个普遍的共识是 promises 属于 microtask 队列Object.observe:异步地监视一个对象的修改MutationObserver:创建并返回一个新的MutationObserver,它会在指定的 DOM 发生变化时被调用
Event Loop
先执行同步任务,然后所有微任务,一个宏任务,所有微任务,一个宏任务。。。在当前的微任务没有执行完成时,是不会执行下一个宏任务的
有两种 Evnet Loop,一种在浏览器上下文中,一种在 works 中。浏览器可以有多个 Event Loop, Event Loop 至少有一个浏览器上下文,所有同源的浏览器上下文可以共用 Event Loop,这样它们之间就可以互相通信。
浏览器上下文是一个将 Document 对象呈现给用户的环境。在一个 web 浏览器内,一个标签页或窗口常包含一个浏览器上下文,
Event Loop 的处理过程: 一个 Event Loop 只要存在,就会不断执行下边的步骤:
- 在tasks队列中,选择最老的一个 task,如果没有可选的,跳去 microtask 部分的步骤6
- 将上边选择的task设置为 正在运行的task
- Run:运行被选择的 task
- 将 Event Loop 的正在运行的 task 设置为 null
- 从 task 队列里移除前边运行的 task
- 执行 microtask 任务检查点,也就是执行 Microtask 队列里的任务,直至清空,如果任务检查点的 falg 是 false,需要以下步骤
- 将 microtask 任务检查点的 flag 设为 true
- 如果 Event Loop 的 microtask 队列为空,直接跳去第7步,否则在队列中选择一个最老的任务
- 将上一个选择的任务设为 正在运行的task
- Run:运行被选择的task
- 将Event Loop 的正在运行的 task 设置为 null
- 从 task 队列中移除前边运行的 task,然后返回到第2步
- Done:给 Environment Settings Object 发一个 Rejected Promises 的通知
- 清理 IndexedDB 的事物
- 将 microtask 任务检查点的 flag 设置为false
- 更新渲染,此时的渲染有以下特性:
- 在一轮 Eveent Loop 里多次修改同一个 dom,只有最后一次会进行绘制
- 渲染更新会在 Event Loop 中的 macrotask 和 microtask 完成后进行,但并不是每一轮 Event Loop 都会重新渲染,这取决于是否修改了 dom 和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在很短的时间内修改多处dom,浏览器可能会把变动攒起来,只进行一次绘制,这是合理的
- 如果希望每次 Event Loop 都及时呈现变动,可以使用
requestAnimationFeame
- 返回第一步 由于 JS 是单线程的,所以很多时候计时器并不是表现的和我们的直观想象一样,计时器设定的延时是没有保证的。
定时器
定时器内部工作原理
计时器回调函数特性:
- 在哪儿初始化就在那儿开始计时
- 回调函数排在 macrotask 队列的最后
- 如果把所有的定时函数都放在队尾,一大串定时回调函数将会没有时间间隔的一起执行,直到完成。
- 如果回调函数在队列前面,浏览器会静静等待
定时器函数
定时器函数:
- setTimeout(fn, delay):创建一个简单的计时器,在经过给定的时间后,回调函数将会被执行。这个函数会返回一个唯一的ID,便于在之后某个时间可以注销这个计时器。
- setInterval(fn, delay):每经过一段时间(给定的延时),所传递的回调函数就会执行一次,直到这个定时器被注销
- setImmediate(fn):setImediate 是 setTimeout(0) 的替代版,是为了保证让代码在下一次 Event Loop 执行
- clearInterval(id) / clearTimeout(id): 停止计时器回调函数的执行
实现一个发布订阅
JavaScript 事件最核心的有:事件监听(addListener)、事件触发(emit)、事件删除(removeListener). eventEmitter 的核心就是事件触发与事件监听器功能的封装。
// 手写一个 EventEmitter
class EventEmitter {
constructor() {
this.listeners = {}
}
on(type, cb) { //type 是事件类型
if(!this.listeners[type]) {
this.listeners[type] = []
}
this.listeners[type].push(cb)
}
emit(type, ...args) {
if(this.listeners[type]) {
this.listeners[type].forEach(cb => cb(...args))
}
}
off(type, cb) {
if (this.listeners[type]) {
const targetIndex = this.listeners[type].findindex(item => item === cb)
if (tragetIndex !== -1) {
this.listeners[type].splice(targetIndex, 1)
}
if(this.listeners[type].length === 0) {
delete this.listeners[type]
}
}
}
offAll(type) {
if(this.listeners[type]) {
delete this.listeners[type]
}
}
}
发布-订阅设计模式
发布-订阅设计模式中,消息的发送方,叫做发布者,消息不会直接发送给特定的接收者,叫做订阅者,他俩之间有一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来。
EventEmitter 就是一个典型的发布订阅模式,实现了事件调度中心,发布者通过 emit 方法发布事件,订阅者通过 on 进行订阅
观察者设计模式
观察者模式在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变,就会自动通知它们。
class Observer {
constructor (cb) {
if (typeof cb === 'function') {
this.cb = cb;
} else {
throw new Error('Observer 构造器必须传入函数类型')
}
}
update() {
this.cb();
}
}
// 目标对象
class Subject {
constructor () {
this.observerList = [];
}
addObserver(observer) {
this.observerList.push(observer)
}
notify() {
this.observerList.forEach(observer => observer.update())
}
}
观察者和发布-订阅的区别
- 观察者模式中,观察者知道subject,subject一直保持对观察者进行记录。而在发布订阅模式内,发布者和订阅着不知道彼此
- 在发布订阅模式中,组件是松散耦合的,和观察者相反
- 观察者模式大部分都是同步的,而发布订阅模式大多时候是异步的
- 观察者模式需要在单个应用程序地址空间中体现,而发布订阅模式更像交叉应用模式
##好题练习 练习题1
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
执行结果:
1
7
6
8
2
4
3
5
练习题2
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
执行结果:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
要先理解 await/async,当我们使用 await时,解释器回都会创建一个 Promise 对象,然后把剩下的 async 函数中的操作放在 then 回调函数里。
练习题3:
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')
分别输出浏览器和nodejs的执行结果
// 浏览器
start => end => promise3 => timer1 => promise1 => timer2 => promise2
// nodejs
start => end => promise3 => timer1 => timer2 => promise1 => promise2
- 在 nodejs 中,microtask 在 Event Loop 的各个阶段之间执行
- 在浏览器端,microtask 在 Event Loop 的 macrotask 执行完后执行
在 node 端,有以下需要注意:
- process.nextTick 独立于 Event Loop,当每个阶段完成后,如果存在 nextTick 队列,就会清空列中所有回调函数,并且优先于其他 microtask
- 在 I/O 回调(fs.readFile)中,setImmediate 永远比 setTimeout 早执行