深刻前端-完全搞懂JS的运行机制

最近看了不少关于JS运行机制的文章,每篇都获益匪浅,但各有不一样,因此在这里对这几篇文章里说的很精辟的地方作一个总结,参考文章连接见最后。本文博客地址

CPU、进程、线程之间的关系

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是创建在进程的基础上的一次程序运行单位,一个进程中能够有多个线程)
  • 不一样进程之间也能够通讯,不过代价较大
  • 单线程多线程,都是指在一个进程内的单和多

了解进程和线程

  • 进程是应用程序的执行实例,每个进程都是由私有的虚拟地址空间、代码、数据和其它系统资源所组成;进程在运行过程当中可以申请建立和使用系统资源(如- 独立的内存区域等),这些资源也会随着进程的终止而被销毁。
  • 而线程则是进程内的一个独立执行单元,在不一样的线程之间是能够共享进程资源的,因此在多线程的状况下,须要特别注意对临界资源的访问控制。
  • 在系统建立进程以后就开始启动执行进程的主线程,而进程的生命周期和这个主线程的生命周期一致,主线程的退出也就意味着进程的终止和销毁。
  • 主线程是由系统进程所建立的,同时用户也能够自主建立其它线程,这一系列的线程都会并发地运行于同一个进程中。

浏览器是多进程的

详情看我上篇总结浏览器执行机制的文章- 深刻前端-完全搞懂浏览器运行机制
  • 浏览器每打开一个标签页,就至关于建立了一个独立的浏览器进程。
  • Browser进程:浏览器的主进程(负责协调、主控),只有一个。做用有
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才建立
  • GPU进程:最多一个,用于3D绘制等
  • 浏览器渲染进程(浏览器内核)

javascript是一门单线程语言

  • jS运行在浏览器中,是单线程的,但每一个tab标签页都是一个进程,都含有不一样JS线程分别执行,,一个Tab页(renderer进程)中不管何时都只有一个JS线程在运行JS程序
  • 既然是单线程的,在某个特定的时刻只有特定的代码可以被执行,并阻塞其它的代码。而浏览器是事件驱动的(Event driven),浏览器中不少行为是异步(Asynchronized)的,会建立事件并放入执行队列中。javascript引擎是单线程处理它的任务队列,你能够理解成就是普通函数和回调函数构成的队列。当异步事件发生时,如(鼠标点击事件发生、定时器触发事件发生、XMLHttpRequest完成回调触发等),将他们放入执行队列,等待当前代码执行完成。
  • javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JS线程在运行JS程序。因此一切javascript版的"多线程"都是用单线程模拟出来的
  • 为何JavaScript是单线程?与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?

任务队列

  • "任务队列"是一个事件的队列(也能够理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务能够进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
  • "任务队列"中的事件,除了IO设备的事件之外,还包括一些用户产生的事件(好比鼠标点击、页面滚动等等),ajax请求等。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
  • 所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  • "任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。可是,因为存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

同步和异步任务

既然js是单线程,那么问题来了,某一些很是耗时间的任务就会致使阻塞,难道必须等前面的任务一步一步执行玩吗?
好比我再排队就餐,前面很长的队列,我一直在那里等岂不是很傻逼,说以就会有排号系统产生,咱们订餐后给咱们一个号码,叫到号码直接去就好了,没交咱们以前咱们能够去干其余的事情。
所以聪明的程序员将任务分为两类:javascript

  • 同步任务:同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务:异步任务指的是,不进入主线程、而进入"任务队列"(Event queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。

任务有更精细的定义:

  • macro-task(宏任务):包括总体代码script(同步宏任务),setTimeout、setInterval(异步宏任务)、I/O、UI 交互事件(优先级较高)、postMessage、MessageChannel、setImmediate(Node.js 环境)。
  • micro-task(微任务):Promise,process.nextTick,ajax请求(异步微任务)

macrotask(又称之为宏任务)

能够理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每个task会从头至尾将这个任务执行完毕,不会执行其它
浏览器为了可以使得JS内部task与DOM任务可以有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行从新渲染
(task->渲染->task->...)html

microtask(又称为微任务),能够理解是在当前 task 执行结束后当即执行的任务

也就是说,在当前task任务后,下一个task以前,在渲染以前
因此它的响应速度相比setTimeout(setTimeout是task)会更快,由于无需等渲染
也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的全部microtask都执行完毕(在渲染前)
前端

执行机制与事件循环

主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各类外部API,它们在"任务队列"中加入各类事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。 java

那怎么知道主线程执行栈为执行完毕?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。程序员

第一轮事件循环:
主线程执行js整段代码(宏任务),将ajax、setTimeout、promise等回调函数注册到Event Queue,并区分宏任务和微任务。
主线程提取并执行Event Queue 中的ajax、promise等全部微任务,并注册微任务中的异步任务到Event Queue。
第二轮事件循环:
主线程提取Event Queue 中的第一个宏任务(一般是setTimeout)。
主线程执行setTimeout宏任务,并注册setTimeout代码中的异步任务到Event Queue(若是有)。
执行Event Queue中的全部微任务,并注册微任务中的异步任务到Event Queue(若是有)。
相似的循环:宏任务每执行完一个,就清空一次事件队列中的微任务。ajax

注意:事件队列中分“宏任务队列”和“微任务队列”,每执行一次任务均可能注册新的宏任务或微任务到相应的任务队列中,只要遵循“每执行一个宏任务,就会清空一次事件队列中的全部微任务”这一循环规则,就不会弄乱。promise

说了那么多来点实例吧

ajax普通异步请求实例

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');

1.执行整个代码,遇到ajax异步操做
2.ajax进入Event Table,注册回调函数success。
3.执行console.log('代码执行结束')。
4.执行ajax异步操做
5.ajax事件完成,回调函数success进入Event Queue。
5.主线程从Event Queue读取回调函数success并执行。浏览器

Promise 的链式 then() 是怎样执行的

new Promise((r) => {
    r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))

new Promise((r) => {
    r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))

执行的结果是 1 4 2 5 3 6数据结构

  • Promise 多个then()链式调用,并非连续的建立了多个微任务并推入微任务队列,由于then()的返回值必然是一个 Promise,然后续的then()是上一步then()返回的 Promise 的回调
  • 传入 Promise 构造器的执行器函数内部的同步代码执行到resolve(),将 Promise 的状态改变为<resolved>: undefined, 而后 then 中传入的回调函数console.log('1')做为一个微任务被推入微任务队列
  • 第二个then()中传入的回调函数console.log('2')此时尚未被推入微任务队列,只有上一个then()中的console.log('1')执行完毕后,console.log('2')才会被推入微任务队列

普通微任务宏任务实例

setTimeout(function(){
    console.log('定时器开始啦')
});

new Promise(function(resolve){
    console.log('立刻执行for循环啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('执行then函数啦')
});

console.log('代码执行结束');

1.整段代码做为宏任务执行,遇到setTimeout宏任务分配到宏任务Event Queue中
2.遇到promise内部为同步方法直接执行-“立刻执行for循环啦”
3.注册then回调到Eventqueen
4.主代码宏任务执行完毕-“代码执行结束”
5.主代码宏任务结束被monitoring process进程监听到,主任务执行Event Queue的微任务
6.微任务执行完毕-“执行then函数啦”
7.执行宏任务console.log('定时器开始啦')多线程

async/await执行顺序

// 1 2 6 4 3 5
//console.log(3)实际上是在async2函数返回的Promise的then语句中执行的
async function async1() {
  console.log(1);
  const result = await async2();
  console.log(3);
}

async function async2() {
  console.log(2);
}
//console.log(async2())

Promise.resolve().then(() => {
  console.log(4);
});

setTimeout(() => {
  console.log(5);
});

async1();
console.log(6);

地狱模式:promise和settimeout事件循环实例

console.log('1');
// 1 6 7 2 4 5 9 10 11 8 3
// 记做 set1
setTimeout(function () {
    console.log('2');
    // set4
    setTimeout(function() {
        console.log('3');
    });
    // pro2
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})

// 记做 pro1
new Promise(function (resolve) {
    console.log('6');
    resolve();
}).then(function () {
    console.log('7');
    // set3
    setTimeout(function() {
        console.log('8');
    });
})

// 记做 set2
setTimeout(function () {
    console.log('9');
    // 记做 pro3
    new Promise(function (resolve) {
        console.log('10');
        resolve();
    }).then(function () {
        console.log('11');
    })
})

第一轮事件循环

1.总体script做为第一个宏任务进入主线程,遇到console.log,输出1。

2.遇到set1,其回调函数被分发到宏任务Event Queue中。

3.遇到pro1,new Promise直接执行,输出6。then被分发到微任务Event Queue中。

4.遇到了set2,其回调函数被分发到宏任务Event Queue中。

  1. 主线程的整段js代码(宏任务)执行完,开始清空全部微任务;主线程执行微任务pro1,输出7;遇到set3,注册回调函数。

第二轮事件循环

1.主线程执行队列中第一个宏任务set1,输出2;代码中遇到了set4,注册回调;又遇到了pro2,new promise()直接执行输出4,并注册回调;

2.set1宏任务执行完毕,开始清空微任务,主线程执行微任务pro2,输出5。

第三轮事件循环

1.主线程执行队列中第一个宏任务set2,输出9;代码中遇到了pro3,new promise()直接输出10,并注册回调;

2.set2宏任务执行完毕,开始状况微任务,主线程执行微任务pro3,输出11。

相似循环...

因此最后输出结果为一、六、七、二、四、五、九、十、十一、八、3。

参考文章

相关文章
相关标签/搜索