什么是事件循环?什么是信息队列?什么是任务队列?

前言
一、事件循环(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这两者有什么区别?

asyncdefer属性在HTML的<script>标签中都用于异步加载JavaScript,但它们之间存在一些重要的区别。

  1. 执行时机async属性的脚本会立即下载,并在下载完成后立即执行。这意味着脚本的执行可能会在主线程上阻塞,从而影响页面的渲染。而defer属性的脚本会等到整个HTML文档解析完成后才执行。因此,使用defer可以确保脚本在执行前,DOM已经完全加载和解析完成。
  2. 加载顺序:对于async属性的脚本,没有明确的加载顺序,可能会并行加载多个脚本。而defer属性的脚本会按照它们在HTML文档中出现的顺序进行加载和执行。
  3. 兼容性async属性是HTML5中引入的,因此在一些较旧的浏览器中可能不被支持。而defer属性在所有主流浏览器中都有良好的兼容性。

总结:根据这些区别,你可以根据需要选择使用asyncdefer属性。如果你希望脚本尽快下载并执行,并且不关心脚本的加载顺序,可以选择使用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");

解释

  • 在第一个宏任务中,同步的打印语句 15 首先执行。
  • 然后,第一个宏任务中使用 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)

什么是事件循环?什么是信息队列?什么是任务队列?

如上图,当同步任务执行完毕后,就会执行所有的宏任务,宏任务执行完成后,会判断是否有可执行的微任务;如果有,则执行微任务,完成后,执行宏任务;如果没有,则执行新的宏任务,形成事件循环。

事件循环机制的整体执行流程如下

  1. 执行同步任务:JavaScript 代码从上到下逐行执行同步任务,直到遇到第一个异步任务。
  2. 处理微任务:请注意,当遇到一个微任务时,将其添加到微任务队列中,而不是立即执行。
  3. 执行宏任务:当同步任务执行完毕或遇到第一个微任务时,执行宏任务队列中的第一个任务。执行宏任务时,如果遇到嵌套的微任务,也会将其添加到微任务队列中等待执行。
  4. 执行微任务:执行完一个宏任务后,立即处理微任务队列中的所有任务,按照顺序依次执行。
  5. 重复上述步骤:不断地循环执行上述步骤,直到任务队列为空。

需要注意的是:微任务比宏任务优先级要高。

在同一个任务中,如果既有微任务又有宏任务,那么微任务会先执行完毕。

在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任务。

总结:事件循环的执行顺序:先执行同步任务,再执行宏任务,下一步询问是否执行完毕宏任务了?如果没有就继续执行完宏任务,再执行微任务,微任务执行完后就退出。

——————————————————–

什么是信息队列?

信息队列是在消息的传输过程中保存消息的容器。

消息队列简介

消息队列(英语: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

(0)
上一篇 2023年12月27日 上午10:26
下一篇 2023年12月27日 上午10:37

相关推荐

发表回复

登录后才能评论