阐述 Vue 3 源码中 `setup` 函数的执行时机,以及它是如何处理 `props`、`context` 和返回值的。

各位观众,晚上好!今天咱们来聊聊 Vue 3 里那个神秘又重要的 setup 函数。这玩意儿啊,就像个魔法入口,组件里很多事情都得通过它来安排。别怕,咱们一步一步揭开它的面纱,保证你听完以后,也能玩转 setup

一、setup 函数的执行时机:早起的鸟儿有虫吃

setup 函数的执行时机非常关键,它在组件实例创建之前,并且只执行一次。你可以把它理解为组件的“初始化向导”,它会赶在组件的其他生命周期钩子之前,帮你把该准备的东西都准备好。

为了方便理解,咱们先来回顾一下 Vue 3 组件的生命周期(简化版):

生命周期钩子 执行时机
beforeCreate 组件实例初始化之后,props 解析/依赖注入之前。
setup beforeCreate 之后,created 之前。仅执行一次。
onBeforeMount 组件挂载之前。
onMounted 组件挂载之后。
onBeforeUpdate 数据更新之前。
onUpdated 数据更新之后。
onBeforeUnmount 组件卸载之前。
onUnmounted 组件卸载之后。

从表格里可以看到,setup 正好卡在 beforeCreatecreated 之间。这意味着什么呢?

  • 拿不到 this 因为组件实例还没创建好呢,this 当然是 undefined。想访问组件实例上的东西?抱歉,setup 里没门儿。
  • 是时候准备数据了: 虽然拿不到 this,但你可以用它来定义响应式数据、方法,这些都会在组件实例创建好之后,顺利地挂载到实例上。
  • 依赖注入的好地方: 如果你用到了 provide/injectsetup 里是接收注入数据的最佳时机。

二、setup 函数的参数:propscontext

setup 函数接收两个参数:

  1. props 一个对象,包含了父组件传递过来的所有 props。它是响应式的,这意味着如果父组件修改了 propssetup 函数里也能实时感知到。

  2. context 一个对象,提供了一些有用的方法和属性。它不是响应式的。包含三个属性:

    • attrs:一个对象,包含了所有没有被 props 声明的 attribute (比如 classstyle)。
    • slots:一个对象,包含了所有父组件传递过来的插槽。
    • emit:一个函数,用于触发自定义事件,让子组件可以和父组件通信。

咱们来看个例子:

// ParentComponent.vue
<template>
  <ChildComponent :message="parentMessage" @custom-event="handleEvent" />
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  setup() {
    const parentMessage = ref('Hello from parent!');

    const handleEvent = (data) => {
      console.log('Event received:', data);
    };

    return {
      parentMessage,
      handleEvent
    };
  }
};
</script>

// ChildComponent.vue
<template>
  <div>
    <p>Message from parent: {{ message }}</p>
    <p>Attribute: {{ attribute }}</p>
    <button @click="handleClick">Click me</button>
    <slot />
  </div>
</template>

<script>
import { onMounted } from 'vue';

export default {
  props: {
    message: {
      type: String,
      required: true
    }
  },
  setup(props, context) {
    console.log('props:', props);
    console.log('context:', context);

    const attribute = context.attrs.customAttribute || 'Default Value'; //访问未声明attribute

    const handleClick = () => {
      context.emit('custom-event', 'Data from child');
    };

    onMounted(() => {
        console.log('Child component mounted');
        //你可以访问 props 的属性,比如 props.message
    });

    return {
      handleClick,
      attribute
    };
  }
};
</script>

在这个例子里:

  • ChildComponentsetup 函数接收到了 props,可以访问 message 属性。
  • context.emit 用于触发 custom-event 事件,并传递数据给父组件。
  • context.attrs 可以访问父组件传递的没有在props声明的attribute,例如自定义的 customAttribute

注意: 在 Vue 3.3 之后,props 解构赋值的默认行为发生了变化。简单来说,如果直接解构 props,会导致解构出来的变量失去响应性。例如:

setup(props) {
  const { message } = props; // 错误!message 不再是响应式的
  // ...
}

正确的做法是使用 toRefstoRef 来保持响应性:

import { toRefs, toRef } from 'vue';

setup(props) {
  const { message } = toRefs(props); // 正确!message 是响应式的 ref 对象
  // 或者
  const messageRef = toRef(props, 'message'); // 正确!messageRef 是响应式的 ref 对象
  // ...
}

toRefs 会将 props 对象的所有属性转换为 ref 对象,而 toRef 只转换指定的属性。选择哪个取决于你的需求。

三、setup 函数的返回值:通往模板的桥梁

setup 函数的返回值非常重要,它决定了哪些数据和方法可以被模板访问。返回值有三种情况:

  1. 返回一个对象: 这是最常见的用法。返回的对象里的属性和方法,都会被合并到组件实例上,可以在模板中直接使用。

    setup() {
      const count = ref(0);
    
      const increment = () => {
        count.value++;
      };
    
      return {
        count,
        increment
      };
    }

    在模板中:

    <template>
      <p>Count: {{ count }}</p>
      <button @click="increment">Increment</button>
    </template>
  2. 返回一个函数: 这种用法比较少见,但也有它的用武之地。如果 setup 函数返回的是一个渲染函数,那么它会覆盖组件的模板。

    import { h } from 'vue';
    
    setup() {
      return () => {
        return h('div', 'Hello from render function!');
      };
    }

    这种方式可以完全控制组件的渲染过程,适合一些特殊的场景。

  3. 不返回任何东西: 这种情况下,setup 函数不会向模板暴露任何数据或方法。但它仍然可以用来执行一些副作用操作,比如监听事件、发起网络请求等等。

    import { onMounted } from 'vue';
    
    setup() {
      onMounted(() => {
        console.log('Component mounted!');
      });
    }

    这种方式通常用于一些只需要在组件初始化时执行一次的操作。

四、关于响应式数据:refreactive

setup 函数里最常用的就是定义响应式数据了。Vue 3 提供了两种主要的 API:refreactive

  • ref 用于创建单个值的响应式引用。你可以把它理解为一个“盒子”,盒子里装着你想变成响应式的值。访问和修改 ref 的值,需要通过 .value 属性。

    import { ref } from 'vue';
    
    setup() {
      const count = ref(0);
    
      const increment = () => {
        count.value++;
      };
    
      return {
        count,
        increment
      };
    }
  • reactive 用于创建对象的响应式代理。它会递归地将对象的所有属性都变成响应式的。访问和修改 reactive 对象的属性,和普通对象一样。

    import { reactive } from 'vue';
    
    setup() {
      const state = reactive({
        name: 'Alice',
        age: 30
      });
    
      const updateName = (newName) => {
        state.name = newName;
      };
    
      return {
        state,
        updateName
      };
    }

选择 ref 还是 reactive

一般来说,如果你的数据是一个简单类型(比如数字、字符串、布尔值),或者只需要对单个值进行响应式处理,那么 ref 是个不错的选择。如果你的数据是一个复杂对象,并且需要对对象的多个属性进行响应式处理,那么 reactive 可能会更方便。

需要注意的是: reactive 只能用于对象类型,如果你尝试用它来包装一个简单类型的值,Vue 会发出警告。

五、setup 语法糖:<script setup>

Vue 3 还提供了一种更简洁的 setup 写法,叫做 <script setup>。它是一种编译时的语法糖,可以让你更方便地使用 Composition API。

使用 <script setup> 之后,你可以直接在 <script> 标签里定义响应式数据、方法,而不需要显式地返回它们。编译器会自动帮你处理好一切。

<template>
  <p>Count: {{ count }}</p>
  <button @click="increment">Increment</button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

是不是感觉清爽多了?

<script setup> 的优点:

  • 更简洁的代码: 不需要显式地返回数据和方法。
  • 更好的性能: 编译器可以进行更多的优化。
  • 更清晰的结构: 代码更易于阅读和维护。

需要注意的是: <script setup> 只能在单文件组件中使用。

六、最佳实践和常见问题

  • 避免在 setup 里直接修改 props props 是从父组件传递过来的,应该由父组件来控制。如果子组件需要修改 props 的值,应该 emit 一个事件通知父组件,让父组件来修改。
  • 谨慎使用 context.attrs 尽量使用 props 来声明组件需要的属性。只有在确实无法预知属性名称的情况下,才考虑使用 context.attrs
  • 合理利用生命周期钩子: setup 函数只执行一次,如果你需要在组件挂载、更新或卸载时执行一些操作,应该使用 onMountedonBeforeUpdateonUnmounted 等生命周期钩子。
  • 注意响应性丢失的问题: 特别是在解构 propsreactive 对象时,要小心避免响应性丢失。

常见问题:

  • 为什么我的数据没有更新? 可能是因为你没有正确地使用 refreactive,或者是因为你忘记了在模板中使用 .value 访问 ref 的值。
  • 为什么我拿不到 this 因为 setup 函数在组件实例创建之前执行,所以拿不到 this
  • 为什么我的 props 没有更新? 可能是因为父组件没有正确地传递 props,或者是因为你没有在 setup 函数里正确地接收 props
  • 为什么我的事件没有触发? 可能是因为你没有正确地使用 context.emit,或者是因为父组件没有监听对应的事件。

七、总结

setup 函数是 Vue 3 Composition API 的核心,掌握了它,你就掌握了组件开发的钥匙。记住它的执行时机、参数和返回值,合理利用 refreactive,熟练使用 <script setup> 语法糖,你就能写出高效、可维护的 Vue 3 组件。

好啦,今天的讲座就到这里。希望大家都能成为 setup 大师!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注