JS是在浏览器中运行的,浏览器为了运行JS, 必须要编译或解释JS,因为JS是高级语言,计算机不认识,必须把它编译或解释成机器语言,其次,在运行JS的过程,浏览器还要创建堆栈,因为程序是在栈中执行,执行过程中的创建的对象是在堆中。浏览器的JS引擎,比如V8,就是做这些事的。JS引擎负责编译或解释JS,并创建堆栈来运行JS。
比如,执行以下代码,
function multiply (x, y) {
return x * y } function printSquare (x) { const s = multiply(x, x) console.log(s) } printSquare(5)
程序初始化,栈为空;程序开始执行,调用printSquare(5),printSquare函数入栈并执行,它调用了multiply(x, x), multiply函数入栈并执行,执行完毕返回25,multiply函数弹栈,回到printSquare, 执行它后面的代码,也就是console.log , console.log 也是函数,进栈,执行完,弹栈,然后回到printSquare,,执行consoe.log 后面的代码,后面没有代码了,printSquare也就执行完了,弹栈,回到调用printSquare的地方,执行它后面的代码,它后面也没有代码,所有程序执行完毕,栈为空。整个调用栈的情况如下图所示,
在JS中,栈就是记录了程序执行到了什么地方,如果调用一个函数,这个函数就放到栈中,如果从函数中返回,就把该函数弹出栈。每一次的调用,都会创建stack frame。程序执行出错,也可以通过调用栈,追踪到程序在什么地方出错。
function foo() {
throw new Error('SessionStack will help you resolve crashes :)'); }
function bar() { foo(); }
function start() { bar(); }
start();
错误信息如下,start调用了bar,bar调用了foo,foo报错了。
如果函数一直调用呢? 那就栈溢出了。因为栈在内存中开辟的,内存不可能无限大,内存是有限的,栈也就是有限。递归处理不好,容易栈溢出。
function f () {
return f() } f()
函数的调用栈如下
通过上面的例子,你会发现,只有一个栈在执行程序,这就是JS的单线程,JS引擎中只有一个调用栈,一次只能处理一件事情。调用栈并不属于JS,它是JS引擎的一部分。
如果仅仅是运行JS,作用也不大,因为JS本身没有输入或输出等与外界交互的能力,因此浏览器除了包含JS引擎,还提供了JS与外界交互的能力。这些能力是通过API提供的,比如document, fetch等等,把它们注入到JS的全局作用域中,在JS运行时,可以直接使用它们。这些API统称为 web API,或外部API,因为它们也不属于JS。运行JS并能和外部交互,这很好,但也会带来一个问题, 比如,fetch() 向服务器请求数据,可能要很长时间,JS是单线程也就意味着,要等到它执行结束,才能执行它后面的代码,如果一直等,那后面的代码就不用执行了,浏览器也就卡死了。如果某件事情执行时间过长,怎么办?异步处理。为了支持异步,浏览器提供了事件循环和事件队列,以及向事件队列中插入事件的功能。因此JS的运行时,也就是浏览器,要包含以下几部分
假设执行如下代码
console.log('js') setTimeout(function cb() { console.log(' awesome!') }, 5000) console.log(' is')
console.log(‘js’),函数入调用栈并执行,控制台输出js, 函数执行完毕,弹栈,
setTimeout()执行,这是一个Web API,是浏览器内部实现的,调用Web API,只是告诉浏览器帮我们做事情,setTimeout是告诉浏览器5s之后执行cb函数,
告诉完浏览器,setTimeout也就执行完了,弹栈,此时浏览器设置定时器,并开始倒数计时,
console.log('is')执行,进栈,出栈,浏览器控制台输出is.
5s 过后,计时器完成计时,浏览器把回调函数cb放到了事件队列中
此时,事件循环发现事件队列中有一个事件,它就会检查调用栈是不是空,如果调用栈为空,它就会把事件拿出来,放到调有栈中。
回调函数cb执行,console.log(‘awesome’) 进栈,出栈,控制台输出awesome,回调函数执行完了, 出栈。
整个程序执行完毕。在整个程序的执行过程中,异步的实现或异步代码的执行,是浏览器帮我们安排的,浏览器安排异步代码,插入到事件队列中,事件循环则调用JS引擎,从事件队列中取出要执行的代码发给它。JS引擎只不过是一个按需执行的环境来执行JS代码。从事件队列中取出事件到调用栈中执行,也称为一个tick.
到了ES6,增加了Promise,情况有所变化。Promise异步的处理方式和传统的回调函数处理方式不一样,promise中注册的回调函数称为Job或Micortask,所以JS从概念上定义了两个队列,Microtask或Job队列和Macrotask队列,而不再是一个队列。Promise完成后的回调,是放到Microtask或Job队列中,传统回调函数放到Macrotask队列,当然,它们不仅仅处理这些。因为有了两个队列,tick的定义也要改一下,从Macrotask队列中取出事件并执行,称为一个tick。主程序执行完毕,先检查Micortask队列中有没有micortask或job(回调函数),如果有,就会执行该micortask,执行完毕后,还是检查Micortask队列中有没有事microtask,直到Micortask队列中所有microtask执行完毕,它才执行Macrotask队列中的macrotask,从中取出一个开始执行(tick),如果在一个tick的执行过程中,有一个Promise完成了,这个Prmise注册的回调函数(microtask),并不是插入到整个Macrotask队列的后面,而是插入到当前tick后面的Micortask队列中,Micotask队列就是附在事件循环中每一个tick后面的队列。当tick执行完毕,从它后面的Microtask队列中取出microtask,进行执行,由于事件中还可能有promise完成,promise注册的回调函数,又会插入到当前tick后的Microtask中,形成一个Microtask队列,所以要等到后面的Microtask队列中所有microtask执行完毕,再从Macrotask取出一个事件执行。
这里要注意,由于Microtask可能执行其它Microtask,Microtask队列可以一直增加下去,如要是这样的话,事件循环就不能从当前tick中跳出,后面的Macrotask就无法执行。为了阻止这种情况发生,浏览器内置了保护机制,一个tick最多执行1000个microtasks,执行完成后,执行下一个macrotask.
console.log('script start') const interval = setInterval(() => { console.log('setInterval') }, 0) setTimeout(() => { console.log('setTimeout 1') Promise.resolve() .then(() => console.log('promise 3')) .then(() => console.log('promise 4')) .then(() => { setTimeout(() => { console.log('setTimeout 2') Promise.resolve().then(() => console.log('promise 5')) .then(() => console.log('promise 6')) .then(() => clearInterval(interval)) }, 0) }) }, 0) Promise.resolve() .then(() => console.log('promise 1')) .then(() => console.log('promise 2'))
程序执行,也可以称为第一个tick。console.log(), 进栈,执行,出栈,控制台输出script start。setInterval进栈,告诉浏览器每隔0s,控制台输出setInterval,浏览器设置定时器,setInterval执行完毕,出栈。setTimeout进栈,告诉浏览器0s后,执行一段代码,浏览器设置定时器,setTimeout执行完毕,出栈. Promise.resovle执行,两个then回调函数放入到microtasks队列中。程序执行完毕,第一个tick执行完毕,此时要检查当前tick后面的microtasks队列有没有task。有,就是Promise.resovle的两个回调,依次执行,控制台输出promise 1 和 promise 2。0s肯定过了,浏览器把setInterval和setTimeout放入到macrotask队列中。
每二个tick,从macrotask队列中取出settInterval 的回调函数,控制台输出settInterval ,它没有产生microtask,也就没有microtasks队列,0s过了,浏览器又到macrotask队列中放入settInterval 。此时macrotask队列中 [setTimeout, settInterval]
第三个tick,setTimeout注册的回调函数执行,控制台输出 setTimeout 1,Promise.resovle执行,三个then放入到microtasks,microtasks是放到当前tick后,tick执行完毕,检查它后面的microtasks队列,有。依次执行,控制台输出Promise 3和 Promise 4,另外一个setTimeout放到macrotask队列中,称它为setTimeout2。此时,macrotask队列[settInterval, setTimeout ]
第四个tick,从macrotask队列中取出settInterval 的回调函数,控制台输出settInterval ,它没有产生microtask,也就没有microtasks队列,0s过了,浏览器又到macrotask队列中放入settInterval 。此时macrotask队列中 [setTimeout2, settInterval]
第五个tick,setTimeout2注册的回调函数执行,控制台输出 setTimeout 2,Promise.resovle执行,三个then放入到microtasks,microtasks是放到当前tick后,tick执行完毕,检查它后面的microtasks队列,有。依次执行,控制台输出Promise 5和 Promise 6,同时清除掉了setInterval,此时,macrotask队列[]。