乐趣区

关于javascript:浏览器与JS运行机制

一、JavaScript 预解析

JavaScript 代码运行分为两个阶段:

  • (1) 预解析

所有函数定义提前,函数体晋升 (当然不包含如 var box = function() {})
形参申明并赋值
变量申明(不赋值)

  • (2) 执行

依照 js 运行机制从,从上到下执行

二、过程与线程

  • 过程是 cpu 资源分配的最小单位(是可能领有资源和独立运行的最小单位)
  • 线程是 cpu 调度的最小单位(线程是建设在过程的根底上的一次程序运行单位,一个过程能够有多个线程

举例:此处有多个工厂,每个工厂有 1 个或多个工人。此时 工厂就好比过程 ,有独自专属本人的工厂资源; 工人就好比是线程,多个工人在工厂中写作工作。工厂的空间是工人们共享的,这象征一个过程的内存空间是共享的,每个线程都能够共享内存。并且每个工厂之间互相独立存在。

  • 应用程序必须运行在某个过程的某个线程上
  • 一个过程至多有一个运行的线程:主线程,过程启动后主动创立

三、浏览器过程

浏览器内核是指反对浏览器运行的最外围的局部,分为渲染引擎和 JS 引擎。当初 JS 引擎比拟独立,内核更加偏向于说渲染引擎

(1)浏览器内核分类

  • Chrome、Safari:Webkit (Bink)
  • Firefox:Gecko
  • IE:Trident
  • 360、搜狗等国内浏览器:Trident+Webkit

(2)浏览器过程

  • 浏览器是多过程的
  • 浏览器之所以能运行,是因为零碎给它的过程调配了资源(cpu、内存)
  • 简略来说,每打一个 Tab 页,就相当于创立了一个独立的浏览器过程

浏览器过程的组成:

  • Browser 过程

浏览器的主过程,负责协调、主控,只有一个。
负责内容:浏览器页面显示;与用户交互(后退、后退等);网络资源的治理、下载;各个页面的治理,创立和销毁其余过程等

  • 第三方插件过程
    每种类型的插件对应一个过程,仅当插件应用时才创立
  • GPU 过程
    最多一个,用于 3D 绘制等
  • 浏览器渲染过程 (浏览器内核,Renderer 过程,外部是多线程的)
    默认 每个 Tab 页面一个过程,互不影响
    负责内容:页面渲染;脚本执行;事件处理

浏览器是多线程的劣势:防止单个 Tab 页解体或单个插件解体影响其余整个浏览器,能够充沛多核优势,方便使用沙盒模型隔离插件等过程,进步浏览器的稳定性。毛病是,内存和 cpu 耗费会更大,有点空间换工夫的意思。

Borwser 过程与浏览器内核(Renderer 过程)的通信过程:

  • Browser 过程收到用户申请,首先须要获取页面内容(譬如通过网络下载资源),随后将该工作通过 RendererHost 接口传递给 Render 过程

    • 渲染线程接管申请,加载网页并渲染网页,这其中可能须要 Browser 过程获取资源和 GPU 过程来帮忙渲染
    • 当然可能会有 JS 线程操作 DOM(可能会造成回流并重绘)
    • 最初 Renderer 过程将后果传递给 Browser 过程
  • Renderer 过程的 Renderer 接口收到音讯,简略解释后,交给渲染线程,而后开始渲染
  • Browser 过程收到后果并将后果绘制进去

四、浏览器渲染过程

对于前端操作来说,最重要的是渲染过程,并且 渲染过程也是多线程的
渲染过程蕴含哪些线程?

  • GUI 渲染线程

    • 负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树和 RenderObject 树,布局和绘制等
    • 负责重绘(Repaint)和回流(Reflow)
    • GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 线程会保留在一个队列里等 js 引擎闲暇时执行。
  • JS 引擎线程

    • 负责解决 JavaScript 脚本,执行代码
  • 事件触发线程

    • 次要负责将筹备好的事件交给 JS 引擎线程执行

比方 setTimeout 定时器计数完结、ajax 等异步申请胜利并触发回调函数、用户触发点击事件等,该线程会将整装待发的事件退出到工作队列的队尾,期待 JS 引擎线程的执行。

  • 定时器触发线程

    • 次要负责异步定时器一类的函数解决,如 setTimeout、setInterval

主线程顺次执行代码时,遇到定时器,会将定时器交给该线程解决。当计数结束后,事件触发线程会将计数结束的事件退出到工作队列的尾部,期待 JS 引擎线程执行。

  • 异步 HTTP 申请线程

    • 负责执行异步申请一类的函数,如:ajax、axios、promise 等

主线程顺次执行代码是,遇到异步申请,会将异步申请函数交给该线程解决。当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数退出到工作队列的尾部,期待 JS 引擎线程执行。

五、事件循环

1 浏览器中的事件循环

JavaScript 语言是单线程的,意思是同一时间只能做一件事。起初为了无效利用多核 CPU 的计算能力,HTML5 提出 Web Server 规范,容许 JavaScript 脚本创立多个线程,然而子线程齐全受主线程管制,并且子线程不能操作 DOM。所以新规范并没有扭转 JavaScript 单线程的实质。

简略形容 JS 的执行机制:

  1. 首先判断 JS 是同步工作还是异步工作,同步工作就进入主线程执行,异步工作进入 event table
  2. 异步工作在 event table 中注册函数,异步函数又分为宏工作 (macro-task) 和微工作(micro-task),当满足触发条件后,宏工作被推入宏工作队列(macro-task queue),微工作被推入微工作队列(micro-task queue)
  3. 同步工作在主线程中始终执行,直到同步工作执行结束,主线程闲暇闲暇时,才去微工作队列 (micro-task queue) 中查看是否有可执行的异步工作,如果有就推入主线程中执行
  4. 直到全副微工作顺次执行结束后,主线程闲暇,再去宏工作队列(macro-task queue) 查看是否有可执行的异步工作,如果有就推入主线程中执行

以上四步循环执行,就是 event loop。

一个残缺的 Event Loop 过程:

① 所有的同步工作都在主线程上执行,造成一个执行栈 (exection context stack),咱们能够认为执行栈是一个函数调用的栈构造,遵循先进后出的准则。除了主线程的执行栈,还存在一个工作队列(task queue),工作队列分为宏工作队列(macro-task queue) 和微工作队列 (micro-task queue)。
一开始执行栈为空,宏工作队列 (macro-task queue) 里只有一个 script 代码 (整体代码),微工作队列(micro-task queue) 队列为空。
② 宏工作队列 (macro-task queue) 中的全局上下文 (script 标签) 会被推入执行栈,同步代码执行。在执行的过程中会判断是同步工作还是异步工作,同步工作顺次执行,异步工作会通过对一些接口的调用而产生新的 macro-task 和 micro-task(只有异步工作有了运行后果,就会在对应的工作队列中搁置一个事件,期待调用)。同步代码执行完了,script 脚本会行和出队的过程。
③ 上一步出队的是一个 macro-task,这一步要解决的是 micro-task。须要留神的是,当 macro-task 出队时,工作是一个一个执行的,而 micro-task 出队时,工作是一队一队执行的。因而,咱们解决 micro-task 这一步,会一一执行队列中的工作并把它出队,直到队列被清空。
④ 执行渲染操作,更新页面
⑤ 查看是否存在 Web worker 工作,如果有,则对其进行进行解决
⑥ 上述过程反复循环,直到两个队列都清空

宏工作队列能够有多个,而微工作队列只有一个:

  • 常见的 macro-task:setTimeout、setInterval、script(整套代码)、I/ O 操作、UI 渲染等;
  • 常见的 micro-task:new Promise().then(回调)、process.nextTick、MutationObserver(HTML5 新个性)等

2 Node 中的事件循环

Node 中的事件循环与浏览器的是齐全不同不同的货色。Node 采纳 V8 作为 js 的解析引擎,而 I / O 解决方面应用本人设计的 libuv。
libuv 是一个基于事件驱动的跨平台形象层,封装了不同操作系统的一些底层个性,对外提供 API,事件循环也是在它外面实现:

NodeJS 运行机制如下:

  • V8 引擎解析 JavaScript 脚本
  • 解析后的代码调用 Node API
  • libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,造成 Event Loop(事件循环),以异步的形式将工作的执行后果返回给 V8 引擎
  • V8 引擎再将后果返回给用户

libuv 引擎的事件循环分为 6 个阶段:

  1. timers 阶段:执行 timers(setTimeout 和 setInterval)的回调
  2. I/O callbacks 阶段:解决上一轮循环多数未执行的的 I / O 回调
  3. idel、prepare 阶段:仅 Node 外部应用
  4. poll 阶段:获取新的 I / O 事件,执行 I / O 回调
  5. check 阶段:执行 setImmediate()回调
  6. close callbacks 阶段:执行 socket 的 close 事件回调

绝大部分的异步工作都在 timers、poll、check 这个 3 个阶段解决

NodeJS 执行环境下的非凡状况:
1)setTimeout 和 setImmediate
二者十分类似,区别次要在于调用机会不同:

  • setImmediate 设计在 poll 阶段实现时执行,即 check 阶段
  • setTimeout 设计在 poll 阶段为闲暇时,且设定阶段达到后执行,但它在 timers 阶段执行
setTimeout(function timeout () {console.log('timeout');
},0);

setImmediate(function immediate () {console.log('immediate');
});

对于以上代码,setTimeout 可能执行在前,也可能执行在后;
取决于 setImmediate 的筹备工夫;因为当 setTimeout 指定工夫小于 4ms,则减少到 4ms(4ms 是 H5de 新规范,2010 年以前的浏览器是 10ms)

然而如果二者在 I /O callback 外部回调时,总是先执行 setImmediate,后执行 setTimeout:

const fs = require('fs')
fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');
    }, 0)
    setImmediate(() => {console.log('immediate')
    })
})
// immediate
// timeout
// 因为这两个代码都写在 I / O 回调中,I/ O 回调是在 poll 阶段执行,当回调执行结束后队列清空,发现 SetImmediate 回调,所以立刻跳转到 check 阶段执行回调。});

2)process.nextTick

process.nextTick 是独立于 Event Loop 之外的,它有一个本人的队列,会优先于其余 micro-task 队列执行:

setTimeout(() => {console.log('timer1')
    Promise.resolve().then(function() {console.log('promise1')
    })
}, 0)

process.nextTick(() => {console.log('nextTick')
    process.nextTick(() => {console.log('nextTick')
        process.nextTick(() => {console.log('nextTick')
            process.nextTick(() => {console.log('nextTick')
            })
           })
     })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

3 浏览器与 Node 的 Event Loop 差别

浏览器环境下,micro-task 的工作队列是每个 macro-task 执行之后执行;
Node 环境下,在 node10 及其以前版本,micro-task 会在事件循环的各个阶段之间执行,也就是一个阶段执行结束,就会执行 micro-task 队列的工作
Node 在 node11 版本开始,Event Loop 的运行原来产生了变动,一旦一个阶段里的宏工作执行完,就会立刻执行微工作队列,这一点与浏览器始终。

4 Web worker

因为 JS 是单线程,当遇到计算密集型或高提早的工作,用户界面可能会短暂“解冻”,不能做其余操作。
于是 HTML5 提出 Web Worker,它容许 JavaScript 发明多线程环境,容许主线程创立 Worker 线程,将一些任务分配给后者。主线程运行的同时,Worker 线程在后盾运行,两者互不烦扰,等到 Worker 实现计算工作,在把后果返回给主线程。
Web Worker 的长处是能够承当一些密集型或高提早工作,使主线程晦涩,不被阻塞或拖慢。
毛病:

  • 不能跨域加载 JS
  • Worker 外部代码不能拜访 DOM
  • 不是所有浏览器都反对这个新个性

Web Worker 应用办法:

  • 主线程调用 Worker 线程:

    1. 主线程通过 new Worker()调用 Worker 构造函数,新建一个 Worker 线程
    2. 主线程调用 worker.postMessage()办法,向 Worker 发消息
    3. 主线程通过 worker.onmessage 指定监听函数,接管子线程发回来的音讯
// 主线程:var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
    var number = input.value
    //1、创立一个 Worker 对象
    var worker = new Worker('worker.js')
    // 3、绑定接管音讯的监听
    worker.onmessage = function (event) {console.log('主线程接管分线程返回的数据:'+event.data)
        alert(event.data)
    }
    // 2、向分线程发送音讯
    worker.postMessage(number)
    console.log('主线程向分线程发送数据:'+number)
}
console.log(this) // window

Worker 线程响应:

  1. Worker 外部通过 onmseeage()监听事件
  2. 通过 postMessage(data)办法向主线程发送数据
//worker.js 文件
function fibonacci(n) {return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  // 递归调用
}
console.log(this)//[object DedicatedWorkerGlobalScope]
this.onmessage = function (event) {
    var number = event.data
    console.log('分线程接管到主线程发送的数据:'+number)
    // 计算
    var result = fibonacci(number)
    postMessage(result)
    console.log('分线程向主线程返回数据:'+result)
    // alert(result)  alert 是 window 的办法, 在分线程不能调用
    // 分线程中的全局对象不再是 window, 所以在分线程中不可能更新界面
}

参考资料:
https://github.com/ljianshu/B…
https://juejin.im/post/5bb054…
深入浅出 JavaScript 运行机制
10 分钟了解 JS 引擎的执行机制
浏览器组成
全面梳理 JS 引擎的运行机制

退出移动版