宏任务和微任务

宏任务和微任务到底是什么?

这个就涉及到JavaScript事件轮询中的宏任务和微任务。
那么,你能说清楚到底宏任务和微任务是什么?
是谁发起的?
为什么微任务的执行要先于宏任务呢?

首先,我们需要先知道JS运行机制。

JS运行机制
概念1: JS是单线程执行
”JS是单线程的”指的是JS 引擎线程。

在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
Node环境中,只有JS 线程。

概念2:宿主
JS运行的环境。一般为浏览器或者Node。

概念3:执行栈
是一个存储函数调用的栈结构,遵循先进后出的原则。、、

注意:

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()
stack.jpg

当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。
根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,
foo 函数后执行,当执行完毕后就从栈中弹出了。

概念4:Event Loop
JS到底是怎么运行的呢?

image
JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
检查是否存在微任务,有则会执行至微任务队列为空;

如果宿主为浏览器,可能会渲染页面;
开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

概念5:宏任务和微任务
ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

所以,总结一下,两者区别为:

  • 宏任务(macrotask) 微任务(microtask
  • 谁发起的 宿主(Node浏览器JS引擎

具体事件 有:。。。。无语

  1. script (可以理解为外层同步代码)
  2. setTimeout/setInterval
  3. UI rendering/UI事件
  4. postMessage,MessageChannel
  5. setImmediate,I/O(Node.js) 1. Promise
  6. MutaionObserver
  7. Object.observe(已废弃;Proxy 对象替代)
  8. process.nextTick(Node.js)
    谁先运行 后运行 先运行
    会触发新一轮Tick吗 会 不会

拓展 1:async和await是如何处理异步任务的?
简单说,async是通过Promise包装异步任务。

比如有如下代码:

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

改为ES5的写法:

new Promise((resolve, reject) => {
  // console.log('async2 end')
  async2() 
  ...
}).then(() => {
 // 执行async1()函数await之后的语句
  console.log('async1 end')
})

当调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码(可以把 await 看成是让出线程的标志)。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置,去执行 then 中的回调。

拓展 2:setTimeout,setImmediate谁先执行?

setImmediate和process.nextTick为Node环境下常用的方法(IE11支持setImmediate),所以,后续的分析都基于Node宿主。

Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。

执行顺序如下:

timers: 执行setTimeout和setInterval的回调
pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
idle, prepare: 仅系统内部使用
poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
check: setImmediate在这里执行
close callbacks: 一些关闭的回调函数,如:socket.on(‘close’, …)
一般来说,setImmediate会在setTimeout之前执行,如下:

console.log('outer');
setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
}, 0);

其执行顺序为:

外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
处理里面的setImmediate,将它的回调加入check阶段的队列
外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
到了check阶段,发现了setImmediate的回调,拿出来执行
然后是close callbacks,队列是空的,跳过
又是timers阶段,执行console.log(‘setTimeout’)
但是,如果当前执行环境不是timers阶段,就不一定了。。。。顺便科普一下Node里面对setTimeout的特殊处理:setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)。

看看下面的例子:

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

其执行顺序为:

遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
遇到setImmediate塞入check阶段
同步代码执行完毕,进入Event Loop
先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
跳过空的阶段,进入check阶段,执行setImmediate回调
可见,1毫秒是个关键点,所以在上面的例子中,setImmediate不一定在setTimeout之前执行了。

拓展 3:Promise,process.nextTick谁先执行?
因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node。

process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。

所以,nextTick和Promise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。

拓展 4:应用场景 - Vue中的vm. n e x t T i c k v m . nextTick vm. nextTickvm.nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。

这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。

因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。

vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
如果不支持,再去检测是否支持原生的MessageChannel
如果也不支持的话就会降级为 setTimeout。
小结
下面是道加强版的考题,大家可以试一试。

先看一到面试题
写出console.log的输出顺序

console.log(100);

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

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

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

console.log(400);

// 100 400 300 200 201
// 为什么300200先打印

为什么300比200先打印这里就引出了宏任务和微任务????

什么是宏任务和微任务

宏任务包括:setTimeout setInterval Ajax DOM事件
微任务:Promise async/await
微任务比宏任务的执行时间要早

异步和单线程
异步和单线程是相辅相成的,js是一门单线程脚本语言,所以需要异步来辅助

异步和同步的区别:
异步不会阻塞程序的执行,
同步会阻塞程序的执行,
前端使用异步的场景:

定时任务:setTimeout,setInverval
网络请求:ajax请求,动态加载
事件绑定
//ajax请求代码示例

console.log('start')
$.get('./data1.json',function(data1){
	console.log(data1)
})
console.log('end')

6
加载示例:

事件绑定示例:

也就是请求处理过程中,你不能闲着,会产生异步的请求,回头再处理,然后继续执行下面的请求。

同步阻塞示例:

console.log(1000)
alert(200)
console.log(3000)

上面就是同步阻塞示例,你不点击alert确认窗口,不会打印3000

js绝大部分都是同步,不过多详解;

同步异步问题
首先,你要知道javascript是单线程语言。js任务需要排队顺序执行,如果一个任务耗时过长,后边一个任务也的等着,但是,假如我们需要浏览新闻,但新闻包含的超清图片加载很慢,总不能网页一直卡着直到图片完全出来,所以将任务设计成了两类:

同步任务
异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,像页面骨架和页面元素的渲染,而加载图片、音乐之类的任务就是异步任务,看一下下边导图:

同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入任务队列(Event Queue)。主线程内的任务执行完毕为空,就去任务队列(Event Queue)读取对应的函数,进入主线程执行。

上述过程会不断重复,也就是常说的Event Loop(事件循环)。

但是,JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入任务队列(event queue),然后再执行微任务,将微任务放入微任务队列(micro task queue),但是,这两个queue不是一个queue。当你往外拿的时候先从微任务队列里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数,如下图:

任务列队和event loop(事件循环)
单线程就意味着,所有的任务都要排队,前一个结束,才会执行后面的任务。如果列队是因为计算量大,CPU忙不过来,倒也算了。但是更多的时候,CPU是闲置的,因为IO设备处理得很慢,例如
ajax读取网络数据。js设计者便想到,主线程完全可以不管IO设备,将其挂起,然后执行后面的任务。等后面的任务结束掉,在反过头来处理挂起的任务。

好,我们来梳理一下:

1)所有的同步任务都在主线程上执行,行成一个执行栈。

2)除了主线程之外,还存在一个任务列队,只要异步任务有了运行结果,就在任务列队中植入一个时间标记。

3)主线程完成所有任务(执行栈清空),就会读取任务列队,先执行微任务队列在执行宏任务队列。

4)重复上面三步。

只要主线程空了,就会读取任务列队,这就是js的运行机制,也被称为 event loop(事件循环)。

三、执行顺序
1.主线程上宏任务、微任务执行顺序

console.log('---start---');//第一轮主线程
 
setTimeout(() => {
  console.log('setTimeout');  // 将回调代码放入个宏任务队列,第二轮宏任务执行
}, 0);
 
new Promise((resolve, reject) => {
  console.log('---Promise第一轮微任务同步执行---');//第一轮微任务同步执行
  resolve()
}).then(()=>{
  console.log('Promise.then实例成功回调执行'); // 将回调代码放入微任务队列,第一轮宏任务执行完后立即执行
});
 
console.log('---end---');//第一轮主线程结束

执行顺序
1、

主线程 >> 主线程上创建的微任务 >> 主线程上创建的宏任务

2.宏任务中包含微任务

// 宏任务队列 1

setTimeout(() => {
  // 宏任务队列 2.1
  console.log('timer_1');
  setTimeout(() => {
    // 宏任务队列 3
    console.log('timer_3')
  }, 0)
  new Promise(resolve => {
    resolve()
    console.log('new promise')
  }).then(() => {
    // 微任务队列 1
    console.log('promise then')
  })
}, 0)
 
setTimeout(() => {
  // 宏任务队列 2.2
  console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')

2、

执行顺序:主线程 >> 主线程上的宏任务队列1 >> 宏任务队列1中创建的微任务

========== Sync queue ==========

1 timer_1
2 new promise
3 promise then
4 timer_2
5 timer_3

————————————————

注意: 这里需要注意的 是1执行问题

  1. 宏任务是浏览器发起的;
  2. 微任务是js引擎发起的;
  3. 除了promise和async 是微任务
  4. 其他的任务都是宏任务,比如说 ajax,计时器,dom节点操作,引入script标签

【至于谁先谁后执行的问题,同步任务先执行,异步任务后执行,
js是一个单线程的语言,同步任务在主线程上,异步任务放在任务队列里,
当主线程上的同步任务执行完成后,任务队列里的异步任务就会被推进主线程里】

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值