主题
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.$emit、this.$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 的 data、computed、methods 和 render 仍可以使用组件实例 this:
jsx
export default {
data() {
return { count: 0 }
},
render() {
return <button onClick={() => { this.count += 1 }}>{this.count}</button>
}
}