Js的执行机制
参考链接:
(转载)js引擎的执行过程(一)
js引擎执行过程分为3个过程:
1、语法分析
2、预编译阶段
3、执行阶段
注:浏览器首先按顺序加载由script标签分割的js代码块,加载js代码块完毕后,立刻进入以上三个阶段,然后再按顺序查找下一个代码块,再继续执行以上三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。
1、语法分析:
js语法代码块加载完毕后,会首先进入语法分析阶段,主要作用是:
分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段
2、预编译阶段:
js代码块通过语法分析阶段后,语法正确则进入预编译阶段。在分析预编译阶段之前,我们先了解一下js的运行环境,运行环境主要有三种:
- 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)
- 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)
- eval(不建议使用,会有安全,性能等问题)
每进入一个不同的运行环境都会创建一个相应的执行上下文,那么在一段js程序中一般都会创建多个执行上下文,js引擎会以栈的方式对这些执行上下文进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文,栈顶永远是当前执行上下文
创建执行上下文:
可以理解为当前的执行环境,创建执行上下文的过程中,主要做了以下三件事:
- 创建变量对象(Variable Object)
- 建立作用域链(Scope Chain)
- 确定this的指向
创建变量对象
//首先创建fun执行上下文 funEC = { //变量对象 VO: { //arguments对象 arguments: { a: undefined, b: undefined, length: 2 }, //test函数 test: <test reference>, //num变量 num: undefined }, //作用域链 scopeChain:[], //this指向 this: window }
预编译阶段的变量对象都是在预编译阶段,没有进入执行阶段,变量对象都是不可以访问的,因为变量对象中的变量属性没有赋值,为undefined,只有到执行阶段,变量对象中的变量属性进行赋值,变量对象才能转为活动对象,才能进行访问,这个过程为VO->AO
建立作用域链
作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
var num = 30; function test() { var a = 10; function innerTest() { var b = 20; return a + b } innerTest() } innerTestEC = { //变量对象 VO: {b: undefined}, //作用域链 scopeChain: [VO(innerTest), AO(test), AO(global)], //this指向 this: window } test()
当调用到innerTest时,全局和test是执行阶段,innerTest是预编译,所以活动对象分别为AO,A0,VO,而innerTest的作用域链由当前执行环境的变量对象(没进入执行阶段前)与上层环境的一系列活动对象组成。
- 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象)
- 最后一项永远是全局作用域
- 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。
3、确定this指向
在全局中国this指向window,函数环境的this指向却较为灵活,需要根据执行环境和执行方法确定。
3、执行阶段
js的异步执行机制是由事件循环event loop解决的。
js是单线程的,但是js执行过程会有4个线程,但是永远只有js引擎线程在执行js脚本程序,其他的三个线程只协助,不参与代码解析和执行。
- JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)
- 事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件队列,等待JS引擎线程执行
- 定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待JS引擎线程执行。
注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。 - HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。
- 注:浏览器对通一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。
总结:永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
宏任务
宏任务就是JS 内部(任务队列里)的任务,严格按照时间顺序压栈和执行的任务。
宏任务(macro-task)可分为同步任务和异步任务:
同步任务指的是在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)。
异步任务指的是不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件,setTimeout等。
理解宏任务中同步任务和异步任务的执行顺序,那么就相当于理解了JS异步执行机制–事件循环(Event Loop)。
微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,
问题:
1、js为什么是单线程的?
js最先设计被用在浏览器上,如果js是多线程的
场景描述:
那么现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?
2、js为什么需要异步?
如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。
对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验
3、js单线程是如果实现异步的?
通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制
js中的event loop
事件循环可以理解成3部分组成,分别是:
- 主线程执行栈
- 异步任务等待触发
- 任务队列
任务队列就是以队列的数据结构对时间任务进行管理,特点是先进先出,后进后出
在JS引擎主线程执行过程中:
- 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈;
- 当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制
- 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中,等待主线程读取执行
- 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
- 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程
微任务
微任务是在es6和node环境中出现的一个任务类型,如果不考虑es6和node环境的话,我们只需要理解宏任务事件循环的执行过程就已经足够了,但是到了es6和node环境,我们就需要理解微任务的执行顺序了。
微任务(micro-task)的API主要有:Promise, process.nextTick
执行宏任务中同步任务,执行结束;
检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件,推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务
执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); //如果then之前有输出的话是当作同步处理的 Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); 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引擎主线程执行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行
- JS引擎主线程执行到console.log('script end');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end
- 主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么立刻执行,输出promise1和promise2
- 微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出setTimeout