前言
一、事件循环(Event Loop):JavaScript 采用单线程模型,即同一时间只能执行一个任务。为了处理多个任务,JavaScript 使用了事件循环机制。事件循环会不断地从任务队列中取出任务来执行,执行完一个任务后再取出下一个任务,如此循环往复。当所有任务都执行完毕后,事件循环会等待新的任务加入队列。
JavaScipt 中的事件循环(event loop),以及微任务 和宏任务的概念
说事件循环(event loop)之前先要搞清楚几个问题。
1. js为什么是单线程的?
试想一下,如果js不是单线程的,同时有两个方法作用dom,一个删除,一个修改,那么这时候浏览器该听谁的?由此js同一时间只能执行一个功能
js的script标签会导致渲染的过程阻塞吗?
答案:会的,因为js是单线程只能一个一个的去执行。
如何解决js在渲染的过程的阻塞问题?
答:
1.可以使用内联样式
<body>
<div class="a">
<p>11</p>
<span>22</span>
<h2></h2>
</div>
<script>
var oDiv = document.querySelector('.a');
var oSpan = document.querySelector('span');
var oStrong = document.createElement('strong');
oStrong.innerHTML = '888'
oDiv.replaceChild(oStrong, oSpan) //用oStrong替换oSpan
</script>
</body>
2.外联样式引入js要加async或者defer,否则会导致js的阻塞
<!-- 使用async属性 -->
<script async src="index.js"></script>
<!-- 使用defer属性 -->
<script defer src="index.js"></script>
async和defer这两者有什么区别?
async
和defer
属性在HTML的<script>
标签中都用于异步加载JavaScript,但它们之间存在一些重要的区别。
- 执行时机:
async
属性的脚本会立即下载,并在下载完成后立即执行。这意味着脚本的执行可能会在主线程上阻塞,从而影响页面的渲染。而defer
属性的脚本会等到整个HTML文档解析完成后才执行。因此,使用defer
可以确保脚本在执行前,DOM已经完全加载和解析完成。 - 加载顺序:对于
async
属性的脚本,没有明确的加载顺序,可能会并行加载多个脚本。而defer
属性的脚本会按照它们在HTML文档中出现的顺序进行加载和执行。 - 兼容性:
async
属性是HTML5中引入的,因此在一些较旧的浏览器中可能不被支持。而defer
属性在所有主流浏览器中都有良好的兼容性。
总结:根据这些区别,你可以根据需要选择使用async
或defer
属性。如果你希望脚本尽快下载并执行,并且不关心脚本的加载顺序,可以选择使用async
属性。如果你希望脚本在DOM完全加载和解析完成后执行,并且需要按照一定的顺序加载脚本,那么使用defer
属性是更好的选择。
想要执行快一点就用async,慢一点就用defer
事件循环也是遵循的宏任务与微任务的,那什么是宏任务?什么是微任务?
直接举例子:
<script>
console.log('同步任务1'); // 宏任务
setTimeout(function () {
console.log('异步任务1'); // 宏任务
}, 0);
Promise.resolve().then(function () {
console.log('同步任务2'); // 微任务
}).then(function () {
console.log('同步任务3'); // 微任务
});
console.log('同步任务4'); // 宏任务
</script>
一、宏任务(Macro Task)
宏任务是指由 JavaScript 主线程执行的任务,它包括但不限于以下情况:
-
包括script整个脚本
-
浏览器事件(如 click、mouseover 等)
-
定时器任务(如 setTimeout 和 setInterval)
-
页面渲染(如 回流或重绘)
-
事件回调(如 I/O、点击事件等)
-
网络请求 (如 XMLHttpRequest 和 fetch 等)
宏任务通常独立于当前任务,并按顺序排队执行。以下是一些常见的代码示例来说明宏任务的概念和用法:
示例 1: 使用事件监听器创建宏任务
javascript
复制代码
// 事件监听器创建宏任务
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked");
});
console.log("Waiting for button click...");
解释:在这个示例中,等待按钮点击的语句是同步任务,而当按钮被点击时,事件回调函数会作为宏任务被执行。
输出结果为:
示例 2: 使用定时器创建宏任务
javascript
复制代码
// 定时器任务
console.log("Start");
setTimeout(() => {
console.log("In Timeout");
}, 2000);
console.log("End");
// 事件监听器创建宏任务
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked");
});
console.log("Waiting for button click...");
解释:在这个示例中,打印 “Start” 和 “End” 的语句是同步任务,而通过 setTimeout 创建的回调函数被作为宏任务,在 2000 毫秒后才执行,所以在执行宏任务之前会先输出同步任务的结果。
输出结果为:
示例 3: 页面渲染
javascript
复制代码
console.log("Start");
// 修改页面样式
document.body.style.backgroundColor = "red";
console.log("End");
解释:在这个示例中,修改页面样式是一个宏任务,当样式被修改后,浏览器会执行重新渲染页面的操作。
输出结果为:
示例 4: 使用 XMLHttpRequest 发起网络请求
javascript
复制代码
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.addEventListener("load", () => {
console.log("Request completed");
});
xhr.send();
console.log("Waiting for request to complete...");
解释:在这个示例中,使用 XMLHttpRequest 发起网络请求是一个宏任务。当请求完成后,load 事件回调函数会作为宏任务被执行。
输出结果为:
总结: 宏任务的使用广泛,包括定时器任务、网络请求、事件监听器等。理解宏任务的概念和用法可以帮助我们正确处理 JavaScript 中的异步操作,并合理安排任务的执行顺序,以提高应用的性能和用户体验。
二、 微任务(Micro Task)
微任务是指由 JavaScript 引擎执行的任务,它在宏任务之后执行,但在下一次渲染之前执行。微任务通常是由宏任务中的某个特定任务触发的,并立即执行。常见的微任务有:
- Promise 的回调函数
- Async/Await 函数
- MutationObserver 的回调函数
- process.nextTick(Node.js 环境下)
示例 1:微任务的执行顺序
还是沿用本文章开头所使用的代码示例,说明微任务的执行顺序:
javascript
复制代码
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
});
Promise.resolve().then(() => console.log("4"));
console.log("5");
解释:
- 在第一个宏任务中,同步的打印语句
1
和5
首先执行。 - 然后,第一个宏任务中使用
setTimeout
创建了一个回调函数,它被添加到宏任务队列中等待执行。 - 在第一个宏任务执行结束前,微任务队列中的回调函数执行。
Promise.resolve().then(() => console.log('4'))
的回调函数首先被添加到微任务队列中,因此会在2
之前执行,打印4
。 - 当第一个宏任务任务队列为空时,开始执行第二个宏任务,打印
2
。 - 然后,Promise 的回调函数
Promise.resolve().then(() => console.log('3'))
会被添加到微任务队列中等待执行。 - 在本轮事件循环中,微任务队列中的任务会按序执行,因此打印
3
。
输出结果为:
结论:
微任务是 JavaScript 中处理异步操作的一种机制,它通过及时响应并在当前任务结束后立即执行,有助于编写更高效和灵活的异步代码。了解微任务的概念和用法能够帮助我们更好地利用异步特性,提升代码的性能和用户体验。
三、宏任务与微任务的区别
宏任务和微任务主要在两个方面有所区别:执行时机和调度机制
1. 执行时机
- 宏任务:宏任务是由 JavaScript 引擎在执行栈(执行同步任务)和任务队列中的任务之间切换时执行的。宏任务在下一个宏任务之前执行,并按照宏任务队列的顺序执行。
- 微任务:微任务是在宏任务执行结束,下一个宏任务开始之前执行的任务。微任务在当前宏任务中执行完后立即执行,并按照微任务队列的顺序执行。
宏任务在主线程上执行,而微任务在宏任务执行完毕之后执行,即在下一轮事件循环的步骤中执行,这也是为什么微任务会在宏任务之前执行的原因。
2. 调度机制
- 宏任务:宏任务由 JavaScript 引擎的任务调度器调度执行。当主线程执行完当前宏任务后,会检查是否存在微任务,如果存在,则会执行所有微任务,然后选择下一个宏任务执行。
- 微任务:微任务同样由 JavaScript 引擎的任务调度器调度执行。当微任务队列中存在微任务时,会依次执行微任务,直到微任务队列为空。
宏任务使用先进先出的调度机制,即它们按照任务的顺序排列,并按顺序执行。
而微任务则使用一个任务队列(microtask queue)进行调度,当某个宏任务执行完毕后,会立即将所有的微任务添加到任务队列中,并按照先进先出的顺序依次执行。
宏任务和微任务的区别在于它们的执行机制和调度机制。宏任务在下一个宏任务执行之前执行,而微任务在当前宏任务结束后立即执行。微任务优先级高于宏任务,因此在同一轮事件循环中,微任务会优先执行。了解宏任务和微任务的区别对于编写高效的异步 JavaScript 代码非常重要。
仅凭文字描述要理解这两个机制并不容易,因此通过下面的事件循环机制的的说明消化一下这两个机制。
四、事件循环机制(Event Loop)
如上图,当同步任务执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。
事件循环机制的整体执行流程如下:
- 执行同步任务:JavaScript 代码从上到下逐行执行同步任务,直到遇到第一个异步任务。
- 处理微任务:请注意,当遇到一个微任务时,将其添加到微任务队列中,而不是立即执行。
- 执行宏任务:当同步任务执行完毕或遇到第一个微任务时,执行宏任务队列中的第一个任务。执行宏任务时,如果遇到嵌套的微任务,也会将其添加到微任务队列中等待执行。
- 执行微任务:执行完一个宏任务后,立即处理微任务队列中的所有任务,按照顺序依次执行。
- 重复上述步骤:不断地循环执行上述步骤,直到任务队列为空。
需要注意的是:微任务比宏任务优先级要高。
在同一个任务中,如果既有微任务又有宏任务,那么微任务会先执行完毕。
在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任务。
总结:事件循环的执行顺序:先执行同步任务,再执行宏任务,下一步询问是否执行完毕宏任务了?如果没有就继续执行完宏任务,再执行微任务,微任务执行完后就退出。
——————————————————–
什么是信息队列?
信息队列是在消息的传输过程中保存消息的容器。
消息队列简介
消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。
消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。
“消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。
消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
js发布订阅原理,代码解析
发布订阅
1.发布订阅原理
2.js代码实现
3.js代码实现原理
发布订阅原理
1.将要处理的时间放入时间队列中存储(订阅)
2.将事件队列中存储的事件,按照要求进行统一的执行(发布)
js代码实现
var dep= {};
dep.list= []; //此数组用来存放订阅事件
//定义listen监听函数 将事件根据 key值来进行区分,存储 (将一类的key的事件存储在一起)
dep.listen = function(key , fn){
if(!this.list[key]){
this.list[key]=[]
}
//将一类的key的事件存储在一起 通过key的值进行分类,将相同key的事件存储在一个单独的数组中
this.list[key].push(fn);
}
//发布事件
dep.trigger = function(){
var key = Array.prototype.shift.call(arguments) //取出实际参数的第一个参数 (key值)
var fns = this.list[key]; //根据key值,获取队列中对应的事件数组
if(! fns || fns.length==0){
return
}
for(var i=0,fn;fn = fns[i++];){
fn.apply(this,arguments) //循环执行数组中的事件
}
}
//测试数据 进行订阅
dep.listen('red',function(size){
console.log('小红颜色的尺寸'+size);
})
dep.listen('red',function(size){
console.log('小红颜色的尺寸'+size);
})
dep.listen('block',function(size){
console.log('小清颜色的尺寸'+size);
})
//进行发布 仅发布 key值为red的队列
dep.trigger('red',37)
js代码实现原理
1.队列通过数组dep实现,
2.每个key对应的一类js事件,每一个key都对应着单独的队列(数组实现)
3.通过key值的判断,取出对应的事件队列,循环执行队列中的事件
发布订阅模式的基本原理
JavaScript 发布订阅模式的基本原理是:有一个主题对象,该对象维护一个订阅者列表,当主题对象发生变化时,主题对象会遍历订阅者列表,调用每个订阅者的更新方法,通知订阅者进行相应的处理。
在 JavaScript 中,可以通过自定义事件和回调函数实现发布订阅模式。主题对象维护一个事件列表,每个事件对应一个或多个回调函数。当主题对象发生变化时,主题对象会触发相应的事件,调用该事件对应的所有回调函数,通知订阅者进行相应的处理。
以下是一个发布订阅模式的简单代码示例:
kotlin
复制代码
// 消息代理
class MessageBroker {
constructor() {
this.subscriptions = {};
}
subscribe(topic, callback) {
if (!this.subscriptions[topic]) {
this.subscriptions[topic] = [];
}
this.subscriptions[topic].push(callback);
}
publish(topic, data) {
if (!this.subscriptions[topic]) {
return;
}
this.subscriptions[topic].forEach((callback) => {
callback(data);
});
}
}
// 发布者
class Publisher {
constructor(broker) {
this.broker = broker;
}
publishMessage(topic, message) {
this.broker.publish(topic, message);
}
}
// 订阅者
class Subscriber {
constructor(broker, name) {
this.broker = broker;
this.name = name;
}
subscribeToTopic(topic) {
this.broker.subscribe(topic, (data) => {
console.log(`Subscriber ${this.name} received message: ${data}`);
});
}
}
// 使用示例
const broker = new MessageBroker();
const publisher = new Publisher(broker);
const subscriber1 = new Subscriber(broker, 'Alice');
const subscriber2 = new Subscriber(broker, 'Bob');
subscriber1.subscribeToTopic('news');
subscriber2.subscribeToTopic('weather');
publisher.publishMessage('news', 'Breaking news: the sky is blue');
publisher.publishMessage('weather', 'It will be sunny tomorrow');
发布订阅模式的应用场景
下面我们来举几个常见的发布订阅模式的应用场景和代码示例。
生产者 & 消费者关系
发布订阅模式适用于需要解耦生产者和消费者之间关系的场景,生产者只需要发布消息,而不需要关心哪些消费者会收到消息。消费者可以订阅自己感兴趣的主题,只有在该主题上有新的消息时才会收到通知。这样可以提高代码的灵活性和可维护性。
以下是一个基于发布订阅模式的具体场景和代码示例:
假设我们正在开发一个在线商城,需要实时更新商品价格和库存信息。我们可以使用发布订阅模式,在商品库存和价格发生变化时,自动向所有关注该商品的客户端推送更新。
kotlin
复制代码
// 消息代理
class MessageBroker {
constructor() {
this.subscriptions = {};
}
subscribe(topic, callback) {
if (!this.subscriptions[topic]) {
this.subscriptions[topic] = [];
}
this.subscriptions[topic].push(callback);
}
publish(topic, data) {
if (!this.subscriptions[topic]) {
return;
}
this.subscriptions[topic].forEach((callback) => {
callback(data);
});
}
}
// 商品类
class Product {
constructor(name, price, stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
setPrice(newPrice) {
this.price = newPrice;
this.broker.publish(`product/${this.name}/price`, this.price);
}
setStock(newStock) {
this.stock = newStock;
this.broker.publish(`product/${this.name}/stock`, this.stock);
}
setBroker(broker) {
this.broker = broker;
}
}
// 客户端类
class Client {
constructor(name) {
this.name = name;
}
subscribeToProduct(product) {
product.broker.subscribe(`product/${product.name}/price`, (data) => {
console.log(`Client ${this.name} received new price for ${product.name}: ${data}`);
});
product.broker.subscribe(`product/${product.name}/stock`, (data) => {
console.log(`Client ${this.name} received new stock for ${product.name}: ${data}`);
});
}
}
// 使用示例
const broker = new MessageBroker();
const product1 = new Product('Product 1', 100, 10);
const product2 = new Product('Product 2', 200, 20);
product1.setBroker(broker);
product2.setBroker(broker);
const client1 = new Client('Alice');
const client2 = new Client('Bob');
client1.subscribeToProduct(product1);
client2.subscribeToProduct(product2);
product1.setPrice(120);
product1.setStock(5);
product2.setPrice(180);
product2.setStock(10);
在上面的示例中,我们创建了一个消息代理 MessageBroker,以及两个商品 Product 和两个客户端 Client。商品类中的 setPrice 和 setStock 方法会在价格和库存发生变化时向代理发送消息,客户端类中的 subscribeToProduct 方法会订阅指定商品的价格和库存主题,并在收到消息时打印出来。在这个示例中,我们使用 console.log 来模拟消息的输出。
消息队列
以下是一个简单的消息队列场景的代码示例,实现了消息的生产和消费:
javascript
复制代码
class MessageQueue {
constructor() {
this.subscriptions = {};
this.queue = [];
}
subscribe(topic, callback) {
if (!this.subscriptions[topic]) {
this.subscriptions[topic] = [];
}
this.subscriptions[topic].push(callback);
}
publish(topic, data) {
if (!this.subscriptions[topic]) {
return;
}
this.subscriptions[topic].forEach((callback) => {
callback(data);
});
}
enqueue(message) {
this.queue.push(message);
}
dequeue() {
return this.queue.shift();
}
process() {
const message = this.dequeue();
if (message) {
this.publish(message.topic, message.data);
}
}
}
// 生产者
const producer = (queue) => {
setInterval(() => {
const message = { topic: 'test', data: new Date().toISOString() };
queue.enqueue(message);
console.log(`Produced message: ${JSON.stringify(message)}`);
}, 1000);
};
// 消费者
const consumer = (queue) => {
setInterval(() => {
queue.process();
}, 500);
};
// 使用示例
const queue = new MessageQueue();
queue.subscribe('test', (data) => {
console.log(`Consumed message: ${data}`);
});
producer(queue);
consumer(queue);
在上面的代码示例中,我们定义了一个 MessageQueue 类,实现了基本的消息队列功能,包括订阅、发布、入队、出队和处理。生产者通过调用 enqueue 方法将消息入队,消费者通过调用 process 方法从队列中取出消息并进行处理。在使用示例中,我们创建了一个消息队列,生产者每隔一秒钟向队列中添加一个消息,消息的内容是当前时间。消费者每隔半秒钟从队列中取出一个消息并输出到控制台。
当我们运行上面的代码示例时,可以看到生产者不断地向队列中添加消息,消费者不断地从队列中取出消息并输出到控制台,实现了一个基本的消息队列。
自定义事件系统
在一些大型的 Web 应用中,可能需要实现自定义的事件系统,以便进行组件间通信和数据交互。这时可以使用 JavaScript 发布订阅模式,将「发布-订阅中心」作为主题对象,将事件监听器作为订阅者,实现自定义事件系统。
示例代码:
javascript
复制代码
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listener) {
if (!this.events[event]) {
return;
}
const index = this.events[event].indexOf(listener);
if (index >= 0) {
this.events[event].splice(index, 1);
}
}
emit(event, ...args) {
if (!this.events[event]) {
return;
}
this.events[event].forEach((listener) => {
listener.apply(this, args);
});
}
}
// 使用示例
const emitter = new EventEmitter();
const listener1 = (msg) => {
console.log(`Listener 1 received: ${msg}`);
};
const listener2 = (msg) => {
console.log(`Listener 2 received: ${msg}`);
};
emitter.on('test', listener1);
emitter.on('test', listener2);
emitter.emit('test', 'test message 1');
// Output:
// Listener 1 received: test message 1
// Listener 2 received: test message 1
emitter.off('test', listener1);
emitter.emit('test', 'test message 2');
// Output:
// Listener 2 received: test message 2
结语
本文介绍了 JavaScript 发布订阅模式的基本原理、应用场景以及各场景的代码示例。在实际开发中,发布订阅模式可以用于解耦对象之间的依赖关系,提高代码的可维护性和可扩展性。不同的实现方式适用于不同的场景和框架,开发者可以根据需要选择合适的实现方式。同时,使用发布订阅模式也需要注意防止事件泄漏和内存泄漏等问题,保证代码的性能和稳定性。希望本文能够帮助读者更深入地了解 JavaScript 发布订阅模式,提高代码的质量和效率。
——————————————————-
什么是任务队列?
在前端开发中,任务队列是一个用于管理和执行异步任务的机制。任务队列是一种先进先出的数据结构,用于存储待执行的任务。这些任务可以是异步操作,如网络请求、定时器回调等。
任务队列中的任务按照它们被添加到队列的顺序排列。当主线程执行完一个任务后,它会查看任务队列是否有待执行的任务。如果有,它会从队列中取出一个任务并开始执行。这个过程会不断重复,直到任务队列为空。
在前端开发中,异步操作是常见的需求,因为这样可以避免阻塞主线程并提高应用程序的性能和响应性。通过使用任务队列,我们可以将异步操作组织起来,按照一定的顺序执行,并在适当的时候处理结果。
常见的任务队列实现包括Web Workers、WebSockets、Fetch API等。这些实现提供了不同的功能和特性,可以满足不同的异步操作需求。
如何使用任务队列来管理异步操作,以实现商品添加到购物车的功能?以下举例:
<script>
// 假设你已经获取到了商品数据,并将其存储在变量中
const product = {
id: 1,
name: "Product 1",
price: 100
};
// 创建一个任务队列
const taskQueue = [];
// 添加商品到购物车的函数
function addToCart(product) {
// 将任务添加到任务队列中
taskQueue.push(product);
// 执行任务队列中的任务
processQueue();
}
// 处理任务队列中的任务的函数
function processQueue() {
if (taskQueue.length === 0) {
// 任务队列为空,停止处理
return;
}
// 从任务队列中取出一个任务
const currentTask = taskQueue.shift();
// 发送网络请求将商品添加到购物车中
fetch('/api/cart', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ productId: currentTask.id })
})
.then(response => response.json())
.then(data => {
// 处理服务器返回的数据,更新UI等操作...
console.log('商品添加成功!');
})
.catch(error => {
// 处理网络请求错误等操作...
console.error('添加商品失败:', error);
});
}
// 调用函数将商品添加到购物车中
addToCart(product);
</script>
总结:任务队列就是用于存储待执行的任务。这些任务可以是异步操作,如网络请求、定时器回调等。
原文链接:https://juejin.cn/post/7316724521310650420 作者:用户7280034348197