作为一个Vue2的深度用户😳,看到Vue3的用法不免水土不服,尤其多了setup这么个东西,不免用起来屡试不爽😟,由于官方版本迭代,不得不顺应时代潮流,因它及其出色,技能爆棚,最终被它杰出身姿折服了,它就是setup。怀念过去,我们先来看看Vue2的组件中script脚本写法:
// 声明式书写组件
export default {
props:{
},
data(){
return {
name:'张三',
}
},
methods(){
myMethods(){ // 自定义方法
}
}
}
相比Vue2,我们可以再看看Vue3中的三种写法:
- 方式一:类似Vue2骨架,多了一个setup函数。
export default {
props:[],
setup(props,ctx) {
const name = ref('张三');// 响应式数据
const myMethods = ()=>{ // 自定义方法
}
return {
name,
myMethods,
}
},
}
- 方式二:全新骨架,多了一个defineComponent。
export default defineComponent({
props:[],
setup(props,ctx) {
....
},
})
- 方式三:全新骨架,脚本文件script标签定义setup。
<script setup>
interface Props {
name: string
}
// const { name, count = 100 } = defineProps<Props>(); // props方式一
withDefaults(defineProps<Props>(), { // props方式二
name: 'hello',
})
const myMethods = ()=>{ // 自定义方法
}
</script>
withDefaults帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。
综合Vue2与Vue3的写法,我们发现Vue3更花里胡哨的,写法与Vue2真的天差地别,细心的你可能发现了:使用了setup的Vue3几乎把Vue2中组件实例属性(data、method...)全部写到了setup中了,你可能会问😵:
- setup是什么?
- setup有什么作用?
- setup需要注意什么?
- defineComponent的作用又是什么?
- 相比Vue2内的options-api,Vue3的setup内使用composition-api使用有什么优势?
好的好的👌!问题既然提出来了,我们就要一一去解决它,接下来让我们一起来揭秘setup㊙!
1 setup是什么?
setup:是Vue3的一个新配置项,值为一个函数。它是组合式API(Composition API)的表演舞台,也就是说Vue3的所有组合式API都在setup里面使用了,它的核心作用就是setup里发生的作用,那么setup里面究竟做了什么(后面会说)?先看一看,既然它是一个函数,那么它的返回值是分为两种方式:对象类型和渲染函数类型:
组合式API是一系列API的集合,使我们可以使用函数而不是声明选项的方式书写Vue组件。它涵盖的API有:响应式API(如ref)、生命周期钩子(如onMounted)、依赖注入(provide、inject)。
setup(props,context){
const msg = ('Hello');
return {msg};// 对象类型,这种方式用的比较多
// return ()=>h('div',msg);// 渲染函数
}
setup可以接受两个参数:props(父组件传递给子组件的数据)和context(包含执行上下文的属性)。prop是响应式数据,在setup函数内是不能修改的,也不能解构props,防止丢失响应性;context是一个对象,可以使用es6语法直接解构(传递对象获取需要的属性如{emit}),context对象内有什么属性?如下图:
context对象内有四个属性:
attrs(获取当前标签上的所有属性的对象,除了props中接收的)、emit(传递给父组件的事件)、expose(控制组件暴露给父组件的内容)、slots(插槽集合)。
setup(props,context){
context.expose({
// 暴露的属性名
})
}
defineComponent中的setup
除了脚本定义setup,还有在defineComponent中使用setup时,这个defineComponent的作用是什么? defineComponent函数,只是对setup函数进行封装,返回options的对象,在TypeScript下,给予组件正确参数类型推断。
从上面源码中我们可以看出defineComponent为什么需要判断传入的options是否为一个函数,因为我们使用defineComponent的时候有两种方式:
// 函数式
export default defineComponent((props,context)=>{
console.log(props,context);
})
// 对象式
export default defineComponent({
name:'app',
setup(props,context){
console.log(props,context);
}
})
script中的setup
上面都是当setup是作为一个函数使用时,作为一个细节控,我们得杠一下,如果是使用的是脚本script定义setup,它们的参数又是怎么传递?我们可以先看看下面例子:
// 方式一
<script setup="props,context">
context.emit
....
</script>
// 方式二
<script setup="props,{emit}"></script>
参数与函数式传递差不多,只不过这里直接在setup属性写入。既然这样,可能我们在想,script下的setup与上面的setup函数式有什么区别?区别是:
-
无需返回属性,前面函数式使用setup必须要返回变量,在template才能获取到变量,而这里不需要return,直接使用。
-
无需手动注册组件:
import test1 from './components/test1.vue' // 方式一:需要注册组件 export default{ components:{ test1, }, } // 方式二:需要注册组件 import { defineComponent } from 'vue' export default defineComponent({ components:{ test1, }, }) // 方式三:不需要注册,直接引入就可以使用 <script setup> import test1 from './components/test1.vue' </script> -
支持async-await:如果使用setup函数是不支持的,因为setup不能是一个async函数,使用async返回值就不是return的对象,而是promise,模板就看不到return对象中属性,而在这里需要与Suspense结合使用,不然报警告信息。
<script lang="ts" setup> import axios from 'axios'; const result = await axios('https://api.github.com/users?per_page=5').then((item:any)=>item); </script> -
props的接收方式改变:
interface Props { name: string } // const { name, count = 100 } = defineProps<Props>(); // props方式一 const props = withDefaults(defineProps<Props>(), { // props方式二: name: 'hello', }) -
emits的接收方式改变:
// 获取 emit const emit = defineEmits(['chang-name']); // 调用 emit emit('chang-name', 'Amy'); -
slots的接收方式改变:使用useSlots()。
-
attrs的获取方式改变:使用useAttrs()。
-
expose暴露方式改变:使用defineExpose。
2 setup有什么作用??
在了解setup有什么作用之前,先在这里了解一下setup的执行时机:
import { defineComponent, onBeforeMount, onMounted } from 'vue'
export default {
beforeCreate(){
console.log('beforeCreate');
},
created(){
console.log('created');
},
setup(props,ctx) {
console.log('setup');
onMounted(()=>{
console.log('onMounted');
})
},
mounted(){
console.log('mounted')
}
}
// setup
// beforeCreate
// created
// onMounted
// mounted
从上面代码我们可以看出,setup的执行时机是在生命周期beforeCreate和created之前的,我们都知道beforeCreate周期只是初始化Vue实例空对象(只有默认生命周期和事件,data、method等属性都未被初始化),created是组件实例已经完全创建,data、methods等属性已初始化,组件实例还没有挂载。考虑Vue3的执行时机,所以在setup函数里this是毫无意义的,this为undefined。Vue3去掉了这两个周期,setup内已经默认实现了这两个周期需要做的事情,Vue3新命名了周期(Vue2周期命名前添加on)以及封装生命周期为组合式api,方便开发者的使用:
<script setup lang="ts">
import { onMounted } from 'vue';
// beforedMounted -> onBeforeMount
// mounted -> onMounted
onMounted(() => {
console.log('mounted!');
});
// beforeUpdate -> onBeforeUpdate
// updated -> onUpdated
// beforeUnmount -> onBeforeUnmount
// unmounted -> onUnmounted
// errorCaptured -> onErrorCaptured
// renderTracked -> onRenderTracked
// renderTriggered -> onRenderTriggered
// activated -> onActivated
// deactivated -> onDeactivated
</script>
既然我们现在知道了什么时候执行setup,那么执行之后setup有什么作用?Vue3使用Composition API组合式API去封装生命周期,丢弃Vue2的选项式api,也就是丢弃需要开发者创建组件实例配置使用默认属性,Vue3使用更灵活,根据需要import引入相关属性,在setup函数可以做计算属性、方法的使用、变量响应式使用等。
2 相比Vue2,Vue3的setup优势如何?
- 提供script标签引入共同业务逻辑的代码块,顺序执行。
- 组件直接挂载,无需注册,这种是针对script setup模式下。
- 自定义的指令,可以在模板中自动获得。
- this不再是活跃实例的引用
- 创建全新Composition的api,如defineProps等。
- 通过ref和reactive灵活控制属性的响应式的创建。
- 兼容性强大,支持Vue2,为Vue2配置中可以访问到setup中的属性、方法,但在setup中不能访问到Vue2配置,如果有重名属性,setup优先
3 setup执行原理是什么?
Vue3的setup函数,是整个组件逻辑组织的入口,我们可以在它内部写组合式API,那么我们有没有想过setup函数是如何执行的?它返回的数据又是如何与模板建立联系的?下面我们一步一步来学习!
首先,我们先来回忆一下组件的渲染流程是:创建vnode、渲染vnode和生成DOM,其中渲染vnode的过程主要是在挂载组件:
在挂载组件过程中主要做了三件事情:
创建组件实例、设置组件实例、设置并运行带副作用的渲染函数。
从源码中可以看到,上面主要是创建instance实例创建了组件实例后,对组件的相关属性进行初始化,然后接着就是组件实例的设置了。执行setup函数主要是在setupComponent函数中。setupComponnet设置组件实例也是主要做了三件事情:
判断当前组件实例是否是一个有状态组件、初始化(Props、Slots)、执行setup处理函数。
判断当前组件实例是否是一个有状态组件,通过isStatefulComponent函数判断当前实例的vnode.type是否为对象类型,我们可能会问shapeFlag是什么,是一种判断vnode.type的一种类型状态,源码中获取shapeFlag是这样的:
const shapeFlag =
isString(type) ? 1 /* ELEMENT */ :
isSuspense(type) ? 128 /* SUSPENSE */ :
isTeleport(type) ? 64 /* TELEPORT */ :
isObject(type) ? 4 /* STATEFUL_COMPONENT */ :
isFunction(type) ? 2 /* FUNCTIONAL_COMPONENT */ : 0;
我们再看看下面例子执行后获取到的vnode.type是啥?
<script src="../dist/vue.global.js"></script>
<div id="app">
{{name}}
</div>
<script>
Vue.createApp({
setup() {
const name = Vue.ref('张三');
return {
name
}
}
}).mount("#app")
</script>
从上面我们可以看出实例的type值就是一个包含setup函数、template的对象,从而获取到isStateful为4对应STATEFUL_COMPONENT。从而执行setupStatefulComponent函数中setup获取结果。setupStatefulComponent函数主要做四件事:
创建渲染上下文代理、创建setupContext(初始化参数context的四个属性)、执行setup函数获取结果、处理setup函数执行结果。
在Vue2初始化组件时候,data中定义的变量name在组件内部是存储在this._ name上的,而模板渲染的时候通过this.name,实际上访问的是this._ data.name,这是因为Vue2初始化data的时候,做了一层proxy代理。
创建渲染上下文代理:在Vue3中为了方便维护,我们把组件中不同状态的数据存储到不同的属性中如:存储到ctx、data、props、setupState中,当执行组件渲染函数,为了方便开发者使用,会直接访问渲染上下文instance.ctx属性,所以需要做一层proxy,对渲染上下文instance.ctx属性的访问和修改,代理到对setupState、ctx、data、props 中的数据的访问和修改。代理proxy中的几个方法:get、set、has、defineProperty,当我们访问instance.ctx渲染上下文中的属性时,就会进入get函数。这里就不一一分析PublicInstanceProxyHandlers函数的实现。
创建setupContext:接下来判断setup中是否定义了传参,如果一个参数就不做处理,也就是传props,从而知道了props内部是不能修改它内部数据的;如果定义了第二个参数context,上面也介绍了context的四个属性,createSetupContext函数主要是对attrs、emit、slots、expose的初始化。这里也不一一分析createSetupContext函数的实现。
执行setup函数获取结果:获取setupResult,也就是执行setup函数获取结果,比如上面例子setupResult结果就是一个包含变量name的对象:
从上面可以看到它其实就是对fn做的一层包装,内部还是执行了fn,函数执行过程中如果有JavaScript执行错误就会捕获错误,并执行handleError函数来处理。执行了setup函数获取结果,那么接下来就需要用handleSetupResult函数来处理结果。
处理setup函数执行结果:当setupResult为一个对象时,需要把它变为响应式并赋值给实例instance属性setupState,这样在模板渲染时就可以从instance.setupState上获取到对应的数据。除了支持对象,setup也支持返回一个函数作为组件的渲染函数,从这里我们可以证实setup函数返回值支持分为两个类型:
渲染函数和对象。到了这里,程序需要执行finishComponentSetup函数去完成组件实例的设置。
finishComponentSetup函数主要做了两件事情:
标准化模板或者渲染函数、兼容Vue2的options-api。在分析之前,我们需要了解一下基本知识!组件最终通过运行render函数生成子树vnode,但是我们很少直接去编写render函数,通常会使用两种方式开发组件。
- 借助webpack编译:使用SFC(Single File Components)单文件的开发方式,开发组件通过编写组件的template模板去描述一个组件的DOM结构,.vue类型文件是无法在web端直接加载,它
需要在webpack的编译阶段通过vue-loader编译生成组件相关的Javascript和Css,并把template部分转换成render函数添加到组件对象的属性中。 - 不借助webpack编译:直接引入Vue,
直接在组件对象template属性中编写组件的模板,然后在运行阶段编译生成render函数。
Vue在Web端有两个版本:runtime-only和runtime-compiled。一般更推荐前者,因为它的体积更小,它们区别在于是否注册了compile方法:
再回看标准化模板或者渲染函数逻辑,先判断instance.render是否存在,如果不存在则开始标准化流程,这里主要需要处理以下三种情况。
-
compile、template属性都存在,渲染函数render不存在的情况:runtime-compiled版本会在js运行时进行模板编译,生成render函数。
// 组件实例不存在render渲染函数 if (!instance.render) { ... if (!isSSR && compile && !Component.render) { const template = (__COMPAT__ && instance.vnode.props && instance.vnode.props['inline-template']) || Component.template || resolveMergedOptions(instance).template if (template) { .... } // 编译生成render函数 Component.render = compile(template, finalCompilerOptions) if (__DEV__) { endMeasure(instance, `compile`) } } } // 给实例render赋值 instance.render = (Component.render || NOOP) as InternalRenderFunction ... } -
compile、渲染函数render都不存在,template属性存在的情况:此时没有compile,这里使用的是runtime-only版本,会警告开发者,想要运行时编译得使用runtime-compiled版本。
if (__DEV__ && !Component.render && instance.render === NOOP && !isSSR) { // compile、渲染函数render都不存在,template属性存在 if (!compile && Component.template) { // 警告信息 } else { // compile、template属性、渲染函数render都不存在 warn(`Component is missing template or render function.`) } } -
compile、template属性、渲染函数render都不存在:警告开发者用户组件缺少了render函数或者remplate模板。 了解了标准化模板或者渲染函数流程,再看一下完成组件实例设置的最后一个流程:兼容Vue2的options-api,主要通过applyOptions实现。
小结:
从初始化组件到最终渲染的流程如下图所示
- 初始化组件。
- 创建组件实例:每个组件都独立存储相关实例信息,比如组件uid、父组件实例、组件种类等信息。
- 设置组件实例:判断当前组件实例是否是一个有状态组件,初始化(Props、Slots),执行setup处理函数。
- 创建渲染上下文:instance.ctx做一层proxy,对渲染上下文instance.ctx属性的访问和修改,代理到对setupState、ctx、data、props 中的数据的访问和修改。
- 创建setup函数上下文:对setup函数参数context的处理,也就是初始化context里的四个属性。
- 执行setup函数获取结果:获取setup函数执行结果setupResult。
- 处理setup函数执行结果:也就是处理setup函数执行结果,比如setup的返回值。
- 完成组件实例设置:主要实现
标准化模板或者渲染函数、兼容Vue2。