一、Compiler 编译过程解密:多框架实现对比
Vue 3 编译流程深度解析(基于 /packages/compiler-core/src/parse.ts)
完整编译链条及技术实现:
01、词法分析阶段(Tokenizer)
1. 核心实现机制
Vue 3 编译器采用**有限状态机(Finite State Machine, FSM)**解析模板字符串,核心逻辑位于/packages/compiler-core/src/parse.ts的parse函数:
// 解析器入口函数
function parse(content: string, options: ParserOptions = {}): RootNode {
// 创建解析上下文
const context = createParserContext(content, options)
// 获取初始光标位置
const start = getCursor(context)
// 解析子节点(核心处理逻辑)
const children = parseChildren(context, [])
// 返回AST根节点
return {
type: NodeTypes.ROOT,
children,
loc: getSelection(context, start), // 记录源码位置信息
components: [], // 组件集合
directives: [], // 指令集合
hoists: [], // 静态提升节点
imports: [], // 导入声明
cached: 0, // 缓存节点计数
temps: 0 // 临时变量计数
}
}
2. 核心正则表达式
Vue 使用以下正则表达式进行词法分析,识别模板中的关键元素:
// 标准标签名(字母/下划线开头,可包含连字符、点、数字)
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
// 带命名空间的标签(如<svg:circle>)
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 开始标签匹配(如<div>)
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 开始标签结束(匹配>或/>)
const startTagClose = /^\s*(\/?)>/
// 属性解析(识别属性名、等号、属性值)
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 结束标签匹配(如</div>)
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 文档类型声明(<!DOCTYPE>)
const doctype = /^<!DOCTYPE [^>]+>/i
// 注释节点(<!-- comment -->)
const comment = /^<!\--/
// 条件注释(<![if IE]>)
const conditionalComment = /^<!\[/
3. 词法分析处理流程
状态机按以下顺序处理模板内容:
-
标签开闭识别
// 处理开始标签 if (startTagOpen.test(html)) { const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) continue } } // 处理结束标签 if (endTag.test(html)) { const endTagMatch = html.match(endTag) if (endTagMatch) { advance(endTagMatch[0].length) parseEndTag(endTagMatch[1]) continue } } -
属性解析
while ( !end(html) && !(endTag.test(html)) && !(startTagOpen.test(html)) && (attr = html.match(attribute)) ) { advance(attr[0].length) match.attrs.push(attr) } -
文本插值处理 (
{{ value }})if (html.indexOf(context.options.delimiters[0]) === 0) { // 解析插值表达式 const [full, content] = parseInterpolation(context) nodes.push({ type: NodeTypes.INTERPOLATION, content: { type: NodeTypes.SIMPLE_EXPRESSION, content, isStatic: false, constType: ConstantTypes.NOT_CONSTANT }, loc: getSelection(context, start) }) } -
指令识别 (
v-if,v-for等)if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) { // 解析指令 const match = /(?:^v-([a-z0-9-]+))?(?:(?::|\.|@|#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(name) if (match) { // 提取指令名、参数、修饰符 const dirName = match[1] || (startsWith(name, ':') ? 'bind' : '' const arg = match[2] ? match[2].trim() : undefined // 构建指令节点 addDirective(node, dirName, arg, modifiers) } } -
特殊符号处理
// 处理<!DOCTYPE> if (doctype.test(html)) { advance(html.match(doctype)[0].length) continue } // 处理注释节点 if (comment.test(html)) { const commentEnd = html.indexOf('-->') if (commentEnd >= 0) { advance(commentEnd + 3) continue } } // 处理条件注释 if (conditionalComment.test(html)) { const conditionalEnd = html.indexOf(']>') if (conditionalEnd >= 0) { advance(conditionalEnd + 2) continue } }
4. 状态机工作流程
词法分析的状态转换流程如下:
5. 错误处理机制
状态机包含完善的错误检测:
// 标签嵌套检查
function parseChildren(context, ancestors) {
const parent = last(ancestors)
while (!isEnd(context, ancestors)) {
// ...解析逻辑...
}
// 检查未闭合标签
if (parent && context.source) {
emitError(
context,
ErrorCodes.X_MISSING_END_TAG,
parent.loc.start
)
}
}
// 自定义错误类型
export const enum ErrorCodes {
X_MISSING_END_TAG = 1, // 缺少结束标签
X_MISSING_INTERPOLATION_END, // 缺少插值结束符号
X_MISSING_DIRECTIVE_NAME, // 指令名缺失
X_MISSING_ATTRIBUTE_VALUE, // 属性值缺失
X_INVALID_DIRECTIVE_ARG, // 无效的指令参数
// ...20+种错误类型...
}
6. 位置信息追踪
编译器精确记录每个节点的源码位置:
// 获取当前光标位置
function getCursor(context: ParserContext): Position {
const { column, line, offset } = context
return { column, line, offset }
}
// 记录节点位置范围
function getSelection(
context: ParserContext,
start: Position,
end?: Position
): SourceLocation {
return {
start,
end: end || getCursor(context),
source: context.originalSource.slice(start.offset, context.offset)
}
}
7. 性能优化策略
-
增量解析:使用
advance()逐步消费模板字符串function advance(context: ParserContext, numberOfChars: number) { const { source } = context // 更新行列计数 advancePositionWithMutation(context, source, numberOfChars) // 截取剩余字符串 context.source = source.slice(numberOfChars) } -
预扫描优化:快速跳过大型文本节点
if (textEnd > 0) { // 批量处理文本内容 const text = context.source.slice(0, textEnd) advance(context, textEnd) return text } -
正则表达式优化:所有正则使用
^开头锚点确保高效匹配
这种基于有限状态机的词法分析设计,使Vue 3编译器能在O(n)时间复杂度内完成模板解析,同时保持精确的错误定位能力,为后续的语法解析和优化阶段奠定坚实基础。
02、语法解析阶段(Parser)
使用递归下降算法构建AST,核心逻辑:
function parseChildren(context: ParserContext, ancestors: ElementNode[]): TemplateChildNode[] {
const nodes: TemplateChildNode[] = []
while (!isEnd(context, ancestors)) {
const s = context.source
let node: TemplateChildNode | undefined
if (startsWith(s, context.options.delimiters[0])) {
// 解析插值表达式 {{ value }}
node = parseInterpolation(context)
} else if (s[0] === '<') {
// 解析元素标签
if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
}
}
if (!node) {
// 解析纯文本内容
node = parseText(context)
}
pushNode(nodes, node)
}
return nodes
}
03、语义优化技术
Vue 3特有的编译时优化:
- 静态节点提升(hoistStatic):将静态节点提取到渲染函数外部,避免重复创建
// 优化前
function render() {
return h('div', [
h('span', '静态内容'),
h('p', dynamicValue)
])
}
// 优化后
const _hoisted = h('span', '静态内容')
function render() {
return h('div', [
_hoisted,
h('p', dynamicValue)
])
}
- 补丁标志(patchFlag):使用位运算标记动态节点类型
// patchFlags 位掩码定义
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态class绑定
STYLE = 1 << 2, // 动态style绑定
PROPS = 1 << 3, // 动态非class/style属性
FULL_PROPS = 1 << 4, // 需要完整props比较
HYDRATE_EVENTS = 1 << 5, // 带事件监听器
STABLE_FRAGMENT = 1 << 6, // 稳定片段(子节点顺序不变)
KEYED_FRAGMENT = 1 << 7, // 带key的片段
UNKEYED_FRAGMENT = 1 << 8, // 无key的片段
NEED_PATCH = 1 << 9, // 需要非props补丁
DYNAMIC_SLOTS = 1 << 10 // 动态插槽
}
- 缓存事件处理程序(cacheHandler):避免重复创建事件处理器
// 优化前
function render() {
return h('button', { onClick: () => handler() })
}
// 优化后
const _cache = {}
function render() {
return h('button', {
onClick: _cache[1] || (_cache[1] = () => handler())
})
}
Vue 2 编译实现深度对比
架构级差异:
1. 解析器实现差异
Vue 2 使用基于正则的字符串处理(/src/compiler/parser/index.js):
// Vue 2 解析器核心
parseHTML(template, {
start(tag, attrs, unary) {
// 处理开始标签
const element = createASTElement(tag, attrs)
processElement(element)
},
end() {
// 处理结束标签
closeElement()
},
chars(text) {
// 处理文本内容
handleText(text)
},
comment(text) {
// 处理注释
handleComment(text)
}
})
Vue 3 使用状态机驱动的位运算(/packages/compiler-core/src/parse.ts):
// 文本解析模式状态枚举
const enum TextModes {
DATA, // 默认模式
RCDATA, // <textarea> 内容模式
RAWTEXT, // <style>,<script> 模式
CDATA, // <![CDATA[ 内容
ATTRIBUTE_VALUE // 属性值解析模式
}
// 状态转换逻辑
function parseTag(context: ParserContext, type: TagType): ElementNode {
const start = getCursor(context)
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1]
advanceBy(context, match[0].length)
advanceSpaces(context)
// 属性解析状态处理
const props = parseAttributes(context, type)
// 其他状态处理...
}
2. AST结构差异
Vue 2 AST节点(简化):平铺式节点结构
{
type: 1, // 元素节点
tag: 'div',
attrsList: [{ name: 'class', value: 'container' }],
children: [
{ type: 2, text: '{{ message }}', expression: '_s(message)' }
]
}
Vue 3 AST节点(增强):树形结构+动态标记
interface ElementNode extends Node {
type: NodeTypes.ELEMENT
tag: string
tagType: ElementTypes
props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[]
codegenNode: CodegenNode | undefined
// 新增优化字段
patchFlag: number
dynamicProps: string[] | null
isStatic: boolean
}
React JSX 编译原理详解
Babel 转换流程:
// JSX 源代码
<div className="container" onClick={handleClick}>
{message}
<Button>提交</Button>
</div>
// 经babel转换后(React 16)
React.createElement(
"div",
{ className: "container", onClick: handleClick },
message,
React.createElement(Button, null, "提交")
)
编译时优化:
// React 17+ 新转换
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
_jsxs("div", {
className: "container",
onClick: handleClick,
children: [
message,
_jsx(Button, { children: "提交" })
]
});
与Vue的核心差异:
- 无静态提升:每次渲染都创建完整的VNode树
- 无补丁标志:依赖Virtual DOM的全量diff
- 无编译时优化:JSX直接转换为运行时函数调用
- 动态处理:所有优化在运行时完成(如React.memo)
编译时优化尝试:
// React Forget编译器(实验性)
function Component(props) {
"use no forget"
const [count, setCount] = useState(0)
const doubled = count * 2 // 自动记忆化
return <div>{doubled}</div>
}
二、响应式系统实现:多框架深度对比
Vue 3 响应式系统(Proxy实现)
核心架构:
生产级实现细节:
// 响应式入口(/packages/reactivity/src/reactive.ts)
function reactive(target: object) {
// 避免重复代理
if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target
}
// 查找现有代理
const existingProxy = proxyMap.get(target)
if (existingProxy) return existingProxy
// 创建代理
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
// 数组方法重写(/packages/reactivity/src/baseHandlers.ts)
const arrayInstrumentations: Record<string, Function> = {}
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
arrayInstrumentations[key] = function (...args: any[]) {
pauseTracking() // 暂停依赖收集
const res = (this as any)[key].apply(this, args)
resetTracking() // 恢复依赖收集
return res
}
})
依赖收集系统:
// 依赖收集器(/packages/reactivity/src/effect.ts)
class ReactiveEffect {
deps: Dep[] = [] // 依赖此effect的所有dep
run() {
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
// 执行前清空依赖
cleanupEffect(this)
// 执行副作用函数
return this.fn()
} finally {
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
}
}
}
// 依赖收集(track函数)
function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
function trackEffects(dep: Dep) {
if (!activeEffect) return
// 建立双向依赖关系
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
生产级优化:
1.嵌套代理缓存
2.数组方法拦截
3.调度器批量更新
Vue 2 响应式系统深度解析
实现原理:
// 响应式入口(/src/core/observer/index.js)
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组响应式处理
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 对象响应式处理
this.walk(value)
}
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
// 属性劫持
function defineReactive(obj, key, val) {
const dep = new Dep()
// 处理嵌套对象
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = val
if (Dep.target) {
dep.depend() // 收集依赖
if (childOb) {
childOb.dep.depend() // 嵌套对象依赖收集
if (Array.isArray(value)) {
dependArray(value) // 数组依赖收集
}
}
}
return value
},
set: function reactiveSetter(newVal) {
if (newVal === value) return
val = newVal
childOb = observe(newVal) // 新值响应式处理
dep.notify() // 触发更新
}
})
}
局限性解决方案:
// Vue.set 实现(/src/core/observer/index.js)
function set(target, key, val) {
// 处理数组
if (Array.isArray(target) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 处理对象
if (hasOwn(target, key)) {
target[key] = val
return val
}
// 新增属性
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
// 将新属性转为响应式
defineReactive(ob.value, key, val)
ob.dep.notify() // 手动触发更新
return val
}
局限性对比:
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 属性增删检测 | ❌ 需Vue.set | 原生支持 |
| 数组索引变更 | ❌需重写方法 | 原生支持 |
| 性能开销 | O(n) 属性级劫持 | O(1) 对象级代理 |
React 响应式系统原理
Hooks 实现机制:
// useState 简化实现(模拟 React 源码)
let hookStates = []
let hookIndex = 0
let scheduleUpdate = null
function useState(initialState) {
const currentIndex = hookIndex
hookStates[currentIndex] = hookStates[currentIndex] || (
typeof initialState === 'function' ? initialState() : initialState
)
const setState = (newState) => {
// 状态对比(使用 Object.is)
if (Object.is(hookStates[currentIndex], newState)) return
// 处理函数式更新
hookStates[currentIndex] = typeof newState === 'function'
? newState(hookStates[currentIndex])
: newState
// 触发重新渲染
scheduleUpdate()
}
hookIndex++
return [hookStates[currentIndex], setState]
}
// useEffect 简化实现
function useEffect(callback, deps) {
const currentIndex = hookIndex
const [lastDeps, cleanup] = hookStates[currentIndex] || [null, null]
// 依赖比较
const hasChanged = !deps || !lastDeps || deps.some((dep, i) => !Object.is(dep, lastDeps[i]))
if (hasChanged) {
// 执行清理函数
if (cleanup && typeof cleanup === 'function') cleanup()
// 异步执行effect
Promise.resolve().then(() => {
const cleanupFn = callback()
hookStates[currentIndex] = [deps, cleanupFn]
})
}
hookIndex++
}
更新机制对比:
| 特性 | Vue 3 | React |
|---|---|---|
| 更新粒度 | 组件内元素级 | 组件级 |
| 依赖跟踪 | 自动 | 手动(依赖数组) |
| 状态变更检测 | Proxy 拦截 | Object.is 比较 |
| 批量更新 | 自动(nextTick) | 自动(事件处理中) |
| 异步更新队列 | 有 | 有 |
| 嵌套更新处理 | 自动防止无限循环 | 最大更新深度限制 |
Vue:自动依赖追踪,精确更新
React:组件树重渲染+差异比对
优化策略:
Vue:响应式依赖追踪
React:memo/shouldComponentUpdate
三、跨框架编译优化策略对比
| 优化策略 | Vue 3 | Vue 2 | React |
|---|---|---|---|
| 静态提升 | ✅ 提取静态节点到渲染函数外部 | ❌ 无 | ❌ 无 |
| 补丁标志 | ✅ 位运算标记动态元素 (PatchFlags) | ❌ 无 | ❌ 无 |
| 事件缓存 | ✅ 自动缓存事件处理器 | ❌ 无 | ❌ 无 |
| 树结构优化 | ✅ Block Tree 减少动态节点遍历 | ❌ 递归全量diff | ✅ Fiber架构增量渲染 |
| 常量折叠 | ✅ 编译时计算静态表达式 | ⚠️ 有限支持 | ❌ 无 |
| 按需更新 | ✅ 元素级精确更新 | ⚠️ 组件级 | ⚠️ 组件级 |
| SSR优化 | ✅ 同构hydration + 静态内容直出 | ⚠️ 基础SSR支持 | ✅ Streaming SSR + Suspense |
| 代码拆分 | ✅ 基于路由的异步组件 | ⚠️ 异步组件 | ✅ React.lazy + Suspense |
| Tree Shaking | ✅ 良好支持 | ⚠️ 有限支持 | ✅ 完善支持 |
| 源映射支持 | ✅ 完整模板到渲染函数的source map | ⚠️ 部分支持 | ✅ JSX到JS的source map |
四、编译与响应式协同工作原理
Vue 3 运行时完整工作流:
协同优势详解:
-
编译时信息利用:
- 编译器识别静态节点,减少运行时比较
- 补丁标志指导运行时diff算法优化路径
// 基于patchFlag的优化diff if (patchFlag & PatchFlags.CLASS) { // 只需更新class updateClass(oldVNode, newVNode) } else if (patchFlag & PatchFlags.STYLE) { // 只需更新style updateStyle(oldVNode, newVNode) } else if (patchFlag & PatchFlags.TEXT) { // 只需更新文本内容 updateText(oldVNode, newVNode) } -
响应式精准更新:
- 依赖收集系统建立数据与视图的精确关联
- 避免不必要的组件重新渲染
// 组件更新条件(/packages/runtime-core/src/componentRenderUtils.ts) shouldUpdateComponent( prevVNode: VNode, nextVNode: VNode, optimized?: boolean ): boolean { // 通过patchFlag快速判断 if (nextVNode.patchFlag > 0) { const flag = nextVNode.patchFlag if (flag & PatchFlags.DYNAMIC_SLOTS) { return true } if (flag & PatchFlags.FULL_PROPS) { // 需要完整props比较 } else { // 仅检查特定props } } } -
内存优化策略:
- 静态节点提升减少内存分配
- 事件缓存减少函数对象创建
- 组件实例复用策略
五、框架设计哲学对比
| 设计维度 | Vue 3 | React |
|---|---|---|
| 核心思想 | 渐进增强 + 编译优化 | 函数式编程 + 不可变数据 |
| 更新机制 | 自动依赖追踪 + 精准更新 | 状态驱动 + 虚拟DOM diff |
| 模板系统 | 声明式模板 + 指令系统 | JSX(JavaScript 语法扩展) |
| 状态管理 | 响应式自动依赖收集 | 手动状态管理 + 依赖数组 |
| 学习曲线 | 模板导向,较低门槛 | JSX导向,较高抽象能力要求 |
| 性能策略 | 编译时优化为主 + 运行时辅助 | 运行时优化为主 |
| 类型支持 | TypeScript 优先 | TypeScript 完善支持 |
| 移动端支持 | Weex/NativeScript | React Native |
| 未来方向 | Vapor模式(更细粒度响应式) | React Forget(编译优化) |
| SSR架构 | Nuxt.js + Vite | Next.js + React Server Components |
六、源码学习指南
Vue 3 核心源码路径:
-
编译器入口:
/packages/compiler-core/src/compile.tsbaseCompile函数:完整编译流程入口transform模块:AST转换优化核心
-
解析器实现:
/packages/compiler-core/src/parse.tsbaseParse函数:模板解析入口parseElement:元素节点解析逻辑parseInterpolation:插值表达式解析
-
响应式系统:
/packages/reactivity/src/reactive.ts:响应式入口effect.ts:副作用管理核心baseHandlers.ts:Proxy处理器实现collectionHandlers.ts:集合类型处理器
-
运行时核心:
/packages/runtime-core/src/renderer.ts:渲染器实现componentRenderUtils.ts:组件渲染工具scheduler.ts:异步调度队列
React 核心源码路径:
-
调和器:
/packages/react-reconciler/src/ReactFiberWorkLoop.js:Fiber调度核心ReactFiberBeginWork.js:Fiber节点处理ReactFiberCompleteWork.js:Fiber完成工作
-
Hooks系统:
/packages/react/src/ReactHooks.js:Hook入口Hooks.js:Hook实现核心
-
JSX运行时:
/packages/react/src/jsx/ReactJSX.js:JSX元素创建ReactJSXElementValidator.js:JSX元素验证
对比学习建议:
-
编译器设计对比:
- Vue 的
transformElementvs Babel 的transform-react-jsx - Vue 的 AST 优化遍历 vs React 的运行时优化
- Vue 的
-
响应式实现对比:
- Vue 的
track/targetMap依赖收集 vs React 的useState/useEffect依赖数组 - Vue 的响应式更新调度 vs React 的 Fiber 调度算法
- Vue 的
-
渲染优化对比:
- Vue 的 Block Tree 机制 vs React 的 Fiber 树
- Vue 的静态节点提升 vs React 的
React.memo
性能优化本质差异:
Vue:通过编译获取模板语义信息,在运行时做最少的工作
React:通过虚拟DOM和差异比对,在运行时做智能的更新决策调试技巧:
- 使用 Vue 的
@vue/compiler-sfc单独测试编译输出- 使用 React 的
__PROFILE__标记进行性能分析- 通过源码映射(sourcemap)在浏览器调试原始代码
总结:现代框架演进趋势
-
编译与运行时的深度融合
- Vue:Vapor 模式(实验性)将更多逻辑移至编译时
// Vapor 模式示例(概念) const __vapor__ = compile(`<div>{{ msg }}</div>`) function setup() { const msg = ref('Hello') return __vapor__({ msg }) }- React:React Forget 编译器自动记忆化
- Svelte:激进编译策略生成极简运行时
-
响应式范式的统一
- Vue:保持基于 Proxy 的响应式
- React:探索 Signal 基础原语
// React Signals 提案(实验性) const count = createSignal(0) const double = createMemo(() => count.value * 2) return <div>{double.value}</div> -
全栈架构的整合
- Vue:Vite + SSR + Pinia 一体化
- React:Next.js + Server Components + Turbopack
- 共同点:服务端组件、岛屿架构(Islands Architecture)
-
类型系统的演进
- Vue:
<script setup>+ TypeScript + Volar - React:TypeScript 优先 + 更严格的类型检查
- 共同点:提升大型应用可维护性
- Vue:
-
性能基准的持续优化
- Vue:优化内存占用(静态提升)
- React:优化交互响应(并发渲染)
- 共同目标:达到接近原生性能的Web体验
码字不易,各位大佬点点赞呗



















