Skip to content

v-bind 与动态属性的转换

v-bind(简写 :)是属性绑定指令。它看起来简单,但涉及 class、style 的特殊处理和无参数的对象绑定。

理解 v-bind 的转换原理,能帮你更好地理解为什么 class 和 style 需要特殊处理。 本章将分析 v-bind 转换器的实现。

转换目标

html
<!-- 原始模板 -->
<div :id="dynamicId" :class="{ active: isActive }" :style="styles">
  Content
</div>
<div v-bind="attrs">With Object</div>
javascript
// 转换后
function render(_ctx) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("div", {
      id: _ctx.dynamicId,
      class: _normalizeClass({ active: _ctx.isActive }),
      style: _normalizeStyle(_ctx.styles)
    }, "Content"),
    
    _createElementVNode("div",
      _normalizeProps(_guardReactiveProps(_ctx.attrs)),
      "With Object"
    )
  ], 64))
}

transformBind 实现

javascript
const transformBind = (dir, _node, context) => {
  const { exp, modifiers, loc } = dir
  const arg = dir.arg
  
  // 处理动态参数
  if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
    arg.children.unshift('(')
    arg.children.push(') || ""')
  } else if (!arg.isStatic) {
    arg.content = `${arg.content} || ""`
  }
  
  // 处理修饰符
  if (modifiers.includes('camel')) {
    if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
      if (arg.isStatic) {
        arg.content = camelize(arg.content)
      } else {
        arg.content = `${context.helperString(CAMELIZE)}(${arg.content})`
      }
    } else {
      arg.children.unshift(`${context.helperString(CAMELIZE)}(`)
      arg.children.push(')')
    }
  }
  
  // .prop 修饰符 - 强制为 DOM property
  if (modifiers.includes('prop')) {
    injectPrefix(arg, '.')
  }
  
  // .attr 修饰符 - 强制为 attribute
  if (modifiers.includes('attr')) {
    injectPrefix(arg, '^')
  }
  
  // 无表达式的绑定
  if (!exp || (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())) {
    context.onError(
      createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc)
    )
    return {
      props: [createObjectProperty(arg, createSimpleExpression('', true))]
    }
  }
  
  return {
    props: [createObjectProperty(arg, exp)]
  }
}

修饰符处理

.camel 修饰符

将属性名转为驼峰:

html
<svg :view-box.camel="viewBox">
javascript
// 转换后
{ viewBox: _ctx.viewBox }

.prop 修饰符

强制绑定为 DOM property:

html
<div :text-content.prop="text">
javascript
// 使用 . 前缀标记
{ '.textContent': _ctx.text }

.attr 修饰符

强制绑定为 HTML attribute:

html
<div :foo.attr="bar">
javascript
// 使用 ^ 前缀标记
{ '^foo': _ctx.bar }

class 的特殊处理

class 支持多种格式,需要运行时规范化:

html
<!-- 对象语法 -->
:class="{ active: isActive, 'text-danger': hasError }"

<!-- 数组语法 -->
:class="[activeClass, errorClass]"

<!-- 混合语法 -->
:class="[{ active: isActive }, errorClass]"

transformElement 中处理:

javascript
function buildProps(node, context) {
  const properties = []
  
  for (const prop of props) {
    if (prop.type === NodeTypes.DIRECTIVE && prop.name === 'bind') {
      const { props: bindProps } = transformBind(prop, node, context)
      
      for (const p of bindProps) {
        if (isStaticExp(p.key)) {
          // class 属性需要 normalizeClass
          if (p.key.content === 'class') {
            p.value = createCallExpression(
              context.helper(NORMALIZE_CLASS),
              [p.value]
            )
          }
          // style 属性需要 normalizeStyle
          else if (p.key.content === 'style') {
            p.value = createCallExpression(
              context.helper(NORMALIZE_STYLE),
              [p.value]
            )
          }
        }
        properties.push(p)
      }
    }
  }
  
  return { properties }
}

生成的代码:

javascript
{
  class: _normalizeClass({ active: _ctx.isActive }),
  style: _normalizeStyle(_ctx.styles)
}

对象绑定(无参数)

html
<div v-bind="attrs">

这是把整个对象展开为属性。需要特殊处理:

javascript
function buildProps(node, context) {
  let mergeArgs = []
  let properties = []
  
  for (const prop of props) {
    if (prop.type === NodeTypes.DIRECTIVE && prop.name === 'bind' && !prop.arg) {
      // 无参数的 v-bind
      if (properties.length) {
        // 先把已收集的属性包装
        mergeArgs.push(createObjectExpression(properties))
        properties = []
      }
      // 添加展开的对象
      mergeArgs.push(prop.exp)
    } else {
      // 正常属性
      // ...
    }
  }
  
  if (mergeArgs.length > 1) {
    // 需要合并多个属性源
    return createCallExpression(
      context.helper(MERGE_PROPS),
      mergeArgs
    )
  }
}

生成的代码:

javascript
// 单个对象绑定
_normalizeProps(_guardReactiveProps(_ctx.attrs))

// 混合静态和动态
_mergeProps({ id: "static" }, _ctx.attrs, { class: "extra" })

normalizeClass 实现

javascript
function normalizeClass(value) {
  let res = ''
  
  if (isString(value)) {
    res = value
  } else if (isArray(value)) {
    for (const v of value) {
      const normalized = normalizeClass(v)
      if (normalized) {
        res += normalized + ' '
      }
    }
  } else if (isObject(value)) {
    for (const key in value) {
      if (value[key]) {
        res += key + ' '
      }
    }
  }
  
  return res.trim()
}

normalizeStyle 实现

javascript
function normalizeStyle(value) {
  if (isArray(value)) {
    const res = {}
    for (const v of value) {
      const normalized = isString(v)
        ? parseStringStyle(v)
        : normalizeStyle(v)
      if (normalized) {
        Object.assign(res, normalized)
      }
    }
    return res
  } else if (isString(value)) {
    return value
  } else if (isObject(value)) {
    return value
  }
}

静态 vs 动态属性

编译器区分静态和动态属性,生成不同的 PatchFlag:

html
<div id="static" :class="dynamic">
javascript
_createElementVNode("div", {
  id: "static",
  class: _normalizeClass(_ctx.dynamic)
}, null, 2 /* CLASS */)

PatchFlag 2 表示只有 class 是动态的,运行时可以优化 Diff。

本章小结

本章分析了 v-bind 转换器的实现:

  • 基本转换:属性名 + 表达式
  • 修饰符:.camel、.prop、.attr
  • class/style:规范化函数包装
  • 对象绑定:展开 + mergeProps
  • PatchFlag:区分静态和动态属性

下一章将分析 v-model——双向绑定的语法糖如何转换。

v-bind 与动态属性的转换 has loaded