宏任务,微任务,彻底搞懂JavaScript 执行机制

1 javascript

javascript是一种单线程的语言,这就意味着,它的代码执行是按照从上到下的顺序来的。所以js的任务也是按照一定的顺序来执行的,就想排队办理业务一样,没有排到的就会先等着。

2 javascript的事件循环机制

既然js是单线程,如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

  • 同步任务
  • 异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。

而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务

说到这里,想必大家都见过这张图

 

1. 在执行栈中执行一个宏任务。 

2. 执行过程中遇到微任务,将微任务添加到微任务队列中。

3. 当前宏任务执行完毕,立即执行微任务队列中的任务。 

4. 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。 

5. 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。

说了这么多,不如直接来看代码

    console.log(1)
    setTimeout(()=>{
      console.log(2)
    },0)
    console.log(3)

 不难理解上面代码的打印顺序是 1 3 2。先执行同步任务,后执行异步任务。

如此似乎很好理解。但是js的事件执行顺序真的是这么简单吗?来看下面代码

setTimeout(function() {
    console.log('1');
})

new Promise(function(resolve) {
    console.log('2');
}).then(function() {
    console.log('3');
})

console.log('4');

注:new Promise的回调函数会立即执行,then是异步的 

上面代码图解执行过程

我们现在来验证一下我们的分析,把代码放在控制台,看下结果。 

开始还以为看错了,再打印一遍发现还是这样的结果,证明我们的分析是错的。那上面的代码究竟是怎么执行的呢?

除了广义的同步任务和异步任务,js对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval, I/O, 网络请求
  • micro-task(微任务):Promise,process.nextTick(nodejs), MutationObserver接口提供了监视对DOM树所做更改的能力。

为什么要有微任务:

大概是需要兼顾代码的效率和实时性,也可以说是为了插队吧。

一个Event Loop,Microtask 是在 Macrotask 之后调用,Microtask 会在下一个Event Loop 之前执行调用完,并且其中会将 Microtask 执行当中新注册的 Microtask 一并调用执行完,然后才开始下一次 Event loop,所以如果有新的 Macrotask 就需要一直等待,等到上一个 Event loop 当中 Microtask 被清空为止。由此可见, 我们可以在下一次 Event loop 之前进行插队

如果不区分 Microtask 和 Macrotask,那就无法在下一次 Event loop 之前进行插队,其中新注册的任务得等到下一个 Macrotask 完成之后才能进行,这中间可能你需要的状态就无法在下一个 Macrotask 中得到同步

状态的同步对于视图来说至关重要,这也就牵扯到了为什么 javascript 是单线程的原因所在。

比如一个监听DOM变化的场景。如果不存在微任务,所有DOM变化触发的事件调用都是同步的,当DOM发生变化时,渲染引擎会同步调用这些接口,这是一个典型的观察者模式。这种模式就会存在一种文体,当DOM频繁变化时,每次变化都会调用接口,那么当前任务的执行时间就会被拉长,从而导致运行效率下降。比如同时修改或创建50个节点,每次触发事件执行时间是5ms,那么处理DOM监听触发事件的时间就是250ms,如果此时浏览器正在执行一个动画效果,那么就会导致动画卡顿。

那有人可能会像,我们可以将响应函数都改成异步函数,且不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。但是改方案会导致实时性问题,说不定当执行到这个响应函数时DOM早已发生变化,甚至已经被删除。

这时微任务就可以派上用场了,在执行代码过程中,每次DOM发生变化,渲染引擎会将其变化封装成微任务,并将该微任务添加到当前的微任务队列中,当该任务执行完毕,再去统一执行微任务列表中的任务。这样就兼顾了效率和实时性。

用图来表示就是这样的:

  • 第一轮打印出 2 和 4 把setTimeout加入宏任务,new Promise的then加入微任务。
  • 第二轮询问微务队列,有任务就加入主线程,开始执行,打印出3
  • 第三轮询问微任务队列 ,无任务再去查询宏任务队列,有任务就加入到主线程打印出1
  • 第四轮询问微任务队列,无任务询问宏任务队列,无任务,执行结束

所以打印结果是 2 4 3 1。检验一下你对上面结论的理解程度吧。

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

  • 第一轮 打印出1 7 异步任务队列分入任务,结果如图

  • 第二轮,询问微任务有没有任务,有,将console.log('6')加入主线程打印出6,主线程执行结束。
  • 第三轮,询问有没有微任务,有,将console.log('8')加入主线程,打印出8,主线程执行结束。
  • 第四轮,询问微任务队列,无,询问宏任务队列。有将第一个setTimeout里面的任务加入主线程。打印出2,将

    process.nextTick(function () {console.log('3');})加入微任务列表,打印出4,将then里面的console.log(5)加入微任务队列。主线程执行结束,如下图

  • 第五轮,询问微任务,有,将console.log('3')加入主线程,打印3,主线程执行结束。
  • 第六轮,询问微任务,有,将console.log('5')加入主线程,打印5,主线程执行结束。
  • 第七轮,询问微任务,无,询问宏任务,有,将setTimeout内部任务加入主线程,打印9,将

    process.nextTick(function () {console.log('10');})加入微任务,打印11,将then加入微任务,主线程结束。

  • 第八轮,询问微任务,有,console.log('10')加入主线程,打印10,主线程结束。

  • 第九轮,询问微任务,有,将console.log('12')加入主线程,打印12,主线程结束。

  • 第10轮,询问微任务,无,询问宏任务,无,执行结束。

最后结果输出为1,7,6,8,2,4,3,5,9,11,10,12。

当遇到async / await会怎么样呢?

3 async/await是什么?

我们创建了 promise 但不能同步等待它执行完成。我们只能通过 then 传一个回调函数这样很容易再次陷入 promise 的回调地狱。实际上,async/await 在底层转换成了 promise 和 then 回调函数。也就是说,这是 promise 的语法糖。

每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。

如果 await 后面跟的不是一个 Promise,那 await 后面表达式的运算结果就是它等到的东西;

如果 await 后面跟的是一个 Promise 对象,await 它会“阻塞”后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值作为 await 表达式的运算结果。但是此“阻塞”非彼“阻塞”这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成“阻塞”,它内部所有的“阻塞”都被封装在一个 Promise 对象中异步执行。(这里的阻塞理解成异步等待更合理)

上面总结为一下几点

  1. async定义的是一个Promise函数和普通函数一样只要不调用就不会进入事件队列。
  2. async内部如果没有主动return Promise,那么async会把函数的返回值用Promise包装。
  3. await关键字必须出现在async函数中,await后面不是必须要跟一个异步操作,也可以是一个普通表达式。
  4. 遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。
  5. await后面如果不是 promise 对象, await会阻塞await后面的代码(不影响await前面的代码),先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。
  6. await后面如果是 promise 对象,await 也会暂停await后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
  7. 如果await后面还有同步任务,会将它放入到then函数中
    console.log('a')
    async function fun1() {
      console.log('f')
      await setTimeout(()=>{
        console.log('d')
      },22)
      console.log('b')
    }
    fun1()
    console.log('c')
    setTimeout(()=>{
      console.log('e')
    },22)

上面代码打印结果为a f c b d e

setTimeout(function () {
  console.log('6')
}, 0)
console.log('1')
async function async1() {
  console.log('2')
  await async2()
  console.log('5')
}
async function async2() {
  console.log('3')
}
async1()
console.log('4')
  1. 6是宏任务在下一轮事件循环执行
  2. 先同步输出1,然后调用了async1(),输出2。
  3. await async2() 会先运行async2(),5进入等待状态。
  4. 输出3,这个时候先执行async函数外的同步代码输出4。
  5. 最后await拿到等待的结果继续往下执行输出5。
  6. 进入第二轮事件循环输出6。
async function async1() {
  console.log('2')
  await async2()
  console.log('7')
}

async function async2() {
  console.log('3')
}

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

console.log('1')
async1()

new Promise(function (resolve) {
  console.log('4')
  resolve()
}).then(function () {
  console.log('6')
})
console.log('5')
  1. 首先输出同步代码1,然后进入async1方法输出2。
  2. 因为遇到await所以先进入async2方法,后面的7处于等待状态。
  3. 在async2中输出3,现在跳出async函数先执行外面的同步代码。
  4. 输出4,5。then回调进入微任务栈。
  5. 现在主线程执行完了,扫描微任务输出6, 7
  6. 微任务执行完毕,扫码宏任务输出8
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值