木灵鱼儿
阅读:164
Event Loop事件队列和宏任务微任务
Event Loop
我们知道JavaScript是一门单线程的语言,他只能一行一行的执行代码,于是我们的代码应该都是同步的,这里我们暂时忘掉所有的异步,看一下这么做会有什么问题。
用户进入了我们的网页,点击了一个按钮,触发加载更多功能,此时浏览器发起ajax请求,如果我们的代码都是同步的,那么页面会在这个请求完成之前是卡主状态的,因为需要等待该代码的完成,此时用户什么都做不了,既不能滚动页面,也无法点击其他内容,如果这个请求需要60s的时间,用户肯定会觉得,这是什么狗屎页面。
这显然是不行的,因为代码阻塞导致体验特别的差,解决这个问题的办法就是加入异步功能,我们将请求的回调作为异步处理,不需要去同步等待它的完成,而是先去执行其他的内容。
当请求完成后再运行这个回调,那么如何协调同步与异步的运行,使用的就是事件循环Event Loop。
上图是一个简单的代码可视化运行环境的图,其中运行栈就是我们代码实际运行的地方,而web api则是我们的运行环境通过的一系列api钩子,比如setTimeout
,这是浏览器为我们提供的,而任务队列我们待会再讲它是做什么的。
console.log(1);
setTimeout(() => {
console.log(2);
},1000);
console.log(3);
我们看这段代码,首先他会将console.log(1)
压入运行栈中,运行,运行完毕后出栈,然后运行setTimeout
,注意,此时setTimeout其实存在于Web Api中,而不是在运行栈中。
接着console.log(3)
被压入运行栈中运行,然后完毕出栈。
我们的setTimeout
会被浏览器处理,当1000ms后,回调函数会被压入任务队列中。
此时,如果我们的运行栈是空的,那么就会去查询任务队列中是否存在待运行的代码,然后它发现了一个回调需要运行,此时运行回调函数,完毕后出栈。
所以上述代码最终的log输出顺序是:
// 1
// 3
// 2
我们的事件循环名字都写着循环了,所以上述的运行过程是在不断循环处理的,不是说代码运行完毕就结束了,它会不断的重复这个过程。
而任务队列是存放异步回调的地方,它会在运行栈空的时候被查询,然后获取一个任务,运行后,如果运行栈没有需要运行的了,又会去任务队列查询是否有需要运行的代码,如果无了又返回自己,空了又来查询,如此往复,形成了一个无限的循环,这个过程就称为事件循环。
上述的例子还是比较简单的,对于入门理解是足够的,但是随着我们的代码深入,你会发现,我们的异步代码有很多啊,比如:promise、setTimeout、ajax、onclick
,这些的执行顺序都是怎么样的,这里就得讲讲宏任务和微任务了。
宏任务和微任务
由于不同的异步代码并不相同,有的是定时器,有的是请求,有的是协议promise,他们的执行被划分为两个区域:宏任务区域、微任务区域。
我们可以将上面的任务队列拆分成两个容器,一个是存放宏任务的,一个存放微任务的。
宏任务:setTimeout
、setInterval
、DOM事件
、ajax请求
、setImmediate(node独有的)
、requestAnimationFrame(浏览器独有)
、IO
、UI render(浏览器独有)
微任务:Promise
、Object.observe
、MutationObserver
、process.nextTick(node 独有)
虽然看上去分类很多,其实不用太考虑一个日常用不到的东西,比如setImmediate这些,我们精简一下之后:
宏任务:setTimeout
、setInterval
、DOM事件
、ajax请求
、requestAnimationFrame
微任务:Promise
注意:这两个任务中,微任务优先级是最高的。
看这段代码:
console.log(1);
setTimeout(() => {
console.log("定时器");
});
new Promise((resolve) => {
console.log(2);
resolve();
console.log(3);
}).then(() => {
console.log("Promise1");
});
console.log(4);
log(1)
先被压入运行栈运行,完毕后出栈!
接着是触发webapi的setTimeout
,没有等待时间,回调被推入宏任务队列中。
触发new Promise,注意new Promise的回调函数执行还是同步的,所以log(2)
被推入栈中,完毕后出栈。
此时触发了resolve()
,then接收的回调被推入微任务队列中。
log(3)
被推入栈中,完毕后出栈。
log(4)
被推入栈中,完毕后出栈。
此时运行栈中已经没有需要运行的代码了,于是先去微任务队列中查询是否有需要运行的内容,发现then的回调,于是log("Promise1")
推入栈中运行,完毕后出栈。
此时运行栈又空了,再去微任务队列查询,发现微任务也空了,于是去宏任务队列查询,发现有需要运行的,于是log("定时器")
被推入栈中,完事后出栈。
最终我们的打印结果是:
// 1
// 2
// 3
// 4
// Promise1
// 定时器
这里你会发现,当微任务运行完一个后还会去微任务队列里面查询,这就会产生一个现象,如果我在运行微任务的是又往微任务里面追加一个任务,那么就会导致线程死循环了。
举个例子:
console.log(1);
setTimeout(() => {
console.log("定时器");
});
new Promise((resolve) => {
console.log(2);
resolve();
console.log(3);
})
.then(() => {
console.log("Promise1");
})
.then(() => {
console.log("Promise2");
})
.then(() => {
console.log("Promise3");
})
.then(() => {
console.log("Promise4");
})
.then(() => {
console.log("Promise5");
});
console.log(4);
当一个then的回调运行完毕后,会将下一个then的回调传入微任务中去,于是我们会发现打印顺序是这样的:
// 1
// 2
// 3
// 4
// Promise1
// Promise2
// Promise3
// Promise4
// Promise5
// 定时器
如果我们的微任务无限添加下去,定时器的回调就永远无法运行,所以我们需要注意这点。
而且我们需要注意一点,不管是什么队列,每次拿取都只会拿取一个,运行完毕后再重复整个循环过程。
示例:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
setTimeout(() => {
console.log(4);
});
});
Promise.resolve().then(() => {
console.log(5);
});
console.log(6);
当我们在宏任务中创建了一个微任务,一个新的宏任务时,在该宏任务运行结束后,如果运行栈空了,他还是会先从微任务队列中查询,所以上述代码的打印顺序是:
1
6
5
2
3
4
略有特别的DOM事件
对于dom事件,我们可能需要通过一个小例子来进行记忆。
在页面中我们存在一个按钮:
<button id="btn">点击我</button>
加入JavaScript代码:
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
console.log(1);
Promise.resolve().then(() => console.log(2));
});
btn.addEventListener("click", () => {
console.log(3);
Promise.resolve().then(() => console.log(4));
});
当用户点击按钮时,我们的执行顺序是怎么样的呢?利用我们刚刚学到的知识。
首先是两个dom事件被触发,他们的回调被传入宏任务队列中。
此时运行栈和微任务都是空的,于是从宏任务队列中拿取第一个回调函数触发。
log(1)
入栈出栈。
创建了一个微任务压入微任务队列中去。
此时运行栈为空,于是先去微任务队列中查询,拿到了刚刚压入的内容,运行回调,log(2)
打印。
此时运行栈又空了,微任务队列也空了,在宏任务队列中拿到第二个回调运行。
log(3)
入栈出栈。
创建了一个微任务压入微任务队列中去。
此时运行栈为空,于是先去微任务队列中查询,拿到了刚刚压入的内容,运行回调,log(4)
打印。
代码结束。
最后的打印顺序如我们所料:
// 1
// 2
// 3
// 4
但是,如果我们通过js去触发click,故事就会变得魔幻起来。
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
console.log(1);
Promise.resolve().then(() => console.log(2));
});
btn.addEventListener("click", () => {
console.log(3);
Promise.resolve().then(() => console.log(4));
});
btn.click();
我们的输出结果是:
// 1
// 3
// 2
// 4
震惊!!!为什么???
当我们手动执行click事件时,处理方式会不同,我们可以这么去理解:
() => {
click1(); //第一个click回调
click2(); //第二个click回调
}
他会将按钮的两个click事件按顺序执行,而不是像上述所说的过程,两个事件回调被压入微任务队列中。这就是他们不同的地方。
由于是两个任务,在log(1)
打印完成,微任务也压入了队列中的时候,并不是结束一个循环回合,因为下面还有click2的代码没有运行,运行栈没有清空,所以继续运行click2的内容,于是log(3)
打印完成,微任务压入新的内容。
此时btn.click()
运行结束了,运行栈清空了,于是查询微任务队列,运行刚刚压入的两个回调。
现在明白了吧!
我们再看一个例子:
<a href="https://www.mulingyuer.com" target="_blank" id="alink">点击跳转</a>
我们现在有一个a链接
const alink = document.getElementById("alink");
const promise1 = new Promise((resolve) => {
alink.addEventListener("click", resolve, { once: true });
});
promise1.then((event) => {
event.preventDefault();
console.log("阻止默认行为");
});
// alink.click();
当用户点击时,我们是可以正常阻止默认行为的,也就是链接跳转的行为被阻止了,但是当我们通过js去触发click事件时,链接会被正常跳转。
a链接被阻止跳转是因为他会判断evnet对象是否是canceled
状态,当我们调用了event.preventDefault()
时,就会将evnet对象标记为canceled,所以它无法跳转。
正常情况下,会在所有click事件结束后判断evnet对象,我们可以理解为这个判断是一个宏任务。
于是then的回调会先触发,evnet对象被标记canceled,跳转被阻止。
但是直接通过alink.click()
调用的事件,它在标准规范中的定义不同,他会同步执行完所有事件回调,也就是我们上面刚演示的那种:
() => {
click1(); //第一个click回调
click2(); //第二个click回调
}
执行完后会直接判断evnet对象,此时它不是一个宏任务了,是一个同步的代码,类似于:
() => {
click1(); //第一个click回调
click2(); //第二个click回调
if(event.canceled) return;
//链接跳转处理
***
}
此时我们的微任务根本没有运行,所以无法阻止跳转,当它运行时就已经来不及了。
其实本质上还是上面说的那种情况,事件回调被依次执行,不再是一个个宏任务了,只不过a链接有些特殊,它的跳转判定方式在不同的调用情况下会有不同。
版权申明
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。
相关推荐
Promise失败重试,可指定重试次数
//模拟异步请求 function axiosFn() { return new Promise((resolve, reject) => { const flge = Math.random(); //随机值 setTimeout(() => { //大于0.7就是成功 if (flge > 0.7) { return resolve(flge); } else { return reject(...
手写Promise
/* * @Author: mulingyuer * @Date: 2021-12-30 22:06:58 * @LastEditTime: 2022-01-03 05:22:30 * @LastEditors: mulingyuer * @Description: 手写promise * @FilePath: \undefinedc:\Users\13219\Desktop\promise.js * 怎么可能会有bug!!! */ /** * @description: 自定义promise * @param {fucntion} executor 执行器函数(同...
promise 队列
数组map实现function fn1() { return new Promise((resolve, reject) => { setTimeout(() => { console.log(1); resolve(); }, 500) }) } function fn2() { return new Promise((resolve, reject) => { setTimeout(() => { console.l...
利用Promise实现一个超时结束等待操作
promise如果没有指定状态,那么就一直会处于pending中,如果长时间不处理,那么这个东西会一直存在于内存中,显然是不合理的。如果是一个超多请求项目,那么我们就需要考虑下性能问题了。promise中有一个rece方法,它接收一个promise作为值的数组,它的特性就是:哪个promise先执行,他就处理那一个,不管是resolve还是reject;在then中,他也只有一个值,不同于Promise.all方法返回的是一个数组,rece返回的值是最快完成的那个promise的返回值。利用这个特性,我们可以制作一个超时处理。function delayPromise(promise, ...
异步队列管理器
造这个轮子其实也是没得办法,搜不到合适的轮子用,就只能自己干了。使用场景我们有N个异步任务promise,他们没有顺序关系,谁先触发都无所谓,但是我们只关心一点,如果某一个任务出错,后续就不要运行了,只有全部都success完成,那么才运行成功后的处理方式then。当然,我们肯定不能使用Promise.all运行N个任务,这等于是同步触发了,如果我有2000个任务,难道你也一口气发2000个任务,那这就不现实了,所以这里我们要引入线程概念,一个进程可以有多个线程,那么进程就是管理器,线程就是我们一次可以发多少个请求。线程是可以配置的,我们可以指定触发多少个线程,当1个线程完成后,我们要填...
Promise
基本了解Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。Promise对象有以下两个特点。(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfil...
koa框架1 基础入门之promise
promise实际是一种格式(封装格式),所有的方法按照promise的要求写,像jq的ajax方法,es6的fetch,axios这些,都是使用了promise标准。他的原理大概如下let p = new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.open("GET", "xxx/api", true); xhr.send(); xhr.onreadystatechange = function() { ...
快速响应的用户界面
ui线程每个浏览器用于执行js代码和更新ui的进程就只有一个,这个被称为‘ui线程’,它基于一个简单的队列系统,也就是根据代码的先后循序将代码插入队列并执行。一般来说,当用户点击一个按钮botton的时候,就会触发ui线程,他会创建两个任务并添加到队列中,第一个任务是更新ui按钮,它需要改变外观来表示它被点击了,然后再触发onclick事件,如果你对这个事件进行了调用其他函数,就会执行所调用的函数。事实上,大多数浏览器会在js代码运行时停止把新的任务加入ui队列中去,也就是说当用户点击一个事件,这个事件所调用的函数运行时间过长,那么在运行的这段时间中用户再点击其他的事件是没有交互的,产生...