JS运行机制

1. 单线程的JavaScript

  • JavaScript是单线程的语言这,由它的用途决定的,作为浏览器的脚本语言,主要负责和用户交互,操作DOM。

  • 假如JavaScript是多线程的,有两个线程同时操作一个DOM节点,一个负责删除DOM节点,一个在DOM节点上添加内容,浏览器该以哪个线程为标准呢?

  • 所以,JavaScript的用途决定它只能是单线程的,过去是,将来也不会变。

  • HTML5的WebWorker允许JavaScript主线程创建多个子线程,但是这些子线程完全受主线程的控制,且不可操作DOM节点,所以JavaScript单线程的本质并没有发生改变。

2. 同步任务和异步任务

  • JavaScript是单线程语言,就意味着任务需要排队执行,只有前一个执行完成,后一个才可以执行。

  • 如果前一个任务非常耗时呢?比如操作IO设备、网络请求等,后面的任务就会被阻塞,页面就会被卡住,甚至崩溃,用户体验非常差。

  • 如果JavaScript的主线程在遇到这些耗时的任务时,将其挂起,先执行后面的任务,等挂起的任务有结果以后再回头执行,这样就可以解决耗时任务阻塞主线程的问题了。

  • 于是,所有的任务就可以分为两种,同步任务和异步任务,同步任务放在主线程中执行,异步任务被挂起,不进入主线程执行(让主线程阻塞等待),当其有结果了,再放入主线程中执行。

3. 任务队列和Event Loop

3.1 任务队列

  • 任务队列是一个事件队列,也可以理解成消息队列,当挂起的异步任务就绪以后就会在任务队列中放置相应的事件,表示该任务可以进入主线程中执行了。

  • 任务队列中的事件,除了IO设备的事件,还有网络请求,鼠标点击、滚动等,只要为事件指定过回调函数,这些事件发生时就会进入任务队列,等待主线程来读取,然后执行相应的回调函数。

  • 回调函数其实就是被挂起来的异步任务,比如:Ajax请求,请求成功或失败以后执行的回调函数就是异步任务。

  • 任务队列是一个先进先出的数据结构,排在前面的事件,只要主线程一空,就会优先被读取。

3.2 Event Loop

  • 主线程从任务队列读取事件,这个过程是循环不断的,所以JavaScript这种运行机制又称为Event Loop(事件循环)

4. 宏任务和微任务

异步任务可进一步划分为宏任务和微任务,相应的任务队列也有两种,分别为宏任务队列和微任务队列。

4.1 宏任务

  • setTimeout、setInterval、setImmediate会产生宏任务

4.2 微任务

  • requestAnimationFrame、IO、读取数据、交互事件、UI render、Promise.then、MutationObserve、process.nextTick会产生微任务

4.3 浏览器中的JavaScript脚本执行过程
4.3.1 过程描述

a. JavaScript脚本进入主线程, 开始执行

b. 执行过程中如果遇到宏任务和微任务,分别将其挂起,只
有当任务就绪时将事件放入相应的任务队列

c. 脚本执行完成,执行栈清空

d. 去微任务队列依次读取事件,并将相应的回调函数放入执行栈运行,如果执行过程中遇到宏任务和微任务,处理方式同 b, 直到微任务队列为空

e. 浏览器执行渲染动作, GUI渲染线程接管,直到渲染结束

f. JS线程接管,去宏任务队列依次读取事件,并将相应的回调函数放入执行栈, 开始下一个宏任务的执行,过程为b -> c -> d -> e
-> f, 如此循环

g. 直到执行栈、宏任务队列、微任务队列都为空,脚本执行结束

4.3.2 示例

// 脚本

console.log(1)

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

const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log(3)
    resolve()
  }, 1000)
  console.log(4)
})

p.then(() => {
  console.log(5)
})

console.log(6)
  • a. 脚本放入执行栈开始实行

  • b. 执行到console.log(1), 输入1

  • c. 执行到setTimeout,遇到宏任务,将其挂起,由于延时 0ms,将在 4ms后在宏任务队列产生一个定时事件, 我们叫定时A

  • d. 程序继续向下执行,执行new Promise(),并运行其参数,遇到第二个定时任务(宏任务),叫它定时B,并将其挂起,执行console.log(4), 输出4

  • e. 遇到微任务p.then(), 将其挂起

  • f. 向下执行遇到console.log(6), 输出6

  • g. 执行栈清空,读取微任务队列,发现为空,因为p.then()含没有就绪,它的就绪依赖与第一个定时任务(定时A)的执行

  • h. 执行栈为空,微任务队列为空,执行浏览器的渲染动作

  • i. 读取宏任务队列,读取第一个就绪的宏任务,为定时任务A,将其回调函数放入执行栈开始执行,执行console.log(2), 输入2

  • j. 执行栈清空,微任务队列为空,渲染

  • k. 开始执行下一个就绪的宏任务,定时任务B,并将其回调函数放入执行栈执行,执行console.log(3), 输出3,并执行resolve(), p.then()就绪,在微任务队列放入相应的事件

  • o. 执行栈清空,读取微任务队列,发现不为空,读取第一个就绪的事件,并将其对应的回调函数放入执行栈执行,执行console.log(5), 输出5

  • p. 执行栈清空,微任务队列为空,渲染,然后发现宏任务队列为空,本次脚本执行彻底结束

输出结果为: 1 4 6 2 3 5

async function async1 () {
  console.log('async1_1')
  await async2()
  console.log('async1_2')
}
async function async2 () {
  console.log('async2')
}
console.log('script start')
setTimeout(() => {
  console.log('setTimeout')
}, 0)
async1()
new Promise(resolve => {
  console.log('promise executor')
  resolve()
}).then(() => {
  console.log('promise then')
})
console.log('script end')

函数前加async,实际上返回的是一个promise,比如这里的async2函数,返回的是一个立即resoved promise

await会将后面的同步代码执行完成(async2),然后让出线程,将异步任务(Promise.then)挂起,这里的立即resolved promise,所以会在微任务队列添加一个事件,且排在下面的Promise.then之前

输出结果: script start => async1_1 => async2 => promise executor => script
end => async1_2 => promise then => setTimeout

总结

  • 如果把JavaScript脚本也当作初始的宏任务,那么JavaScript在浏览器端的执行过程就是这样:

    • 先执行一个宏任务, 然后执行所有的微任务

    • 再执行一个宏任务,然后执行所有的微任务

    • 如此反复,执行执行栈和任务队列为空

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值