新年祝福
过年啦,过年啦!lotoze在这里为你们送上新年祝福:
祝@全部人,在新的一年里个个身体健康,万事如意!
祝@武汉人民,跨过一切困难险阻,打败一切疫情!
祝@爸妈爷爷奶奶,幸福美满,健康长寿!
祝@爱人,一直永远开心下去,我将永远爱你!
祝@我,可以让我身边的人在2020年更好!javascript
前言
【JS深渊】系列是我用来对前端原生javascript语言相关技术进行的深度探究或者笔记记录,我我的是一个很爱去挖层的这么一我的,但不幸的是觉悟的太晚,但我不会放弃。决定开始写博客目的有两个,一个是有仪式性的去持续提升本身的技术能力,另外一个就是但愿向上再向上。不过本身能力也是有限的,若是写的很差或者出现什么纰漏又或者错误的地方,脱裤式欢迎你们的建议、指正。没错看着我,对,盯着你的屏幕,别眨眼,请记住个人这句话:
您的反馈是就是我持续进步的动力。
好了,收!biu~html
正文
在当咱们写完js代码执行的时候,其内部的执行机制很是复杂,但主要会经历三个过程:语法分析、预编译、真正执行。这其中,在真正执行这一过程当中是最庞杂的。此篇幅就是为真正执行这一过程的分析而诞生。 若是对前两个过程有疑惑请点击此处【JS深渊】干它!必定要完全弄懂javascript执行机制(一)前端
我记得我当时研究js的执行机制,是由于一个问题:java
js是一个单线程语言,是否表明着参与js执行过程的线程只有一个?git
答案是否认的。在我回答为何以前咱们须要去了解一些知识点。这是我为了解决这个问题去百度的线程知识点。github
进程与线程
- 进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 线程
线程(英语:thread)是操做系统可以进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运做单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中能够并发多个线程,每条线程并行执行不一样的任务。在UnixSystemV及SunOS中也被称为轻量进程(lightweightprocesses),但轻量进程更多指内核线程(kernelthread),而把用户线程(userthread)称为线程。
线程是独立调度和分派的基本单位。线程能够为操做系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的所有系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),本身的寄存器环境(register context),本身的线程本地存储(thread-local storage)。 一个进程能够有不少线程,每条线程并行执行不一样的任务。
以上是摘自百度百科,由于我再总结也没有百度总结的好啊。可是上边的这些是给非人类看的。也就是看不懂的话——俗称“废话”。promise
对于咱们来讲,无需把全部都记住,由于知识是学不完的,要只学本身须要的。
须要的是要对进程和线程的有个基本认识:
- 进程和线程都是计算机系统中重要概念。
- 进程是操做系统结构的基础,是程序的实体,是系统资源分配的最小单位。
- 线程是cpu调度的最小单位,也是程序执行的最小单位。
- 进程和线程也有着不可分割的联系。进程是线程的容器。也就是说进程中能够包含若干线程。
那么回到一开始的问题上来,刚才说答案是否认的,缘由是什么?浏览器
咱们一般所说的js基本是依赖于浏览器的。浏览器也是咱们前端打交道最多的。咱们说浏览器是计算机系统上的一个软件。进程是程序的实体,是系统资源分配的最小单位。也就是说当你启动浏览器时,其实就是启动了一个浏览器进程。进程是线程的容器,一个进程下能够并发包含多个线程。浏览器能够作的事情远远不止一件事,除了能够解释执行js代码,还能够渲染页面、异步执行、计时器/定时器、事件监听等。因此这确定不只仅是一个线程就能够作到的。因此参与js执行过程的线程不止一个。bash
既然js执行过程参与的线程不止一个,那么参与的线程有哪些呢?数据结构
参与的有4个线程,其中JS引擎线程只负责执行js,其余三个线程进行配合。值得一提的是,GUI渲染线程负责渲染html元素,可是在js引擎线程执行脚本的时候,GUI渲染引擎是被挂起的。
- JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)
- 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推动事件队列,等待JS引擎线程执行
- 定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,知足定时器的触发条件,则将定时器的处理函数推动事件队列中,等待JS引擎线程执行。 注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。
- HTTP异步请求线程:经过XMLHttpRequest链接后,经过浏览器新开的一个线程,监控readyState状态变动时,若是设置了该状态的回调函数,则将该状态的处理函数推动事件队列中,等待JS引擎线程执行。 注:浏览器对通一域名请求的并发链接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。
总结一下,都是javascript线程在运行js脚本,其余三个线程都是进行在知足条件是将执行函数推入事件队列中,等待JS引擎主线程执行。
执行阶段
为了更好的理解执行过程,须要引用个例子(英文原版),这个例子很是经典,建议英文基础好的阅读,很是不错的文章。
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0)
Promise.reslove().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
})
console.log("script end");
复制代码
这里直接按照执行过程划分代码,这里只简单描述一下过程。
- 宏任务(macro-task),宏任务又按执行顺序分为同步任务和异步任务
console.log("script start");
console.log("script end");
复制代码
setTimeout(function () {
console.log("setTimeout");
}, 0)
复制代码
- 微任务(micro-task)
Promise.resolve().then(function () {
console.log("promise1");
}).then(function () {
console.log("promise2");
})
复制代码
在js引擎的执行过程当中,进入执行阶段,代码的执行顺序以下:
宏任务(同步任务)---> 微任务 ---> 宏任务(异步任务)
复制代码
输出结果为:
"script start"
"script end"
"promise1"
"promise2"
"setTimeout"
复制代码
在ES6或Node的环境中,JS的任务分为两种,分别是宏任务(macro-task)和微任务(micro-tsk),在最新的ECMAScript中,微任务称为jobs,宏任务称为task,他们的执行顺序如上。不少人对上面的执行顺序分析不是很理解,那么咱们接下来继续对上面例子进行详细分析。
宏任务
宏任务(macro-task)可分为同步任务和异步任务。
- 同步任务指的是在JS引擎上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,造成一个栈(stack,这个栈能够理解为函数调用栈) 。
- 异步任务指的是不直接进入JS引擎主线程,而是知足条件时,相关的线程将异步任务推动任务队列(task queue),等待JS引擎主线程上的任务所有执行完毕,空闲时读取这个任务队列执行的任务,例如异步Ajax、Dom事件、setTimeout等。
理解宏任务中的同步任务和异步任务的执行顺序,那么就至关于理解了JS异步任务执行机制——事件轮询(Event Loop)
事件轮询
事件轮询能够理解为由三个部分组成,分别是:
任务队列(task queue)就是以队列的数据结构对事件任务进行管控,特色是先进先出,后进后出。
这里直接引用一张著名的图片(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》),帮助咱们理解:
在JS引擎主线程执行过程当中:
- 首先执行宏任务的同步任务,在主线程上造成一个执行栈,能够理解为函数调用栈。
- 当执行栈中的函数调用到一些异步的API(例如Ajax、Dom事件、setTimeout等),则会开启对应线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
- 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推动主线程中,按任务队列顺序执行。
- 当JS引擎主线程上的任务执行完毕以后,则会再次读取任务队列中的事件任务,如此循环反复,这就是事件轮询(Event Loop)。
若是仍是不理解,那么咱们再拿上面的例子进行详细的分析,该例子中宏任务的代码部分是:
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0)
console.log("script end");
复制代码
执行过程以下:
- JS引擎主线程按代码自上而下顺序依次解释执行,当执行console.log("script start");,JS引擎主线程任务该任务是同步任务,因此马上执行输出script start,而后向下执行。
- JS引擎主线程执行到setTimeout(function () {console.log("setTimeout")}, 0),JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制setTimeout任务。因为W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么计时器到4ms时,定时器线程就把该回调处理函数推动任务队列中等待主线程执行,而后JS引擎主线程继续向下解释执行。
- JS引擎主线程执行到console.log("script end");,JS引擎主线程认为该任务是同步任务,因此当即执行输出script end。
- JS引擎主线程的任务执行完毕(输出script start和script end)后,主线程变为空闲,则开始读取任务队列中的事件任务,将该任务队列里的事件任务推动主线程中,按照任务队列顺序执行,最终输出setTimeout,因此输出顺序为script start --> script end --> setTimeout。
以上就是JS引擎执行宏任务的整个过程。
理解了宏任务中的执行过程后,跟lotoze放飞一下,拓展性思考几个问题:
setTimeout和setInterval都是异步任务的定时器,须要添加到任务队列中等待JS引擎主线程空闲时间读取任务队列执行,那么使用setTimeout实现setInterval,会有区别吗?
答案是有区别的。不妨思考一下:
- setTimeout实现setInterval只能经过递归来实现。
- setTimeout是到了指定的时间就把事件推到任务队列中,只有当任务队列中的setTimeout事件任务被主线程执行后,才能继续再次到了指定时间的时候把事件推到任务队列中,依次反复,那么setTimeout的事件执行确定比指定时间要久,具体相差多少跟代码执行事件有关。
- setInterval则是每次都精确的间隔一段时间就向任务队列推入一个事件,不管上一个setInterval事件是否已经执行,因此有可能存在setInterval的事件任务累积,致使setInterval的代码重复连续执行好屡次,影响页面性能。
lotoze我在不久之前对于seTimeout和setInterval的认知还停留在setTimeout只执行一次,setInterval能够按照时间间隔循环不停的执行。可是如今却有了不少新的认识。
- 使用setTimeout实现计时器功能比setInterval要好一些,但setInterval的事件任务累积也是有办法解决的,最有效的办法就是加锁。
- setTimeout模拟setInterval最本质的区别是setTimeout必须在任务队列中的一个setTimeout事件任务被JS引擎主线程执行完后下一个才能再推动任务队列中来,而setInterval不管你任务队列中有没有setInterval的事件任务或者有没有已经执行完毕了在指定时间到了都会再次推动任务队列中来,形成任务累积。
- 若是不考虑浏览器兼容性问题,使用requestAnimationFrame是更好的选择。
高频率触发事件(例如滚动监听、input输出)触发频率太高会影响页面性能,甚至形成页面卡顿,咱们是否能够利用计时器的原理来进行优化呢?
是能够的。咱们能够利用setTimeout实现计时器的原理,对高频率的事件监听进行优化,实现点在于多个事件合并成一个,这就是防抖和节流。这里我会在后面的文章中去详细讲解一下实现方式,这里就很少说啦。
微任务
微任务是在ES6和Node环境中出现的一个任务类型,若是不考虑ES6和Node环境的话,咱们只须要理解宏任务中事件轮询的执行过程就足够了,可是到了ES6和Node环境,就须要理解掺杂了微任务的执行顺序了。
微任务(micro-task0)的API主要有:Promise、process.nextTick。
这里直接引用一张流程图帮助咱们理解一下:
在宏任务中执行的任务有两种,分别是
同步任务和
异步任务,由于异步任务会在知足触发条件时才会推动任务队列(task queue),而后等待主线程上的任务执行完毕,再读取任务队列中的任务事件,最后推动主线程执行,因此这里将异步任务即任务队列看作是新的宏任务,执行过程如上图所示:
- 执行宏任务中的同步任务,执行结束。
- 检查是否存在可执行的微任务,有的话执行全部的微任务,而后读取任务队列的异步任务事件,推动主线程造成新的宏任务。没有的话则读取任务队列的异步任务事件,推动主线程造成新的宏任务。
- 执行新宏任务(任务队列中异步任务),再检查是否存在可执行的微任务,如此反复循环。
这就是加入微任务后的详细事件轮询,若是尚未理解,那么再次对一开始的例子作一个全面的分析以及补充:
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0)
Promise.reslove().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
})
console.log("script end");
复制代码
执行过程:
- 代码块先经过语法分析,通读代码,看一看有没有低级的语法错误,若是有则抛出错误,若是没有则直接进入预编译阶段。须要注意的是JS代码是解释一行执行一行。
- 预编译过程是对代码真正执行的前一刻准备工做,在这一过程当中会生成GO(Global Object)以及一些AO(Active Object)对象,造成彼此联系的做用域链。
- 程序进入执行阶段,当JS引擎主线程按代码自上而下顺序依次解释执行,当执行console.log("script start");,JS引擎主线程任务该任务是同步任务,因此马上执行输出script start,而后向下执行。
- JS引擎主线程执行到setTimeout(function () { console.log("setTimeout") }, 0),JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制setTimeout任务。因为W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么计时器到4ms时,定时器线程就把该回调处理函数推动任务队列中等待主线程执行,而后JS引擎主线程继续向下解释执行。
- JS引擎主线程执行到Promise.resolve().then(function () { console.log("promise1") }).then(function () { console.log("promise2") });,JS引擎主线程认为Promise这是一个微任务(micro-task),就把该任务划分为微任务,等待执行。
- JS引擎主线程执行到console.log("script end");,JS引擎主线程认为该任务是同步任务,因此当即执行输出script end。
- 主线程上的宏任务执行完毕,则开始检查是否存在可执行的微任务, 检测到一个Promise微任务,那么马上执行,输出promise1和promise2。
- 微任务执行完毕,主线程开始读取任务队列中的定时器任务setTimeout,推入主线程造成新宏任务,而后在主线程中执行,输出setTimeout。
最后的输出结果是:
"script start"
"script end"
"promise1"
"promise2"
"setTimeout"
复制代码
怎么样?你是否懂了?😁😁
参考:
lotoze | 【原创】
着重说明:
里面一些表情图片并不是原创,只是为了读者读起来不是那么枯燥乏味。但若是原做者以为有侵犯版权的意思,请使用下方联系方式与我联系,为了尊重原创做者的辛苦创做,我将及时处理!
固然,没事也能够联系啦😘😘欢迎交流!
求赞/求关注
写做不易,
若是您还以为凑合,就给个赞!
若是以为确实以为: “老家伙,有你的啊!”就加个关注!
若是文章有任何的错误,脱裤式欢迎你们来进行批评指正!
每个鼓励都是lotoze我持续抛头颅,撒鸡血的创做动力!
每个批评反馈也都是lotoze我持续成长的台阶!