前端深入浏览器架构(2)

消息队列和事件循环

每一个渲染进程都有一个主线程,既要处理DOM,又要计算样式,还要处理布局,同时还需要处理js任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊执行,需要一个系统来统筹调度,这个系统就是消息队列和事件循环系统

队列:先进先出

62.png
  • 一个消息队列用来调度任务,接收其他线程的任务
  • IO线程用来接收其他进程产生的任务
  • 渲染主线程会循环的从消息队列头部中读取任务,执行任务

消息队列中的任务类型

  • 输入事件(鼠标滚动、点击、移动)
  • 微任务
  • 文件读写
  • websocket
  • js定时器
  • js执行,解析DOM,样式计算,布局计算,css动画

宏任务微任务

消息队列中的任务执行过程中,会产生新的任务,如果将这些新产生的任务添加到消息队列尾部,那么这些任务会丧失实时性,如果任务直接出发,阻断当前任务的执行,又会影响当前任务的执行效率

所以最好的时机是:当前任务执行完,下个任务开始前;

eg: dom节点的监控;

消息队列中的任务称为宏任务,每个宏任务都包含了一个微任务队列,在执行宏任务的过程中,产生的微任务会添加到列表中,当宏任务主要功能执行完后,会执行当前宏任务的微任务列表

宏任务

  • 渲染事件(如解析DOM、计算布局、绘制)
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
  • JavaScript脚本执行事件
  • 网络请求完成、文件读写完成事件

宏任务可以满足我们大部分的日常需求,不过如果有对时间精度要求较高的要求,宏任务就难以胜任了。
页面的渲染事件、各种IO的完成事件、执行js脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,js代码不能准确掌控任务要添加到队列中的位置,因此很难控制开始执行任务的时间。

微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

  1. 微任务的产生

    • 使用MutationObserver监控某个DOM节点,通过JS修改这个节点,当DOM发生变化时,就会产生DOM变化记录的微任务

    • Promise,当调用Promise.resolve()或者reject时

  2. 微任务的执行

    • 宏任务快执行完成时,也就在js引擎准备退出全局执行上下文并清空调用栈的时候,js会检查全局执行上下文的微任务队列

    • 在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,v8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束

63.png
  • 微任务和宏任务是绑定的,每个宏任务执行时,会创建自己的微任务队列
  • 微任务的执行时长会影响到当前宏任务的时长。
  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况,微任务都早于宏任务执行

Promise

单线程架构决定了 web 页面的异步回调,而多次回调会导致代码的逻辑不连贯、不线性。Promise 封装异步代码,让处理流程变得线性

64.png

我们来看一下 Promise 的常用用法

let b = function(resolve, reject) {
  let xhr = new XMLHttpRequest()
  xhr.open('get', 'url', true)
  xhr.ontimeout = function(e) {
    reject(e)
  }

  xhr.onreadystatechange = function() {
    resolve(this.responseText, this)
  }
}
let a = new Promise(b)

a.then(res => {}).catch(e => {})

观察上面代码,可以发现:

  • 构建 Promise 对象时,需要传入一个 b 函数,业务流程都在 b 函数中执行
  • 如果运行 b 函数中的业务执行成功了,会调用 resolve 函数;如果执行失败,则调用 reject 函数
  • 在 b 函数中调用 resolve 函数时,会触发 Promise.then 中设置的回调函数;调用 reject 函数时,会触发 Promise.catch 设置的回调函数

首先,Promise 实现了回调函数的延时绑定
其在代码上的提现就是先创建了 Promise,执行业务逻辑;再使用 then 来设置回调函数。resolve 函数会触发设置的回调函数

function b(resolve, reject) {
  resolve(100)
}
let a = new Promise(b)

function onResolve(value) {
  console.log(value)
}

a.then(onResolve)

其次,需要将回调函数 onResolve 的返回值穿透到最外层

function resolve(value) {
  let a2 = new Promise((resolve, reject) => {
    resolve(value)
  })
  return a2 //then的返回值也是一个promise
}

这样就可以实现透传:a.then().then(value);实现透传之后,Promise 对象的错误就具有冒泡性质,会一直向后传递,知道被 catch

我们来实现一个简易版 Promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  this.state = PENDING
  this.value = null
  this.resolveCallbacks = []
  this.rejectCallbacks = []
  const that = this

  function resolve() {
    //   这个地方实际用的是微任务,目的是让then中的回调先执行
    setTimeout(function() {
      if (that.state === PENDING) {
        that.resolveCallbacks.forEach(item => {
          item(that.value)
        })
      }
    }, 0)
  }

  function reject() {
    同上
  }

  try {
    fn(resolve, reject)
  } catch (e) {}
}

// 简易版

MyPromise.prototype.then = function(cb) {
  const that = this
  //   简易的透传,cb不存在时
  cb = typeof cb === 'function' ? cb : v => v
  if (that.state === PENDING) {
    that.resolvedCallbacks.push(cb)
  }
  if (that.state === RESOLVED) {
    cb(that.value)
  }
}
// then函数返回Promise
MyPromise.prototype.then = function(cb) {
  const that = this
  if ((that.state = PENDING)) {
    return new MyPromise((resolve, reject) => {
      that.resolvedCallbacks.push(() => {
        const x = cb(that.value) //此时执行了then1的回调
        resolve(x) //返回上一步的返回值
      })
    })
  }
}

你可能感兴趣的:(前端深入浏览器架构(2))