Node.js-Events 模块总结与源码解析
2021/5/21 20:28:22
本文主要是介绍Node.js-Events 模块总结与源码解析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Events
描述
- 大多数 Node.js API 采用异步事件驱动架构,这些对象都是
EventEmitter
类的实例(Emitter),通过触发命名事件(eventName or type)来调用函数(监听器,listener) - Emitter 触发事件时,可以向监听器函数传递任意数量的参数,所有注册到该事件上的监听器函数都会依次同步执行,函数的返回值会被忽略
事件
- 命名规范:驼峰式字符串级任何有效的 JavaScript 属性键
error
error
事件被视为特殊情况,如果没有注册监听器会导致抛出错误、打印堆栈跟踪并退出 Node.js 进程,应始终为error事件注册监听器
errorMonitor
-
为
errorMonitor
事件注册的监听器不会消耗 error,如果没有为 error 事件注册监听器,依然会导致抛出错误、打印堆栈跟踪并退出 Node.js 进程。'use strict' const EventEmitter = require('events').EventEmitter; const ee = new EventEmitter({ captureRejections: true }); ee.on(EventEmitter.errorMonitor, function () { console.log('ErrorMonitor, call first'); }) ee.on('error', () => { console.log('customer error listener'); }) ee.emit('error');
newListener
-
当有新的监听器被添加时,所有
Emitter
都会触发'newListener'
事件。 -
为该事件注册监听器相等于一个钩子函数,可以获取到
事件的名称
和要添加的监听器的引用
class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); // 只处理一次,避免无限循环。 myEmitter.once('newListener', (event, listener) => { console.log(`为${event}添加事件${listener}`) })
removeListener
- 当现有的监听器被移除时,所有
Emitter
都会触发'removeListener'
事件。 - 为该事件注册监听器相等于一个钩子函数,可以获取到
事件的名称
和要添加的监听器的引用
监听器
this指向
普通函数中的this是触发事件的Emitter
,箭头函数中的this是{}
const EventEmitter = require('events').EventEmitter; const ee = new EventEmitter(); function f1() { console.log('run f1, this = ', this); } const f2 = () => { console.log('run arrow function, this = ', this); } ee.on('test', f1); ee.on('test', f2); ee.emit('test');
异步监听器
如果添加异步监听器,需要开启captureRejections
选项且实现captureRejectionSymbol
方法
const { EventEmitter, captureRejectionSymbol } = require('events'); const ee = new EventEmitter({ captureRejections: true }); // 3中方式获取 kRejection 的 Symbol 值 ee[ee.constructor.captureRejectionSymbol] = function (err, event, ...args) { console.log('rejection happened for', event, 'with', err, ...args); } ee[EventEmitter.captureRejectionSymbol] = function (err, event, ...args) { console.log('rejection happened for', event, 'with', err, ...args); } ee[captureRejectionSymbol] = function (err, event, ...args) { console.log('rejection happened for', event, 'with', err, ...args); } async function f() { return Promise.reject('async rejection'); } ee.on('test', f); ee.emit('test');
文档中这句话没太明白
The
'error'
events that are generated by thecaptureRejections
behavior do not have a catch handler to avoid infinite error loops: the recommendation is to not useasync
functions as'error'
event handlers.
执行次数
-
通过
EventEmitter#on()
方式注册的监听器,每次触发命名事件都会执行 -
通过
EventEmitter#once()
方式注册的监听器,触发命名事件只会执行一次触发
once
事件时,先触发removeListener
事件移除监听器,再调用执行。在removeListener监听器
中可拿到once 监听器
,可多次执行const EventEmitter = require('events').EventEmitter; let ee = new EventEmitter() ee.once('test', () => console.log('test')) ee.on('removeListener', (eventName, listener) => { console.log(`eventName: ${eventName}`) listener() // 多次执行 listener() listener() }) ee.emit('test') // eventName: test // test // test // test // test
源码 v16.10
代码注释 https://github.com/lfp1024/node/blob/master/lib/events.js
常量
const kRejection = SymbolFor('nodejs.rejection'); const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); const kMaxEventTargetListenersWarned = Symbol('events.maxEventTargetListenersWarned');
前缀 k 表示常量 (德语 konstant)
构造函数
function EventEmitter(opts) { EventEmitter.init.call(this, opts); // 使 init 方法中的 this 指向新创建的实例,而非 EventEmitter 本身 } module.exports = EventEmitter;
-
opts
captureRejections
- 类型:Boolean
- 描述:是否开启自动捕获异步监听器的 rejection
- false「默认」不开启
- true 开启
静态属性
captureRejectionSymbol
用来自定义异步监听器 rejection 的处理方法
EventEmitter.captureRejectionSymbol = kRejection;
errorMonitor
事件名称
在该事件上注册的监听器,只监听error
事件,且在常规error
事件监听器调用之前被调用,不消耗 error
EventEmitter.errorMonitor = kErrorMonitor;
captureRejections
是否自动捕获异步监听器的rejection
ObjectDefineProperty(EventEmitter, 'captureRejections', { get() { return EventEmitter.prototype[kCapture]; // 返回原型上的 kCapture }, set(value) { if (typeof value !== 'boolean') { throw new ERR_INVALID_ARG_TYPE('EventEmitter.captureRejections', 'boolean', value); } EventEmitter.prototype[kCapture] = value; // 设置原型上的 kCapture }, enumerable: true });
defaultMaxListeners
单个事件默认最大可注册监听器个数
- 默认情况下,每个事件可以最多注册
10
个监听器 - 可以使用
EventEmitter.defaultMaxListeners
属性改变所有EventEmitter
实例的默认值(包括之前创建的)。 如果此值不是一个正数,则抛出RangeError
let defaultMaxListeners = 10; ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', { enumerable: true, get: function() { return defaultMaxListeners; }, set: function(arg) { if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) { throw new ERR_OUT_OF_RANGE('defaultMaxListeners', 'a non-negative number', arg); } defaultMaxListeners = arg; } });
静态方法
init
实例初始化
EventEmitter.init = function(opts) { // new EventEmitter(),在init方法执行之前,先创建了一个对象,this就指向该对象 if (this._events === undefined || this._events === ObjectGetPrototypeOf(this)._events) { // 避免给原型添加`_events`属性,导致所有实例共享 this._events = ObjectCreate(null); // 纯粹的键值对存储对象,没有原型 this._eventsCount = 0; } // 如果原型上有_maxListener则新实例继承(_events不可以继承,_maxListener可以继承) this._maxListeners = this._maxListeners || undefined; if (opts?.captureRejections) { if (typeof opts.captureRejections !== 'boolean') { throw new ERR_INVALID_ARG_TYPE('options.captureRejections', 'boolean', opts.captureRejections); } this[kCapture] = Boolean(opts.captureRejections); } else { // Assigning the kCapture property directly saves an expensive // prototype lookup in a very sensitive hot path. this[kCapture] = EventEmitter.prototype[kCapture]; // 默认值「false」 } };
setMaxListeners
EventEmitter.setMaxListeners = function(n = defaultMaxListeners, ...eventTargets) { if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n); if (eventTargets.length === 0) { defaultMaxListeners = n; } else { if (isEventTarget === undefined) isEventTarget = require('internal/event_target').isEventTarget; for (let i = 0; i < eventTargets.length; i++) { const target = eventTargets[i]; if (isEventTarget(target)) { target[kMaxEventTargetListeners] = n; target[kMaxEventTargetListenersWarned] = false; } else if (typeof target.setMaxListeners === 'function') { target.setMaxListeners(n); } else { throw new ERR_INVALID_ARG_TYPE( 'eventTargets', ['EventEmitter', 'EventTarget'], target); } } } };
示例
const { EventEmitter } = require('events'); const ee = new EventEmitter(); console.log(ee.getMaxListeners()); EventEmitter.setMaxListeners(5, ee) // 可以用其静态方法修改某个ee的最大监听器个数 console.log(ee.getMaxListeners());
原型属性
Symbol('kCapture')
在原型和实例上各有一份,节省到原型上查找的开销
是否捕获异步监听器的rejection
const kCapture = Symbol('kCapture'); ObjectDefineProperty(EventEmitter.prototype, kCapture, { value: false, writable: true, enumerable: false }); // 获取方式 const ee = new EventEmitter(); console.log('ee.kCapture = ', ee[Reflect.ownKeys(ee)[3]]); // false
实例属性
Symbol('kCapture')
在原型和实例上各有一份,节省到原型上查找的开销
是否捕获异步监听器的rejection
// init 方法中 // Assigning the kCapture property directly saves an expensive // prototype lookup in a very sensitive hot path. this[kCapture] = EventEmitter.prototype[kCapture]; // 默认值「false」
_events
保存 监听事件 和 注册在该事件上的监听器
EventEmitter.prototype._events = undefined;
_eventsCount
当前实例中监听事件的个数
EventEmitter.prototype._eventsCount = 0;
_maxListeners
当前实例单个事件最大可注册监听器个数
EventEmitter.prototype._maxListeners = undefined;
实例方法
添加
on
同 addListener
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
addListener
把监听器添加到指定事件监听器数组的末尾,多次添加相同的监听器会多次调用
返回实例的引用,以便可以链式调用
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); // 原型上的方法提出去,将this传入即可 }; function _addListener(target, type, listener, prepend) { let m; let events; let existing; checkListener(listener); // 检测listener是否为function,否则抛异常 events = target._events; if (events === undefined) { // 未初始化 events = target._events = ObjectCreate(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { // 是否监听 `newListener` 事件,每次注册监听器都会触发,类似钩子 target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; // 重新赋值 } existing = events[type]; } if (existing === undefined) { // 之前没有监听该事件 // Optimize the case of one listener. Don't need the extra array object. events[type] = listener; //events 和 target._events 指向同一个对象 ++target._eventsCount; } else { // 之前已经监听该事件 if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. } else if (prepend) { existing.unshift(listener); } else { existing.push(listener); } // Check for listener leak 检测最大监听器个数 m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; // 是否多余? // No error code for this since it is a Warning // eslint-disable-next-line no-restricted-syntax const w = new Error('Possible EventEmitter memory leak detected. ' + `${existing.length} ${String(type)} listeners ` + `added to ${inspect(target, { depth: -1 })}. Use ` + 'emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; process.emitWarning(w); // 发出警告 } } return target; }
prependListener
把监听器添加到指定事件的监听器数组开头
返回实例的引用,以便可以链式调用
EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); };
once
添加单次监听器到指定事件的监听器数组末尾,触发的时候先移除再执行
返回实例的引用,以便可以链式调用
function onceWrapper() { if (!this.fired) { this.target.removeListener(this.type, this.wrapFn); // 先移除监听器「同步操作」 this.fired = true; // 标记已触发,保证只调用一次。【移除监听器】和【保证只调用一次】是两个分开的逻辑 if (arguments.length === 0) // 再调用监听器 return this.listener.call(this.target); return this.listener.apply(this.target, arguments); } } function _onceWrap(target, type, listener) { const state = { fired: false, wrapFn: undefined, target, type, listener }; const wrapped = onceWrapper.bind(state); wrapped.listener = listener; // 挂载原始监听器,一同传递给 removeListener 方法 state.wrapFn = wrapped; // 挂载包裹后的监听器,用于移除 return wrapped; } EventEmitter.prototype.once = function once(type, listener) { checkListener(listener); this.on(type, _onceWrap(this, type, listener)); // 通过 once 监听的事件获取监听器的方式为 listener.listener return this; };
prependOnceListener
添加单次监听器到指定事件的监听器数组开头
返回实例的引用,以便可以链式调用
EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { checkListener(listener); this.prependListener(type, _onceWrap(this, type, listener)); return this; };
移除
off
同 removeListener
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
removeListener
从指定事件的监听器数组中移除指定的监听器
在事件触发之后,最后一个监听器执行完成之前, 移除监听器不会影响已触发的监听器执行
返回实例的引用,以便可以链式调用
EventEmitter.prototype.removeListener = function removeListener(type, listener) { checkListener(listener); const events = this._events; if (events === undefined) // 未初始化 return this; const list = events[type]; if (list === undefined) return this; // 事件只有一个 listener,则会发生 list === listener( once 注册的listener 为 list.listener) if (list === listener || list.listener === listener) { if (--this._eventsCount === 0) // event实例只有一个监听事件 this._events = ObjectCreate(null); else { // event实例有多个监听事件 delete events[type]; // 先移除后触发 if (events.removeListener) // 如果监听了 `removeListener` 事件,则触发,类似钩子 this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { // events[type] 只可能是函数或数组类型,这里判断非函数,则为数组类型? let position = -1; // 利用 `-1` 这个标志 for (let i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { position = i; break; } } if (position < 0) return this; if (position === 0) list.shift(); else { if (spliceOne === undefined) spliceOne = require('internal/util').spliceOne; // 类似 Array#splice(),但是速度较快 spliceOne(list, position); } if (list.length === 1) events[type] = list[0]; // 单个listener不用数组 if (events.removeListener !== undefined) this.emit('removeListener', type, listener); } return this; };
removeAllListeners
移除所有监听器或指定事件的所有监听器
返回实例的引用,以便可以链式调用
EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { const events = this._events; if (events === undefined) // 未初始化 return this; // 逻辑1: 没有注册 removeListener 事件 // Not listening for removeListener, no need to emit if (events.removeListener === undefined) { if (arguments.length === 0) { // 等价于没传参数,type === undefined,删除所有监听事件 this._events = ObjectCreate(null); this._eventsCount = 0; } else if (events[type] !== undefined) { if (--this._eventsCount === 0) // event实例只有一个监听事件 this._events = ObjectCreate(null); else delete events[type]; } return this; } // 逻辑2: 注册了 removeListener 事件 // Emit removeListener for all listeners on all events if (arguments.length === 0) { // 删除所有监听事件 for (const key of ReflectOwnKeys(events)) { // 较 Object#keys(),Reflect#ownKeys() 包含 Symbol 值的属性名 if (key === 'removeListener') continue; // 把其他事件删除后,再删除。如果先删除,后面的事件就走逻辑1了 this.removeAllListeners(key); // 递归,走逻辑3,然后返回 } this.removeAllListeners('removeListener'); // 递归,走逻辑3,然后返回 this._events = ObjectCreate(null); this._eventsCount = 0; return this; } // 逻辑3: 真正删除事件监听器 const listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners !== undefined) { // LIFO order for (let i = listeners.length - 1; i >= 0; i--) { this.removeListener(type, listeners[i]); } } return this; };
修改
setMaxListeners
修改当前 EventEmitter
实例单个事件最大监听器个数。设为 Infinity
(或 0
)表示不限制监听器的数量
返回实例的引用,以便可以链式调用
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) { throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n); } this._maxListeners = n; return this; };
getMaxListeners
返回当前EventEmitter
实例单个事件最大监听器个数
EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return _getMaxListeners(this); // 把 this(实例)传进去 }; function _getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; }
获取
listeners
返回一个数组,包含指定事件的所有监听器(拆包之后)
function _listeners(target, type, unwrap) { const events = target._events; if (events === undefined) return []; const evlistener = events[type]; if (evlistener === undefined) return []; if (typeof evlistener === 'function') return unwrap ? [evlistener.listener || evlistener] : [evlistener]; return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener); } EventEmitter.prototype.listeners = function listeners(type) { return _listeners(this, type, true); };
rawListeners
返回一个数组,包含指定事件的所有(原始)监听器
如果是通过 once 注册的监听器,则返回的是被包装(wrap)后的监听器
EventEmitter.prototype.rawListeners = function rawListeners(type) { return _listeners(this, type, false); };
listenerCount
返回指定事件上注册的监听器个数
EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { const events = this._events; if (events !== undefined) { const evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener !== undefined) { return evlistener.length; } } return 0; }
eventNames
返回一个数组,包含所有监听事件。元素类型为 String 或 Symbol
EventEmitter.prototype.eventNames = function eventNames() { // 所有监听事件 return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; };
触发
emit
触发指定事件并同步调用注册到该事件上的所有监听器
如果事件有监听器,则返回 true
,否则返回 false
EventEmitter.prototype.emit = function emit(type, ...args) { let doError = (type === 'error'); // error 事件,特殊处理 const events = this._events; if (events !== undefined) { // 有监听事件 if (doError && events[kErrorMonitor] !== undefined) // 如果是 error 事件,且用户添加了 kErrorMonitor 监听器 this.emit(kErrorMonitor, ...args); doError = (doError && events.error === undefined); // 如果是 error 事件,且没有对应的 handler } else if (!doError) // events === undefined && !doError => 未初始化且不是 error 事件,则返回 false return false; // 触发 error 事件且没有handler,抛出异常(参数中的error实例或自己生成的error) // If there is no 'error' event listener then throw. if (doError) { // 1. 有监听事件 && 触发error事件 && 没有error事件的 handler // 2. 未初始化(肯定也没有error事件的 handler) && 触发error事件 let er; if (args.length > 0) er = args[0]; if (er instanceof Error) { // 第一个入参是 Error 实例 try { // 给 er 添加当前栈信息(emit部分) const capture = {}; ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { value: enhanceStackTrace.bind(this, er, capture), configurable: true }); } catch {} // Note: The comments on the `throw` lines are intentional, they show // up in Node's output if this results in an unhandled exception. throw er; // Unhandled 'error' event } let stringifiedEr; const { inspect } = require('internal/util/inspect'); try { stringifiedEr = inspect(er); } catch { stringifiedEr = er; } // At least give some kind of context to the user const err = new ERR_UNHANDLED_ERROR(stringifiedEr); err.context = er; throw err; // Unhandled 'error' event } const handler = events[type]; // 如果是 error 事件,至此一定有handler,如果是其他类型事件,仍然需要判断handler的存在性 if (handler === undefined) return false; // 单个handler是函数,多个handler是数组 if (typeof handler === 'function') { const result = handler.apply(this, args); // 参见 https://github.com/nodejs/node/pull/38248 // We check if result is undefined first because that // is the most common case so we do not pay any perf // penalty if (result !== undefined && result !== null) { addCatch(this, result, type, args); // 如果是handler是异步函数,捕获其rejection异常 } } else { const len = handler.length; // 确定监听器的个数,避免监听器中再监听同一个事件造成死循环 const listeners = arrayClone(handler); // 克隆handler数组,防止在事件触发后,已注册监听器的变动 for (let i = 0; i < len; ++i) { const result = listeners[i].apply(this, args); // 同步依次调用 // We check if result is undefined first because that // is the most common case so we do not pay any perf // penalty. // This code is duplicated because extracting it away // would make it non-inlineable. if (result !== undefined && result !== null) { addCatch(this, result, type, args); } } } return true; }; function addCatch(that, promise, type, args) { if (!that[kCapture]) { return; } // 符合 Promises/A+ 规范的 promise 的属性 then 可能具有 get 方法,二次调用可能会报错,因此采用call方式 // 因此下面采用的是 call 方式调用 // Handle Promises/A+ spec, then could be a getter // that throws on second use. try { const then = promise.then; if (typeof then === 'function') { then.call(promise, undefined, function(err) { // The callback is called with nextTick to avoid a follow-up // follow-up rejection ? // rejection from this promise. process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); }); } } catch (err) { that.emit('error', err); } } function emitUnhandledRejectionOrErr(ee, err, type, args) { if (typeof ee[kRejection] === 'function') { // 实例(用户)自己实现了 kRejection 函数 ee[kRejection](err, type, ...args); } else { // We have to disable the capture rejections mechanism, otherwise // we might end up in an infinite loop. // 防止 error 事件 handler 也抛出 error,导致死循环。 // 先保存之前的值,再关闭,如果程序未「退出」再恢复之前的值 const prev = ee[kCapture]; // If the error handler throws, it is not catcheable and it // will end up in 'uncaughtException'. We restore the previous // value of kCapture in case the uncaughtException is present // and the exception is handled. // 如果没有自定义 rejected promise 的处理函数,则走 error 事件的处理函数 // 如果 error 事件 handler 也抛出 error,则不会被捕获,nodejs 会触发 uncaughtException 事件并「退出」 // 如果用户处理了 uncaughtException 事件(通过 process#on() 处理),则恢复 kRejection 的值 try { ee[kCapture] = false; ee.emit('error', err); } finally { ee[kCapture] = prev; } } }
其他
primordials
primordials对象用来保证内建模块可以访问真正的不受用户干扰的全局变量
https://stackoverflow.com/a/60215269/11089100
spliceOne
更高效的移除数组中的元素
function spliceOne(list, index) { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; // 从被移除元素开始,将后面的元素值依次向前复制 list.pop(); // 移除最后一个元素 }
创建实例未初始化
'use strict'; const { EventEmitter, captureRejectionSymbol } = require('events'); function MyEventEmitter() { // EventEmitter.call(this); } MyEventEmitter.prototype = EventEmitter.prototype; MyEventEmitter.prototype.constructor = MyEventEmitter const ee = new MyEventEmitter(); // 上述创建 EventEmitter 实例的方式并没有调用 EventEmitter.init() 初始化,因此实例属性值都是 undefined console.log('ee._events = ', ee._events); console.log('ee._eventsCount = ', ee._eventsCount); ee.on('test', () => { console.log('tttttttt') }); ee.emit('test'); console.log('ee.eventNames = ',ee.eventNames());
参考
通过源码解析 Node.js 中 events 模块里的优化小细节
通过源码分析nodejs原理
中文文档
这篇关于Node.js-Events 模块总结与源码解析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-16Vue3资料:新手入门必读教程
- 2024-11-16Vue3资料:新手入门全面指南
- 2024-11-16Vue资料:新手入门完全指南
- 2024-11-16Vue项目实战:新手入门指南
- 2024-11-16React Hooks之useEffect案例详解
- 2024-11-16useRef案例详解:React中的useRef使用教程
- 2024-11-16React Hooks之useState案例详解
- 2024-11-16Vue入门指南:从零开始搭建第一个Vue项目
- 2024-11-16Vue3学习:新手入门教程与实践指南
- 2024-11-16Vue3学习:从入门到初级实战教程