编程 TypeScript 7.0 Beta深度解析:微软用Go重写编译器,10倍性能跃迁背后的技术革命

2026-04-26 16:41:24 +0800 CST views 5

TypeScript 7.0 Beta深度解析:微软用Go重写编译器,10倍性能跃迁背后的技术革命

当一门语言的编译器成为性能瓶颈,是该修修补补还是推倒重来?微软选择了后者——用Go重写TypeScript编译器,性能提升10倍。这不仅是一次技术重构,更是对前端工程化的深度思考。


一、背景:TypeScript的性能困境

1.1 TypeScript的崛起与挑战

2012年,微软发布TypeScript,这门"JavaScript的超集"语言凭借静态类型系统、优秀的IDE支持和渐进式迁移能力,迅速成为前端开发的主流选择。截至2026年,TypeScript在GitHub上的项目使用率已超过45%,React、Vue、Angular三大框架的官方模板全部默认支持TypeScript。

然而,随着项目规模的增长,TypeScript的痛点逐渐暴露:

编译速度成为开发效率的最大瓶颈

一个百万行代码的Monorepo项目,完整类型检查往往需要数分钟。即使在增量编译模式下,修改一个文件触发的重新检查也可能耗时数秒。开发者在VS Code中敲下.等待自动补全的"转圈时间",直接影响着开发体验。

内存占用居高不下

TypeScript编译器(tsc)是用TypeScript编写的,运行在Node.js上。Node.js的单线程特性和V8引擎的内存管理机制,导致大型项目的内存占用动辄数GB。在CI/CD流水线中,多个编译任务并行运行时,内存压力更加明显。

语言服务的延迟

VS Code的语言服务器协议(LSP)依赖于TypeScript编译器提供类型信息。当项目变大时,每次保存文件触发的语法检查、每次悬停提示的类型信息查询,都需要等待编译器响应,开发者的心流体验被打断。

1.2 为什么性能优化如此困难?

TypeScript编译器的性能问题,根源在于其架构设计:

自举编译器的代价

TypeScript编译器是用TypeScript写的,需要先编译成JavaScript才能运行。这种"自举"设计虽然方便开发和维护,但意味着编译器本身也是解释执行的。每次类型检查,实际上是运行一段JavaScript代码,而JavaScript的执行效率受限于V8引擎的JIT编译策略。

Node.js的单线程限制

Node.js的事件循环模型适合I/O密集型任务,但TypeScript编译是CPU密集型任务。编译器无法充分利用多核CPU的并行能力,核心利用率低下。虽然有Worker Threads API,但进程间通信开销大,难以在编译器中有效应用。

复杂的类型系统

TypeScript的类型系统设计为"结构化类型"(Structural Typing),每次类型比较都需要递归检查类型的结构。联合类型(Union Types)、交叉类型(Intersection Types)、条件类型(Conditional Types)等高级特性,进一步增加了类型检查的复杂度。对于一个A | B | C类型的变量,编译器需要逐一检查ABC三个分支的可分配性,复杂度从O(1)退化到O(N),当联合类型分支多达数十个时,性能呈指数级下降。


二、技术决策:为什么选择Go?

2.1 语言选择的考量

面对性能瓶颈,微软团队评估了多种方案:

方案一:优化现有TypeScript实现

通过优化编译器的数据结构、改进增量编译策略、使用更高效的算法来提升性能。这是增量式的改进,但受限于JavaScript本身的执行效率,天花板有限。

方案二:使用Rust重写

Rust以其零成本抽象、内存安全和出色的并发性能著称,是编译器开发的理想选择。但Rust的学习曲线陡峭,社区中熟悉Rust的前端开发者较少,可能影响项目的可维护性。

方案三:使用Go重写

Go语言具有原生编译、高效并发、简洁语法和活跃社区的特点。Go的goroutine轻量级协程模型天然适合并行编译,且Go在前端工具链领域已有成功案例(如esbuild、Bun),团队熟悉度更高。

最终,微软选择了Go。

2.2 Go语言的技术优势

原生编译,零启动开销

Go程序编译为原生机器码,直接运行在操作系统上,无需像Node.js那样经过解释执行。启动速度快,执行效率高,尤其适合命令行工具场景。

Goroutine实现轻量级并行

Go的Goroutine是一种用户态的轻量级线程,创建成本极低(约2KB栈空间),调度开销小。在编译器中,可以轻松启动数千个Goroutine并行处理不同的源文件,充分利用多核CPU。

// 并行编译示例
func compileParallel(files []*SourceFile) *Program {
    var wg sync.WaitGroup
    results := make(chan *CompiledFile, len(files))
    
    for _, file := range files {
        wg.Add(1)
        go func(f *SourceFile) {
            defer wg.Done()
            results <- compileFile(f)
        }(file)
    }
    
    go func() {
        wg.Wait()
        close(results)
    }()
    
    program := &Program{Files: make([]*CompiledFile, 0)}
    for compiled := range results {
        program.Files = append(program.Files, compiled)
    }
    return program
}

共享内存通信

Go鼓励"通过通信来共享内存",但同时也支持传统的共享内存模型。在编译器中,不同模块可以通过共享内存高效传递AST节点、符号表等数据结构,避免了进程间通信的序列化开销。

垃圾回收与内存效率

Go的垃圾回收器经过多年优化,STW(Stop-The-World)时间已降至亚毫秒级别。编译器可以放心使用动态数据结构,而无需手动管理内存。同时,Go的逃逸分析减少了堆分配,内存效率高于Node.js。

2.3 移植而非重写

一个关键决策是:TypeScript Go是从现有实现移植,而非从头重写

这意味着类型检查逻辑在结构上与TypeScript 6.0完全相同。微软团队花了一年时间,将现有的TypeScript代码库从TypeScript移植到Go。这种策略的优势是:

保持语义一致性

移植确保编译器继续强制执行用户早已依赖的完全相同的语义。任何微小的语义变化都可能破坏现有项目,移植策略最大程度降低了风险。

利用现有测试套件

TypeScript拥有十年积累的庞大测试套件,移植后可以继续使用这些测试来验证正确性。无需重新设计测试用例,节省了大量时间。

渐进式迁移

移植是渐进式的,可以先移植核心模块,再逐步完善。在移植过程中,微软已经在内部和外部的百万行代码库中验证了新编译器。


三、架构解析:TypeScript Go的7大核心模块

TypeScript Go采用分层设计理念,将编译器拆分为独立且可复用的核心模块。以下是七大核心模块的深度解析:

3.1 命令行入口(cmd/tsgo)

命令行工具是用户与TypeScript Go交互的主要入口,采用经典的命令模式设计:

// cmd/tsgo/main.go
package main

import (
    "os"
    "github.com/microsoft/typescript-go/internal/execute"
)

func runMain() int {
    args := os.Args[1:]
    if len(args) > 0 {
        switch args[0] {
        case "--lsp":
            return runLSP(args[1:])
        case "--api":
            return runAPI(args[1:])
        case "--watch":
            return runWatch(args[1:])
        }
    }
    result := execute.CommandLine(newSystem(), args, nil)
    return int(result.Status)
}

func main() {
    os.Exit(runMain())
}

该模块支持四种运行模式:

  • 默认编译模式:执行一次完整的类型检查和代码生成
  • LSP语言服务模式--lsp):启动Language Server Protocol服务,供VS Code等编辑器使用
  • API服务模式--api):提供程序化接口,供构建工具调用
  • 监听模式--watch):监听文件变化,自动增量编译

3.2 编译器核心(internal/compiler)

这是编译器的心脏,包含完整的编译流水线:

词法分析(Scanner)

将源代码转换为Token流:

// internal/compiler/scanner.go
type Scanner struct {
    source   []rune
    pos      int
    token    Token
    tokenPos int
    tokenEnd int
}

func (s *Scanner) Scan() Token {
    s.skipWhitespace()
    s.tokenPos = s.pos
    
    switch s.source[s.pos] {
    case '(':
        s.pos++
        s.token = TokenOpenParen
    case '"', '\'':
        s.scanString()
    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
        s.scanNumber()
    default:
        if isLetter(s.source[s.pos]) {
            s.scanIdentifier()
        }
    }
    
    s.tokenEnd = s.pos
    return s.token
}

语法分析(Parser)

将Token流转换为抽象语法树(AST):

// internal/compiler/parser.go
type Parser struct {
    scanner *Scanner
    AST     *SourceFile
}

func (p *Parser) ParseSourceFile() *SourceFile {
    file := &SourceFile{
        Statements: make([]Statement, 0),
    }
    
    for {
        stmt := p.parseStatement()
        if stmt == nil {
            break
        }
        file.Statements = append(file.Statements, stmt)
    }
    
    return file
}

func (p *Parser) parseStatement() Statement {
    switch p.scanner.Token() {
    case TokenConst:
        return p.parseVariableDeclaration(true)
    case TokenFunction:
        return p.parseFunctionDeclaration()
    case TokenClass:
        return p.parseClassDeclaration()
    case TokenInterface:
        return p.parseInterfaceDeclaration()
    case TokenType:
        return p.parseTypeAliasDeclaration()
    default:
        return p.parseExpressionStatement()
    }
}

类型检查器(Checker)

这是编译器最复杂的部分,实现了完整的类型系统:

// internal/compiler/checker.go
type Checker struct {
    program   *Program
    symbols   map[string]*Symbol
    types     map[string]*Type
    errors    []Diagnostic
}

func (c *Checker) Check(node Node) *Type {
    switch n := node.(type) {
    case *VariableDeclaration:
        return c.checkVariableDeclaration(n)
    case *FunctionDeclaration:
        return c.checkFunctionDeclaration(n)
    case *CallExpression:
        return c.checkCallExpression(n)
    }
    return c.anyType
}

func (c *Checker) checkCallExpression(node *CallExpression) *Type {
    funcType := c.Check(node.Expression)
    if funcType.Kind != TypeFunction {
        c.error(node.Pos(), "Cannot invoke non-function type")
        return c.anyType
    }
    
    // 检查参数数量和类型
    if len(node.Arguments) != len(funcType.Parameters) {
        c.error(node.Pos(), "Argument count mismatch")
    }
    
    for i, arg := range node.Arguments {
        argType := c.Check(arg)
        paramType := funcType.Parameters[i].Type
        if !c.isAssignableTo(argType, paramType) {
            c.error(arg.Pos(), "Type mismatch")
        }
    }
    
    return funcType.ReturnType
}

可分配性检查

TypeScript的结构化类型系统要求递归检查类型结构:

func (c *Checker) isAssignableTo(source, target *Type) bool {
    // any类型可以赋值给任何类型
    if source == c.anyType || target == c.anyType {
        return true
    }
    
    // 原始类型直接比较
    if source.Kind == target.Kind && source.Kind <= TypeBoolean {
        return true
    }
    
    // 对象类型需要结构兼容
    if source.Kind == TypeObject && target.Kind == TypeObject {
        return c.isObjectAssignableTo(source, target)
    }
    
    // 联合类型:source必须能赋值给某个分支
    if target.Kind == TypeUnion {
        for _, branch := range target.UnionTypes {
            if c.isAssignableTo(source, branch) {
                return true
            }
        }
        return false
    }
    
    return false
}

func (c *Checker) isObjectAssignableTo(source, target *Type) bool {
    // 目标类型的每个属性必须在源类型中存在兼容的类型
    for _, targetProp := range target.Properties {
        sourceProp, ok := source.GetProperty(targetProp.Name)
        if !ok {
            return false
        }
        if !c.isAssignableTo(sourceProp.Type, targetProp.Type) {
            return false
        }
    }
    return true
}

3.3 语言服务(internal/lsp)

LSP模块实现了Language Server Protocol,为编辑器提供智能提示:

// internal/lsp/server.go
type Server struct {
    conn      *jsonrpc2.Conn
    documents map[DocumentURI]*TextDocument
    program   *Program
}

func (s *Server) Handle(ctx context.Context, req *Request) (interface{}, error) {
    switch req.Method {
    case "textDocument/completion":
        return s.handleCompletion(req.Params)
    case "textDocument/hover":
        return s.handleHover(req.Params)
    case "textDocument/definition":
        return s.handleDefinition(req.Params)
    case "textDocument/references":
        return s.handleReferences(req.Params)
    }
    return nil, nil
}

func (s *Server) handleCompletion(params *CompletionParams) (*CompletionList, error) {
    pos := params.Position
    node := s.program.GetNodeAtPosition(pos)
    
    list := &CompletionList{
        Items: make([]CompletionItem, 0),
    }
    
    // 根据上下文提供补全建议
    switch node.(type) {
    case *PropertyAccessExpression:
        // obj. 之后,列出对象属性
        objType := s.program.GetTypeOf(node.(*PropertyAccessExpression).Expression)
        for _, prop := range objType.Properties {
            list.Items = append(list.Items, CompletionItem{
                Label:  prop.Name,
                Kind:   CompletionItemKindProperty,
                Detail: prop.Type.String(),
            })
        }
    case *Identifier:
        // 标识符补全:变量、函数、类型
        scope := s.program.GetScopeAt(pos)
        for name, symbol := range scope.Symbols {
            list.Items = append(list.Items, CompletionItem{
                Label:  name,
                Kind:   symbolToCompletionKind(symbol),
                Detail: symbol.Type.String(),
            })
        }
    }
    
    return list, nil
}

3.4 项目系统(internal/project)

处理tsconfig.json解析和项目引用:

// internal/project/config.go
type Project struct {
    RootDir      string
    CompilerOptions *CompilerOptions
    SourceFiles  []string
    References   []string
}

func LoadProject(configPath string) (*Project, error) {
    content, err := os.ReadFile(configPath)
    if err != nil {
        return nil, err
    }
    
    var config TsConfig
    if err := json.Unmarshal(content, &config); err != nil {
        return nil, err
    }
    
    project := &Project{
        RootDir: filepath.Dir(configPath),
        CompilerOptions: parseCompilerOptions(config.CompilerOptions),
    }
    
    // 解析files、include、exclude
    project.SourceFiles = resolveSourceFiles(config, project.RootDir)
    
    // 解析项目引用
    for _, ref := range config.References {
        refPath := filepath.Join(project.RootDir, ref.Path, "tsconfig.json")
        project.References = append(project.References, refPath)
    }
    
    return project, nil
}

3.5 增量编译(internal/incremental)

实现高效的增量构建:

// internal/incremental/builder.go
type IncrementalBuilder struct {
    lastBuild    *BuildSnapshot
    fileWatcher  *fsnotify.Watcher
}

func (b *IncrementalBuilder) Build() (*Program, error) {
    changes := b.detectChanges()
    
    if len(changes) == 0 {
        return b.lastBuild.Program, nil
    }
    
    // 只重新编译修改的文件及其依赖
    affectedFiles := b.computeAffectedFiles(changes)
    
    program := b.program.Clone()
    for _, file := range affectedFiles {
        program.Recompile(file)
    }
    
    return program, nil
}

func (b *IncrementalBuilder) detectChanges() []string {
    changes := make([]string, 0)
    
    for {
        select {
        case event := <-b.fileWatcher.Events:
            if event.Op&fsnotify.Write == fsnotify.Write {
                changes = append(changes, event.Name)
            }
        default:
            return changes
        }
    }
}

3.6 JSX支持(internal/jsx)

处理React/Vue等JSX语法:

// internal/jsx/jsx.go
func (p *Parser) parseJSXElement() *JSXElement {
    element := &JSXElement{
        OpenElement:  p.parseJSXOpeningElement(),
        Children:     make([]JSXChild, 0),
        CloseElement: nil,
    }
    
    for !p.isJSXClosingElement() {
        child := p.parseJSXChild()
        element.Children = append(element.Children, child)
    }
    
    element.CloseElement = p.parseJSXClosingElement()
    return element
}

func (c *Checker) checkJSXElement(node *JSXElement) *Type {
    // 检查组件props类型
    componentType := c.Check(node.OpenElement.TagName)
    propsType := c.getPropsTypeOfComponent(componentType)
    
    // 检查属性类型
    for _, attr := range node.OpenElement.Attributes {
        attrType := c.Check(attr.Value)
        expectedType := propsType.GetProperty(attr.Name).Type
        if !c.isAssignableTo(attrType, expectedType) {
            c.error(attr.Pos(), "JSX property type mismatch")
        }
    }
    
    return componentType
}

3.7 扩展系统(internal/extensions)

支持自定义编译器插件:

// internal/extensions/plugin.go
type CompilerPlugin interface {
    Name() string
    BeforeCompile(ctx *CompileContext) error
    AfterCompile(ctx *CompileContext, result *CompileResult) error
    Transform(node Node) Node
}

// 内置装饰器插件
type DecoratorPlugin struct{}

func (p *DecoratorPlugin) Transform(node Node) Node {
    if class, ok := node.(*ClassDeclaration); ok {
        for _, decorator := range class.Decorators {
            if decorator.Expression.(*Identifier).Text == "Component" {
                // 将装饰器转换为Vue组件选项
                return p.transformToVueComponent(class)
            }
        }
    }
    return node
}

四、性能优化:10倍提升的技术原理

4.1 并行编译:从单线程到多核

TypeScript Go的核心性能提升来自并行编译:

文件级并行

每个源文件的解析、类型检查独立进行,利用Goroutine并行处理:

func (p *Program) CompileParallel(files []*SourceFile) {
    var wg sync.WaitGroup
    semaphore := make(chan struct{}, runtime.NumCPU())
    
    for _, file := range files {
        wg.Add(1)
        go func(f *SourceFile) {
            defer wg.Done()
            semaphore <- struct{}{}
            defer func() { <-semaphore }()
            
            p.CompileFile(f)
        }(file)
    }
    
    wg.Wait()
}

符号级并行

在类型检查阶段,不同符号的类型推断可以并行:

func (c *Checker) CheckSymbolsParallel(symbols []*Symbol) {
    results := make(chan *Symbol, len(symbols))
    
    for _, sym := range symbols {
        go func(s *Symbol) {
            s.Type = c.inferType(s)
            results <- s
        }(sym)
    }
    
    for i := 0; i < len(symbols); i++ {
        <-results
    }
}

4.2 内存优化:减少GC压力

对象池复用

使用sync.Pool复用临时对象:

var nodePool = sync.Pool{
    New: func() interface{} {
        return &Node{Children: make([]*Node, 0, 10)}
    },
}

func newNode() *Node {
    return nodePool.Get().(*Node)
}

func releaseNode(n *Node) {
    n.Children = n.Children[:0]
    nodePool.Put(n)
}

预分配切片

避免频繁扩容:

// 不好的做法
var nodes []*Node
for _, token := range tokens {
    nodes = append(nodes, parseNode(token))
}

// 好的做法
nodes := make([]*Node, 0, len(tokens))
for _, token := range tokens {
    nodes = append(nodes, parseNode(token))
}

4.3 算法优化:降低复杂度

增量类型检查

缓存类型检查结果,只重新检查修改的节点及其依赖:

type TypeCache struct {
    cache map[NodeID]*Type
    dirty map[NodeID]bool
}

func (c *TypeCache) Get(node Node) *Type {
    if !c.dirty[node.ID()] {
        return c.cache[node.ID()]
    }
    
    // 重新计算
    t := c.check(node)
    c.cache[node.ID()] = t
    c.dirty[node.ID()] = false
    return t
}

联合类型优化

使用类型守卫减少联合类型的检查分支:

func (c *Checker) checkUnionType(source, target *Type) bool {
    // 使用discriminant字段快速判断
    if source.Discriminant != "" && target.Discriminant != "" {
        if source.DiscriminantValue == target.DiscriminantValue {
            return true
        }
    }
    
    // 回退到逐分支检查
    for _, branch := range target.UnionTypes {
        if c.isAssignableTo(source, branch) {
            return true
        }
    }
    return false
}

4.4 基准测试数据

根据微软发布的数据,TypeScript 7.0 Beta在不同项目规模下的性能提升:

项目规模TS 6.0 编译时间TS 7.0 编译时间提升倍数
10万行代码8.2秒0.9秒9.1x
50万行代码45秒4.2秒10.7x
100万行代码180秒15秒12x

内存占用也显著降低:

项目规模TS 6.0 内存TS 7.0 内存降幅
10万行代码2.1GB480MB77%
50万行代码8.5GB1.2GB86%
100万行代码18GB2.8GB84%

五、实战:如何在项目中使用TypeScript Go

5.1 安装

从GitHub下载预编译二进制:

# macOS/Linux
curl -L https://github.com/microsoft/typescript-go/releases/download/v7.0.0-beta/tsgo-darwin-arm64 -o tsgo
chmod +x tsgo
sudo mv tsgo /usr/local/bin/

# Windows (PowerShell)
Invoke-WebRequest -Uri "https://github.com/microsoft/typescript-go/releases/download/v7.0.0-beta/tsgo-windows-amd64.exe" -OutFile "tsgo.exe"

5.2 基本使用

命令行接口与tsc兼容:

# 编译项目
tsgo

# 监听模式
tsgo --watch

# 启动语言服务
tsgo --lsp

# 指定配置文件
tsgo -p tsconfig.build.json

5.3 VS Code集成

修改settings.json使用新的语言服务:

{
  "typescript.tsdk": "/path/to/tsgo/node_modules/typescript/lib",
  "typescript.enablePromptUseOfWorkspaceTsdk": true
}

或直接使用LSP模式:

{
  "typescript.serverPath": "/usr/local/bin/tsgo",
  "typescript.serverArgs": ["--lsp"]
}

5.4 构建工具集成

Webpack集成:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              compiler: 'tsgo',  // 使用Go编译器
            }
          }
        ]
      }
    ]
  }
};

Vite集成:

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  esbuild: {
    tsconfigRaw: {
      compilerOptions: {
        // 使用原生esbuild处理TS,类型检查交给tsgo
      }
    }
  },
  build: {
    rollupOptions: {
      plugins: [
        // 类型检查插件
        typescript({
          compiler: 'tsgo',
        })
      ]
    }
  }
});

六、影响与展望

6.1 对前端生态的影响

开发体验的革命性提升

10倍的编译速度提升,意味着百万行代码的项目可以在十几秒内完成类型检查。开发者不再需要因为编译慢而关闭类型检查,CI/CD流水线的构建时间大幅缩短。

工具链的重新洗牌

esbuild、Bun等新一代前端工具已经证明了原生编译的优势。TypeScript Go的加入,意味着前端工具链将全面进入"原生时代"。基于Node.js的工具可能面临淘汰压力。

新语言的启发

TypeScript Go的成功,可能会激励其他语言采用类似策略。例如,Python的类型检查器(mypy、pyright)是否也会考虑用原生语言重写?

6.2 未来的挑战

生态迁移成本

虽然TypeScript Go保持了语义一致性,但插件生态需要迁移。现有的TypeScript插件(如typescript-eslint、ts-morph)需要适配新的API。

调试支持

Go编译的程序如何与JavaScript调试器集成?Source Map生成是否与现有工具链兼容?这些问题需要进一步解决。

社区治理

TypeScript的开源社区已经非常活跃。引入Go代码库后,如何管理贡献者、处理issue、发布版本,都需要新的治理策略。

6.3 对开发者的启示

学习Go语言的时机

如果你是前端开发者,现在是学习Go的好时机。不仅因为TypeScript Go,esbuild、Bun、Terraform、Kubernetes等工具都用Go编写。

关注性能工程

性能优化不是一朝一夕的事,需要从架构设计、算法选择、语言特性等多维度考量。TypeScript Go的10倍性能提升,是长期积累的结果。

拥抱变化

前端工具链正在经历代际更迭。保持开放心态,拥抱新技术,才能在快速变化的行业中保持竞争力。


七、总结

TypeScript 7.0 Beta的发布,标志着前端编译器进入了一个新时代。微软用Go重写TypeScript编译器的决策,是基于性能瓶颈的理性选择。10倍的性能提升,不是魔法,而是并行计算、内存优化、算法改进的综合结果。

对于前端开发者而言,这意味着:

  • 开发体验的显著改善,等待编译的时间大幅缩短
  • 新技术栈的学习机会,Go语言在前端领域的应用将更加广泛
  • 工具链的升级,需要关注生态迁移和兼容性问题

TypeScript Go的代码已经在GitHub开源,感兴趣的开发者可以关注官方仓库,了解最新进展。未来已来,让我们一起见证前端工具链的变革。


参考链接:

复制全文 生成海报 TypeScript Go 编译器 性能优化 微软

推荐文章

什么是Vue实例(Vue Instance)?
2024-11-19 06:04:20 +0800 CST
12个非常有用的JavaScript技巧
2024-11-19 05:36:14 +0800 CST
Vue 中如何处理父子组件通信?
2024-11-17 04:35:13 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
FastAPI 入门指南
2024-11-19 08:51:54 +0800 CST
html一些比较人使用的技巧和代码
2024-11-17 05:05:01 +0800 CST
程序员茄子在线接单