执行环境中的JS

璃安猫: 本文参考阮一峰老师的网络日志、朴灵老师对其的点评及其他学习资料文档整理出的自己对Event Loop的理解,如有不对之处请多指正。

一些前提知识

JS组成

我们常说JS由三个部分组成,它们分别是ECMAScript、DOM以及BOM。

实际上这应该基于浏览器环境或者说Web平台。

DOM是操作HTML文档的API BOM是操作浏览器的API,因此我认为BOM及DOM是Web平台的API。

但JS是可以在Node平台上单独运行的,此时JS组成是ECMAScript与Node API。

因此总的来说JS组成是ECMAScript与运行环境的API。

执行模式

前端人都知道JS语言是单线程,所有任务需要排队,前一个任务结束,才会执行后一个任务。

那说的同步模式和异步模式又是什么情况呢?

这里说的同步模式与异步模式指的是执行环境运行JS时的工作模式,而不是JS的工作模式。

同步模式

同步模式是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,这也是JS单线程的工作模式。

同步模式中的任务称为同步任务,即任务开启后会立即执行任务内容直到任务完成。

以下为同步模式下代码执行过程解析

//首先将以下代码添加到一个匿名函数内  然后逐行执行

// 1.压入调用栈 执行完毕后出栈
console.log('global begin')


//函数 / 变量的声明不会产生调用
function bar () {
    // 2.2.1 打印结果 执行完毕出栈
	console.log('bar task')
}

function foo() {
    // 2.1 打印结果
	console.log('foo task')
    // 2.2 bar函数调用 压入调用栈  执行完毕后出栈
	bar()
}

// 2. foo函数调用 压入调用栈  
foo()

// 3. 最后压入调用栈 执行完毕后清空调用栈
//此时为纯同步的任务执行情况   
//存在问题: 
//存在阻塞——若中间某一任务执行时间过长,则后续任务则会被延迟执行
console.log('global end')

异步模式

异步模式是将任务开启过后就会执行下一个任务,开启的任务后续逻辑一般会通过回调函数的方式定义。

而该模式中的任务称为异步任务,即任务开启后不会立即执行任务内容,而会通过回到函数的方式来执行任务内容。

因此使用异步模式用于解决耗时任务的问题 如Ajax node.js 的大文件读写,用户事件(如setTimeout、onClick等)等作为异步任务执行。

以下为异步模式下代码执行过程解析

// 1.压入调用栈 执行完毕后出栈
console.log('global begin');

// 2. 将计数器放入API计时后继续执行
setTimeout( function timer1() {
  // 7. API计时器结束则进入事件回调 
  // 压入调用栈 执行完毕后出栈
	console.log('timer1 inxole')
}, 1800)

// 3. 将计数器放入API计时后继续执行
setTimeout(function timer2() {
  // 5. API计时器结束则进入事件回调 
  // 压入调用栈 执行完毕后出栈
	console.log('timer1 invoke')
  
  // 6. 将计数器放入API计时后继续执行
	setTimeout(function inner() {
    // 8. API计时器结束则进入事件回调 
    // 压入调用栈 执行完毕后出栈
		console.log('inner invoke')
	}, 1000)
}, 1000)

// 4.压入调用栈 执行完毕后出栈
console.log('global end')

//事件回调
//API计时器结束则进入事件回调 
//以上例子 先结束timer2() 然后将inner的计数器放入API
//接下来timer1()结束 执行结束后出栈
//inner计数器结束后  执行结束后出栈

执行环境执行JS时将任务分为同步任务与异步任务并用不同的工作模式执行。
执行环境主线程一开始会以同步模式执行JS,当执行到异步任务时,会将其交给执行环境API执行,而主线程继续执行下一个任务。而不是等待任务队列将该任务的执行结果返回后再继续执行。

Event Loop

Event Loop为事件循环,主要讲的其实是JS执行环境异步模式下的工作机制,在将异步任务开启后续如何执行其回调, 这一过程可不断循环。

Evenet Loop

上图中完整描述了事件循环的过程。
主线程执行JS时会产生堆和栈,而JS执行命令都会将命令放入调用栈中,执行完毕后则将该命令出栈。
完成的过程描述大致如下:

  1. 主线程逐条执行JS事件,同步任务压入调用栈后等执行完毕则将其出栈。
  2. 执行到异步任务(如定时器、用户事件等)时,将其交给Web(执行环境)API等待异步任务触发(如定时器计时完毕或用户触发事件),然后调用栈将异步任务出栈继续往下执行,直到主线程事件执行完毕。
  3. 而WebAPI的异步任务触发后则会将回调事件放入消息队列(回调队列)中,此处消息队列中回调事件的顺序并不是按照WebAPI中异步任务排队的顺序,而是根据异步任务触发的先后顺序。
  4. 主线程的事件执行完毕后,从消息队列中按顺序将回调事件压入调用栈,此后不断循环。

宏任务与微任务

ES6后,由于Promise函数的出现,异步任务又分为了宏任务与微任务。

宏任务指的是回调队列(消息队列)中的任务,宏任务当中有额外的任务(如绝大部分的异步调用)则会作为一个新的宏任务进到队列中排队

微任务是直接在当前任务结束过后立即执行,因此比宏任务的优先级高

// 事件循环代码示意
// 主线程
while(macroQueue.waitForMessage()) {
    // 1. 执行完调用栈上当前的宏任务(同步任务)
    // call stack
    
    // 2. 遍历微任务队列, 把微任务队里上的所有任务都执行完毕(清空微任务队列)
    // 微任务又可以往微任务队列中添加微任务
    for(lei i = 0; o < microQueue.length; i++) {
        // 获取并执行下一个微任务(先进先出)
        microQueue[i].processNextMessage()
    }
    
    // 3. 渲染(渲染线程)
    
    // 4.从宏任务队列中取 一个 任务,进入下一个消息循环
    macroQueue.processNextMessage()
}

任务队列和微任务队列的区别

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务,在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行

  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会被依次执行。不同的时它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务

产生宏任务的方式

  • script中的代码块
  • setTimeout()
  • setInterval()
  • setImmediate() (非标准 IE和Node.js)
  • 注册事件

产生微任务的方式

  • Promise
  • MutationObserver 监视DOM的变化
  • queueMicrotask() 简单的执行一个微任务

使用微任务的时机和原因

微任务的执行时机,晚于当前本轮事件循环的Call Stack(调用栈)中的代码(宏任务),早于事件处理函数和定时器的回调函数

使用原因:

  • 减少操作中用户可感知到的延迟
  • 确保任务顺序的一致性,即便当结果或数据是同步可用
  • 批量操作的优化
//确保任务顺序的一致性  此时不一致
customElement.prototype.getData = url => {
	if(this.cache[url]) {
		this.data = this.cache[url]
		this.dispatchEvent(new Event("load"))
	} else {
		fetch(url).then(result => result.arrayBuffer()).then(data => {
		this.cache[url] = data
		this.data = data
		this.dispatchEvent(new Event("load"))
		})
	}
}

element.addEventListener("load", () => console.log("Loaded data"))
console.log("Fetching data...")
element.getData()
console.log("Data fetched")
//确保任务的一致 使用微任务
customElement.prototype.getData = url => {
	if(this.cache[url]) {
		queueMicrotask(() => {
			this.data = this.cache[url]
			this.dispatchEvent(new Event("load"))
		})	
	} else {
		fetch(url).then(result => result.arrayBuffer()).then(data => {
		this.cache[url] = data
		this.data = data
		this.dispatchEvent(new Event("load"))
		})
	}
}
//批量操作
let messageQueue = []
let sendMessage = message => {
	messageQueue.push(message)
	if (messageQueue.length === 1) {
		queueMicrotask(()=> {
			const json = JSON.stringify(messageQueue)
			messageQueue.length = 0
			// fetch("url-of-receiver".json)
			console.log(json)
		})
	}
}

queueMicrotasj(() => {
	console.log('queueMicrotask')
})

sendMessage('刘备')
sendMessage('关羽')
sendMessage('曹操')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值