一、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();

How JavaScript Works how javascript works购买_Stack

堆栈溢出

在某些情况下,调用堆栈中函数调用的数量超出了调用堆栈的实际大小,浏览器将抛出 错误终止


How JavaScript Works how javascript works购买_异步任务_02

递归可能导致该错误

function foo() {
    foo();
}
	foo();

How JavaScript Works how javascript works购买_Stack_03

三、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的部分,我们对上面的代码进行分析

  1. 调用console.log('HI') 进入到调用栈中,控制台打印Hi
  2. 执行定时器,加入到调用栈中
  3. 在WebAPIs中创建一个Timer,并将定时器的内容移过去
  4. 定时器部分执行完毕,弹出调用栈,此时定时器内的内容被保存在WebAPIs环境当中
  5. 调用console.log('Bye') 进入到调用栈中,控制台打印Bye
  6. console.log('Bye')弹出调用栈
  7. 等待WebAPIs中的timer执行,将cb1加入到回调队列中
  8. 通过事件循环将回调队列中的cb1重新压入到调用栈中
  9. cb1内调用了console.log('cb1')所以也要压入到调用栈中,控制台打印cb1
  10. 弹出console.log('cb1')
  11. 弹出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

事件循环的顺序

  1. 先执行所有同步任务
  2. 遇到的异步任务分发到对应Event Queue
  3. 主线程任务执行完毕
  4. 先执行微任务Event Queue :
  5. 再执行宏任务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