首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >薛定谔的bug?不,是我还得练!

薛定谔的bug?不,是我还得练!

原创
作者头像
花花Binki
发布2024-11-29 23:33:24
发布2024-11-29 23:33:24
5300
举报
封面
封面

有个经典思想实验,将一只猫关在装有少量镭和氰化物的密闭容器里。镭的衰变存在几率,如果镭发生衰变,会触发机关打碎装有氰化物的瓶子,猫就会死;如果镭不发生衰变,猫就存活。只有打开它才会知道结果。

在计算机中也有这样类似的现象,Debug 的时候是正常的,而 Run 起来,结果又不一样。本文就一起来探讨背后的奥秘。

原案例

按钮按下触发clickEvent方法,执行一些操作后,触发请求访问再做一些其他操作.代码见下:

代码语言:javascript
复制
	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);
		}
	});

预想执行顺序是

代码语言:txt
复制
按钮被点击
1成功返回: hello
模拟其他事件
2成功返回: hello2

可实际效果是: 顺序并不一定准确.

前端顺序测试
前端顺序测试

而当 debug 执行时,顺序保证了,但只保证一点.两次请求的结果依然会在最后输出。

为什么会出现这种情况呢?看一下真实的事件执行顺序。

通过控制台-性能的录制,抽象出下图。

执行解析
执行解析

最快的解决方法就是,在 Ajax 中添加async: false,变为同步访问。

  • 如何避免

想要保留异步请求,又要保证顺序,就需要调整代码结构。

从顺序执行,改为链式执行.讲白话就是,在success回调中执行剩余逻辑。这种方法是可以嵌套多层的.

不过,话又说回来,不建议这样各种处理混用.对于一个函数中,请求处理请放在最后,有且保证仅有一个.

  • 如何利用

任何事物都是有两面性的,我们可以利用这个特性,处理一些需要长时间执行,但又不需要得到结果的任务。

代码语言:javascript
复制
setTimeout(()=>{
  // 长时间的任务
},0);

需要注意,多过的延时会让性能变差。这里的 0 并不是真正的 0,会根据浏览器或者Node环境设置1、2这样很小的值。

背后的真相

上面算是对Bug有了初步认知。这么一番搜寻下来,对背后浏览器运行的机制有了一点兴趣,经过腾讯元宝的指点,Bug背后的宏任务与微任务哥俩浮出水面。

进程与线程

进程:资源分配的最小单位。

线程:CPU调度的最小单位。

线程依附于进程,一个进程有多个线程。

JavaScript 是单线程的,这句话常听。但运行平台-浏览器是多进程的,这就有点陌生了。下面使用 Edge 打开腾讯云开发者社区,再打开任务管理器-进程页,得到下图。

任务管理器-进程
任务管理器-进程

看着很丰富,其实也就分为主进程,GPU渲染进程,网络进程,扩展/插件进程等。其中 JavaScript 就在渲染进程中运行着。

渲染进程中的线程

上述提到进程是包含多个线程的,渲染进程也不例外。

  • JavaScript 引擎线程:

负责解析和执行JS。JS引|擎线程和GUI渲染线程是互斥的,同时只能一个在执行。

  • GUI 渲染进程:

解析html和CSS,构建DOM树,CSSOM树,(Render)渲染树、和绘制页面等。

当 JS 瞬时进行大量的Dom操作,并且没有进行分段渲染处理,再打开性能监控,将会明显感受到两者运行的顺序。

  • 事件触发线程:

主要用于控制事件循环。比如计时器(setTimeout/setlnterval),异步网络请求等等,会把任务添加到事

件触发线程,当任务符合触发条件触发时,就把任务添加到待处理队列的队尾,等JS引擎线程去处理。

  • 异步 HTTP 请求线程:

ajax的异步请求,fetch请求等。原案例中所说的解决方案,同步就不算在内。

  • 定时触发器线程:

setTimeout 和 setlnteval 计时的线程。额外说明一点,由于要保持计时的准确性,定时器不是由会阻塞的JS实现的,而是交给浏览器。

再进一步拆解,这些进程包含两种类型任务。宏任务 Macro tasks 和 微任务 Micro tasks

宏任务与微任务

先来看一个图:

事件循环下的宏任务与微任务
事件循环下的宏任务与微任务

执行一段程序、执行一个事件回调或一个 interval / timeout 被触发之类的标准机制而被调度的任意JavaScript代码。

  • Events 典型的就是用户交互事件,要注意并不是所有的事件都会走宏任务,有些是走其他队列。
  • Parsing 热行HTML解析。
  • Callbacks 执行专门的回调函数。
  • Using a resource 使用一项资源,比如网络请求,文件读取等
  • Reacting toDoM manipulation 响应DOM解析之类。

微任务

  • Promise
  • MutationObserver

到目前为止,看起来这俩兄弟都比较底层,不需要业务开发人员考虑。下面开始介绍一个使用他们的性能优化案例,感受他们的魅力。

分段 Dom 渲染,体验事件循环

当进行大量 Dom 渲染时,过程中做不了任何事,只能硬等。并且当渲染时间过长,浏览器甚至出现卡死的现象,给用户很不好怕的体验。不防我们把宏任务拆分,让其渲染一部分,这样降低了线程占用,而且渲染过程中可以进行其他操作。话不多说,代码展示:

修改前

代码语言:html
复制
    <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到页面,只要点了基本卡住。

修改后

代码语言:javascript
复制
//渲染数据
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原案例
  • 背后的真相
    • 进程与线程
    • 渲染进程中的线程
    • 宏任务与微任务
  • 分段 Dom 渲染,体验事件循环
    • 修改前
    • 修改后
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档