浏览器中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队列[]。

 

posted @ 2022-03-06 08:37  SamWeb  阅读(1159)  评论(0编辑  收藏  举报