JavaScript代码执行机制

前言

  一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML网页上使用,用来给HTML网页增加动态功能。JavaScript兼容于ECMA标准,因此也称为ECMAScript。

一、代码块

  JS中的代码块是指由<script>标签分割的代码段。JS是按照代码块来进行编译和执行的,代码块间相互独立(即就算代码块1出错,但不影响代码块2的加载和执行),但变量和方法共享。

  在加载HTML页面的时候,当浏览器遇到内嵌的JS代码时会停止处理页面,先执行JS代码,然后再继续解析和渲染页面。同样的情况也发生在外链的JS文件中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它,在这个过程中,页面的渲染和用户互交完全被阻塞。由于现代浏览器都允许并行下载JS文件,因此<script>标签在下载外部资源时不会阻塞其他的<script>标签,但仍然会阻塞其他资源的下载。

为了安全起见,我们一般都在页面初始化完毕之后才允许JavaScript代码执行,这样可以避免网速对JavaScript执行的影响,同时也避开了HTML文档流对于JavaScript执行的限制。如果在一个页面中存在多个windows.onload事件处理函数,则只有最后一个才是有效的

二、预编译

       在执行JS代码的时候,JS引擎并不是按照我们书写的顺序从上到下顺序编译并且执行的,首先是按照自己的规则对我们的代码先进行编译,然后从上到下执行编译后的代码。

    在全局作用域中,JS首先会对我们的函数或者变量进行声明,就是我们经常听到的变量提升机制,然后才是按照我们书写代码的顺序,来进行编译,然后再执行编译的代码。

       对于函数的变量提升,如果是函数式声明,那么直接提升(即可以在声明前调用),而如果是变量式声明,则是只有变量提升,值为undefined,当执行到赋值语句的时候,才能够调用函数。

wrap();   //  1
function wrap(){
  console.log(1);
}
foo();     // undefined
var foo = function (){
  console.log(2);
}

在函数作用域中的编译顺序为:首先对函数的存在的参数进行声明,然后是内部的函数,在然后是变量,然后在顺序编译我们书写的代码

三、执行期

在对JS代码进行预编译后,就按照编译后的代码进行从上到下的执行,遇到赋值的语句才对之前提升的变量进行赋值。同时在同一作用域中对变量进行赋值的时候,函数的赋值要快于一般常量的赋值。

在赋值的时候就需要注意到一点——作用域,这点主要体现在函数内的变量赋值。

var a = "134";
function b(){
 alert(a);   // undefined
 var a="123";
  alert(a);     // "123"
}

从上面的例子中可以看出,在对访问变量的时候,首先会在当前作用域里查找,如果没有,则会顺着作用域链向上直到找到全局的作用域链

四、执行机制

JS是单线程语言,异步执行,JS的执行机制是事件循环Event Loop。

首先了解一下JS的任务,分为同步任务和异步任务。注意,只有主线程空了,才会去读取"任务队列",这就是JS的运行机制,这个过程会不断重复。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕了,才会执行后一个任务。

异步任务:在主线程之外,还存在一个“任务列队”,异步任务就是不进入主线程,而是进入“任务列队”的任务,只有“任务列队”通知主线程,某个异步任务可以执行了并且同步任务执行完毕,该任务才会进入主线程执行。

一个浏览器环境,只能有一个事件循环,而一个事件循环可以多个任务队列,队列之间可有不同的优先级,同一队列中的任务按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行

运行机制:

  • 所有同步任务直接按照顺序在主线程上执行,形成一个执行栈(execution context stack)
  • 如果是异步任务,直接调用浏览器的Web API,依赖浏览器的多线程机制进行计算,当异步任务有运行结果后,将事件推到主线程之外的“任务队列”(task queue)
  • 一旦“执行栈”中的所有同步任务执行完毕,系统就会自动的读取“任务队列”中的事件,进入执行栈,然后执行

但如果异步任务里面包含异步任务怎么办呢?请看下面的例子

setTimeout(function(){  
     console.log('定时器开始啦')  
 });  
   
 new Promise(function(resolve){  
     console.log('马上执行for循环啦');  
     for(var i = 0; i < 10000; i++){  
         i == 99 && resolve();  
     }  
 }).then(function(){  
     console.log('执行then函数啦')  
 });  
   
 console.log('代码执行结束'); 

如果按照上面的分析,首先遇到异步任务setTimeout,推入“任务队列”;然后同步任务Promise,进入“执行栈”,执行console.log,然后遇到回调函数then,异步任务,推入“任务队列”;最后执行console.log,执行完毕后,执行栈空,马上读取任务队列,任务队列是先进先出,按道理应该先执行setTimeout,然后执行回调函数then。

那么此时的打印结果应该是:马上执行for循环啦---代码执行结束---定时器开始啦---执行then函数啦,然而实际的结果却是:

这个时候估计就要问为什么了?难道异步任务的执行顺序,不是先后顺序?事实上,按照异步和同步的划分方式,并不准确。
而准确的划分方式是:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval

micro-task(微任务):Promise的回调,process.nextTick

这里需要注意一下await标志,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

按照这种分类方式,JS的执行机制是:

  • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的“事件队列”里

  • 当目前的宏任务执行完成后,会查看微任务的“事件队列”,并将里面全部的微任务依次执行完,然后接着执行下一个宏任务

  • 重复以上2步骤,结合两种event loop,就是更为准确的JS执行机制了

尝试按照刚学的执行机制,去分析刚才的例子:

  • 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的“队列”里

  • 遇到 new Promise直接执行,打印"马上执行for循环啦"

  • 遇到then方法,是微任务,将其放到微任务的“队列”里。

  • 打印 "代码执行结束"

  • 本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数,打印"执行then函数啦"

  • 到此,本轮的event loop 全部完成。

  • 下一轮的循环里,先执行一个宏任务,发现宏任务的“队列”里有一个setTimeout里的函数,执行打印"定时器开始啦"

所以最后的执行顺序是:马上执行for循环啦---代码执行结束---执行then函数啦---定时器开始啦

【注意】参考博客:http://www.ruanyifeng.com/blog/2014/10/event-loop.html

         https://blog.csdn.net/highboys/article/details/79110116

         https://juejin.im/post/59e85eebf265da430d571f89

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值