How JavaScript Work
一、chrome浏览器组成
JavaScript的运行时模型
- JavaScript引擎——V8引擎
- WebAPIs——由宿主环境提供的额外API不属于引擎的原生部分
- EventLoop & CallbackQueue 事件循环和回调队列——属于宿主环境提供的机制,用于辅助引擎工作
二、JavaScript 引擎
如图V8引擎主要由两部分构成
- 内存堆(Memory Heap)—— 用于分配内存的位置
- 调用栈(Call Stack)—— 用于执行代码的位置
(一)Call Stack 调用栈
调用栈是解释器(如浏览器中 JavaScript 解释器)追踪函数执行流的一种机制
也称执行栈,拥有后进先出(LIFO)的数据结构,被用来存储代码运行时创建的所有执行上下文
JvaScript 是一种单线程编程语言,这意味着它只有一个 Call Stack 。因此,它一次仅能做一件事
当V8引擎遇到你编写的代码时,会创建全局的执行上下文并压入当前调用栈中,每当引擎遇到一个函数调用,它会为该函数创建一个新的函数执行上下文并压入栈的顶部
引擎会执行位于栈顶的函数,正在调用栈中执行的函数如果调用了其他函数,新函数也将添加到调用栈顶,立即执行
当前函数执行完毕后,解释器将该函数执行上下文从栈中弹出,继续执行当前执行环境下的剩余的代码
当分配的调用栈空间被占满时,会引发“堆栈溢出”错误
正常运行
function first() {
console.log('Inside first function');
second();
}
function second() {
console.log('Inside second function');
}
first();
追踪异常
Call Stack 的每个入口被称为 Stack Frame(栈帧)
这正是在抛出异常时如何构建 stack trace 的方法——基本上是在异常发生时的 Call Stack 的状态
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
堆栈溢出
在某些情况下,调用堆栈中函数调用的数量超出了调用堆栈的实际大小,浏览器将抛出 错误终止
递归可能导致该错误
function foo() {
foo();
}
foo();
三、WebAPIs
由于 JavaScript 只有一个调用堆栈,理论上当某段代码运行变慢(比如网络请求、下载图片)时就会发生阻塞,导致浏览器不能执行后面的简单操作
但是,实际上可以看到即使进行网络请求等操作后续代码依然执行。
怎么处理的,最简单的方式提供——异步回调
(一)Async Callbacks & Call Stack
console.log('hi')
setTimeout( function cb1() {
console.log('cb1')
}, 5000)
console.log('bye')
神奇的,setTimeout
的异步回调没有在执行定时器时立即执行,而是执行下一个函数,后执行异步回调函数
为什么呢,setTimeout
并不是JavaScript引擎所拥有的API,而是浏览器提供的WebAPI
考虑到定时器为web API的部分,我们对上面的代码进行分析
-
调用
console.log('HI')
进入到调用栈中,控制台打印Hi
-
执行定时器,加入到调用栈中
-
在WebAPIs中创建一个Timer,并将定时器的内容移过去
-
定时器部分执行完毕,弹出调用栈,此时定时器内的内容被保存在WebAPIs环境当中
-
调用
console.log('Bye')
进入到调用栈中,控制台打印Bye
-
console.log('Bye')
弹出调用栈 -
等待WebAPIs中的timer执行,将
cb1
加入到回调队列中 -
通过事件循环将回调队列中的
cb1
重新压入到调用栈中 -
cb1
内调用了console.log('cb1')
所以也要压入到调用栈中,控制台打印cb1
-
弹出
console.log('cb1')
-
弹出
cb1
四、EventLoop & CallbackQueue
我们由上面知道,任务分为同步任务和异步任务。比如我们打开网站时,网页的渲染过程就是一大堆同步任务,页面骨架和页面元素的渲染等;而加载图片音乐之类占用资源大耗时久的任务,就是异步任务
为什么javascript是一门单线程语言,可以进行异步任务。原来javascript的多线程都是用单线程模拟出来的,单线程这一核心仍未改变
分析导图可以看出
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue
- 主线程内的任务执行完毕为空(存在monitoring process进程),会去Event Queue读取对应的函数,进入主线程执行
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- 首先ajax发送异步的网络请求,进入Event Table,注册回调函数
success
- 执行
console.log()
,主线程任务为空 - 网络请求完成,回调函数
success
进入Event Queue - 主线程从Event Queue读取回调函数
success
并执行
(一)macro-task & micro-task
除了广义的同步任务和异步任务,我们对任务有更精细的定义
- macro-task(宏任务):整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue
事件循环的顺序
- 先执行所有同步任务
- 遇到的异步任务分发到对应Event Queue
- 主线程任务执行完毕
- 先执行微任务Event Queue :
- 再执行宏任务Event Queue
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
// promise console then setTimeout
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
//1 7 6 8 2 4 3 5 9 11 10 12