前言

“JavaScript 是单线程、异步、非阻塞、解释型脚本语言。”

浏览器进程

进程:浏览器一个页面就是新的一个进程,进程是CPU资源分配的最小单位(系统会给它分配内存);

  • Browser进程 (http通信)
  • 第三方插件进程
  • GPU进程(加速,3D渲染,一次)
  • Renderer进程(新开页面渲染进程)

图片说明

Render进程(浏览器渲染进程)

线程:线程包含在每个进程内,线程是CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程);

1. GUI 渲染线程

  • 1. 解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
  • 2. 构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
  • 3. 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
  • 4. 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来

2. JavaScript引擎线程(主线程执行栈)

永远只有JS引擎(JS内核)线程在执行JS脚本程序,
负责解析执行Javascript脚本程序的主线程(例如V8引擎)
  • js引擎执行顺序
    • 宏任务(同步任务)直接执行,其他线程先进入任务队列等待执行
    • 然后任务队列中先执行微任务(只有异步任务)
    • 再执行宏任务(异步任务)(如果有任务内还包含宏任务(同步任务),继续依此执行1)

3. GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

单线程与多线程

单线程语言:JavaScript 的设计就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),决定了它是一门单线程语言。【处理任务是一件接着一件处理,从上往下顺序执行】

如果有多个线程,它们同时在操作 DOM,那网页将会一团糟。

当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给** webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了异步非阻塞**。

同步与异步

JavaScript 中有同步代码与异步代码。

  • 同步:会依次执行,执行完了后便会返回结果
  • 异步:网络请求、计时器、DOM时间监听...

事件循环(event loop)和消息队列(task queue)

  • 事件循环 机制和 消息队列 的维护是由事件触发线程控制的。
  • JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由事件触发线程将异步对应的回调函数加入到消息队列中,消息队列中的回调函数等待被执行。
  • 同时,JS引擎线程会维护一个执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。
  • 如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
  • 执行完了后,执行栈再次为空,事件触发线程会重复上一步操作,再取出一个消息队列中的任务,这种机制就被称为事件循环(event loop)机制。

宏任务与微任务

宏任务(macrotask)

  • 定时触发器线程(宏任务(异步任务))
    • setTimeout
    • setInterval
    • setImmediate
    • requestAnimationFrame
  • 事件触发线程(宏任务(异步任务))
  • 异步http请求线程(宏任务(异步任务))
  • script方法(宏任务(同步任务))
  • new Promise(宏任务(同步任务)) 立即执行

微任务(microtask)

由于Es6 和node出现产生的微任务

  • Promise.then() catch() finally(),一旦一个pormise有了结果,回调产生一个微任务
  • process.nextTick
  • MutationObserver

执行机制

  • 1. 执行一个宏任务(栈中没有就从事件队列中获取)
  • 2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 4. 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 5. 渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
    console.log('script start')
    setTimeout(function() {
      console.log('timer over')
    }, 0)
    Promise.resolve().then(function() {
      console.log('promise1')
    }).then(function() {
      console.log('promise2')
    })
    console.log('script end')
    // script start
    // script end
    // promise1
    // promise2
    // timer over

JS引擎线程首先执行主代码块。

每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。

在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾。

microtask必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。

也就是说,在某一个macrotask执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

这样就可以解释 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。

在node环境下,process.nextTick的优先级高于Promise,也就是说:在宏任务结束后会先执行微任务队列中的nextTickQueue,然后才会执行微任务中的Promise。

吃透这些例子 包你掌握js执行顺序及promise知识https://www.jianshu.com/p/e585e737fb6f