写点什么

Javascript 执行机制 - 任务队列

用户头像
Sakura
关注
发布于: 2021 年 04 月 08 日

总所周知,javascript 是一门依赖宿主环境的单线程的弱脚本语言,这意味着什么?

  • javascript 的运行环境一般都由宿主环境(如浏览器、Node、Ringo 等)和执行环境(Javascript 引擎 V8,JavaScript Core 等)共同构成;

  • 弱类型定义语言:数据类型可以被忽略的语言。例如计算时会在不同类型之间进行隐式转换;

  • 在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行;

本文主要讲的就是第三点,从中引出下一个问题

单线程的设计原因?

Javascript 当初诞生的目的其实就是因为当年网络技术十分低效,如表单验证等个几十秒才能得到反馈的用户体验十分糟糕,为了给浏览器做些简单处理以前由服务器端负责的一些表单验证。被 Netscape 公司指派花了十天就负责设计出一门新语言的 Javascript 之父就是 Brendan Eich。尽管他并不喜欢自己设计的这作品,就有了大家都听过的一句话:

"与其说我爱 Javascript,不如说我恨它。它是 C 语言和 Self 语言一夜情的产物。十八世纪英国文学家约翰逊博士说得好:'它的优秀之处并非原创,它的原创之处并不优秀。'(the part that is good is not original, and the part that is original is not good.)"

作为浏览器脚本语言而诞生的 JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只需要是单线程就足以解决目的,否则会带来很复杂的同步问题。但是没想到的是之后的网络越发的发达,这些年来的浏览器大战为了争夺地盘,反而让 Javascript 被赋予了更多的职责跟可能性,今时今日的 Javascript 必须想方设法把自身的潜力激发出来,而单线程的弱点就被无限放大了,因为在阻塞任务的过程中不一定是因为 CPU 被占用了,而可能是因为 I/O 太慢(如 AJAX 请求,定时器任务,Dom 事件交互等并不消耗 CPU 的等待造成资源时间浪费)。

浏览器中 Javascript 执行线程

我们一直都在说 Javascript 是单线程,但浏览器是多线程的,在内核控制下互相配合以保持同步,主要的常驻线程有:

  • GUI 渲染线程:负责渲染界面,解析 HTML,CSS,构建 DOM 和 Render 树布局绘制等。如果过程中遇到 JS 引擎执行会被挂起线程,GUI 更新保存在一个队列中等待 JS 引擎空闲才执行;

  • JS 引擎线程:负责解析运行 Javascript;执行时间过程会导致页面渲染加载阻塞;

  • 事件触发线程,浏览器用以控制事件循环。当 JS 引擎执行过程中触发的事件(如点击,请求等)会将对应任务添加到事件线程中,而当对应的事件符合触发条件被触发时会把对应任务添加到处理队列的尾部等到 JS 引擎空闲时处理;

  • 定时器触发线程:因为 JS 引擎是单线程容易阻塞,所以需要有单独线程为 setTimeout 和 setInterval 计时并触发,同样是符合触发条件(记时完毕)被触发时会把对应任务添加到处理队列的尾部等到 JS 引擎空闲时处理;W3C 标准规定时间间隔低于 4ms 被算为 4ms。

  • 异步 http 请求线程:XMLHttpRequest 在连接后浏览器新开线程去请求,检测到状态变化如果有设置回调函数会产生状态变更事件,然后把对应任务添加到处理队列的尾部等到 JS 引擎空闲时处理;

好像铺垫的有点多,往外偏了,接下来往回拉一点谈谈这些怎么运行的。

什么是堆(heap)和栈(stack)?

function addOne(n) {  var x = n + 1;  return addTwo(x);}
function addTwo(n) { return n + 2;}
console.log(addOne(1)) //4;
复制代码

以这个例子做说明。当调用 addOne 时创建一个包含 addOne 入参和局部变量的帧并添加进去 stack,当调用到 addTwo 时也同样创建一个包含 addTwo 入参和局部变量的帧并添加进去在首部,执行完 addTwo 函数并返回时 addTwo 帧被移出 stack,addOne 执行完后 addOne 帧也被移除。原理:当执行方法时都会建立自己的内存栈,在这个方法内定义的入参变量都会保存在栈内存里,执行结束后该方法的内存栈也将自然销毁了。

一般来说,程序会划分有两种分配内存的空间 -- 堆(heap)栈(stack)

因为栈只能存放下确定大小的简单数据,所以像变量(其实也就是一个记录了指向复杂结构数据的地址指向,所以变量也是保存在栈里的)和基本类型 Undefined、Null、Boolean、Number 和 String 等是按值传递的都会保存在栈里,随着方法执行完毕而被销毁。堆负责存放复杂结构的对象,数组,函数等创建成本较高并且可重用数据,即使方法执行完也不会被销毁,直到系统的垃圾回收机制核实了没有任何引用才会回收。其实这只是栈的含义之一

有时候我们代码有问题导致栈堆溢出原因大概是这种情况:

好了,现在再看回上图,除了 heap 和 stack 之外还有一个。。。

什么是 Queue(任务队列)?

Javascript 里分两种队列:

  • 宏任务队列(macro tasks):事件循环中可以有多个 macro tasks,每次循环只会提取一个,包括 script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering 等.

  • 微任务队列(micro tasks):事件循环中只有一个并且有优先级区别 micro tasks,每次循环会提取多次直至队列清空,包括 process.nextTick, Promise, Object.observer, MutationObserver 等.

console.log('log start!');setTimeout(function () {  console.log('setTimeout300');}, 300)
Promise.resolve().then(function () { console.log('promise resolve');}).then(function () { console.log('promise resolve then');})
new Promise(function (resolve, reject) { console.log('promise pending'); resolve();}).then(function () { console.log('promise pending then');})
setTimeout(function () { console.log('setTimeout0'); Promise.resolve().then(function () { console.log('promise3 in setTimeout'); })}, 0)console.log('log end!');
// log start!// promise pending// log end!// promise resolve// promise pending then// promise resolve then// setTimeout0// promise3 in setTimeout// setTimeout300
复制代码

例子过程,具体分析下面再说。第一次执行事件打印:log start!, promise pending, log end!, promise resolve,promise pending then,promise resolve then;第二次执行事件打印:setTimeout0,promise3 in setTimeout;第三次执行事件打印:setTimeout300;

用户头像

Sakura

关注

还未添加个人签名 2020.09.22 加入

还未添加个人简介

评论

发布
暂无评论
Javascript执行机制-任务队列