通过一个JS动画了解JS中的EventLoop
日期:2019-11-30 10:22:00
更新:2019-11-30 10:22:00
标签:前端, Javascript, node
分类:Javascript

前言
上一篇文章我们说了如何实现一个Promise,这次我们就通过一个JS 动画了解一下什么是EventLoop。
一个 JS 动画
基本思路已经有了,下面就开始码吧~~
1.创建 Animate 类
- 首先需要创建一个 Animate 的方法,在调用时初始化信息
- 实例化的时候需要传两个参数,
head的 element 标签和body的 element 标签
- 初始化信息包括创建三个 HTML 标签,分别是
输出的信息的box,输出style的box和更新样式的style标签
class Animate {
constructor(headElement, bodyElement) {
this.element = {
headElement,
bodyElement,
contentElement: null,
styleElement: null,
styleContent: null,
};
this.init();
}
init = () => {
const styleElement = createElement("style");
const contentElement = createElement("div", { class: "text_content" });
const styleContent = createElement("div", { class: "style_content" });
this.element.headElement.appendChild(styleElement);
this.element.bodyElement.appendChild(contentElement);
this.element.bodyElement.appendChild(styleContent);
this.element.contentElement = contentElement;
this.element.styleElement = styleElement;
this.element.styleContent = styleContent;
};
}
function createElement(elementName, elementObj = {}, styleObj = {}) {
const element = document.createElement(elementName);
for (const option of Object.keys(elementObj)) {
element.setAttribute(option, elementObj[option]);
}
for (const styleName of Object.keys(styleObj)) {
element.style[styleName] = styleObj[styleName];
}
return element;
}
const head = document.querySelector("head");
const body = document.querySelector("body");
const animate = new Animate(head, body);
调用后我们需要用来展示和改变样式的节点都已经创建完成了,接下来需要实现往text_content中添加信息

2.往 text 中添加信息
- 上面已经说了,我们需要用
setInterval往内容盒子中添加信息,并且将总输出时间为用户输出的时间,那就需要计算输出每一个字的间隔时间,公式也很简单输出字体间隔时间 = 输出总时间 / 文字总长度。
在Animate类中添加text方法,输出文字的公共方法,还有一个输出字体间隔时间的方法
text = (elName, elOption, styleObj, text, during) => {
const element = createElement(elName, elOption, styleObj);
this.element.contentElement.appendChild(element);
this.printText(element, text, during);
}
printText = (element, text, during) => {
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);
}
getTextDuring(textLen, during) {
return (during / textLen).toFixed(4);
}
const head = document.querySelector('head');
const body = document.querySelector('body');
const animate = new Animate(head, body);
animate.text('p', {}, {color: 'red'}, 'Hello World', 2000);
调用animate.text后可以看到他已经可以输出啦:

3.EventLoop(事件循环机制)
我们的Animate写到这里,它的最基础的输出文字功能已经实现啦。接下来需要实现的是让每一段文字按顺序输出,要实现按顺序地输出必须要让每一个输出方法按顺序地执行。但是 JavaScript 它不会让我们实现的方法轻易地得逞~
测试一下:
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);
输出:

我们理想的状态应该是每一个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 方法
constructor() {
this.runArr = [];
}
run = () => {
this.runArr.length ? this.runArr.splice(0,1)[0]() : 0;
}
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();
})
}
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);
})
}
修改完后我们再来调用刚刚的测试例子
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 这个对象本身就可以了,这里就不多做解释啦,最终的代码:
"use strict";
class Animate {
constructor(headElement, bodyElement) {
this.element = {
headElement,
bodyElement,
styleElement: null,
contentElement: null,
styleContent: null,
};
this.runArr = [];
this.init();
setTimeout(() => {
console.log(this.runArr);
this.run();
}, 0);
}
init = () => {
const styleElement = createElement("style");
const contentElement = createElement("div", { class: "text_content" });
const styleContent = createElement("div", { class: "style_content" });
this.element.headElement.appendChild(styleElement);
this.element.bodyElement.appendChild(contentElement);
this.element.bodyElement.appendChild(styleContent);
this.element.contentElement = contentElement;
this.element.styleElement = styleElement;
this.element.styleContent = styleContent;
};
run = () => {
this.runArr.length ? this.runArr.splice(0, 1)[0]() : 0;
};
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();
});
return this;
};
style = (selector, styleObject, during) => {
this.runArr.push(async () => {
const parentElement = createElement("ul", { class: "style_row" });
this.element.styleContent.appendChild(parentElement);
await this.printStyle(parentElement, selector, styleObject, during);
this.run();
});
return this;
};
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);
});
};
printStyle = (parentElement, selectorName, styleObject, during) => {
return new Promise(async (resolve) => {
const styleStr = JSON.stringify(styleObject).length;
const textLen = selectorName.length + styleStr + 2;
const time = this.getTextDuring(textLen, during);
const list = createElement("li", { class: "selector" });
const selector = createElement("span", { class: "selector" });
const bracketsLeft = createElement("span", { class: "style_brackets" });
const bracketsRight = createElement("span", { class: "style_brackets" });
list.appendChild(selector);
list.appendChild(bracketsLeft);
parentElement.appendChild(list);
await this.printText(selector, selectorName, time * selectorName.length);
await this.printText(bracketsLeft, " { ", time * 3);
this.element.styleElement.innerHTML += `${selectorName} {\n`;
for (const style of Object.keys(styleObject)) {
const el = this.createStyleElement(list);
await this.printText(el.styleName, style, time * style.length);
await this.printText(el.colon, ": ", time * 2);
await this.printText(
el.style,
`${styleObject[style]};\n`,
time * styleObject[style].length
);
this.element.styleElement.innerHTML += `${style} : ${styleObject[style]}; \n`;
}
list.appendChild(bracketsRight);
await this.printText(bracketsRight, "}", time);
this.element.styleElement.innerHTML += `} \n`;
resolve();
});
};
createStyleElement = (list) => {
const p = createElement("p", { class: "style_row" });
const style = createElement("span", { class: "style" });
const styleName = createElement("span", { class: "style_name" });
const colon = createElement("span", { class: "style_colon" });
p.appendChild(styleName);
p.appendChild(colon);
p.appendChild(style);
list.appendChild(p);
return {
style,
styleName,
colon,
};
};
getTextDuring(textLen, during) {
return (during / textLen).toFixed(4);
}
}
function createElement(elementName, elementObj = {}, styleObj = {}) {
const element = document.createElement(elementName);
for (const option of Object.keys(elementObj)) {
element.setAttribute(option, elementObj[option]);
}
for (const styleName of Object.keys(styleObj)) {
element.style[styleName] = styleObj[styleName];
}
return element;
}
测试:
"use strict";
const head = document.querySelector("head");
const body = document.querySelector("body");
const animate = new Animate(head, body);
animate
.text("p", { class: "text" }, {}, "hello!", 200)
.text("p", { class: "text_yellow" }, {}, "我想被变黄", 500)
.style(".text_yellow", { color: "yellow" }, 1000)
.text("p", { class: "text" }, {}, "成功啦!", 1000);

小结
- 源码地址
- 我们通过一个动画的例子来了解了 JS事件循环的执行机制,代码在浏览器/node 中是如何执行的。
- 宏任务:script,setInterval,setTimeout,I/O,requestAnimationFrame,setImmediate(Node.js)
- 微任务:Promise.then,MutationObserver,process.nextTick(Node.js)
- 先执行宏任务,后执行微任务
- 最后,通过这一个小动画例子我们可以利用代码给自己做一个好玩的东西,例如:自动展示的简历 😁