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