Zephyr poll

这篇具有很好参考价值的文章主要介绍了Zephyr poll。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

简介

  • 在Zephyr中,polling是一种等待事件的机制,它可以同时等待多个 IPC 事件。
  • 在Zephyr中,可以使用k_poll()函数来等待事件。k_poll() 函数接受一个k_poll_event数组,数组中的每个元素都描述了一个等待的事件。
  • 在等待期间,线程会被阻塞,直到所有事件都发生或等待超时。
  • 在等待结束后,k_poll() 函数会返回一个整数,返回0代表发生了一个或者多个事件,通过读取轮询状态进行判断,返回负数代表超时或者出错。
  • poll 中除了提供等待机制,还存在一种轻量级的同步对象 k_poll_signal,与队列,fifo,这些同步通信方式类型,但是它比较简单,只能向等待线程发送一个信号被触发的通知,再加上一个返回值,除此之外没有其他的功能。

struct k_poll_event

struct k_poll_event {
    /* _node 第一个成员,确保内存 k_poll_event 与双链表节点对齐,
     * 可以将 k_poll_event 插入到双链表中,当等待的事件还未发生时,
     * 调用 k_poll 的线程会将 k_poll_event 插入到目标对象中的 poll_events 双链表中,
     * 当目标对象发生指定事件时,IPC 对象会从 poll_events 取出节点并唤醒线程
     */
    sys_dnode_t _node;

    /* poller 不需要创建,它指向需要等待的线程中的 poller,
     * poller 代表的是线程的等待方式,调用 k_poll 函数时会自动设置对应的模式,
     * 同时将 k_poll_event 中的 poller 进行初始化(指向当前线程),
     * 除此之外 poller 还有一个额外的功能,此处 poller 记录的是线程中 poller 变量的地址,
     * 线程中 poller 变量相对于其 tcb 首地址的偏移是固定的,
     * 通过 poller 地址减去偏移可以获得对应线程 tcb
     */
    struct z_poller *poller;

    /* optional user-specified tag, opaque, untouched by the API */
    uint32_t tag:8;

    /* 等待的事件类型,此处类型需要与下方union中传入的指针类型相对应,
     * 否则会出现无法预料的错误
     */
    uint32_t type:_POLL_NUM_TYPES;

    /* 等待事件的状态,在调用轮询api之前应设置为 K_POLL_STATE_NOT_READY
     * 函数调用返回之后通过 state 的值判断对应的事件是否发生,
     * 如需再次使用需要重新初始化 state 为 K_POLL_STATE_NOT_READY
     */
    uint32_t state:_POLL_NUM_STATES;

    /* 轮询线程在可用时不获取对象的所有权 */
    uint32_t mode:1;

    /* unused bits in 32-bit word */
    uint32_t unused:_POLL_EVENT_NUM_UNUSED_BITS;

    /* 保存具体对象的地址,用于访问对象中的 poll_events 双链表 */
    union {
        void *obj;
        struct k_poll_signal *signal;
        struct k_sem *sem;
        struct k_fifo *fifo;
        struct k_queue *queue;
        struct k_msgq *msgq;
#ifdef CONFIG_PIPES
        struct k_pipe *pipe;
#endif
    };
};

struct k_poll_event 功能

  • struct k_poll_event 可以当作一个双链表节点,里面除了双链表的pre和next指针外,还包含等待的信息,将线程与IPC对象之间联系起来。

  • 当事件未发生,线程需要挂起时可以访问 IPC 对象中的 poll_events 链表,将 event 放入链表。文章来源地址https://www.toymoban.com/news/detail-408117.html

static inline void register_event(struct k_poll_event *event,
				 struct z_poller *poller)
{
    switch (event->type) {
    case K_POLL_TYPE_SEM_AVAILABLE:
        __ASSERT(event->sem != NULL, "invalid semaphore\n");
        add_event(&event->sem->poll_events, event, poller);
        break;

    /* ... */
    }

    event->poller = poller;
}
  • 当线程调用 k_poll 进行等待时,线程可以获取 IPC 状态判断对应事件是否发生,以决定是否需要继续等待。
static inline bool is_condition_met(struct k_poll_event *event, uint32_t *state)
{
    switch (event->type) {
    case K_POLL_TYPE_SEM_AVAILABLE:
        if (k_sem_count_get(event->sem) > 0U) {
            *state = K_POLL_STATE_SEM_AVAILABLE;
            return true;
        }
        break;

	/* ... */
    }

    return false;
}
  • 当 IPC 对象中发生指定事件时,会从 poll_events 链表中取出 event 节点,设置 event 的状态,然后把等待线程唤醒。
void z_handle_obj_poll_events(sys_dlist_t *events, uint32_t state)
{
    struct k_poll_event *poll_event;

    poll_event = (struct k_poll_event *)sys_dlist_get(events);
    if (poll_event != NULL) {
        (void) signal_poll_event(poll_event, state);
	}
}

static int signal_poll_event(struct k_poll_event *event, uint32_t state)
{
    struct z_poller *poller = event->poller;
    int retcode = 0;

    if (poller != NULL) {
        if (poller->mode == MODE_POLL) {
            /* 将等待的线程设置为就绪态 */
            retcode = signal_poller(event, state);
        } else if (poller->mode == MODE_TRIGGERED) {
            retcode = signal_triggered_work(event, state);
        } else {
            /* Poller is not poll or triggered mode. No action needed.*/
            ;
        }

        poller->is_polling = false;

        if (retcode < 0) {
            return retcode;
        }
    }

    /* 设置state */
    set_event_ready(event, state);
    return retcode;
}

轮询类型

  • 下面是Zephyr中支持的等待类型:
#define K_POLL_TYPE_IGNORE                0
#define K_POLL_TYPE_SIGNAL                Z_POLL_TYPE_BIT(_POLL_TYPE_SIGNAL)              // 1
#define K_POLL_TYPE_SEM_AVAILABLE         Z_POLL_TYPE_BIT(_POLL_TYPE_SEM_AVAILABLE)       // 2
#define K_POLL_TYPE_DATA_AVAILABLE        Z_POLL_TYPE_BIT(_POLL_TYPE_DATA_AVAILABLE)      // 4
#define K_POLL_TYPE_FIFO_DATA_AVAILABLE   K_POLL_TYPE_DATA_AVAILABLE                      // 4
#define K_POLL_TYPE_MSGQ_DATA_AVAILABLE   Z_POLL_TYPE_BIT(_POLL_TYPE_MSGQ_DATA_AVAILABLE) // 8
#define K_POLL_TYPE_PIPE_DATA_AVAILABLE   Z_POLL_TYPE_BIT(_POLL_TYPE_PIPE_DATA_AVAILABLE) // 16
  • Zephyr内核支持等待信号,信号量,队列,FIFO,msgq,管道这几种类型的事件。
  • 一般的IPC通信方式虽然会提供等待机制,但是其只能针对一种事件进行等待,而不能等待多个事件(此处的事件特指 IPC 事件),例如我们无法在等待信号量的同时等待队列,或者同时等待多个队列,Zephyr中为我们提供了Polling API,它可以同时等待多个事件到来。

轮询状态

#define K_POLL_STATE_NOT_READY           0
#define K_POLL_STATE_SIGNALED            Z_POLL_STATE_BIT(_POLL_STATE_SIGNALED)           // 1
#define K_POLL_STATE_SEM_AVAILABLE       Z_POLL_STATE_BIT(_POLL_STATE_SEM_AVAILABLE)      // 2
#define K_POLL_STATE_DATA_AVAILABLE      Z_POLL_STATE_BIT(_POLL_STATE_DATA_AVAILABLE)     // 4
#define K_POLL_STATE_FIFO_DATA_AVAILABLE K_POLL_STATE_DATA_AVAILABLE                      // 4
#define K_POLL_STATE_MSGQ_DATA_AVAILABLE Z_POLL_STATE_BIT(_POLL_STATE_MSGQ_DATA_AVAILABLE)// 16
#define K_POLL_STATE_PIPE_DATA_AVAILABLE Z_POLL_STATE_BIT(_POLL_STATE_PIPE_DATA_AVAILABLE)// 32
#define K_POLL_STATE_CANCELLED           Z_POLL_STATE_BIT(_POLL_STATE_CANCELLED)          // 8
  • 在调用k_poll之前需将状态设置为 K_POLL_STATE_NOT_READY,避免调用返回后读取到错误的状态。
  • 每个状态与轮询的类型一一对应,除此之外,如果在等待过程中其他线程调用了取消等待的函数,被等待的线程会被唤醒并返回一错误码。

初始化

#define K_POLL_EVENT_INITIALIZER(_event_type, _event_mode, _event_obj) \
	{ \
	.poller = NULL, \
	.type = _event_type, \
	.state = K_POLL_STATE_NOT_READY, \
	.mode = _event_mode, \
	.unused = 0, \
	{ \
		.obj = _event_obj, \
	}, \
	}

#define K_POLL_EVENT_STATIC_INITIALIZER(_event_type, _event_mode, _event_obj, \
					event_tag) \
	{ \
	.tag = event_tag, \
	.type = _event_type, \
	.state = K_POLL_STATE_NOT_READY, \
	.mode = _event_mode, \
	.unused = 0, \
	{ \
		.obj = _event_obj, \
	}, \
	}
  • 在Zephyr中提供了两个宏用于静态初始化。
    • _event_type 代表等待的事件类型。
    • _event_mode 固定为 K_POLL_MODE_NOTIFY_ONLY。
    • _event_obj 代表目标对象地址,可以是struct k_poll_signal、struct k_sem、struct k_fifo、struct k_queue、struct k_msgq、struct k_pipe类型的变量,传入的对象需与等待的类型一致。
void k_poll_event_init(struct k_poll_event *event, uint32_t type, int mode, void *obj);
  • 除了使用宏在定义变量时进行初始化之外,还可以使用 k_poll_event_init 进行初始化,它的功能和上面的两个宏功能一样,函数可以直接传入变量地址后即可初始化,而上面两个宏只能在变量定义时初始化,当执行完一次 k_poll 之后需要再次使用时,需要将 k_poll_event 中的 state 重设为 K_POLL_STATE_NOT_READY。
struct k_poll_event events[2] = {
K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_SEM_AVAILABLE,
                                        K_POLL_MODE_NOTIFY_ONLY,
                                        &my_sem, 0),
K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_FIFO_DATA_AVAILABLE,
                                        K_POLL_MODE_NOTIFY_ONLY,
                                        &my_fifo, 0),
};

struct k_poll_event events[2];

void some_init(void)
{
	k_poll_event_init(&events[0],
                          K_POLL_TYPE_SEM_AVAILABLE,
                          K_POLL_MODE_NOTIFY_ONLY,
                          &my_sem);
	k_poll_event_init(&events[1],
                          K_POLL_TYPE_FIFO_DATA_AVAILABLE,
                          K_POLL_MODE_NOTIFY_ONLY,
                          &my_fifo);

}

事件等待

  • k_poll 在 poll 整个过程中最重要的函数之一,用于等待事件发生。
int z_impl_k_poll(struct k_poll_event *events, int num_events,
		  k_timeout_t timeout)
{
	int events_registered;
	k_spinlock_key_t key;
	struct z_poller *poller = &_current->poller;

	poller->is_polling = true;
	poller->mode = MODE_POLL;

	__ASSERT(!arch_is_in_isr(), "");
	__ASSERT(events != NULL, "NULL events\n");
	__ASSERT(num_events >= 0, "<0 events\n");

	SYS_PORT_TRACING_FUNC_ENTER(k_poll_api, poll, events);
	
	/* register_events 首先会轮询 events 中的IPC对象,等待的事件是否已经发生,如果已经发生会将 poller->is_polling 设为 false,
	 * 如果等待的事件没有发生则会将 event 添加到对应的 IPC 对象中的 poll_events 链表中, 等待事件来临并唤醒被挂起的线程
	 */
	events_registered = register_events(events, num_events, poller,
					    K_TIMEOUT_EQ(timeout, K_NO_WAIT));

	key = k_spin_lock(&lock);

	/* 如果poller->is_polling为false,说明已经有事件发生,就不需要再等待了 */
	if (!poller->is_polling) {
		/* 从queue、sem、msgq、pipe等中移除事件 */
		clear_event_registrations(events, events_registered, key);
		k_spin_unlock(&lock, key);

		SYS_PORT_TRACING_FUNC_EXIT(k_poll_api, poll, events, 0);

		return 0;
	}

	poller->is_polling = false;

	/* 如果等待的事件没有发生,timeout等于K_NO_WAIT,就不需要等待了,返回-EAGAIN (超时)*/
	if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		k_spin_unlock(&lock, key);

		SYS_PORT_TRACING_FUNC_EXIT(k_poll_api, poll, events, -EAGAIN);

		return -EAGAIN;
	}

	static _wait_q_t wait_q = Z_WAIT_Q_INIT(&wait_q);

	// 如果等待的事件没有发生,timeout不等于K_NO_WAIT,就需要等待,调用z_pend_curr()等待
	int swap_rc = z_pend_curr(&lock, key, &wait_q, timeout);

	// 从queue、sem、msgq、pipe等中移除事件
	key = k_spin_lock(&lock);
	clear_event_registrations(events, events_registered, key);
	k_spin_unlock(&lock, key);

	SYS_PORT_TRACING_FUNC_EXIT(k_poll_api, poll, events, swap_rc);

	// swap_rc 为0,说明等待的事件发生了,否则等待超时或者等待被中断
	return swap_rc;
}

static inline int register_events(struct k_poll_event *events,
				  int num_events,
				  struct z_poller *poller,
				  bool just_check)
{
	int events_registered = 0;

	for (int ii = 0; ii < num_events; ii++) {
		k_spinlock_key_t key;
		uint32_t state;

		key = k_spin_lock(&lock);
		/* K_POLL_EVENT_INITIALIZER 会将 obj 进行初始化,而obj等价于queue、sem、msgq、pipe,
		   当queue、sem、msgq、pipe中不为空时,代表等待的事件已经发生,此时就会调用is_condition_met(),
		*/
		if (is_condition_met(&events[ii], &state)) {
			/* set_event_ready 会设置初始状态并将 k_poll_event 中的poller设置为NULL */
			set_event_ready(&events[ii], state);
			poller->is_polling = false;
		} 
		/* 如果等待的事件没有发生,就会调用register_event(),将事件添加到queue、sem、msgq、pipe等中,
		   当等待的事件发生时,IPC 对象调用 z_handle_obj_poll_events(),将事件从queue、sem、msgq、pipe等中移除,
		   从而唤醒等待的线程
		*/
		else if (!just_check && poller->is_polling) {
			register_event(&events[ii], poller);
			events_registered += 1;
		} else {
			/* Event is not one of those identified in is_condition_met()
			 * catching non-polling events, or is marked for just check,
			 * or not marked for polling. No action needed.
			 */
			;
		}
		k_spin_unlock(&lock, key);
	}

	return events_registered;
}

事件处理

  • 在Zephyr中,IPC 对象发生指定事件时,会将 poll_events 链表中等待的线程进行唤醒,以 queue 为例子,向 queue 插入数据时,会发生 K_POLL_TYPE_DATA_AVAILABLE 类型的事件,而在 queue 的 poll_events 链表中,存放的都是等待该事件而被挂起的线程,当发生该事件时会调用 z_handle_obj_poll_events 从其中取出一个节点,唤醒对应线程。
void z_handle_obj_poll_events(sys_dlist_t *events, uint32_t state)
{
	struct k_poll_event *poll_event;

	/* 等待事件的线程会被放入到IPC对象的 poll_events 链表中,这里从链表中取出事件,
	 * 然后调用signal_poll_event()函数处理事件
	 */
	poll_event = (struct k_poll_event *)sys_dlist_get(events);
	if (poll_event != NULL) {
		(void) signal_poll_event(poll_event, state);
	}
}
  • signal_poll_event 中会调用 signal_poller,最终将唤醒等待线程。
static int signal_poll_event(struct k_poll_event *event, uint32_t state)
{
	struct z_poller *poller = event->poller;
	int retcode = 0;


	/* k_poll_event 中的pooller可以根据mode的不同,进行不同处理 */
	if (poller != NULL) {
		if (poller->mode == MODE_POLL) {
			retcode = signal_poller(event, state);
		} else if (poller->mode == MODE_TRIGGERED) {
			retcode = signal_triggered_work(event, state);
		} else {
			/* Poller is not poll or triggered mode. No action needed.*/
			;
		}
		
		poller->is_polling = false;

		if (retcode < 0) {
			return retcode;
		}
	}
	
	/* 修改对应事件的状态为就绪态 */
	set_event_ready(event, state);
	return retcode;
}

static int signal_poller(struct k_poll_event *event, uint32_t state)
{
	struct k_thread *thread = poller_thread(event->poller);

	__ASSERT(thread != NULL, "poller should have a thread\n");

	// 如果线程不再等待,直接返回0
	if (!z_is_thread_pending(thread)) {
		return 0;
	}

	// 如果线程处于等待超时状态,直接返回-EAGAIN
	if (z_is_thread_timeout_expired(thread)) {
		return -EAGAIN;
	}

	// 将线程从等待队列中移除
	z_unpend_thread(thread);
	// 设置线程的返回值,如果state == K_POLL_STATE_CANCELLED,返回-EINTR,否则返回0
	arch_thread_return_value_set(thread,
		state == K_POLL_STATE_CANCELLED ? -EINTR : 0);

	if (!z_is_thread_ready(thread)) {
		return 0;
	}

	// 将线程添加到就绪队列中
	z_ready_thread(thread);

	return 0;
}

struct k_poll_signal

  • 在上述轮询类型中其中一种是 K_POLL_TYPE_SIGNAL,是一个“直接”由信号通知的轮询事件,可以看作是一个轻量级的二进制信号量,只有一个线程可以等待。

初始化

  • 轮询信号是一个类型为 struct k_poll_signal 的单独对象,类似于信号量或 FIFO,必须附加到 k_poll_event 上,它必须首先通过 K_POLL_SIGNAL_INITIALIZER宏 或者 k_poll_signal_init 函数进行初始化。
struct k_poll_signal signal;

void do_stuff(void)
{
	k_poll_signal_init(&signal);
}

发出信号和等待信号

  • 它通过 k_poll_signal_raise 发出信号,这个函数可以携带一个 result 参数,可以用来向等待的线程传递额外的信息,该参数对于API而言是不透明的,如果是在一个循环中使用k_poll,在每次使用之后应将 k_poll_signal 中的 signaled 设置为0, 同时将 k_poll_event 中的 state 重设为 K_POLL_STATE_NOT_READY。
struct k_poll_signal signal;

void thread_a(void)
{
	k_poll_signal_init(&signal);

	struct k_poll_event events[1] = {
	K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL,
                                     K_POLL_MODE_NOTIFY_ONLY,
                                     &signal),
	};

	for (;;) 
	{
		k_poll(events, 1, K_FOREVER);

		if (events[0].signal->result == 0xffffffff) 
		{
			// A-OK!
		} 
		else 
		{
            // weird error
        }

        events[0].signal->signaled = 0;
        events[0].state = K_POLL_STATE_NOT_READY;
    }
}

void thread_b(void)
{
	k_poll_signal_raise(&signal, 0xffffffff);
}

总结

  • poll 机制实现了在同一时间内等待多个 IPC 事件的功能,当其中一个事件便会立即返回,程序通过判断事件状态判断哪些事件已经发生。
  • poll 除了支持 fifo,queue,sem,msgq,pipe 这几种类型之外,还提供了一种轻量级的同步通信方式 poll_signal,当多个进程之间只需要传递事件发生或未发生,而不需要携带其他信息时,使用 poll_signal 会更加便捷。

到了这里,关于Zephyr poll的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Web自动化测试进阶:网页中难点之等待机制 —— 强制等待,隐式等待

    为什么要添加等待 避免页面未渲染完成后操作,导致的报错 经常会遇到报错:selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {\\\"method\\\":\\\"xpath\\\",\\\"selector\\\":\\\"//*[text()=\\\'个人中心\\\']\\\"} 页面还在加载时,就在进行查收元素,此时元素还没显示加载出来,而报

    2024年02月05日
    浏览(58)
  • TCP高并发服务器简介(select、poll、epoll实现与区别)

    一、创建套接字(socket函数): 二、填充服务器的网络信息结构体: 三、套接字和服务器的网络信息结构体进行绑定(bind函数): 四、套接字设置成被动监听(listen函数): 五、创建要监听的文件描述符集合: 使用select函数后,会将 没有就绪的文件描述符 在集合中 去除

    2024年01月19日
    浏览(38)
  • unity 等待事件之协程和Invoke

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 提示:这里可以添加本文要记录的大概内容: 协程的用法 和 Invoke 的等待事件使用 提示:以下是本篇文章正文内容,下面案例可供参考 代码如下(示例): 好记性不如烂笔头!

    2024年04月13日
    浏览(28)
  • selenium常见等待机制及其特点和使用方法

    目录 1、强制等待  2、隐式等待  3、显示等待  强制等待是在程序中直接调用Thread.sleep(timeout) ,来完成的, 该用法的优点是使用起来方便,语法也比较简单,缺点就是需要强制等待固定的时间,可能会造成测试的时间过长。 引入等待的原因是很多时候,程序运行的速度是大

    2024年02月14日
    浏览(40)
  • JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 指令重排序概述         1.1 指令重排序主要分为两种类型         1.2 指令重排序所引发的问题         2.0 内存可见性概述         2.1 导致内存可见性问题主要涉及两个方面      

    2024年04月15日
    浏览(38)
  • 【JavaSE专栏84】线程让步,一种线程调度的机制

    作者主页 :Designer 小郑 作者简介 :3年JAVA全栈开发经验,专注JAVA技术、系统定制、远程指导,致力于企业数字化转型,CSDN学院、蓝桥云课认证讲师。 主打方向 :Vue、SpringBoot、微信小程序 本文讲解了 Java 中线程让步的语法和应用场景,并给出了样例代码。线程让步是一种

    2024年02月11日
    浏览(22)
  • Transformer模型简介:一种革命性的深度学习模型

    Transformer模型是一种革命性的深度学习模型,最初用于自然语言处理任务,如机器翻译和语言建模。与传统的序列模型相比,如循环神经网络(RNN)和卷积神经网络(CNN),Transformer模型采用一种全新的方式来处理序列数据,即通过注意力机制来学习序列中的关系。 在传统的序列模

    2024年02月15日
    浏览(40)
  • 探索Java并发编程利器:LockSupport,一种高效的线程阻塞与唤醒机制

    关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习 Java基础知识 ,温故知新。 LockSupport 是 Java SE 9 及以上版本中引入的一个线程同步工具类,用

    2024年02月16日
    浏览(42)
  • Elasticsearch:ESQL 简介 — 一种用于灵活、迭代分析的新查询语言

    作者:Seth Payne 特别声明 :截止撰写该博文,在目前的公开发行版中,该功能还不能公开测试。这个功能将在未来的发行版中发布。 长期以来,Elastic Platform 一直被视为搜索用例和机器生成数据的分析系统。 分析专注于处理摄入的数据,其中重要的思想是如何在 Elasticsearch

    2023年04月23日
    浏览(26)
  • JavaScript中的事件冒泡和事件捕获机制

    JavaScript中的事件冒泡和事件捕获机制是开发中非常重要的概念,掌握了这两种机制,可以更好地理解事件处理和DOM操作。本文将深入探讨JavaScript中的事件冒泡和事件捕获机制,包括它们的工作原理、如何使用它们、以及它们的优缺点。 一、什么是事件冒泡和事件捕获机制?

    2024年02月03日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包