🧩 深入浅出讲解:analyzeScriptBindings —— Vue 如何分析 <script> 里的变量绑定

107 阅读3分钟

一、这段代码是干什么的?

Vue 组件有两种写法:

类型示例特点
普通 <script>export default { data(){...}, props:{...} }传统写法
<script setup>顶层直接写 const count = ref(0)Vue3 新写法

而 Vue 编译器在处理 .vue 文件时,需要知道:

每个变量来自哪里?它是 props 吗?data 吗?methods 吗?

这段源码的作用就是:
👉 当我们用“普通写法”时,分析出每个变量的“来源类型”。


二、运行结果长什么样?

假设我们有个组件:

export default {
  props: ['title'],
  data() {
    return { count: 0 }
  },
  methods: {
    inc() { this.count++ }
  }
}

经过这段分析函数后,会得到这样的结果对象:

{
  title: 'props',
  count: 'data',
  inc: 'options',
  __isScriptSetup: false
}

这就告诉 Vue:

  • title 来自 props;
  • count 来自 data;
  • inc 是 methods;
  • 不是 <script setup>

三、从头到尾一步步看源码逻辑

Step 1️⃣:找到 export default { ... }

export function analyzeScriptBindings(ast: Statement[]): BindingMetadata {
  for (const node of ast) {
    if (
      node.type === 'ExportDefaultDeclaration' &&
      node.declaration.type === 'ObjectExpression'
    ) {
      return analyzeBindingsFromOptions(node.declaration)
    }
  }
  return {}
}

🧠 意思:

  • AST 是整段脚本的语法树。

  • 遍历每个语句,找到:

    export default { ... }
    
  • 然后调用 analyzeBindingsFromOptions() 来分析里面的对象内容。


Step 2️⃣:创建结果对象并标记类型

const bindings: BindingMetadata = {}
Object.defineProperty(bindings, '__isScriptSetup', {
  enumerable: false,
  value: false,
})

📘 这一步干嘛?

  • 初始化一个结果对象;
  • 加一个隐藏属性 __isScriptSetup=false,告诉系统“这是普通 script”。

Step 3️⃣:逐个分析对象里的属性

例如:

export default {
  props: ['foo'],
  data() { return { msg: 'hi' } },
  methods: { sayHi() {} }
}

程序就会循环每个属性(props、data、methods...),判断它是哪种类型。


Step 4️⃣:不同类型的属性,分别分析

(1) props 分析

if (property.key.name === 'props') {
  for (const key of getObjectOrArrayExpressionKeys(property.value)) {
    bindings[key] = BindingTypes.PROPS
  }
}

🧠 支持两种写法:

  • props: ['foo', 'bar']
  • props: { foo: String }

结果:

{ foo: 'props', bar: 'props' }

(2) inject 分析

else if (property.key.name === 'inject') {
  for (const key of getObjectOrArrayExpressionKeys(property.value)) {
    bindings[key] = BindingTypes.OPTIONS
  }
}

对应:

inject: ['token']

👉 结果 { token: 'options' }


(3) methods / computed 分析

else if (
  property.value.type === 'ObjectExpression' &&
  (property.key.name === 'computed' || property.key.name === 'methods')
)

📘 当 methods: { sayHi(){} }computed: { total(){} } 时,
把每个函数名记录下来:

{ sayHi: 'options', total: 'options' }

(4) data / setup 分析

else if (
  property.type === 'ObjectMethod' &&
  (property.key.name === 'setup' || property.key.name === 'data')
)

这时要进入函数体里查找 return 的内容:

data() {
  return { count: 0 }
}
setup() {
  return { foo: ref(0) }
}

📘 分析结果:

  • data 返回的变量 → BindingTypes.DATA
  • setup 返回的变量 → BindingTypes.SETUP_MAYBE_REF

四、辅助函数们(简化理解)

1️⃣ 获取对象的键名

function getObjectExpressionKeys(node) {
  // 从 { foo: 1, bar: 2 } 中提取出 ['foo', 'bar']
}

2️⃣ 获取数组的键名

function getArrayExpressionKeys(node) {
  // 从 ['foo', 'bar'] 中提取出 ['foo', 'bar']
}

3️⃣ 自动判断是对象还是数组

export function getObjectOrArrayExpressionKeys(value) {
  // 根据类型选择上面的函数
}

五、整体运行逻辑图

AST语法树
   ↓
找到 export default {}
   ↓
进入 analyzeBindingsFromOptions()
   ↓
循环每个属性:
   - props → PROPS
   - inject → OPTIONS
   - methods/computed → OPTIONS
   - data → DATA
   - setup → SETUP_MAYBE_REF
   ↓
返回 BindingMetadata

六、为什么这么做?

因为 Vue 在模板编译时,需要知道哪些名字是:

  • 响应式变量(data、setup)
  • 只读输入(props)
  • 普通函数(methods)

这样模板里写的:

<p>{{ count }}</p>

才能被编译成正确的访问代码:

_ctx.count

或者:

_props.title

七、你可以怎么用它?

如果你想做:

  • 自定义 Vue 编译工具;
  • 分析 .vue 文件中定义的变量;
  • 或者写一个 ESLint 规则来检测组件结构;

就可以直接复用这段逻辑,让它帮你快速“读懂” Vue 组件结构。


八、潜在问题

问题说明
不支持动态 key[foo]: value 这种会被忽略
不识别 TS 类型如果写 props: { foo: String as PropType<number> } 不会处理
无法分析复杂 setup 返回逻辑例如条件 return 不被识别

✅ 总结一句话

这段代码的作用就是让编译器“看懂”一个普通 Vue 组件里的变量来源,区分哪些是 props、data、methods、setup 返回的。


本文部分内容借助 AI 辅助生成,并由作者整理审核。