浏览器中JS的执行
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队列[]。
in a single iteration, the event loop first checks
the macrotask queue, and if there’s a macrotask waiting to be executed, starts its exe-
cution. Only after the task is fully processed (or if there were no tasks in the queue),
the event loop moves onto processing the microtask queue. If there’s a task waiting in
that queue, the event loop takes it and executes it to completion. This is performed
for all microtasks in the queue. Note the difference between handling the macrotask
and microtask queues: In a single loop iteration, one macrotask at most is processed
(others are left waiting in the queue), whereas all microtasks are processed.
When the microtask queue is finally empty, the event loop checks whether a UI
render update is required, and if it is, the UI is re-rendered. This ends the current iter-
ation of the event loop, which goes back to the beginning and checks the macrotask
queue again.
几个细节:
Both task queues are placed outside the event loop, to indicate that the act of adding tasks to their matching queues happens outside the event loop the acts of detecting and adding tasks are done separately from the event loop.
All microtasks should be executed before the next rendering, because their goal is to update the application state before rendering occurs.
The browser usually tries to render the page 60 times per second, to achieve 60 frames
per second (60 fps), a frame rate that’s often considered ideal for smooth
motion, such as animations—meaning, the browser tries to render a frame every 16 ms.
Notice how the “Update rendering” action, shown in figure 13.1, happens
inside the event loop, because the page content shouldn’t be modified by
another task while the page is being rendered. This all means that, if we want to
achieve smooth-running applications, we don’t have much time to process tasks
in a single event-loop iteration. A single task and all microtasks generated by that task
should ideally complete within 16 ms.
Note
that it’s important that the event detection and addition to the task queue happens
outside the event loop; the tasks are
added to the task queue even while main-
line JavaScript code is being executed.
当使用setTimeInteval的时候,它每隔一段时间就会向task queue里面添加事件或(事件处理函数),但是,如果前一次添加的事性,没有处理,还是在task队列中,这一次添加的事件就会被丢弃。
The browser won’t queue up more than one instance of a specific interval handler.