05_浏览器页面循环系统

一. 线程模型

第一版——单线程

通过单线程处理安排好的任务

  • 任务代码已经按照顺序写进线程里,线程按顺序执行这些任务

第二版——事件循环机制

线程处理过程中,接收并执行新任务

  • 循环机制:线程语句后添加for 循环语句。线程循环执行
  • 事件:执行过程中可以暂停等待用户输入

第三版——消息队列

线程处理其他线程发送过来的任务

  • 消息队列:数据结构,存放要执行的任务,先进先出
    • 任务类型:输入事件、微任务、文件读写、WebSocket、JS定时器等;JS执行、解析DOM、样式计算、布局计算、CSS动画等
  • 实现步骤:
    1. 构造队列,成为消息队列
    2. 其他线程的任务添加至队列尾部
    3. 主线程从队列头部中读取任务并执行
    4. 安全退出:设置一个退出标志变量
  • Chrome:渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程
  • 单线程消息队列的缺点:
    1. 执行效率低,效率和实时性无法统筹兼顾
      1.1 解决方法:微任务
    2. 单个任务执行时间过长
      2.1 解决方法:回调功能

二. WebAPI——事件循环的应用

1. setTimeout

定时器,指定某个函数在多少毫秒后执行

  • 要求:为保证回调函数能在指定时间内执行,不能将定时器的回调函数直接添加到消息队列中。
  • 解决:渲染进程会将定时器的回调任务添加到延迟队列

延迟队列:另外一个消息队列,这个队列中维护了 需要延迟执行的任务列表,包括定时器和 Chromium 内部一些需要延迟执行的任务

  • 实现步骤:
    1. 渲染进程创建一个回调任务,添加到延迟队列
    2. 消息循环系统调用内部专门用来处理延迟任务的函数(ProcessDelayTask 函数),执行延迟任务
    3. 执行完成后,循环进入下一个任务
  • 注意事项:
    1. 如果当前任务执行时间过久,会影延迟到期定时器任务的执行
    2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
    3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
    4. 延时执行时间有最大值
    5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉,指向全局环境,使用匿名函数或 bind 方法解决

2. XMLHttpRequest

从 Web 服务器获取数据,操作 DOM 来更新页面内容,整个过程只更新网页的一部分,不会刷新整个页面

  • 准备知识:

    1. 同步回调:回调函数 callback 在主函数返回之前执行的过程,是在当前主函数的上下文中执行回调函数
    2. 异步回调:回调函数在主函数外部执行的过程。
      2.1 一种情况把异步函数做成一个任务,添加到信息队列尾部
      2.2 另一种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务

    将一个函数作为参数传递给另外一个函数,作为参数的这个函数就为回调函数

    // 回调函数
    let callback = function(){
    	console.log('i am do homework')
    }
    function doWork(cb) {
     	console.log('start do work')
     	cb()
     	console.log('end do work')
    }
    doWork(callback)
    
    // 异步函数
    let callback = function(){
     	console.log('i am do homework')
    }
    function doWork(cb) {
     	console.log('start do work')
     	setTimeout(cb,1000) 
     	console.log('end do work')
    }
    doWork(callback)
    
    1. 系统调用栈:类似于JS的调用栈,当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈
  • 运行机制:

    1. 创建 XMLHttpRequest 对象,用来执行实际的网络请求
    2. 为xhr 对象注册回调函数

    ontimeout,用来监控超时请求,如果后台请求超时了,该函数会被调用;

    onerror,用来监控出错信息,如果后台请求出错了,该函数会被调用;

    onreadystatechange,用来监控后台请求过程中的状态,比如可以监控到 HTTP 头加载 完成的消息、HTTP 响应体消息以及数据加载完成的消息等

    1. 配置基础请求信息

    2. 发起请求

      4.1 渲染进程会将请求发送给网络进程然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程
      4.2 渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候, 就会根据相关的状态来调用对应的回调函数

      如果网络请求出错了,就会执行 xhr.onerror;

      如果超时了,就会执行 xhr.ontimeout;

      如果是正常的数据接收,就会执行 onreadystatechange 来反馈相应的状态。

    function GetWebData(URL){
     	/**
     	* 1: 新建 XMLHttpRequest 请求对象
     	*/
     	let xhr = new XMLHttpRequest()
     	/**
    	 * 2: 注册相关事件回调处理函数
     	*/
     	xhr.onreadystatechange = function () {
     		switch(xhr.readyState){
     			case 0: // 请求未初始化
     				console.log(" 请求未初始化 ")
     				break;
     			case 1://OPENED
     				console.log("OPENED")
     				break;
     			case 2://HEADERS_RECEIVED
     				console.log("HEADERS_RECEIVED")
     				break;
     			case 3://LOADING 
     				console.log("LOADING")
     				break;
     			case 4://DONE
     				if(this.status == 200||this.status == 304){
     					console.log(this.responseText);
     				}
     				console.log("DONE")
     				break;
     		}
     	}
     	xhr.ontimeout = function(e) { console.log('ontimeout') }
     	xhr.onerror = function(e) { console.log('onerror') }
     	/**
     	* 3: 打开请求
     	*/
     	xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步
     	/**
     	* 4: 配置参数
     	*/
     	xhr.timeout = 3000 // 设置 xhr 请求的超时时间
     	xhr.responseType = "text" // 设置响应返回的数据格式
    	xhr.setRequestHeader("X_TEST","time.geekbang")
     	/**
     	* 5: 发送请求
     	*/
     	xhr.send();
    }
    
  • 注意事项

    1. 跨域问题:默认情况下,跨域请求是不被允许的

    2. HTTPS 混合内容:包含了不符合 HTTPS 安全要求的内容

      混合内容: HTTP 资源,通过 HTTP 加载的图像、 视频、样式表、脚本等

三. 微任务 vs 宏任务

宏任务

消息队列中的任务称为宏任务

  • 事件循环系统机制:
    1. 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
    2. 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任 务;
    3. 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
    4. 最后统计执行完成的时长等信息
  • 不足之处:难以完成对时间精度要求较高的需求,如页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件

微任务

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

可以在实时性和效率之间做一个有效的权衡

在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列,用来存放微任务

  • 产生方式:

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

    2. 第二种方式:使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务

  • 执行时机

    WHATWG 把执行微任务的时间点称为检查点

    • 在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务
  • 注意:如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行

微任务应用
1. MutationObserver

采用了 “异步 + 微任务” 的策略:

  • 通过异步操作解决了同步操作的性能问题
  • 通过微任务解决了实时性的问题
  • 响应函数改成异步调用,可以不用每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化
  • 采用微任务保持消息通知的及时性,在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。当执行到检查点的时候,V8 引擎就会按照顺序执行微任务
2. Promise
  • 与微任务关系
    • 通过微任务将定时器改造从而实现回调函数延时调用,提升代码的执行效率
// 原来resolve延迟调用onResolve_是使用定时器的方法
function resolve(value) {
 	setTimeout(()=>{
 		onResolve_(value)
 	},0)
}

二者联系

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

四. 异步编程的改进

Promise

异步回调:

  • 页面编程的一大特点。

异步编程模型:

  • 在执行一项耗时的任务时,比如下载网络文件任务、获取摄像头等设备信息任务, 这些任务都会放到页面主线程之外的进程或者线程中去执行,等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,并触发相关的回调操作
  • 产生原因:异步回调代码逻辑不连贯、不线性,不符合人的直觉,影响了我们的编码方式,从而出现 Promise 封装异步代码
使用
function XFetch(request) {
 	function executor(resolve, reject) {
 		let xhr = new XMLHttpRequest()
 		xhr.open('GET', request.url, true)
 		xhr.ontimeout = function (e) { reject(e) }
 		xhr.onerror = function (e) { reject(e) }
 		xhr.onreadystatechange = function () {
 			if (this.readyState === 4) {
 				if (this.status === 200) {
 					resolve(this.responseText, this)
 				} else {
 					let error = {
 						code: this.status,
 						response: this.response
 					}
 					reject(error, this)
 				}
 			}
 		}
 		xhr.send()
 	}
	return new Promise(executor)
}

var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))
var x2 = x1.then(value => {
 	console.log(value)
 	return XFetch(makeRequest('https://www.geekbang.org/column'))
})
var x3 = x2.then(value => {
 	console.log(value)
 	return XFetch(makeRequest('https://time.geekbang.org'))
})
x3.catch(error => {
 	console.log(error)
})

说明

  1. 引入 Promise,在调用 XFetch 时,会返回一个 Promise 对象。
  2. 构建 Promise 对象时,需要传入一个executor 函数,XFetch 的主要业务流程都在 executor 函数中执行。
  3. 如果运行在 excutor 函数中的业务执行成功了,会调用 resolve 函数;
  4. 如果执行失败 了,则调用 reject 函数。
  5. 在 excutor 函数中调用 resolve 函数时,会触发 promise.then 设置的回调函数;而调用 reject 函数时,会触发 promise.catch 设置的回调函数
功能
  1. 消灭嵌套调用
    1.1 回调函数延时绑定
    1.2 将回调函数创建好的 Promise 对象返回到最外层
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
  resolve(100)
}
let x1 = new Promise(executor)
//x1 延迟绑定回调函数 onResolve
function onResolve(value){
  console.log(value)
  let x2 = new Promise((resolve, reject) => {
      resolve(value + 1)
  })
  console.log(x2)
  return x2
}
let x2 = x1.then(onResolve)
x2.then((value) => {
  console.log(value)
  console.log(x2)   
})
  1. 合并多个错误
    2.1 冒泡性质,使用最后一个对象捕获所有异常
function executor(resolve, reject) {
  let rand = Math.random();
  console.log(1)
  console.log(rand)
  if (rand > 0.5)
  	resolve()
  else
  	reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
  console.log("succeed-1")
  return new Promise(executor)
})
var p3 = p1.then((value) => {
  console.log("succeed-2")
  return new Promise(executor)
})
var p4 = p3.then((value) => {
  console.log("succeed-3")
  return new Promise(executor)
})
p4.catch((error) => {
  console.log("error")
})
console.log(2)

async/await

使用同步代码实现异步访问资源的能力,并且使得代码逻辑更 加清晰

生成器 vs 协程
  1. 生成器函数:带星号函数,可以暂停执行和恢复执行,协程的实现

    1.1 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
    1.2 外部函数可以通过 next 方法恢复函数的执行。

  2. 协程:一种比线程更加轻量级的存在,是跑在线程上的任务。

    一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。

    如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程

function* genDemo() {
 	console.log(" 开始执行第一段 ")
 	yield 'generator 2'
 	console.log(" 开始执行第二段 ")
 	yield 'generator 2'
 	console.log(" 开始执行第三段 ")
 	yield 'generator 2'
 	console.log(" 执行结束 ")
 	return 'generator 2'
}
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)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

说明

  • 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。

  • 要让 gen 协程执行,需要通过调用 gen.next。

  • 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。

  • 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程, 并将 return 后面的内容返回给父协程。

  1. 协程配合Promise

    执行器:执行生成器的代码封装成的函数

//foo 函数 同步代码实现异步操作
function* foo() {
 	let response1 = yield fetch('https://www.geekbang.org')
 	console.log('response1')
 	console.log(response1)
 	let response2 = yield fetch('https://www.geekbang.org/test')
 	console.log('response2')
 	console.log(response2)
}

/*
	执行器 co框架
	co(foo());
*/

// 执行 foo 函数的代码
let gen = foo() // 创建 gen 协程
function getGenPromise(gen) {
 	return gen.next().value // 主线程的控制权交给父协程
}
getGenPromise(gen).then((response) => {
    console.log('response1')
 	console.log(response)
 	return getGenPromise(gen)
}).then((response) => {
 	console.log('response2')
 	console.log(response)
})

async

异步执行、隐式返回 Promise

  • 隐式返回 Promise
async function foo() {
 return 2
}
console.log(foo()) // Promise {<resolved>: 2}
await
async function foo() {
 	console.log(1)
 	let a = await 100 // 1. 创建一个Promise对象,主线程控制权转交给父协程执行,并返回 							promise_对象
 	console.log(a) // 4. foo 协程激活之后,会把刚才的 value 值赋给变量 a,然后 foo 协						程继续执行后续语句,执行完成之后,将控制权归还给父协程
 	console.log(2)
}
console.log(0)
foo() 
// 2. 调用 promise_.then 来监控 promise状态
console.log(3)
// 3. 父协程执行结束,执行微任务队列里的 resolve(100) ,触发 promise_.then 中的回调函数,将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程

/*
	0
	1
	3
	100
	2
*/

学习资料

李兵:《15丨消息队列和事件循环:页面是怎么“活”起来的?》

​ 《16丨WebAPI:setTimeout是如何实现的?》

​ 《17丨WebAPI:XMLHttpRequest是怎么实现的?》

​ 《18丨宏任务和微任务:不是所有任务都是一个待遇》

​ 《19丨Promise:使用Promise,告别回调函数》

​ 《20丨async-await:使用同步的方式去写异步代码》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值