浏览器中的页面循环系统

消息队列和事件循环:页面是怎么“活”起来的?

浏览器页面是由消息队列和事件循环系统来驱动的。

  1. 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。

  2. 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。

  3. 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
    消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点。通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。

跨进程发送消息

WebAPI:setTimeout是如何实现的?

setTimeout 是一个定时器,用来指定某个函数在多少毫秒之后执行。它会返回一个整数,表示定时器的编号,同时你还可以通过该编号来取消这个定时器(clearTimeout)。

  1. 浏览器怎么实现 setTimeout
    为了支持定时器的实现,浏览器增加了延时队列。这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。
    添加一个ProcessDelayTask 函数专门用来处理延迟执行任务。当处理完消息队列中的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。

  2. 使用 setTimeout 的一些注意事项

    • 如果当前任务执行时间过久,会影响延迟到期定时器任务的执行
      要使用 JavaScript 来实现动画效果,函数 requestAnimationFrame 就是个很好的选择。requestAnimationFrame 提供一个原生的 API 去执行动画的效果,它会在一帧(一般是16 ms)间隔内根据选择浏览器情况去执行相关动作。

    • 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
      在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为4 毫秒。

    • 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
      目的是为了优化后台页面的加载损耗以及降低耗电量

    • 延时执行时间有最大值
      Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行。

    • 使用 setTimeout 设置的回调函数中的 this 不符合直觉
      如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境(即 window,如果是严格模式,会被设置为 undefined),而不是定义时所在的那个对象。

      var name= 1;
      var MyObj = {
          name: 2,
          showName: function() {
              console.log(this.name); // 1
          }
      }
      setTimeout(MyObj.showName, 1000)
      

WebAPI:XMLHttpRequest是怎么实现的?

  1. 回调函数 VS 系统调用栈
    将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数。
    回调函数 callback 在主函数返回之前执行的,称为同步回调。
    异步回调是指回调函数在主函数外部执行的过程,一般有两种方式:第一种是把异步函数做成一个任务,添加到信息队列尾部;第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。
    消息队列和主线程循环机制保证了页面有条不紊地运行,当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈。

  2. XMLHttpRequest 运作机制

    • 创建 XMLHttpRequest 对象
    • 为 xhr 对象注册回调函数
    • 配置基础的请求信息
    • 发起请求
  3. XMLHttpRequest 使用过程中的“坑”

    • 跨域问题

    • HTTPS 混合内容的问题
      HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,比如包含了 HTTP 资源,通过 HTTP 加载的图像、视频、样式表、脚本等,都属于混合内容。

setTimeout 是直接将延迟任务添加到延迟队列中,而 XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

宏任务和微任务:不是所有任务都是一个待遇

  1. 宏任务
    页面中的大部分任务都是在主线程上执行的,这些任务包括了:

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

    主线程采用一个 for 循环,不断地从任务队列(比如延迟执行队列和普通的消息队列)中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。

    宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如监听 DOM 变化的需求。

  2. 微任务
    异步回调主要有两种方式:

    • 把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。比如 setTimeout 和
      XMLHttpRequest 的回调函数。
    • 在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。

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

    每个宏任务都关联了一个微任务队列,在现代浏览器里面,产生微任务有两种方式:

    • 使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。

    • 使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

    在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。执行微任务的时间点称为检查点。
    在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。

微任务添加和执行流程示意图

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

  1. 监听 DOM 变化方法演变
    Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。解决了实时性的问题,但也正是这种实时性造成了严重的性能问题。
    MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化即微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。
    MutationObserver 采用了“异步 + 微任务”的策略。通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。

Promise:使用Promise,告别回调函数

Promise 对象用于表示一个异步操作的最终完成(或失败),及其结果值。Promise 一旦被创建,会立即执行,共有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步结果可以决定当前是哪一种状态,状态一旦被确定就再也不会更改。也就是说, Promise 对象状态的更改只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。

Promise 中的执行函数是同步进行的,但是里面存在着异步操作,在异步操作结束后会调用 resolve 方法,或者中途遇到错误调用 reject 方法,这两者都是作为微任务进入到 EventLoop 中。

  1. Promise 解决的是异步编码风格的问题,消灭嵌套调用和多次错误处理。

    • Promise 实现了回调函数的延时绑定
    • 将回调函数 onResolve 的返回值穿透到最外层
    • Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。具备了这样“冒泡”的特性后,就不需要在每个 Promise 对象中单独捕获异常了。
  2. Promise 与微任务
    Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的,把 resolve(reject) 回调的执行放在当前宏任务的末尾。

    • 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
    • 放到当前宏任务最后执行,解决了回调执行的实时性问题。

async/await:使用同步的方式去写异步代码

ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。

  1. 生成器(Generator)VS 协程(Coroutine)
    生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。

    function* genDemo() {
         console.log(" 开始执行第一段 ")
         yield 'generator 1'
    
         console.log(" 开始执行第二段 ")
         yield 'generator 2'
    
         console.log(" 执行结束 ")
         return 'generator 3'
    }
    console.log('main 0')
    
    let gen = genDemo()
    console.log(gen.next().value)
    
    console.log('main 1')
    console.log(gen.next().value)
    
    console.log('main 2')
    console.log(gen.next().value)
    // main 0 -> 开始执行第一段 -> generator 1 -> main 1 -> 开始执行第二段 -> generator 2 -> main 2 -> 执行结束 -> generator 3
    
    • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
    • 外部函数可以通过 next 方法恢复函数的执行。

    协程是一种比线程更加轻量级的存在。一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
    协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
    在 JavaScript 中,生成器就是协程的一种实现方式。通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器。

协程执行流程图
  1. async/await
    • async
      async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

    • await
      当执行到 await 100时,会默认创建一个 Promise 对象,JavaScript 引擎会将 resolve 函数提交给微任务队列。然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。接下来继续执行父协程的流程,随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列。

async/await 执行流程图

Promise.then方法的返回值问题

  1. then 方法没有返回值,或者返回 undefined
// then 方法没有返回值,或者返回 undefined,then 方法的 promise 的 resolve 的值将传递出 undefined
Promise.resolve(42).then(value => {
    console.log(value)
    // return undefined
}).then(value => {
    console.log(value)
})
// 42 undefined
  1. 返回一个值
// 返回一个值,return 的值将作为 then 方法返回的 promise 的 resolve 的值传递出
Promise.resolve(42).then(value => {
    console.log(value)
    return value + 2
}).then(value => {
    console.log(value)
})
// 42 44
  1. 返回一个Promise
// 返回一个Promise,新建 promise 的 resolve 传出的值将作为 then 方法返回的 promise 的 resolve 的值传递出
Promise.resolve(42).then(value => {
    console.log(value)
    return new Promise(function(resolve, rejected) {
        resolve(value + 1)
    })
}).then(value => {
    console.log(value)
})
// 42 43
  1. 新建一个promise,使用 reslove 返回值
// 新建一个promise,使用 reslove 返回值,但是没有向它返回的 promise 传递返回值,因此 resolve 返回的都是 undefined
Promise.resolve(42).then(value => {
    console.log(value)
    new Promise(function(resolve,rejected){
        resolve(value + 3)
    })
}).then(value => {
    console.log(value)
})
// 42 undefined
  1. 新建一个 promise,使用 return 返回值
// 新建一个 promise,使用 return 返回值,但是没有向它返回的 promise 传递返回值,因此 resolve 返回的都是 undefined
Promise.resolve(42).then(value => {
    console.log(value)
    new Promise(function(resolve,rejected){
        return(value + 4)
    })
}).then(value => {
    console.log(value)
})
// 42 undefined

执行顺序小测验

Promise.resolve().then(() => {
    console.log(0)
}).then(console.log(3))
.then((res) => {
    console.log(9)
})

Promise.resolve().then(() => {
    console.log(1)
}).then(() => {
    console.log(2)
})
// 3 0 1 9 2

/*
 * 步骤分析:
 * 1. Promise.resolve()用于将现有对象转换为Promise对象,从而控制异步流程,立即resolve的Promise对象是在本轮“事件循环”(Event loop)的结束时,而不是在下一轮“事件循环”的开始时。
 * 2. .then 的回调函数是异步,微任务。若为值,则为同步,故先执行:3
 * 3. 再执行异步:0 1
 * 4. 最后执行微任务:9 2
 */
console.log('1')
setTimeout(function () {
    console.log('2')
})

new Promise(function (resolve, reject) {
    console.log('3')
}).then(console.log('4'))

console.log('5')

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log('6')
    })
}).then(console.log('7'))

setTimeout(function () {
    new Promise(function (resolve, reject) {
        console.log('8')
    })
})
// 1 3 4 5 7 2 6 8
/*
 * 步骤分析:
 * 由于 Promise 中的执行函数是同步进行的,.then 方法是同步的,其回调才是异步的。
 * 直接打印同步调用:1 3 4 5 6
 * 执行异步队列:打印2 6 8
 */
async function foo() {
    console.log('foo')
}

async function bar() {
    console.log('bar start')
    await foo()
    console.log('bar end')
}

console.log('script start')

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

bar()

new Promise(function (resolve) {
    console.log('promise executor')
    resolve()
}).then(function () {
    console.log('promise then')
})
console.log('script end')
// script start -> bar start -> foo -> promise executor -> script end -> bar end -> promise then -> setTimeout

/* 
 * 1. 先执行同步队列:script start -> bar start; 
 * 2. 执行 await foo,默认创建 Promise,同步执行:foo
 * 3. 执行下一个Promise,同步执行:promise executor
 * 4. 同步执行:script end
 * 5. 执行 then 的异步回调(微任务):bar end -> promise then
 * 6. 执行延迟队列:setTimeout
 */
Promise.resolve().then(() => {
    console.log(0)
    return 6
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1)
}).then(() => {
    console.log(2)
}).then(() => {
    console.log(3)
}).then(() => {
    console.log(4)
}).then(() => {
    console.log(5)
})
// 0 1 6 2 3 4 5
/*
 * 步骤分析:
 * 微任务产生顺序:
 * 1. [console.log(0), console.log(1)]
 * 2. [console.log(6), console.log(2)]
 * 3. [console.log(3)]
 * 4. [console.log(4)]
 * 5. [console.log(5)]
 */
Promise.resolve().then(() => {
    console.log(0)
    return Promise.resolve(6)
}).then((res) => {
    console.log(res)
}).then(() => {
    console.log(7)
}).then(() => {
    console.log(8)
})

Promise.resolve().then(() => {
    console.log(1)
}).then(() => {
    console.log(2)
}).then(() => {
    console.log(3)
}).then(() => {
    console.log(4)
}).then(() => {
    console.log(5)
})
// 0 1 2 3 6 4 7 5 8
/*
 * 步骤分析:
 * 微任务产生顺序:
 * 1. [console.log(0), console.log(1)]
 * 2. [Promise.resolve(6), console.log(2)]
 * 第二个微任务队列,一个是 Promise,一个是普通函数。Promise.resolve(6) :Promise 异步转换为 6,.then 再接收。console.log(2) :隐式执行了 return undefined,故 console.log(2) 后的 console.log(3) 比 console.log(6) 快。故为 0 1 2 3 6
 * 由于 6 比 3 慢,故 6 后的 7 8 皆慢于 3 后的 4 5,执行顺序为:4 7 5 8
 */
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容