
有个经典思想实验,将一只猫关在装有少量镭和氰化物的密闭容器里。镭的衰变存在几率,如果镭发生衰变,会触发机关打碎装有氰化物的瓶子,猫就会死;如果镭不发生衰变,猫就存活。只有打开它才会知道结果。
在计算机中也有这样类似的现象,Debug 的时候是正常的,而 Run 起来,结果又不一样。本文就一起来探讨背后的奥秘。
按钮按下触发clickEvent方法,执行一些操作后,触发请求访问再做一些其他操作.代码见下:
console.log("按钮被点击");
$.ajax({
url: '/hello',
type: 'GET',
success: (data) => {
console.log("1成功返回: ",data);
}
});
console.log("模拟其他事件");
$.ajax({
url: '/hello2',
type: 'GET',
success: (data) => {
console.log("2成功返回: ",data);
}
});预想执行顺序是
按钮被点击
1成功返回: hello
模拟其他事件
2成功返回: hello2可实际效果是: 顺序并不一定准确.

而当 debug 执行时,顺序保证了,但只保证一点.两次请求的结果依然会在最后输出。
为什么会出现这种情况呢?看一下真实的事件执行顺序。
通过控制台-性能的录制,抽象出下图。

最快的解决方法就是,在 Ajax 中添加async: false,变为同步访问。
想要保留异步请求,又要保证顺序,就需要调整代码结构。
从顺序执行,改为链式执行.讲白话就是,在success回调中执行剩余逻辑。这种方法是可以嵌套多层的.
不过,话又说回来,不建议这样各种处理混用.对于一个函数中,请求处理请放在最后,有且保证仅有一个.
任何事物都是有两面性的,我们可以利用这个特性,处理一些需要长时间执行,但又不需要得到结果的任务。
setTimeout(()=>{
// 长时间的任务
},0);需要注意,多过的延时会让性能变差。这里的 0 并不是真正的 0,会根据浏览器或者Node环境设置1、2这样很小的值。
上面算是对Bug有了初步认知。这么一番搜寻下来,对背后浏览器运行的机制有了一点兴趣,经过腾讯元宝的指点,Bug背后的宏任务与微任务哥俩浮出水面。
进程:资源分配的最小单位。
线程:CPU调度的最小单位。
线程依附于进程,一个进程有多个线程。
JavaScript 是单线程的,这句话常听。但运行平台-浏览器是多进程的,这就有点陌生了。下面使用 Edge 打开腾讯云开发者社区,再打开任务管理器-进程页,得到下图。

看着很丰富,其实也就分为主进程,GPU渲染进程,网络进程,扩展/插件进程等。其中 JavaScript 就在渲染进程中运行着。
上述提到进程是包含多个线程的,渲染进程也不例外。
负责解析和执行JS。JS引|擎线程和GUI渲染线程是互斥的,同时只能一个在执行。
解析html和CSS,构建DOM树,CSSOM树,(Render)渲染树、和绘制页面等。
当 JS 瞬时进行大量的Dom操作,并且没有进行分段渲染处理,再打开性能监控,将会明显感受到两者运行的顺序。
主要用于控制事件循环。比如计时器(setTimeout/setlnterval),异步网络请求等等,会把任务添加到事
件触发线程,当任务符合触发条件触发时,就把任务添加到待处理队列的队尾,等JS引擎线程去处理。
ajax的异步请求,fetch请求等。原案例中所说的解决方案,同步就不算在内。
setTimeout 和 setlnteval 计时的线程。额外说明一点,由于要保持计时的准确性,定时器不是由会阻塞的JS实现的,而是交给浏览器。
再进一步拆解,这些进程包含两种类型任务。宏任务 Macro tasks 和 微任务 Micro tasks
先来看一个图:

执行一段程序、执行一个事件回调或一个 interval / timeout 被触发之类的标准机制而被调度的任意JavaScript代码。
微任务
到目前为止,看起来这俩兄弟都比较底层,不需要业务开发人员考虑。下面开始介绍一个使用他们的性能优化案例,感受他们的魅力。
当进行大量 Dom 渲染时,过程中做不了任何事,只能硬等。并且当渲染时间过长,浏览器甚至出现卡死的现象,给用户很不好怕的体验。不防我们把宏任务拆分,让其渲染一部分,这样降低了线程占用,而且渲染过程中可以进行其他操作。话不多说,代码展示:
<button id="btnLog" class="btnLog">操作点击事件</button>
<button class="start">开始添加dom</button>
<script>
var startBtn = document.querySelector(".start");
var array = [];
for (var i = 1; i <= 300000; i++) {
array.push(i); //制造300000条数据
};
console.log("数据制造完成");
//渲染数据
var renderDomList = function (data) {
for (var i = 0, l = data.length; i < l; i++) {
var div = document.createElement('div');
div.innerHTML = `列表${i}`;
document.body.appendChild(div);
}
};
startBtn.onclick = function () {
console.log("startBtn clicked:", new Date().toLocaleTimeString());
renderDomList(array);
}
btnLog.onclick = function () {
console.log("btnLog clicked:", new Date().toLocaleTimeString());
}
</script>没有什么特殊说明,就是追加30万个条dom到页面,只要点了基本卡住。
//渲染数据
var renderDomList = function (data, startIndex, endIndex) {
if (startIndex < endIndex && endIndex <= data.length) {
setTimeout(() => {
for (let i = startIndex; i < endIndex; i++) {
var div = document.createElement('div');
div.innerHTML = `列表${i}`;
document.body.appendChild(div);
}
let nextIndex = endIndex + step > data.length ? data.length : endIndex + step;
let nextStartIndex = endIndex > data.length ? data.length : endIndex;
renderDomList(data, nextStartIndex, nextIndex);
}, 0)
}
};
startBtn.onclick = function () {
console.log("startBtn clicked:", new Date().toLocaleTimeString());
renderDomList(array, 0, 0 + step);
}
btnLog.onclick = function () {
console.log("btnLog clicked:", new Date().toLocaleTimeString());
}主要使用setTimeout(fn, 0)来运行,并将数据分组。没有没有微任务,根据开头的流程来进行,就会达到分段渲染的效果。
监控第一个项目,浏览崩溃了,没看到结果图,大概运行十几秒。
监控第二个项目,因为分段了,运行时间就长了很多,三四分钟有了。但并不会崩溃,而且另一个按钮随时可以点击。

以上就是这个Bug的发现,解决与背后深究。可能有很多有认知错误,不过学习嘛就是打破与在建立。希望本篇的经验对你也有帮助!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。