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.params[0]

接下来我们按照看源码的惯例: 向前找入参 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
    }`
  }
}

总结

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

相关推荐
Qrun29 分钟前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp30 分钟前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.1 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl3 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫5 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友5 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理7 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻7 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front8 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰8 小时前
纯flex布局来写瀑布流
前端·javascript·css