浏览器如何运行 JavaScript

浏览器如何运行 JavaScript

须要铺垫的知识点:执行环境 + 执行栈web

执行环境(Execution Context)

执行环境是函数被调用时所在的环境。执行环境中存储了函数执行时相关的全部事物。当咱们在函数内访问某一变量时,这个变量其实也是该函数执行环境提供的。由于执行环境是不能直接被访问的(不能被访问表明咱们在函数内部不能使用任何变量),因此ECMA标准在函数被调用时,会构造一个可以被访问的对象——执行环境对象(Execution Context Object),这个对象上包含三个属性:变量对象、做用域链、this的值。浏览器

这三个属性的做用:数据结构

  1. 变量对象:提供对「与执行环境相关的变量和函数」的访问和存储的一个对象,也就是当前做用域。(Variable Object 下文简称VO)

在全局执行环境中,VO 就是全局对象。在函数执行环境中,由于 VO 不能被直接访问的,这是会提供一个活动对象(Activation Object)简称 AO 来扮演 VO 的角色。AO 在进入函数执行环境的那一刻被建立。 函数接收到的参数,存储在变量对象的arguments属性上。 函数内部定义的变量和函数,都会在变量对象上拥有一个同名属性,变量的属性值为变量值,函数的属性值为该函数的指针。多线程

  1. 做用域链:当咱们在函数内部访问某一变量时,就会在做用域链上进行查找。查找顺序是从做用域链顶部到最外层做用域也就是全局做用域,当找到变量或者找到全局做用域为止。

做用域是用来查找标识符的一种规则。决定了在当前函数内声明的标识符的可做用范围。 若是发生做用域嵌套,则会造成做用域链。做用域链包括当前做用域(也就是当前执行环境的变量对象)加上外层做用域链。 每一个函数对象的做用域链都被存储在其内部属性[[Scope]]上,做用域链上的外部做用域是经过复制外部函数的[[Scope]]属性构成的。 即Scope Chain = [AO].concat([[Scope]])dom

  1. this的值:由于函数能够在不一样的执行环境被执行,因此 JS 设计出了 this 的概念,用于指向真正运行的执行环境。

当函数做为某个对象的属性调用时,this指向那个属性。当函数自主调用时,this指向undefined,在非严格模式下,this又指向全局对象,在浏览器中则是window对象。异步

执行环境分为三种:async

  1. 全局执行环境(Global Execution Context)
    全局执行环境是JS代码一被加载时,就会生成的默认执行环境。是最外围最大的执行环境,有且仅有一个。浏览器在加载 JS 代码时,指定 window为全局执行环境的变量对象,全部变量和函数都是定义在window 对象上的某个属性。 全局执行环境是一直存在的,直到浏览器关闭窗口后,才会被销毁。
  2. 函数执行环境(Functional Execution Context)
    函数执行环境又叫作局部执行环境,顾名思义,就是 JS 引擎在识别到有函数被调用,即会建立出的一个函数执行环境,能够有多个。当一个函数被执行完毕时,它所在的执行环境就会被销毁。
  3. Eval 执行环境
    在 eval 函数中的执行环境。

执行栈(Execution Stack)

浏览器用 JS 引擎执行 JS 代码,而 JS 引擎构建出执行栈用来记录程序运行状况。执行栈遵循栈数据结构,先进先出,每当遇到一个函数调用,就会建立出其执行环境压入执行栈栈顶,当函数执行完成后将其推出执行栈。保证执行流按照执行栈的顺序有序执行。编辑器

须要铺垫的知识点:浏览器的多个线程函数

JS 是单线程

JS 是单线程语言意味着只会有一条线程在执行 JS,一次也只会执行一个 JS 任务,其他任务都须要排队等待上一个任务执行完毕才能执行。oop

同步 & 异步

同步:每一个任务按照顺序进行执行,必须等待前一个任务执行完毕,后一个任务才能够开始。

异步:能够将一个任务分红两段,先执行第一段,而后执行其余任务,等作好了准备,再来执行第二段。

为何 JS 要设定为单线程

单线程的特色就是同一时间只能作一件事情,而 JS 是被做为浏览器脚本语言使用的,主任务是提供用户与页面交互的能力。设想一下若是浏览器是多线程,有两个线程同时在对一个 dom 进行操做,这时浏览器就会不知道以哪一个线程为准。

为何须要异步任务

咱们知道,浏览器上有不少操做是很耗时的,好比请求数据、加载媒体文件等,若是都是同步任务,则须要等待一个一个耗时操做的完成,而相对次要的耗时操做其实不该该让用户有等待的感受,用户体验会很是的很差。因此会经过一些其余线程来实现异步的形式。

浏览器运行 JS 时涉及到的几个线程

  1. JS 引擎线程:用于执行 JS 代码,JS 引擎有多个线程,由一个主线程和 n 个其余线程配合一块儿工做。主线程用于执行当前执行栈内的任务。
  2. 事件触发线程:用来存放异步事件,每个异步事件触发后,都会交给事件触发线程管理,造成一个任务队列。
  3. 定时任务线程:用于管理设定的定时任务,到达设定事件后,会将定时任务的回调函数推到事件触发线程管理的任务队列上。

浏览器运行 JS 的工做流程

至关于执行栈与以上三个线程的工做流程

  1. 浏览器加载 JS 代码后,JS 引擎主线程会构建出一个执行栈,用于主线程读取当前可执行任务。
  2. 执行流按行读取JS代码,每遇到一个函数调用便会建立出一个新的执行环境并将其压入执行栈栈顶。
  3. 主线程读取执行栈栈顶的任务进行执行,执行流进入函数内部开始执行,执行完成后将执行环境作出栈操做,执行流回到下一个执行环境开始执行。
  4. 主线程只会执行同步任务代码,当遇到异步事件时会将其托管给对应的其余线程代为执行,当异步事件知足执行回调的条件时,会将其回调函数放入事件触发线程管理的任务队列的末尾,当可执行栈内为空时,主线程才会去读取任务队列列头的任务压入执行栈进行执行。JS 的工做流程就是反复上述执行过程。

主线程在执行栈和任务队列进行反复轮询的过程就是咱们常说的 JS 执行机制 —— 事件循环(Event Loop)。

微任务 & 宏任务

JS 又把异步任务分为了2类:宏任务 + 微任务

全部异步任务一开始都会被汇总到一个事件列表(Event Table)中,当知足塞入事件队列(Event Queue)的条件时(好比异步请求完成、定时任务到达时间等),会将事件从 Event Table 中取出,并按照该异步任务类型将其回调函数放入到宏任务事件队列微任务事件队列中,JS 引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空读取事件队列时,会优先读取微任务事件队列上的全部事件,并压入执行栈开始执行,然后执行栈又为空时再读取宏任务事件队列列头的第一个任务压入执行栈,开启第二轮事件循环

在执行机制上两者的区别:

  1. 微任务处理优先级高于宏任务。
  2. 读取微任务事件队列时会一会儿读取一整个队列,而读取宏任务事件队列时只会取出第一个任务压入执行栈执行。

常见的一些异步任务分类

  1. 宏任务(Marco Task): script中的代码、 setTimeoutsetInterval
  2. 微任务(Micro Task): Promise

运行机制流程图

任务队列里有啥?

Javascript 任务分为同步任务和异步任务,同步任务是前一个任务结束后一个任务才能开始。异步任务则不用等前一个任务完成就能够开始。

而异步任务又包括异步请求、异步回调(dom操做的回调、定时任务的回调)等,这些任务都会被放置在任务队列中。

对于异步请求和定时任务会先交给浏览器代为处理,等处处理完毕后将异步任务的回调函数推入任务队列的末尾,等待主线程读取。

async/await

讲到主线程和执行栈,就有一个不得不提的内容 async/await

async函数会返回一个Promise对象,便于回调函数管理,await是一个运算符,用于组成表达式,await xxx的计算结果取决于await它等待的东西,也就是xxx。若是它等待的不是一个Promise,那么它的计算结果就是它等待的东西。

await等待的是一个Promise时,它会对当前await后面声明的代码进行阻塞,直到Promise返回后才会继续执行后续代码。

当执行流遇到await functionXX(): Promise<any>时,由于发生了函数调用,因此functionXX会被压入执行栈,执行流会进入到函数内部,将return Promise以外的代码先执行一遍,Promise相关的异步操做会交由浏览器执行。

举个定时任务的例子

当咱们设定了一个 10s 的定时任务,浏览器定时触发线程中的计数器在 10s 后准时的将定时任务的回调函数添加到任务队列末尾。

到这一步都是很正常的,可是这个被添加进消息队列的回调函数何时会被读取呢?

只有当主线程执行完全部任务后才会依次读取任务队列中的任务。

也就是说虽然浏览器按时的将定时任务发送到了任务队列中,但真正被主线程读取的时间可能超过了设定的时间。这也就是为何有些定时任务被执行的时间和设定时间不一致的缘由。



写在最后

本篇内容以理论为主,后续还会开一篇内容专门用代码作例子分析。本文的所有内容,纯本人理解后手敲的文字,有不对的地方,欢迎指出和纠正,感谢阅读,欢迎点赞 🦀🦀

本文使用 mdnice 排版

相关文章
相关标签/搜索