Skip to content

【JS】EventLoop #3

@GrowthCoder

Description

@GrowthCoder

0 一如既往的开场白

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

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

console.log('console');

1 EventLoop

js是单线程的,因为可以操作DOM,如果是多线程的话,容易导致冲突,比如一个线程删除一个dom节点,另一个线程又需要操作这个dom节点,就会导致冲突。
如果某些任务执行时间过长,又会造成阻塞现象,导致其他任务难以被执行。

js的任务分为同步任务、异步任务
同步任务指的是在主线程上执行的任务,形成一个执行栈。异步任务不在主线程上,在任务队列(task queue)中,只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,如果任务队列有事件,则异步任务结束等待,进入执行栈,开始执行。

主线程从任务队列读取任务,这个过程是循环不断的,所以整个的这种运行机制又被称为事件循环。

异步任务分为:宏任务、微任务。
image.png

1.1 宏任务

  • setTimeout、setInterval
  • requestAnimationFrame
  • 修改url
  • 页面加载
  • 用户交互
  • 执行主线程js代码

1.2 微任务

  • promise
  • process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

event.jpg

// js 
let promiseGlobal = new Promise(resolve=>{
  console.log(1)
  resolve('2')
})
console.log(3) 

promiseGlobal.then(data=>{
  console.log(data)
  let setTimeoutInner = setTimeout(_=>{
  console.log(4)
   },1000)
   
   let promiseInner =new Promise(resolve=>{
     console.log(5) 
      resolve(6)
    }).then(data=>{
     console.log(data)
   })
})
let setTimeoutGlobal = setTimeout(_=>{
  console.log(7);
 let promiseInGlobalTimeout = new Promise(resolve=>{
      console.log(8); 
      resolve(9)
  }).then(data=>{
     console.log(data)
  })
},1000) 

1.3 执行流程

  1. js内核加载代码到执行栈
  2. 执行栈依次执行主线程的同步任务,过程中若遇到调用了异步Api则会添加回调事件到回调队列。微任务添加事件到微任务队列中,宏任务添加事件到宏任务队列中,直到当前执行栈中代码执行完毕。
  3. 开始执行当前所有微任务队列中微任务回调事件。相当于清空队列,如果在执行微任务的过程中,生成了新的微任务,则也会在最后执行此微任务。
  4. 取出宏任务队列中的第一个宏任务,放到执行栈中执行。
  5. 执行当前执行栈宏任务,若在此过程中又遇到微任务或者宏任务,则会继续添加进相应的队列,本轮宏任务执行完毕后,又把本轮新产生的微任务队列执行并清空。
  6. 以上操作往复循环
  7. EventLoop

2 requestAnimationFrame

定义:浏览器用于定时循环操作的一个接口,类似于setTimeout,主要==用途是按帧对网页进行绘制==,充分利用浏览器的刷新机制,每秒60帧,16ms刷新一次,利用这个频率对浏览器进行重绘。

==针对同步代码,浏览器会将同步代码捆绑一起执行,然后以执行结果为当前状态重新渲染,除非有计算样式的代码==

// 不会出现闪动情况
document.body.appendChild(el)
el.style.display = 'none'
//会闪现 存在异步队列
document.body.appendChild(el)
setTimeout(()=>{
    el.style.display = 'none'
}, 0)

如果切换到别的标签,就会自动停止刷新。

requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

使用方式:
需要传入一个callback,==这个回调函数会在浏览器重新绘制之前触发。==

requestID = window.requestAnimationFrame(callback);

如果需要一直在浏览器重新绘制,需要再回调函数中再去调requestAnimationFrame

function repeat () {
    requestAnimationFrame(repeat)
}

requestAnimationFrame(repeat)

2.1 cancelAnimationFrame 取消重绘

window.cancelAnimationFrame(requestID);

2.2 执行顺序

执行队列里的全部任务,但如果任务本身又新增 Animation callback就不会当场执行了,因为那是下一个循环。

// 浏览器合并优化,渲染最后结果值500px,如何实现想要的效果?
elem.style.transform = 'translateX(1000px)'
// elem.style.transition = 'transform 1s ease'
// elem.style.transform = 'translateX(500px)'

// 解决方案:下一帧开始前执行
requestAnimationFrame(() => {
  // 下下一帧开始前执行
  requestAnimationFrame(() => {
    elem.style.transform = 'translateX(500px)',
    elem.style.transition = 'transform 1s ease'
  })
})
// 两次animation callback依次执行
requestAnimationFrame(() => {
  elem.style.background = 'red'
})

3. 题目 含async

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout')
},0)  
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
}).then(function(){
    console.log('promise2')
})
console.log('script end')

参考链接

浏览器的 16ms 渲染帧

requestAnimationFrame

EventLoop

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions