JavaScript加载与运行机制

JavaScript加载与运行机制

这其实是两个问题分为JavaScript的加载机制和运行机制

看完这篇文章后,对浏览器多进程,JS单线程,JS事件循环机制这些都能有一定理解

JavaScript加载机制

浏览器在获得一个html之后,会“自上而下”加载,并且在加载过程中进行解析渲染,

我们知道一般来说CSS和IMAGE的加载是异步的,不会阻碍文档的加载,但是JavaScript的加载是会导致文档挂起渲染进程,不仅要等待文档中js文件加载完毕,还要等待解析执行完毕,才可以恢复html文档的渲染线程。

此外如果JavaScript中有对CSS的操作,那么也会导致CSS的加载变为阻塞的。

延迟加载,所谓延迟加载就是等页面加载完成之后再加载JavaScript文件,JavaScript延迟加载有助于提高页面的加载速度,一般有以下的方式

  • defer属性
  • async属性
  • 动态创建DOM方式
  • 使用jQuery的getScript方法
  • 使用setTimeout延迟方法
  • 把JavaScript放到body的最后

2. JavaScript运行机制

我们上一节其实说到了JavaScript的事件循环,后续我们在详细了解

2.1 线程与进程

先看一下官方描述:

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

不同进程之间也可以通信,但是代价比较大,一般来说我们说的单线程和多线程说的是在同一个进程中

2.2 浏览器的多进程

首先我们要明确,浏览器是多进程的;浏览器之所以能够运行是因为系统给它的进程分配了资源;没打开一个tab页就相当于创建了一个独立的浏览器进程。

浏览器有以下一些主要的进程:

  • Browser进程:浏览器的主进程,只有一个
    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程
  • GPU进程
  • 浏览器渲染进程
    • 页面渲染,脚本执行,事件处理等

如果学习过electron就能很好的理解了,Browser进程相当于app主进程,浏览器渲染进程相当于BrowserWindows

我们重点看一下浏览器渲染进程,包括我们这一节说到的JavaScript加载和运行,还有后续会分享的回调函数、渲染等都是在这个进程中完成的。

我们看一下这个渲染进程包含那些线程:

  • 浏览器GUI渲染线程
    • 负责渲染浏览器页面,解析HTML、CSS、构建DOM树、布局和绘制
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • 这个线程和JavaScript引擎线程是互斥的
  • JavaScript引擎线程
    • 也称为JS内核,负责处理Javascript脚本程序,例如V8引擎
    • JavaScript引擎线程负责解析Javascript脚本,运行代码
    • 如果JavaScript引擎执行时间过长,可能导致渲染不流畅
  • 浏览器定时器触发线程(setTimeout)
    • setInterval与setTimeout所在线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
  • 浏览器事件触发线程
    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  • 浏览器http异步请求线程(.jpg 这类请求)
    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行

最后我们梳理一下浏览器内核中线程之间的关系

GUI渲染线程与JS引擎线程互斥

JS阻塞页面加载

2.3 WebWorker

我们上面提到了如果JavaScript如果执行时间很长,会导致GUI渲染进程在渲染的时候卡顿,也就是说对于CPU密集的运算,单纯靠JavaScript引擎是不够的,这种情况我们可以让JS引擎向浏览器申请开一个子线程,这个子线程不能操作DOM,然后子进程和JS引擎线程通过特定的方式通信就可以了。
这个描述的过程其实就是WebWorker

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

2.4 浏览器的渲染流程

其实本来应该说一下浏览器的渲染流程,但是后续有相关的分享,这里就先不说了。

2.5 任务队列(同步任务和异步任务)

任务队列,也就说每个任务的执行是有顺序的,一个任务接着一个任务。一般来说排队有两种原因

  • 任务的计算量很大,CPU处于忙碌状态
  • 任务需要的东西还没准备好,导致CPU闲置,例如ajax的返回

这种情况我们可以先运行后续的任务,将等待的任务挂起,等准备好了再运行等待中的任务。根据策略的不一样,我们分为同步任务和异步任务

  • 同步任务

    需要执行的任务在主线程上排队,一个接一个,前一个完成了再执行下一个

  • 异步任务

    没有马上被执行但需要执行的任务,存放在“任务队列”(task queue)中,“任务队列”会通知主线程什么时候哪个异步任务可以执行,然后这个任务就会进入主线程并被执行。所有的同步执行都可以看作是没有异步任务的异步执行

其实理解了同步任务和异步任务我们就理解了JavaScript的运行机制,具体来说是:

  • 所有的同步任务都在主线程上执行,形成一个执行栈,也就是说这些能够理解被执行的任务都排好了对,我们一个一个执行就可以了
  • 而主线程之外还有一个任务队列,只要异步任务有了运行的结果,就在任务队列中放置一个事件,
  • 主线程中的任务执行完毕后,会读取任务队列,看是否有异步任务对应的事件,如果有就把这个异步任务推入执行栈,开始执行
  • 主线程重复上面的步骤,就是JavaScript的执行机制

2.6 事件

上一节说到的任务队列,其实是一个事件的队列,IO设备完成一项任务,就会在“任务队列”中添加一个时间,表示相关的异步任务可以进入“执行栈”。接着主线程读取“任务队列”,查看里面有哪些事件。

“任务队列”中的事件除了IO设备的事件之外,还包括一些用户产生的事件,只要指定过回调函数,这些事件发生之后就会推入到任务队列,等待主线程的读取

主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的运行机制又称为“Event Loop”(事件循环)

执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行

var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

2.7 定时器

除了放置异步任务的事件,“任务队列”还可以放置定时器,setTimeout()只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
setTimeout(function(){console.log(1);}, 0);
console.log(2);

只有在执行完第二行以后,系统才会去执行”任务队列”中的回调函数

2.8 从Event Loop谈JS的运行机制

浏览器环境下的Event Loop:

  • 当主线程运行的时候,JS会产生堆和栈(执行栈)
  • 主线程中调用的webaip所产生的异步操作(dom事件、ajax回调、定时器等)只要产生结果,就把这个回调塞进“任务队列”中等待执行。
  • 当主线程中的同步任务执行完毕,系统就会依次读取“任务队列”中的任务,将任务放进执行栈中执行。
  • 执行任务时可能还会产生新的异步操作,会产生新的循环,整个过程是循环不断的。

看一个例子把

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3)
    setTimeout(function(){
        console.log(6);
    })
},0)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},0)
console.log(5)

输出结果1,2,5,3,4,6,7

这里有个问题,就是任务队列里面的所有任务都会一个个排队执行么?其实这个大家也好理解,我们去排队,也是有人会排队,有人会插队的把,JS中有些任务就是插队的任务,因为它们有特权。正常的任务,我们叫宏任务(macro-task),有特权的任务,我们叫微任务(micro-task)。

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise')
    })
})
setTimeout(function(){
    console.log(3);
})

结果:

1 2 promise 3

这个结果的原因是,promise是微任务,当主线程执行完毕,微任务会排在宏任务前面先去执行,不管是不是后来的

微任务包括: 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver, MessageChannel

宏任务包括:setTimeout, setInterval, setImmediate, I/O

猜你喜欢

转载自blog.csdn.net/lihangxiaoji/article/details/80697416
今日推荐