JS底层原理

JS解析引擎

  1. 定义:能够“读懂”JavaScript代码,并准确地给出代码运行结果的一段程序
  2. 对于静态语言(如:C语言,Java),处理上述的过程称为编译器,对于JS这种动态语言来说则叫做解释器
  3. 编译器和解释器区别:
    • 编译器是在执行程序前,将整个源代码编译为目标代码(如机器码、字节码)之后,计算机直接再执行此目标代码即可
    • 解释器是在执行程序时,一条一条地将源代码解释成机器语言来让计算机执行,直接解析并将代码的运行结果输出
  4. JS 语言是由浏览器的解析引擎解释的,不同的浏览器的解析引擎也不同,
1. Chrome  :  webkit/blink :  V8
2. FireFox :  Gecko        :  SpiderMonkey
3. Safari  :  webkit       :  JavaScriptCore
4. IE      :  Trident      :  Chakra
  • 注:JS 不一定非要在浏览器中运行,只要有引擎即可,最典型的比如 NodeJs,采用了谷歌的 V8 引擎,使 JS 完全脱离浏览器运行
  1. 其实,现在很难去界定 JS 引擎到底是解释器还是编译器,比如 Ghrome V8,它为了提高 JS 的运行性能,在运行前就会先把 JS 编译成本地的机器码,然后再去执行机器码,这样会快很多;不过,也不需要过分强调 JS 引擎到底是什么,只需要了解他做了什么事情就可以了
  2. JS 解析引擎与 ECMAScript 是什么关系?
    • 我们写的 JS 代码是一段程序,而 JS 引擎也同样是一段程序(如 V8 就是用 C/C++ 编写的),如何让程序去读懂程序呢?这就需要定义规则,这里的 ECMAScript 就定义了一套标准的规则,而标准的 JS 引擎就会根据规则去实现
    • 除了标准之外,当然也有不按标准来的,如 IE 的 JS 引擎,也就是为什么 JS 会有兼容性问题
    • 简单说,两者关系为:ECMAScript 定义了语言的标准,JS 引擎根据它来实现。

7、JS 解析引擎与浏览器又是什么关系?

  • 概括说,JS 引擎大多存于浏览器的内核中,是浏览器的组成成分之一
  • 浏览器当然还有其他的事情,比如解析页面,渲染页面,Cookie 管理,历史记录等等

JS执行过程

JS 执行过程可分为两步:(1)语言检查;(2)运行

语法检查

  1. 语法检查可分为:词法分析语法分析
  2. 词法分析:
    • 将字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元
    • 例如:会将 var a = 1; 分解成:vara=2;
    • 深入了解词法分析可读此文:JS 词法分析
  3. 语法分析:
    • 会将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树
    • 例如:上面的词法单元流var a = 2 ; 会被转为下方所示的AST
      AST
    • 深入语法分析和抽象树可读此文:JavaScript 语法解析、AST、V8、JIT

运行阶段

  1. 运行阶段可分为:预编译执行
  2. 预编译:将生成的 AST 复制到当前执行的上下文中,对当前 AST变量声明函数声明函数形参进行属性填充
  3. 执行:逐行读取并运行代码

引入:JS单线程

  1. 深入理解浏览器进程与 JS 线程及其运行机制,请参考:从浏览器多进程到 JS 单线程,JS 运行机制
  2. 区分 进程与线程:
    • 一个形象的比喻
      • 进程 是一个独立的工厂,每个工厂都有它独立的资源,工厂之间相互独立
      • 线程 是工厂里的工人,工厂内有一个或多个工人 — 工人之间共享工厂的空间等资源
    • 一个专业的描述
      • 进程 是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位,系统会给进程分配 内存)
      • 线程 是 CPU 调度的最小单位(线程是建立在进程的基础上的一次运行单位,一个进程可以有多个线程)
    • 不同进程之间也可以通信,不过代价很大
    • 现在通常说的 “单线程” 与 “多线程” 都指的是在一个进程内部的 “单” 和 “多”
    • 所以讨论前提还得属于一个进程才行。
  3. 浏览器是多进程的
    • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存),每打开一个 Tab 页就创建了一个 独立的浏览器进程
      浏览器进程
    • 浏览器的进程(仅列举主要进程):
      (1)Browser 进程:浏览器的主进程(负责协调,主控),此进程只有一个
      (2)第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
      (3)GPU 进程:最多一个,用于 3D 绘制等
      (4)浏览器渲染进程(浏览器内核,Renderer 进程,内部是多线程的):默认每个 Tab 页面一个进程,互不影响
    • 注意: 在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了。所以每一个 Tab 标签对应一个进程并不一定是绝对的
  4. JS 语言的一大特点就是:单线程;即:同一时间只能做一件事情
    • JavaC# 中的异步均是通过多线程实现的,没有循环队列一说,直接在子线程中完成相关的操作
  5. JS 的用途决定了 JS 必须是单线程的:作为浏览器脚本语言,JS 的主要用途是与用户互动,以及操作 DOM
    • 例如,假设有多个线程,一个线程在某 DOM 节点上添加内容,另一线程要删除这个节点,这时浏览器应该以哪个线程为准?
    • 所以,为了避免复杂性,从一诞生,JS 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
    • 为了利用多核 CPU 的计算能力,HTML5 提供了 Web Worker 标准,允许 JS 脚本创建多线程,但是子线程完全受主线程的控制,而且不能操作 DOM。所以,这个新的标准并没有改变 JS 单线程的本质

JS 异步运行机制

  1. 同步与异步如图:
    同步异步说明图
  2. 同步:
    • 若在函数返回结果时,调用者能够拿到预期的结果 (即函数计算的结果),那么这个函数就是同步的,例如:
  // 在函数返回时,获得了预期值,即2的平方根
  Math.sqrt(2);
  // 在函数返回时,获得了预期的效果,即在控制台上打印了'hello'
  console.log('hello');
  • 若函数是同步的,即使调用函数执行任务比较耗时,也会一直等到执行结束。如:
  function wait(){
      var time = (new Date()).getTime();    //获取当前的unix时间戳
      while((new Date()).getTime() - time > 5000){}
      console.log('5秒过去了');
  };
  wait();
  console.log('慢死了');
  • 上面代码中,函数 wait() 是一个耗时程序,持续5秒,在它执行的这漫长的5秒中,下面的 console log() 函数只能等待,这就是同步
  1. 异步:
    • 如果在函数返回的时候,调用者还不能得到预期结果,而是将来通过一定的手段得到(例如回调函数),这就是异步。例如 ajax 操作。
    • 如果函数是异步的,发出调用之后,马上返回,但是不会马上返回预期结果。调用者不必主动等待,当被调用者得到结果之后会通过回调函数主动通知调用者,例如:
  //读取文件
  fs.readFile('Hello.text', 'utf-8', function(err, data){
      console.log(data);
  });
  //网络请求
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = xxx;    //添加回调函数
  xhr.open('GET',url);
  xhr.send();
  • 上述中的读取文件函数 readFile() 和网络请求的发起函数 send() 都将执行耗时操作,虽然函数会立刻返回,但是不能立刻得到预期的结果,因为耗时操作交给其他线程执行,暂时不能获取预期结果。
  • 而上述过程中,通过回调函数 function(err, data){ console.log(data) }onreadystatechange,在耗时操作执行完成后会把相应的结果信息传递给回调函数,通知执行 JS 代码的线程进行回调

任务队列 与 事件循环

  1. 浏览器
    • 现在我们已经知道了:JS 是单线程的 和 异步的概念;
    • 现在问题来了,既然 JS 是单线程的,怎么还会异步呢,谁去执行异步的那些耗时操作呢?
    • 首先,我们得清楚,JavaScript 仅仅是一门语言 ,我们讨论单线程以及多线程都得结合具体的运行环境。既然 JS 通常是在浏览器中运行的,那么我们从浏览器角度思考一下:
      • 目前主流浏览器为:Chrome,Safari,FireFox,Opera(或许还应有 IE)。浏览器的内核是多线程的。
      • 对于 JS 的宿主环境 — 浏览器,浏览器的内核是多线程的;
      • 在内核控制下各线程相互配合以保持同步,浏览器通常由以下 常驻线程 组成:
1)渲染引擎线程:        负责页面的渲染
  (2JS引擎线程:         负责JS的解析和执行
  (3)定时触发器线程:      处理定时事件,比如setTimeout, setInterval
  (4)事件触发线程:        处理DOM事件
  (5)异步http请求线程:    处理http请求

注意:渲染引擎线程JS 引擎线程 是不能同时进行的,渲染引擎线程在执行任务是,JS引擎线程会被挂着,因为 JS 可以操作 DOM,与正在渲染中的 DOM 可能发生矛盾

  • 既然异步操作是靠浏览器中多个线程的合作完成的,那么 异步的回调函数 又是怎样执行的呢 ? 这还得从 任务队列事件循环 说起
  1. 任务队列 (Task Queue) :
    • JS 是单线程的,但单线程就意味着:所有任务需要排队,前一个任务结束,才会执行后一个任务。即使前一个任务耗时很长,后一个任务也必须一直等着。
    • 耗时的情况通常不是因为计算量过大使得 CPU 忙不过来,而是因为 I/O 设备很慢,比如 Ajax 操作从网络中读取数据,只能等到结果出来才能执行下一步
    • JS 语言的设计者意识到,这时主线程完全可以不管 I/O 设备,挂起处于等待中的任务,先运行排在后面的任务。等到 I/O 设备返回了结果,再回过头,把挂起的任务继续执行下去。
    • 于是,所有的任务都分成两种,即 同步任务异步任务
      • 同步任务:是在主线程排队执行的任务,前一个任务执行结束,才能执行后一个
      • 异步任务:是不进入主线程,而进入任务队列的任务,只有当任务队列告知主线程,某个任务可以执行了,该任务才会进入主线程执行
      • 具体来说,异步执行的运行机制如下:
1、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack).
2、除主线程外,还有任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件
3、执行栈中所有同步任务完成时,系统就会读取任务队列,看看里面有哪些事件
   这些事件又分别对应哪些异步任务,于是该任务结束等待,进入到执行栈执行
4、主线程不断重复上面的第3
  • 下面是主线程和任务队列的示意图:
    主线程和任务队列
    • 只要主线程空了,就去读取 “任务队列”,这就是 JS 的运行机制:一个主线程 + 一个任务队列,这个过程会不断重复。
    • 注:任务队列里面的事件,除了 I/O 设备的事件之外,还包括 用户产生的事件(比如鼠标点击、页面滚动…),只要指定过 回调函数 ,这些事件发生时,就会进入任务队列,等待主线程读取。
  • 任务队列是一个先进先出的数据结构,只要 “执行栈” 一清空,就会读取任务队列上的事件,但是由于存在后文存提到的 “定时器”功能,主线程首先要检查一下执行时间,某些事件,只有到了规定时间,才能返回主线程。
  1. 回调函数 (callback):
    • 所谓的回调函数,就是被主线程挂起来的代码
    • 前文提到:只要异步任务有了运行结果,就在任务队列中放置一个事件,这个事件,就是 注册异步任务时添加的回调函数
    • 异步任务必须执行回调函数,当主线程执行的异步任务,就是执行对应的回调函数。
  2. 事件循环(Event Loop):
    • 实际上,主线程只会做两件事情,就是从任务队列里面:读取任务、执行任务,反复如此,这种机制就叫做 事件循环机制,一次 读取 + 执行 就叫一次 循环
    • 事件循环用代码表示大概是这样的:
  while (true) {
      var message = queue.get();
      execute(message);
  }
  • 为了更好的理解 Event Loop ,请看下图:
    事件循环
  • 上图中,主线程运行的时候,产生 堆 (heap)栈 (stack),栈中的代码调用各种外部 API,它们在任务队列中加入各种事件(click,load,done),只要栈中的代码执行完毕,主线程就会去读取 “任务队列”,依次执行那些事件,所对应的回调函数。
  • 执行栈中的代码(同步任务),总是在读取任务队列(异步任务)之前执行,如下例:
  var req = new XMLHttpRequest();
  req.open('GET', url);    
  req.onload = function (){};    
  req.onerror = function (){};    
  req.send();
  • 上面的代码中,req.send() 方法是一个异步任务,是用过 Ajax 操作向服务器发送数据,这意味着只有当前脚本的所有代码都执行完,系统才会去读取任务列表,所以,它等价于:
  var req = new XMLHttpRequest();
  req.open('GET', url);
  req.send();
  req.onload = function (){};    
  req.onerror = function (){}; 
  • 也就是说,指定回调函数部分(onloadonerror),在 send() 方法的前面或者后面都无关紧要,因为它们就在执行栈中,系统会执行完它们之后才去读取任务队列

定时器

  1. 除了放置异步任务事件,任务队列还可以放置 定时事件,即指定某些代码在指定事件后执行,这就是定时器功能,即定时执行的代码
  2. 定时器功能主要由 setTimeout() 和 setInterval() 这两个函数完成,其内部机制完全一样,区别在于:前者指定的代码是一次性执行,后者则为反复执行
  3. JS 定时器参考博文:

————————————————
原文作者:Ozzie
转自链接:https://learnku.com/articles/38201

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值