通过一个JS动画了解JS中的EventLoop

前言
上一篇文章我们说了如何实现一个Promise
,这次我们就通过一个JS动画了解一下什么是EventLoop。
一个JS动画
这次需要实现的动画是一个通过JS向前端页面添加文本和样式的动画。最终的效果如下图:
调用代码
根据实现结果和调用代码,可以想到的实现思路是:
- 通过链式调用将数据的向前端一行一行地输出。
- 主要通过
text
和style
两个方法向前端输出 - 输出的每一行都可以有时间控制
- 通过
setTimeout
向前端输出文字
基本思路已经有了,下面就开始码吧~~
1.创建Animate类
- 首先需要创建一个Animate的方法,在调用时初始化信息
- 实例化的时候需要传两个参数,
head
的element标签和body
的element标签 - 初始化信息包括创建三个HTML标签,分别是
输出的信息的box
,输出style的box
和更新样式的style标签
1 | class Animate { |
调用后我们需要用来展示和改变样式的节点都已经创建完成了,接下来需要实现往text_content
中添加信息
2.往text中添加信息
- 上面已经说了,我们需要用
setInterval
往内容盒子中添加信息,并且将总输出时间为用户输出的时间,那就需要计算输出每一个字的间隔时间,公式也很简单输出字体间隔时间 = 输出总时间 / 文字总长度
。
在Animate
类中添加text
方法,输出文字的公共方法,还有一个输出字体间隔时间的方法
1 | /** |
调用animate.text
后可以看到他已经可以输出啦:
3.EventLoop(事件循环机制)
我们的Animate
写到这里,它的最基础的输出文字功能已经实现啦。接下来需要实现的是让每一段文字按顺序输出,要实现按顺序地输出必须要让每一个输出方法按顺序地执行。但是JavaScript它不会让我们实现的方法轻易地得逞~
测试一下:
1 | // 测试 |
输出:
我们理想的状态应该是每一个text
都按顺序执行,而不是同时执行。这时因为Javascript是单线程的,所以JS在执行的时候会有一个执行队列(先进先出),它将所有要执行任务放到执行队列中按序执行,但执行任务又是什么?
JS中执行任务分为两种:宏任务(macrotask)和微任务(microtask),他们在JS中的执行顺序是:先执行宏任务后执行微任务。
- 宏任务有:script,setInterval,setTimeout,I/O,requestAnimationFrame,setImmediate(Node.js)
- 微任务有:Promise.then,MutationObserver,process.nextTick(Node.js)
了解完执行队列,再来看一下刚刚写的测试例子。当调用animate.text
时,因输出文字方法中有setInterval
,setInterval
不会先执行,而是插入到任务队列
中,如下图
当三个animate.text
执行完后,任务队列添加了三个setInterval
宏任务,当script的方法执行完后,第一插入的setInterval
开始执行并输出H
,然后在指定的时间往任务队列最后面添加插入宏任务(因为setInterval
一个循环执行的方法,每隔一段时间会执行,直到计时器被清除)
了解完JS的执行队列,回到动画代码中,我们需要怎样才能实现一个一个text
方法按顺序执行。
上一篇文章在实现Promise的时候,使用了两个运行队列(resolveArr,rejectArr)来装等待状态改变时执行的方法。这里同样也可以这样做,在类的构造器中添加一个函数数组,将所有执行时的方法在script
宏任务执行时就添加到数组中,script
中的代码执行完后再新建一个宏任务去一个一个执行数组中的方法。
接下来要:
- 往构造器中添加一个函数数组
- 添加一个执行函数数组的
run
方法 - 添加一个宏任务去执行
run
- 将
text
方法中执行的代码放到函数数组中,每次执行完后都调用run
方法去执行函数数组中的下一个方法 - 修改
printText
方法为Promise方法修改完后我们再来调用刚刚的测试例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53constructor() {
this.runArr = []; // 函数数组

}
// 执行函数的方法
run = () => {
this.runArr.length ? this.runArr.splice(0,1)[0]() : 0;
}
/**
* 添加text
* @param {Element} appendNode 插入文本的节点
* @param {string} elName 标签名
* @param {Object} elOption 标签设置
* @param {Object} styleObj 内联样式
* @param {Object} text 输出的文字
* @param {number} during 输出文字的总时间
*/
text = (elName, elOption, styleObj, text, during) => {
this.runArr.push(async () => {
const element = createElement(elName, elOption, styleObj);
this.element.contentElement.appendChild(element);
await this.printText(element, text, during);
this.run();
})
}
/**
*
* @param {Element} element 输出文字的标签
* @param {string} text 输出的文字
* @param {number} during 输出文字的总时间
*/
printText = (element, text, during) => {
return new Promise(resolve => {
const len = text.length;
const time = this.getTextDuring(len, during);
let index = 0;
let timer = null;
timer = setInterval(() => {
if (index < len) {
element.innerHTML += text[index];
index++;
} else {
clearInterval(timer);
resolve();
}
}, time);
})
}1
2
3
4
5
6
7// 测试
const head = document.querySelector('head');
const body = document.querySelector('body');
const animate = new Animate(head, body);
animate.text('p', {}, {color: 'red'}, 'Hello World1', 2000);
animate.text('p', {}, {color: 'red'}, 'Hello World2', 2000);
animate.text('p', {}, {color: 'red'}, 'Hello World3', 2000);
成功了!现在再来看一下它的执行队列图:
4.最后添加style和链式调用
到这里我们的方法已经可以实现按顺序向界面输出文字了,最后需要做的是添加style
和链式调用
,添加style
的实现方法和添加文字大致是相同的,链式调用其实就是在每个方法执行后return这个对象本身就可以了,这里就不多做解释啦,最终的代码:
1 | ; |
测试:
1 | ; |
小结
- 源码地址
- 我们通过一个动画的例子来了解了JS事件循环的执行机制,代码在浏览器/node中是如何执行的。
- 宏任务:script,setInterval,setTimeout,I/O,requestAnimationFrame,setImmediate(Node.js)
- 微任务:Promise.then,MutationObserver,process.nextTick(Node.js)
- 先执行宏任务,后执行微任务
- 最后,通过这一个小动画例子我们可以利用代码给自己做一个好玩的东西,例如:自动展示的简历😁