JavaScript运行机制总结

152 阅读10分钟

单线程非阻塞的脚本语言

这是由最初的用途来决定的:与浏览器交互

  • 单线程:JavaScript代码在执行的时候,都只有一个主线程来处理所有的任务。
  • 非阻塞:当代码需要进行一项异步任务(无法立即返回结果,需要花一定的时间才能返回任务,如IO事件),主线程会挂起(Pending)这个任务,然后在异步返回结果的时候再根据一定规则去执行相应的回调。

JavaScript的单线程,与它的用途有关,也是JavaScript这门语言的基石,JavaScript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,此时应该如何处理呢? 因此,为了避免复杂性,JavaScript从一诞生就是单线程,这样就可以保证程序执行的一致性。

单线程缺点

单线程在保证了执行顺序的同时也限制了JavaScript的效率,为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程Web Worker

Web Worker

这项技术号称让JavaScript成为一门多线程语言然而,使用Web Worker开的多线程有着诸多限制。

例如:所有新线程都受主线程控制,不能独立执行,不能操作DOM,这些子线程并没有执行IO权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,因此,这个新标准并没有改变JavaScript单线程的本质。

非阻塞(Event Loop)

单线程意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO很慢(比如ajax操作从网络读取数据),不得不等着结果车来,再往下执行。

任务队列

  • 同步任务

  • 异步任务

    同步任务指的是在主线程上排队执行的时候,只有前一个任务执行完毕,才能执行后一个任务.

    异步任务指的是,不进入主线程,而进入任务队列(task queue),只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

只要主线程空了(所有同步任务执行完毕)就会去读取异步任务(任务队列),这就是JavaScript的运行机制。

异步任务的运行机制如下:

1.所有同步任务都在主线程上执行,形成一个执行栈。

2.主线程之外,还存在一个"任务队列(task queue)"。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

3.一旦所有同步任务(“执行栈”)执行完毕,系统就会去读取"任务队列",看看对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

4.主线程不断重复上面的第三步

异步任务里面又包含有同步任务和异步任务

同步任务总是在读取异步任务之前执行。只有同步的代码执行完,才会开始去执行异步

两个栈

  • 同步栈(按顺序执行)

  • 异步栈(任务队列,所有异步的任务形成一个队列,"先进先出",当同步栈里面执行完毕,异步栈里面的第一个任务就会开始进入主线程执行) 反复循环上面的步骤。

    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制,这个过程会不断重读。

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

macro task 和 micro task

Event Loop(事件循环)是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。 不同的异步任务分为两类

宏任务(micro task):

  • setInterval()
  • setTimeout()

微任务(macro task):

  • new Primise()、
  • new MutaionObserver()//等待所有脚本任务完成后,才会执行,即采用异步方式。

在一个事件循环(Event Loop)中,异步事件返回结果后会被放到一个任务队列中。

根据这个异步事件的类型,这个事件会被对应的宏任务队列或者微任务队列中去。

当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在,如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈。如果存在,则会依次执行队列中事件对应的回调,直到微任务为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈 ,如此反复循环。

当前执行栈执行完毕时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。
同一次事件循环中,微任务永远在宏任务之前执行。

浏览器环境下JavaScript引擎的事件循环机制

1.执行栈与事件队列

当JavaScript代码执行的时候会将不同的变量存于内存中的不同位置:

  • 堆(heap):存放一些对象
  • 栈(stack):存放基础类型变量以及对象的指针,调用各种外部API,在"任务队列"中加入各种事件(click,load,down)。

当调用一个方法的时候,JavaScript会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。

执行上下文:存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。

当一系列方法被一次调用的时候,因为JavaScript是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方,这个地方被称为执行栈

当一个脚本第一次执行的时候,JavaScript引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。

如果当前的执行是一个方法,那么JavaScript会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。

当这个执行环境中的代码执行完毕并返回结果后,JavaScript会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。

这个过程反复进行,直到执行栈中的代码全部执行完毕。

一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境过程中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

JavaScript采用词法作用域(静态作用域)

词法作用域:函数的作用域在函数定义的时候就决定了

JavaScript引擎并非一行一行地执行和分析程序,而是一段一段地执行分析。

当执行代码的时候,会进行一个"准备工作"。

可执行代码 当执行到一个函数的时候,就会进行准备工作--执行上下文 执行上下文栈 当JavaScript要解析执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会像执行上下文栈压入一个全局执行上下文。

对于每个执行上下文,都有三个重要属性 1.变量对象 2.作用域链条 3.this

this

JavaScript语言之中,一切皆对象,运行环境也是对象,所以函数是在某个对象之中运行,this就是函数运行时所在的对象(环境)

1.全局环境

全局环境使用this,它指的是顶层window

2.构造函数

构造函数中的this,指的是实例对象

3.对象的方法

如果对象的方法里面包含this,this的指向就是方法运行时所在的对象,该方法赋值给另一个对象,就会改变this的指向。

由于this的指向是不确定的,所以切勿在函数中包含多层的this,第一层指向对象,第二层指向全局对象。

一个解决方法是在第二层改用一个指向外层this的变量。

const that = this

上面代码定义了变量that,固定指向外层this,然后在内层使用that,就不会发生this指向的改变。

事实上,使用一个变量固定this的值,使用内层函数调用这个变量,是非常常见的方法,请务必掌握。

避免数组处理方法中的this

数组的map的foreach方法,允许提供一个函数作为参数,这个函数内部不应该使用this

foreach方法的回调函数中的this,其实是指向window对象,因此取不到值。原因跟上一层的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。

避免回调函数中的this

回调函数中的this往往会改变指向,最好避免使用 this总是返回一个对象 this用在构造之中,表示实例对象。 由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的。 只要函数被赋给另一个变量,this的指向就会改变。

执行环境与作用域的区别与联系

执行环境分为:

  • 全局执行环境
  • 局部执行环境(函数执行环境中创建的)

执行环境中存放着这个方法的私有作用域,上层作用域的指向,方法的参数以及这个作用域中定义的变量和这个作用域的this对象.

而当一系列方法被一次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方,叫做

作用域链

作用域链是基于执行环境的变量对象,当代码在一个环境中执行时,会创建变量对象的一个作用域链。

作用域链的用途:是保证对执行环境所有变量和函数的有序访问。

作用域链的前端始终都是当前执行的代码所在环境的变量对象。

不同执行上下文之间的变量命名冲突通过攀爬作用域链解决,从局部到全局。

这让具有相同名称的局部变量在作用域链中有更高的优先级。

函数提升

JavaScript创建函数有两种方式:

  • 函数声明式

  • 函数字面量式。只有函数声明才存在函数提升!如:

      console.log(f1) // functioon f1(){}
      console.log(f2) // undefined
      function f1(){}
      var f2 = function(){}
    

变量提升

var a
console.log(a) // 由于未赋值,所以输出undefined
a = 10

foo()
function foo() {
	 console.log('aaa')
	}
输出aaa