Vue Macro编译原理: 以 type-only defineProps 为例

文章发布于个人博客 ray-d-song.com

Vue macro(宏) 随着<script setup>写法一同引入 Vue 生态, 进一步丰富了 Vue 在编译期的想象力.

本文主要分析 Vue defineProps 的 type-only 写法是如何根据类型信息生成运行时代码.

我们先拉取 Vue 的源代码, 并切换到3.0.3版本, 这是最早引入 script setup 和 defineProps macro 的版本.

Vue 的 script setup 编译器源码位于packages/compiler-sfc/src/compileScript.ts, 接下来所有的代码都出自这个文件

为了方便查看运行结果和打印, 在根目录下的package.json新增test-compiler命令, 这条命令表示只运行packages/compiler-sfc文件夹下的with TypeScript测试.

json 复制代码
"scripts": {
  "test-compiler": "jest packages/compiler-sfc --testPathPattern='packages/compiler-sfc' --testNamePattern='with TypeScript' ",
  },

先看 defineProps<{}>() 会被编译成什么

packages/compiler-sfc/__tests__/compileScript.spec.ts文件的451 行打个console.log(content), 查看编译完成的内容

输入:

typescript 复制代码
import { defineProps } from 'vue'


defineProps<{
  string: string
  number: number
  boolean: boolean
  object: object
  // ...
}>()

输出:

typescript 复制代码
import { defineComponent as _defineComponent } from 'vue'


export default _defineComponent({
  expose: [],
  props: {
    string: { type: String, required: true },
    number: { type: Number, required: true },
    boolean: { type: Boolean, required: true },
    object: { type: Object, required: true },
    // ...
  } as unknown as undefined,
  setup(__props: {
        string: string
        number: number
        boolean: boolean
        object: object
        // ...
      }) {

return {  }
}

})

可以看到 defineProps 被编译为defineComponent方法中的 options props 写法, 同时还定义了 defineComponent 方法中的 setup 函数选项并保留了类型的定义.

定位

packages/compiler-sfc/src/compileScript.ts文件中, 我们一眼能找到一个看起来是处理 props 的方法:

typescript 复制代码
function processDefineProps(node: Node): boolean {
  if (isCallOf(node, DEFINE_PROPS)) {
    hasDefinePropsCall = true
    // context call has type parameters - infer runtime types from it
    if (node.typeParameters) {
      const typeArg = node.typeParameters.params[0]
      if (typeArg.type === 'TSTypeLiteral') {
        propsTypeDecl = typeArg
      }
    }
    return true
  }
  return false
}

这个函数接受参数node, 并将propsTypeDecl赋值为 node.typeParameters.params0

接下来我们按照看源码的惯例: 向前找入参 node, 向后找 propsTypeDecl 的作用.

向前看: node 是什么, 从哪来

我们在 processDefineProps 方法中打印一下 node:

typescript 复制代码
Node {
  type: 'CallExpression',
  start: 101,
  end: 732,
  loc: SourceLocation {
    start: Position { line: 7, column: 6 },
    end: Position { line: 30, column: 10 },
    filename: undefined,
    identifierName: undefined
  },
  range: undefined,
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  callee: Node {
    type: 'Identifier',
    start: 101,
    end: 112,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: 'defineProps'
    },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    name: 'defineProps'
  },
  arguments: [],
  typeParameters: Node {
    type: 'TSTypeParameterInstantiation',
    start: 112,
    end: 730,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    params: [ [Node] ]
  }
}

熟悉 babel 的哥们应该一眼能看出这是 babel 的 AST(抽象语法树).

抽象语法树可以简单理解为分析源代码产生的相关信息

我们继续找, 看看是哪里提供了这个 node.

在 552 行, 我们可以找到 node 是 scriptSetupAst 的遍历子节点

typescript 复制代码
// line 552
for (const node of scriptSetupAst)

scriptSetupAst 是调用了 parse 函数, 传入了<script setup>标签内的内容

typescript 复制代码
// parse <script setup> and  walk over top level statements
const scriptSetupAst = parse(
  scriptSetup.content,
  {
    plugins: [
      ...plugins,
    ],
    sourceType: 'module'
  },
  startOffset
)

继续寻找 parse 函数的定义, 在 209 行.

parse 函数调用了_parse 函数, 而 _parse 函数是@babel/parser包中 parse 函数的别名, 该函数返回的就是 Babel AST 格式的 AST, 证实了之前的猜想

typescript 复制代码
function parse(
  input: string,
  options: ParserOptions,
  offset: number
): Statement[] {
  try {
    return _parse(input, options).program.body
  } catch (e) {
    ...
  }
}

到这里, 「向前看」的工作已经完成, 大体流程为

向后看: propsTypeDecl 有什么用, 怎么处理

processDefineProps 的作用是对 propsTypeDecl 赋值, 那么赋值后对 propsTypeDecl 进行了哪些操作就是生成运行时 props 定义的关键.

按照惯例, 首先打印 propsTypeDecl

typescript 复制代码
propsTypeDecl:  Node {
  type: 'TSTypeLiteral',
  start: 113,
  end: 729,
  loc: SourceLocation {
    start: Position { line: 7, column: 18 },
    end: Position { line: 30, column: 7 },
    filename: undefined,
    identifierName: undefined
  },
  range: undefined,
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  members: [
    Node {
      type: 'TSPropertySignature',
      start: 123,
      end: 137,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    Node {
      type: 'TSPropertySignature',
      start: 146,
      end: 160,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    Node {
      type: 'TSPropertySignature',
      start: 169,
      end: 185,
      loc: [SourceLocation],
      range: undefined,
      leadingComments: undefined,
      trailingComments: undefined,
      innerComments: undefined,
      extra: undefined,
      key: [Node],
      computed: false,
      typeAnnotation: [Node]
    },
    ...
  ]
}

可以看到, propsTypeDecl 就是 defineProps 类型声明的 AST. 主要内容是members字段, 每一个Node对应着一个 props 元素声明

接下来我们找哪里使用了这个propsTypeDecl

propsTypeDecl 有两处引用

生成运行时props(重点)

typescript 复制代码
// 4. extract runtime props/emits code from setup context type
if (propsTypeDecl) {
  extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
}

参数除了 propsTypeDecl 之外, 还有

  • typeDeclaredProps: 类型定义为Record<string, string[]>, 默认为{}的变量
  • declaredTypes: 类型Record<string, string[]>, 默认{}的变量

接下来看看这个函数都做了啥操作

typescript 复制代码
function extractRuntimeProps(
  node: TSTypeLiteral,
  props: Record<string, PropTypeData>,
  declaredTypes: Record<string, string[]>
) {
  // members 即 literal type 的AST数组
  for (const m of node.members) {
    // 判断是否为 literal type
    if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
      // 为 typeDeclaredProps 添加字段
      props[m.key.name] = {
        key: m.key.name,
        required: !m.optional,
        type:
          // dev 下生成 type 字段, 生产环境不需要类型信息, 直接赋值 null
          __DEV__ && m.typeAnnotation
            ? inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
            : [`null`]
      }
    }
  }
}

该函数的作用就是根据 AST 的信息生成运行时的 props 声明, 并赋值给第二个参数typeDeclaredProps, 这个参数最终就是编译完成的 Props.

这里还调用了inferRuntimeType方法, 方法主体就是 switch 语句, 根据不同的node.type字段返回不同的运行时类型声明

截取其中一小段:

typescript 复制代码
switch (node.type) {
  case 'TSStringKeyword':
    return ['String']
  case 'TSNumberKeyword':
    return ['Number']

生成 __props 字段

propsTypeDecl 另一处引用是用来生成defineProps方法中的__props字段:

typescript 复制代码
// 9. finalize setup() argument signature
let args = `__props`
if (propsTypeDecl) {
  args += `: ${scriptSetup.content.slice(
    propsTypeDecl.start!,
    propsTypeDecl.end!
  )}`
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (propsIdentifier) {
  s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
}
if (emitIdentifier) {
  args +=
    emitIdentifier === `emit` ? `, { emit }` : `, { emit: ${emitIdentifier} }`
  if (emitTypeDecl) {
    args += `: {
      emit: (${scriptSetup.content.slice(
        emitTypeDecl.start!,
        emitTypeDecl.end!
      )}),
      slots: any,
      attrs: any
    }`
  }
}

总结

到这里我们分析完了整个的流程, 如下:

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒11 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端