Vue 3源码深度解析之:`setup`函数的参数:`props`和`context`的底层实现。

大家好,欢迎来到今天的Vue 3源码深度解析小课堂!今天咱们要扒一扒Vue 3里那个神秘又重要的setup函数,特别是它的两个参数:propscontext。别怕,不会让你对着密密麻麻的代码发呆,咱们用大白话+代码示例,把它们的老底都给揭了!

开场白:setup,你的组件好帮手

setup函数是Vue 3组件里的一块宝地,咱们可以在这里面做数据初始化、响应式状态管理、事件处理等等。它就像是组件的“大脑”,负责指挥整个组件的运作。而propscontext,就是这个大脑里的两个重要助手,帮助我们获取外部信息和操作组件。

第一部分:props,数据通道的守护者

props,顾名思义,就是properties的缩写,代表着父组件传递给子组件的数据。它就像一条数据通道,让父组件可以把信息传递给子组件。

1.1 props的定义和类型校验

在Vue 3中,我们可以在组件选项里用props选项来定义组件可以接收哪些props。它支持两种写法:数组形式和对象形式。

  • 数组形式(简单粗暴):
// MyComponent.vue
export default {
  props: ['message', 'count'],
  setup(props) {
    console.log(props.message); // 可以访问到父组件传递的 message
    console.log(props.count);   // 可以访问到父组件传递的 count
    return {};
  }
}

// ParentComponent.vue
<template>
  <MyComponent message="Hello" :count="123" />
</template>

这种写法简单,但是缺少类型校验,容易出错。

  • 对象形式(更严谨):
// MyComponent.vue
export default {
  props: {
    message: {
      type: String,  // 限制类型为字符串
      required: true, // 必须传递
      default: 'Default Message' // 默认值
    },
    count: {
      type: Number,  // 限制类型为数字
      default: 0,
      validator(value) { // 自定义校验器
        return value >= 0; // 必须大于等于0
      }
    }
  },
  setup(props) {
    console.log(props.message);
    console.log(props.count);
    return {};
  }
}

// ParentComponent.vue
<template>
  <MyComponent :message="parentMessage" :count="parentCount" />
</template>
<script>
export default {
  data() {
    return {
      parentMessage: "Hello from Parent",
      parentCount: 5
    };
  }
};
</script>

对象形式可以定义props的类型、是否必传、默认值、自定义校验器等等,让我们的代码更加健壮。

1.2 props的底层实现

Vue 3在组件初始化的时候,会解析组件选项里的props选项,并根据定义的类型、默认值、校验器等信息,创建一个props对象。这个props对象会被传递给setup函数作为第一个参数。

简单来说,Vue 3内部会做以下事情:

  1. 定义props选项: 解析组件选项中的props字段(数组或对象)。
  2. 创建props对象: 根据props定义,创建一个响应式的props对象。
  3. 类型校验和默认值: 对传入的props进行类型校验,设置默认值(如果定义了)。
  4. 传递给setup 将创建好的props对象作为第一个参数传递给setup函数。

1.3 props的响应式

props对象是响应式的,这意味着当父组件更新传递给子组件的props时,子组件的setup函数中接收到的props也会自动更新,视图也会随之重新渲染。

// ParentComponent.vue
<template>
  <MyComponent :message="parentMessage" />
  <button @click="updateMessage">Update Message</button>
</template>
<script>
import { ref } from 'vue';

export default {
  setup() {
    const parentMessage = ref("Hello from Parent");

    const updateMessage = () => {
      parentMessage.value = "New Message from Parent";
    };

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

// MyComponent.vue
export default {
  props: {
    message: {
      type: String,
      required: true
    }
  },
  setup(props) {
    console.log("MyComponent props.message:", props.message); // 每次 parentMessage 更新都会触发
    return {};
  }
};

在这个例子中,当父组件的parentMessage改变时,子组件MyComponentsetup函数会重新执行,打印出新的props.message值。

第二部分:context,组件的工具箱

context对象是setup函数的第二个参数,它提供了一组有用的属性和方法,让我们可以操作组件的状态、事件、插槽等等。

2.1 context的组成

context对象主要包含以下三个属性:

属性 类型 描述
attrs 对象 包含组件上所有没有被props声明的attribute绑定 (相当于 $attrs )。
emit 函数 用于触发自定义事件 (相当于 $emit )。
slots 对象 包含所有传递给组件的插槽 (相当于 $slots )。
expose 函数 用于显式地暴露组件内部的状态给父组件。 只有在使用 <script setup> 的单文件组件中,才需要使用 defineExpose 编译器宏。在其他情况下,所有返回的响应式状态都会自动暴露。

2.2 context.attrs

context.attrs包含了组件上所有没有被props声明的attribute绑定。 比如:

// ParentComponent.vue
<template>
  <MyComponent message="Hello" data-id="123" :style="{ color: 'red' }" />
</template>

// MyComponent.vue
export default {
  props: ['message'],
  setup(props, context) {
    console.log(props.message);   // "Hello"
    console.log(context.attrs);    // { data-id: "123", style: { color: 'red' } }
    return {};
  }
}

context.attrs对于创建高阶组件或者需要访问未声明的attribute绑定非常有用。

2.3 context.emit

context.emit用于触发自定义事件,让子组件可以向父组件传递信息。

// MyComponent.vue
export default {
  setup(props, context) {
    const handleClick = () => {
      context.emit('custom-event', 'Data from child'); // 触发 custom-event 事件
    };

    return {
      handleClick
    };
  },
  template: `
    <button @click="handleClick">Click Me</button>
  `
}

// ParentComponent.vue
<template>
  <MyComponent @custom-event="handleCustomEvent" />
</template>
<script>
export default {
  methods: {
    handleCustomEvent(data) {
      console.log('Received:', data); // "Received: Data from child"
    }
  }
};
</script>

context.emit的第一个参数是事件名称,后面的参数是传递给父组件的数据。

2.4 context.slots

context.slots包含了所有传递给组件的插槽,让我们可以灵活地渲染父组件传递的内容。

// MyComponent.vue
export default {
  setup(props, context) {
    return () => (
      <div>
        <p>MyComponent Content</p>
        {context.slots.default ? context.slots.default() : <p>No Default Slot</p>}
        {context.slots.header ? <div>Header: {context.slots.header()}</div> : null}
      </div>
    );
  }
}

// ParentComponent.vue
<template>
  <MyComponent>
    <template v-slot:default>
      <p>Default Slot Content</p>
    </template>
    <template v-slot:header>
      <h1>Header Content</h1>
    </template>
  </MyComponent>
</template>

context.slots是一个对象,包含了所有具名插槽和默认插槽。我们可以通过插槽名称来访问对应的插槽内容。

2.5 context.expose (仅用于 <script setup>)

context.expose函数用于显式地暴露组件内部的状态给父组件。 在 <script setup> 语法糖中,默认情况下,组件内部的任何变量都是私有的,不会暴露给父组件。 如果需要父组件访问子组件的某些状态或方法,就需要使用 defineExpose 宏(在 <script setup> 内部)或者 context.expose (在非 <script setup> 中)。

// MyComponent.vue (非 <script setup>)
<script>
import { ref } from 'vue';

export default {
  setup(props, context) {
    const count = ref(0);

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

    context.expose({
      count,
      increment
    });

    return {
      count,
      increment
    }
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button @click="increment">Increment</button>
    </div>
  `
};
</script>

// ParentComponent.vue
<template>
  <MyComponent ref="myComponent" />
  <button @click="accessChild">Access Child</button>
</template>
<script>
import { ref, onMounted } from 'vue';
import MyComponent from './MyComponent.vue';

export default {
  components: {
    MyComponent
  },
  setup() {
    const myComponent = ref(null);

    const accessChild = () => {
      console.log(myComponent.value.count);    // 访问子组件暴露的 count
      myComponent.value.increment(); // 调用子组件暴露的 increment 方法
    };

    onMounted(() => {
      console.log(myComponent.value); // 此时可以访问到子组件
    });

    return {
      myComponent,
      accessChild
    };
  }
};
</script>

2.6 context的底层实现

Vue 3在组件初始化的时候,会创建一个context对象,并将attrsemitslots等属性添加到该对象上。这个context对象会被传递给setup函数作为第二个参数。

第三部分:props + context = 组件的无限可能

propscontextsetup函数的两个重要参数,它们一起为我们提供了操作组件的强大能力。

  • props让我们接收父组件传递的数据,实现组件的定制化。
  • context让我们访问组件的attribute绑定、触发自定义事件、渲染插槽内容,实现组件的交互和扩展。

掌握了propscontext,就等于掌握了setup函数的核心,也就掌握了Vue 3组件的灵魂。

总结:propscontext,你的Vue 3好伙伴

propscontext是Vue 3组件中不可或缺的两个助手。它们帮助我们构建灵活、可复用、可扩展的组件。希望通过今天的讲解,大家能够对propscontext有更深入的理解,并在实际开发中灵活运用它们,写出更加优雅的Vue 3代码!

今天的课程就到这里,谢谢大家!下次再见!

发表回复

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