终与Vue3的setup握手言欢!

817 阅读13分钟

作为一个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中了,你可能会问😵:

IMG_2022-12-10_12-58-54.jpeg

  • 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对象内有什么属性?如下图:

image.png context对象内有四个属性:attrs(获取当前标签上的所有属性的对象,除了props中接收的)、emit(传递给父组件的事件)、expose(控制组件暴露给父组件的内容)、slots(插槽集合)。

setup(props,context){
    context.expose({
      // 暴露的属性名
    })
}

defineComponent中的setup

除了脚本定义setup,还有在defineComponent中使用setup时,这个defineComponent的作用是什么? defineComponent函数,只是对setup函数进行封装,返回options的对象,在TypeScript下,给予组件正确参数类型推断。

image.png 从上面源码中我们可以看出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结合使用,不然报警告信息。

    image.png

    <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的过程主要是在挂载组件: image.png 在挂载组件过程中主要做了三件事情:创建组件实例设置组件实例设置并运行带副作用的渲染函数

image.png 从源码中可以看到,上面主要是创建instance实例创建了组件实例后,对组件的相关属性进行初始化,然后接着就是组件实例的设置了。执行setup函数主要是在setupComponent函数中。setupComponnet设置组件实例也是主要做了三件事情:判断当前组件实例是否是一个有状态组件初始化(Props、Slots)执行setup处理函数

image.png 判断当前组件实例是否是一个有状态组件,通过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>

image.png 从上面我们可以看出实例的type值就是一个包含setup函数、template的对象,从而获取到isStateful为4对应STATEFUL_COMPONENT。从而执行setupStatefulComponent函数中setup获取结果。setupStatefulComponent函数主要做四件事:创建渲染上下文代理创建setupContext(初始化参数context的四个属性)、执行setup函数获取结果处理setup函数执行结果

image.png 在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的对象:

image.png

image.png

从上面可以看到它其实就是对fn做的一层包装,内部还是执行了fn,函数执行过程中如果有JavaScript执行错误就会捕获错误,并执行handleError函数来处理。执行了setup函数获取结果,那么接下来就需要用handleSetupResult函数来处理结果。

image.png 处理setup函数执行结果:当setupResult为一个对象时,需要把它变为响应式并赋值给实例instance属性setupState,这样在模板渲染时就可以从instance.setupState上获取到对应的数据。除了支持对象,setup也支持返回一个函数作为组件的渲染函数,从这里我们可以证实setup函数返回值支持分为两个类型:渲染函数对象。到了这里,程序需要执行finishComponentSetup函数去完成组件实例的设置。

image.png 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-onlyruntime-compiled。一般更推荐前者,因为它的体积更小,它们区别在于是否注册了compile方法:

image.png 再回看标准化模板或者渲染函数逻辑,先判断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实现。

小结:

从初始化组件到最终渲染的流程如下图所示

image.png

  • 初始化组件
  • 创建组件实例:每个组件都独立存储相关实例信息,比如组件uid、父组件实例、组件种类等信息。
  • 设置组件实例:判断当前组件实例是否是一个有状态组件,初始化(Props、Slots),执行setup处理函数。
  • 创建渲染上下文:instance.ctx做一层proxy,对渲染上下文instance.ctx属性的访问和修改,代理到对setupState、ctx、data、props 中的数据的访问和修改。
  • 创建setup函数上下文:对setup函数参数context的处理,也就是初始化context里的四个属性。
  • 执行setup函数获取结果:获取setup函数执行结果setupResult。
  • 处理setup函数执行结果:也就是处理setup函数执行结果,比如setup的返回值。
  • 完成组件实例设置:主要实现标准化模板或者渲染函数兼容Vue2