终于有勇气说出js是单线程语言~

203 阅读8分钟

背景: 开发中存在了一个疑惑,js不是单线程的吗,但是在浏览器中的展现形式并不是如此,比如ajax请求,定时任务等都是在页面渲染的同时在调用,抱着这样的疑惑,参考了一些文章,解开了谜团~~~ 归根结底还是对浏览器的认识不够,因此利用这篇文章记录一下

参考文章:

  1. juejin.cn/post/693276…
  2. blog.csdn.net/BReezeFK/ar…
  3. mp.weixin.qq.com/s?__biz=MzU…

1. 什么是线程(Process)

线程是进程中的⼀个执⾏单元,负责当前进程中程序的执⾏,⼀个进程中⾄少有⼀个线程。⼀个进程中是可以有多个线程的,这个应⽤程序也可以称之为多线程程序。

通俗来说就是:进程是一个工厂车间,而线程就是在这个车间工作的工人

2. 多线程的浏览器

浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“渲染引擎”,不过我们一般习惯将之称为“浏览器内核”。

浏览器内核是多线程的,一般是由GUI渲染线程、js引擎线程、事件触发线程、定时任务线程、ajax请求线程

image.png

GUI渲染线程: 主要任务是负责渲染浏览器界面,GUI任务更新会被保存在一个队列中,等待js引擎空闲后立即执行,当触发重绘或因为某些操作导致回流时,该线程就会执行

js引擎线程: 负责处理JavaScript脚本程序,JS引擎一直等待着任务队列中任务的到来,然后加以处理,这也就是为什么说js是单线程的了

定时触发线程: setIntervalsetTimeout所在线程, 计数线程,浏览器定时计数器并不是由JS引擎计数的

事件触发线程: 属于浏览器而不是JS引擎,当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时,该线程会把是事件添加到待处理队列的队尾,等待JS引擎的处理。

ajax请求线程: XMLHttpRequest在连接后是通过浏览器新开的一个线程请求。当检测到状态更新时,如果没有设置回调函数,异步线程就产生状态 变更事件,将这个回调再放入事件队列中,等待JS引擎执行。

注意: GUI渲染线程和js引擎线程是互斥的,因为js是可以操作dom的,也就是可以导致页面重绘或回流,渲染就会由于前后不一致造成不可预测的结果

3. JavaScript的执行机制

了解js执行机制之前,我们先了解一下同步和异步的区别~

3.1 同步

"同步模式"就是一个任务的开始需要等待上一个任务完成

graph TD
主线程遇到异步ajax --> 通知ajax线程 --> ajax线程此时正在忙 --> 主线程一直等待ajax线程的回应 --> ajax完成工作后回应主线程 --> 主线程收到相应后继续执行主线程队列中的下个任务
console.log(1);
console.log(2);
console.log(3);
// 执行后的结果为1,2,3

3.2异步

"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

graph TD
主线程遇到异步ajax --> 通知ajax线程 --> ajax线程回应发送 --> 主线程继续执行同步任务 --> ajax线程请求响应后将取得的数据以对象的形式存在消息队列中并通知主线程 --> 主线程当前进行的任务结束后开始处理
console.log(1);
setTimeout(() =>{
   console.log(2);
},0);
setTimeout(() =>{
   console.log(3);
},0);
setTimeout(() =>{
   console.log(4);
},0);
console.log(5);
// 此时的输出顺序是1,5,2,3,4 

通过这个输入我们可以看出,即使我们给定时器的延迟设置为0s,定时器中的函数还是在流程的最后才执行,定时器中的函数依然会被放入到定时触发线程的队列中执行,等待主线程中的同步任务完成后,主线程才会在定时触发线程中的消息队列中取数据(ps: 队列是遵循先进先出的原则的);

主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。

异步函数通常具有以下的形式:

image.png

异步函数通俗来说就是异步任务注册函数,args、callbackFn都是这个函数的参数,在主线程的角度上,一个异步过程包括下面两个要素:注册函数A和回调函数callbackFn。它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果

3.3 事件循环

image.png 流程如下:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入事件列表中并注册异步任务的回调函数(也就是注册异步任务执行的时机)。
  • 当指定的事情完成时,事件列表会将这个函数移入事件队列中。
  • 主线程内的任务执行完毕为空,会去事件队列读取对应的函数,进入主线程执行。
  • 这也就是所说的事件循环

我们来结合实际代码来说一下

let data = [];
$.ajax({
   url: 'www.javas.com',
   data: data,
   success: () =>{
      console.log("ajax数据响应成功!")
   }
});
console.log("代码执行结束!")

流程如下:

  • 主线程执行任务,ajax进入到事件列表中,注册回调函数;
  • 执行代码console.log("代码执行结束!")
  • ajax线程中的事件完成,回调函数success进入到事件队列中,并通知主线程
  • 主线程从事件队列中读取回调函数sucess并执行(这个前提是主线程的事任务队列中的数据已经执行完了,任务队列为空[])

相信有了搞明白了上面的知识点,对JavaScript的执行机制也有了浅浅的认识,那再深入了解一下这个机制是怎样的吧~~~

异步任务又包括宏任务(macrotask )和微任务(microtask )

3.4 宏任务

宏任务一般包括: script、setTimeout、setInterval、setImmediate、I/O、UI rendering

3.5 微任务

微任务包括:promise、MutationObserver等

3.6 执行机制

image.png

我们来通过一段代码来梳理一下执行流程:

console.log('1');
setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
})
new Promise(function(resolve) {
    console.log('5');
    resolve();
}).then(function() {
    console.log('6')
})

setTimeout(function() {
    console.log('7');
    new Promise(function(resolve) {
        console.log('8');
    }).then(function() {
        console.log('9')
    })
})
console.log('10');

// 1 5 10 6 2 3 4 7 8

分析: 我们不妨先对其分类:

任务类名代码所在行数
同步任务1、26
异步任务2-10、18-25(宏任务) 11-16(微任务)

流程:

  1. 进入主线程,执行console.log(1)
  2. 遇到setTimeout,通知定时触发线程,继续向下执行
  3. 遇到promise中的console.log('5'); .then回调的处理同样是通知相应线程
  4. 遇到setTimeout,通知定时触发线程,继续向下执行
  5. 执行console.log('10');
  6. 主线程处理完同步任务后,此时定时触发线程中的任务也已完成,主线程再定时触发线程中的消息队列中取
  7. 取出队列中的第一个出来的function() { console.log('2'); new Promise(function(resolve) { console.log('3'); resolve(); }).then(function() { console.log('4') }) }回调函数,然后主线程开始执行,重复1~6步骤
  8. 最终结果为1 5 10 6 2 3 4 7 8

搞懂了这个流程,相信对JavaScript的执行机制会有一定的了解

我们了解了浏览器其中的四个线程,那么GUI渲染线程的作用又是干嘛的呢? 那我们再谈一谈浏览器是怎样通过GUI引擎渲染到界面上的吧~

4. 浏览器的渲染(未细化)

当浏览器获得一个html文件时,会自上而下的加载,并在加载过程中进行解析渲染。

  1. 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
  2. 将CSS解析成 CSS Rule Tree 。
  3. 根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。
  4. 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。
  5. 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点

ps: 了解了浏览器相关的知识,对浏览器执行的过程有了更深入的了解,同时也建立起来知识体系的雏形,后续学习以此展开~~~ 思考: 浏览器是如何处理解析代码的? 又是如何构建渲染的呢? 正在学习中ing 感谢黑色的枫大佬,读了他的文章学到了很多东西~~