Skip to content

setup() 组合式组件

Vue 2.7 内置 Composition API。默认配置 compositionAPI: 'native' 会从 vue 导入 JSX render function 所需的 h,并对齐 @vue/babel-sugar-composition-api-render-instance 的生成代码处理逻辑。

两类 this 必须区分

业务源码中的 this

Vue 2.7 调用 setup() 时不会把组件实例绑定为 this。因此用户源码中的下面写法仍然是编译错误:

jsx
export default {
  setup() {
    return () => (
      <button onClick={() => this.$emit('submit')}>
        提交
      </button>
    )
  }
}

应该使用 setup context:

jsx
export default {
  setup(_props, { emit }) {
    const submit = () => emit('submit')
    return () => <button onClick={submit}>提交</button>
  }
}

插件不会把用户写下的 this.$emitthis.$attrs 或任意其他 this.* 偷偷改写成组件实例。

JSX sugar 生成的实例辅助调用

Vue 2 JSX 的 v-model sugar 为成员表达式生成赋值代码时,会使用 Vue 2 实例辅助方法。例如:

tsx
setup() {
  const form = reactive({ name: '' })
  return () => <ModelInput vModel={form.name} />
}

v-model lowering 需要生成等价于下面的回调:

js
callback: $$v => {
  this.$set(form, 'name', $$v)
}

这个 this 不是用户源码,而是 JSX sugar 生成的内部代码。@vue/babel-sugar-composition-api-render-instance 的职责就是在 setup() 中捕获当前实例,并把这类生成代码改为实例变量访问。

本插件在 Vue 2.7 native 模式下输出:

js
import { getCurrentInstance, h } from 'vue'

setup() {
  const __currentInstance = getCurrentInstance().proxy

  return () => h(ModelInput, {
    model: {
      value: form.name,
      callback: $$v => {
        __currentInstance.$set(form, 'name', $$v)
      }
    }
  })
}

为什么使用 .proxy

旧 Babel sugar 最初输出 getCurrentInstance()。Vue 2.7 的 getCurrentInstance() 返回 { proxy },传统 Vue 2 实例方法 $set_n_q_i_k 位于 proxy 上,因此本插件捕获 getCurrentInstance().proxy

对齐范围

实例只在生成代码确实需要时注入:

JSX 功能生成的 Vue 2 实例辅助方法
成员表达式 v-model$set
v-model_number_n
checkbox / radio 比较_q_i
v-on 按键过滤 sugar_k

简单 setup JSX 不会导入 getCurrentInstance

jsx
setup() {
  return () => <div>plain setup render</div>
}

onClick={() => this.$emit('ping')} 也不属于 render-instance sugar 的处理范围,它仍会因源码使用 this 而报错。

转换顺序

text
识别 setup() 作用域

检查用户源码中的 this

导入 Composition API h

编译 JSX / v-model / v-on

发现生成的 $set / _n / _q / _i / _k

按需注入 getCurrentInstance().proxy

完整 render-instance Demo

下面的组件同时覆盖组件 v-model、数字修饰符和 checkbox 数组:

vue
<script lang="tsx">
import { defineComponent, reactive } from 'vue'
import ModelInput from '../shared/ModelInput.vue'

export default defineComponent({
  name: 'SetupRenderInstanceSfc',
  setup() {
    const form = reactive({
      name: '',
      amount: 1,
      skills: [] as string[]
    })

    return () => (
      <article class="demo-card">
        <span class="case-label">composition render instance</span>
        <h3>setup() 中的 v-model 运行时辅助</h3>
        <p>
          源码没有使用 this;编译器仅为 v-model 生成的
          $set、_n、_i 辅助调用捕获当前 Vue 实例。
        </p>

        <ModelInput
          label="姓名"
          placeholder="输入姓名"
          vModel={form.name}
        />

        <label class="field-row">
          <span>数量</span>
          <input
            class="control"
            type="number"
            vModel_number={form.amount}
          />
        </label>

        <div class="inline-options">
          {['JSX', 'TSX'].map(skill => (
            <label key={skill}>
              <input
                type="checkbox"
                value={skill}
                vModel={form.skills}
              />
              {skill}
            </label>
          ))}
        </div>

        <p class="result-line">
          name: {form.name || '-'} · amount: {form.amount} · skills:{' '}
          {form.skills.join(', ') || '-'}
        </p>
      </article>
    )
  }
})
</script>

SFC setup render

vue
<script lang="jsx">
import { computed, defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'SetupSfcJsx',
  setup() {
    const count = ref(0)
    const label = computed(() => `setup JSX:${count.value}`)

    return () => (
      <article class="demo-card">
        <span class="case-label">setup() SFC / JSX</span>
        <h3>{label.value}</h3>
        <p>Vue 2.7 原生 ref、computed 与 setup render function。</p>
        <button class="button" onClick={() => { count.value += 1 }}>count + 1</button>
      </article>
    )
  }
})
</script>
vue
<script lang="tsx">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'SetupSfcTsx',
  setup() {
    const items = ref<string[]>(['Oxc', 'Vue 2.7'])
    const addItem = (): void => {
      items.value.push(`item-${items.value.length + 1}`)
    }

    return () => (
      <article class="demo-card">
        <span class="case-label">setup() SFC / TSX</span>
        <h3>类型化 setup render</h3>
        <ul>{items.value.map(item => <li key={item}>{item}</li>)}</ul>
        <button class="button" onClick={addItem}>添加项目</button>
      </article>
    )
  }
})
</script>

setup context

vue
<script lang="jsx">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'SetupContextSfc',
  inheritAttrs: false,
  props: {
    pingTotal: { type: Number, default: 0 }
  },
  setup(props, { attrs, emit, listeners, slots }) {
    const count = ref(0)

    const emitPing = () => {
      count.value += 1
      emit('ping', count.value)
    }

    return () => {
      const rootData = { attrs }

      return (
        <article {...rootData} class="demo-card">
          <span class="case-label">setup(props, context)</span>
          <h3>使用 setup context,不使用 this</h3>
          <p>emit、attrs、listeners 和 slots 都从 setup 的第二个参数取得。</p>
          <p>父级监听器:{Object.keys(listeners).join(', ') || 'none'}</p>
          <p class="result-line">父组件累计收到 ping:{props.pingTotal}</p>
          {slots.default ? slots.default() : null}
          <button class="button" onClick={emitPing}>emit ping:{count.value}</button>
        </article>
      )
    }
  }
})
</script>

独立 setup 模块

jsx
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'SetupModuleJsx',
  setup() {
    const active = ref(false)
    return () => (
      <article class={['demo-card', { active: active.value }]}>
        <span class="case-label">setup module / JSX</span>
        <h3>独立 .jsx setup 组件</h3>
        <button class="button" onClick={() => { active.value = !active.value }}>
          active: {String(active.value)}
        </button>
      </article>
    )
  }
})
tsx
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'SetupModuleTsx',
  setup() {
    const value = ref<number>(10)
    return () => (
      <article class="demo-card">
        <span class="case-label">setup module / TSX</span>
        <h3>独立 .tsx setup 组件</h3>
        <button class="button" onClick={() => { value.value += 5 }}>
          value: {value.value}
        </button>
      </article>
    )
  }
})

Options API 与 setup 的区别

Options API 的 datacomputedmethodsrender 仍可以使用组件实例 this

jsx
export default {
  data() {
    return { count: 0 }
  },
  render() {
    return <button onClick={() => { this.count += 1 }}>{this.count}</button>
  }
}

基于 Oxc Parser 的 Vue 2.7 JSX/TSX 转换器