js浏览器下并发模型和事件循环

  • js是单线程语言,意味着同一时间只能处理一个函数。所以每一个消息任务被完整执行完之后,才会执行下一个消息。

执行到完这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web应用就无法处理用户的交互,例如点击或滚动。浏览器用“程序需要过长时间运行”的对话框来缓解这个问题。

  • 从概念来看,根据任务的执行时间的长短,js中把各种任务分成两大类:同步和异步。
    同步任务:在它没有完成之前,无法执行其他任务。
    异步任务:在执行过程中,则会被先挂起,先执行其他任务,等到程序空闲了,再来执行异步任务的剩余内容。
  • 单线程的JS是通过多任务队列和事件循环(event loop)机制异步任务不阻塞其他任务。

任务:js中一些较为费时的操作被定义为任务

  • 微任务(microtasks):process.nextTick、promise.then、Object.observe、MutationObserver
  • 宏任务(macrotasks,也称作task):setTimeout、setInterval、setImmediate、I/O、UI渲染、script标签中的整体代码、postMessage、MessageChannel
    微任务,宏任务

任务队列:存放task集合的数据结构

  • 任务队列(task queue or macrotask queue ):其实是Sets结构(列表中的每个元素都不可重复),因为事件循环模型的第一步任务是抓取第一个可运行的task而不是直接出队第一个任务。
    宏任务可以有多个队列。

  • 微任务队列(microtask queue)不属于任务队列。
    微任务只有一个队列。

显然,微任务队列中均为微任务,宏任务同理。

JS执行栈

  • 可以简单理解为js代码用来执行的数据结构

当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。
这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。
而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。
如果当前执行的是一个方法Function,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。
当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。
这个过程反复进行,直到执行栈中的代码全部执行完毕。
如果在执行过程中遇到异步代码,会先将其挂起,继续处理同步代码。而异步事件回调的处理,则是依靠事件循环和任务列队机制来调度。
JS执行栈

JS主线程

主线程是用来执行代码的,会不断的去取执行栈中的代码执行,执行完毕之后,代码从主线程中出栈。

事件循环(event loop)具体过程

下列代码中,Promise.then和setTimeout为异步操作,其他代码执行则是同步的。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 1000);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
function test(){
  console.log('test');
}
console.log('script test');
test();

setTimeout(function() {
  console.log('setTimeout last');
}, 0);

代码参考:tasks-microtasks-queues-and-schedules/
输出结果:

script start
script end
script test
test
promise1
promise2
setTimeout last
setTimeout
  • 当JS代码执行时,会创建一个执行栈、一个或者多个任务队列、一个微任务队列。

以上述代码为例子:

1.执行栈初始化,console.log压入栈中,主线程执行,打印script start ,然后出栈。
2.setTimeout压入执行栈中,主线程调用时,发现是异步操作,且为宏任务,等待时间是1s,此时回调还没有返回,因此把setTimeout先挂起。
3.Promise.resolve在执行栈中执行,但是其是微任务,Promise.then 1回调进入微任务队列中。
4.console.log压入执行栈,直接执行完毕,打印script end
5.继续读取代码,后续console.log('script test')test();都是同步代码,在执行栈中执行结束,打印了script testtest
6.第二个setTimeout压入执行栈,主线程执行,其回调是异步事件,且为宏任务,等待时间是0,setTimeout被挂起,然后其回调立刻进入宏任务队列。
7.执行栈已经空闲,接着查看微任务队列是否有任务,取出了Promise.then 1任务到执行栈中执行。打印了promise1,then1返回了undefined,执行到了then2,then2回调触发,进入微任务队列当中。
8.then1执行完后出对队了,执行栈也空出来了,此时微任务队列当中还有then2的回调没有执行,then2的回调开始执行,打印了promise 2,执行完之后出队,此时微任务队列被清空了。
9.在2步之后,如果时间超过1s,第一个setTimeout回调已经返回,进入宏任务队列,此时队列中,按照先后顺序,分别是console.log(setTimeout last)回调和console.log(setTimeout)回调。
10.微任务队列空了之后,查看宏任务队列,队列中还有setTimeout的回调没有处理,所以处理这个回调,打印setTimeout

  • 总的来说,就是js中遇到同步代码的时候,会直接在执行栈当中执行。遇到异步代码的时候,异步代码的调用是同步的,但是其回调是异步的,异步回调会等到返回结果之后,根据类型,放入微任务队列或者宏任务队列当中。

  • 当执行栈为空的时候,先处理微任务队列中的任务,清空微任务队列之后,再执行宏任务队列的任务。

event loop 介绍
mozilla Memory_Management
event-loops
《Help, I'm stuck in an event-loop》
ruanyifeng-event-loop

内容关联问题

1.addEventListener多次绑定和onclick多次绑定执行结果差别和原因?
2.setTimeout任务执行时间为什么跟实际执行时间有差异

  • setTimeout的指定的任务等待时间,一般实际等待时间>=指定的等待时间。因为setTimeout的回调会进入宏任务队列去等待。它必须要等执行栈空,且微任务队列空才能执行,因此实际执行时间往往会比指定的要晚。

3.介绍Promise等异步任务的执行顺序

你可能感兴趣的:(js浏览器下并发模型和事件循环)