Vue 3源码深度解析之:`Vue`的`render context`:`slots`、`attrs`和`emit`的内部实现。

各位观众,晚上好!我是今天的主讲人,很高兴能和大家一起探索 Vue 3 的奥秘。今天我们要聊的是 Vue 组件渲染上下文中的三个重要成员:slotsattrsemit。它们就像组件的“三驾马车”,驱动着组件的渲染、交互和通信。

准备好了吗?让我们系好安全带,发车!

一、渲染上下文:组件的工具箱

在深入了解 slotsattrsemit 之前,我们先简单回顾一下什么是渲染上下文。简单来说,渲染上下文就是 Vue 组件在渲染过程中可以访问的一系列属性和方法,它为组件提供了所需的一切资源,包括数据、属性、插槽、事件等等。

你可以把渲染上下文想象成一个工具箱,组件可以从中取出各种工具来完成任务。slotsattrsemit 就是这个工具箱里的三个重要工具。

二、slots:内容分发的瑞士军刀

slots 允许父组件向子组件传递内容,从而实现更灵活的组件组合。Vue 3 对插槽进行了重构,使得插槽的使用更加简洁高效。

2.1 具名插槽与默认插槽

插槽分为具名插槽和默认插槽。默认插槽只有一个,用 default 命名,而具名插槽可以有多个,每个都有自己的名字。

示例:

<!-- 父组件 -->
<template>
  <MyComponent>
    <template #header>
      <h1>我是头部</h1>
    </template>
    <template #default>
      <p>我是默认内容</p>
    </template>
    <template #footer>
      <p>我是底部</p>
    </template>
  </MyComponent>
</template>

<!-- 子组件 MyComponent -->
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

在这个例子中,MyComponent 组件定义了三个插槽:headerdefaultfooter。父组件通过 v-slot 指令(简写为 #)将内容分别插入到对应的插槽中。

2.2 作用域插槽

作用域插槽允许子组件向父组件传递数据,父组件可以根据这些数据来定制插槽的内容。

示例:

<!-- 父组件 -->
<template>
  <MyList :items="items">
    <template #default="slotProps">
      <li>{{ slotProps.item.name }} - {{ slotProps.item.price }}</li>
    </template>
  </MyList>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { name: '苹果', price: 5 },
        { name: '香蕉', price: 3 },
      ],
    };
  },
};
</script>

<!-- 子组件 MyList -->
<template>
  <ul>
    <li v-for="item in items" :key="item.name">
      <slot :item="item"></slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: {
    items: {
      type: Array,
      required: true,
    },
  },
};
</script>

在这个例子中,MyList 组件将 item 数据传递给默认插槽,父组件通过 slotProps 对象访问这些数据,并用它们来渲染列表项。

2.3 slots 的内部实现

在 Vue 3 中,slots 是一个对象,包含了所有插槽的渲染函数。这些渲染函数是由编译器根据模板中的插槽定义生成的。

当我们访问组件实例的 $slots 属性时,实际上是访问了一个经过处理的插槽对象,它包含了插槽的名称、渲染函数和作用域数据。

简单来说,Vue 3 会将父组件传递的插槽内容编译成一个个函数,这些函数接收子组件传递的数据(如果有的话),然后返回渲染后的 VNode。

代码示例(简化版):

// 假设这是编译后的插槽渲染函数
const headerSlot = () => {
  return h('h1', '我是头部');
};

const defaultSlot = () => {
  return h('p', '我是默认内容');
};

const footerSlot = () => {
  return h('p', '我是底部');
};

// 组件实例的 $slots 对象
const slots = {
  header: headerSlot,
  default: defaultSlot,
  footer: footerSlot,
};

// 在组件的 render 函数中调用插槽渲染函数
const render = () => {
  return h('div', [
    h('header', slots.header()),
    h('main', slots.default()),
    h('footer', slots.footer()),
  ]);
};

这个例子只是一个简化版的演示,实际的实现要复杂得多,涉及到 VNode 的创建、更新和渲染。

2.4 slots 的使用场景

  • 布局组件: 使用插槽可以创建灵活的布局组件,允许父组件自定义头部、内容和底部。
  • 列表组件: 使用作用域插槽可以创建可定制的列表组件,允许父组件自定义列表项的渲染方式。
  • 表单组件: 使用插槽可以创建可扩展的表单组件,允许父组件自定义表单项的结构和样式。

三、attrs:传递未声明的属性

attrs 对象包含了所有传递给组件但未被声明为 props 的属性。这些属性会被添加到组件的根元素上。

3.1 属性穿透

attrs 的一个重要特性是属性穿透。当一个组件接收到未声明的属性时,这些属性会自动传递到组件的根元素上。

示例:

<!-- 父组件 -->
<template>
  <MyButton class="primary" @click="handleClick">点击我</MyButton>
</template>

<!-- 子组件 MyButton -->
<template>
  <button>
    <slot></slot>
  </button>
</template>

在这个例子中,MyButton 组件没有声明 classclick 属性,但是这些属性会被自动传递到 button 元素上。

3.2 attrs 的内部实现

在 Vue 3 中,attrs 对象是在组件渲染之前创建的,它包含了所有未声明的属性。在组件的 render 函数中,这些属性会被添加到根元素上。

代码示例(简化版):

// 假设这是组件的 props
const props = {
  // ...
};

// 假设这是父组件传递的 attributes
const attributes = {
  class: 'primary',
  onClick: () => {
    console.log('点击了按钮');
  },
};

// 创建 attrs 对象
const attrs = {};
for (const key in attributes) {
  if (!(key in props)) {
    attrs[key] = attributes[key];
  }
}

// 在组件的 render 函数中使用 attrs
const render = () => {
  return h('button', attrs, '点击我');
};

这个例子只是一个简化版的演示,实际的实现要复杂得多,涉及到属性的合并、事件的处理和 VNode 的更新。

3.3 attrs 的使用场景

  • 样式定制: 使用 attrs 可以方便地为组件添加样式,例如添加 classstyle 等属性。
  • 事件处理: 使用 attrs 可以方便地为组件绑定事件,例如绑定 clickmouseover 等事件。
  • 第三方库集成: 使用 attrs 可以方便地将第三方库的属性传递给组件。

3.4 禁用属性继承 inheritAttrs: false

有时候我们不希望组件自动将 attrs 传递到根元素上,可以使用 inheritAttrs: false 选项来禁用属性继承。

示例:

<template>
  <div>
    <button>
      <slot></slot>
    </button>
  </div>
</template>

<script>
export default {
  inheritAttrs: false,
  mounted() {
    console.log(this.$attrs); // 可以手动访问 attrs 对象
  },
};
</script>

在这个例子中,MyButton 组件禁用了属性继承,classclick 属性不会自动传递到 button 元素上。但是,我们仍然可以通过 $attrs 属性来访问 attrs 对象,并手动处理这些属性。

四、emit:组件间的信使

emit 函数用于触发自定义事件,允许子组件向父组件传递信息。

4.1 触发事件

使用 emit 函数可以触发自定义事件,并传递数据给父组件。

示例:

<!-- 子组件 MyInput -->
<template>
  <input type="text" @input="handleInput" />
</template>

<script>
export default {
  emits: ['update:modelValue'], // 声明组件会触发的事件
  props: {
    modelValue: String,
  },
  methods: {
    handleInput(event) {
      this.$emit('update:modelValue', event.target.value);
    },
  },
};
</script>
<!-- 父组件 -->
<template>
  <MyInput v-model="message" />
  <p>Message: {{ message }}</p>
</template>

<script>
export default {
  data() {
    return {
      message: '',
    };
  },
};
</script>

在这个例子中,MyInput 组件通过 emit 函数触发 update:modelValue 事件,并将输入框的值传递给父组件。父组件使用 v-model 指令监听该事件,并更新 message 数据。

4.2 emit 的内部实现

在 Vue 3 中,emit 函数实际上是调用了组件实例的 $emit 方法。$emit 方法会查找组件的父组件是否监听了对应的事件,如果监听了,则调用相应的事件处理函数。

代码示例(简化版):

// 组件实例
const instance = {
  // ...
  emit(event, ...args) {
    // 获取事件处理函数
    const handlers = this.props[`on${event}`]; // 假设事件名是驼峰命名
    if (handlers) {
      // 调用事件处理函数
      if (Array.isArray(handlers)) {
        handlers.forEach(handler => handler(...args));
      } else {
        handlers(...args);
      }
    }
  },
};

// 在组件的 methods 中调用 emit 函数
const handleInput = (value) => {
  instance.emit('update:modelValue', value);
};

这个例子只是一个简化版的演示,实际的实现要复杂得多,涉及到事件的命名规范、事件处理函数的绑定和解绑、事件冒泡和捕获等。

4.3 emits 选项

Vue 3 引入了 emits 选项,用于声明组件会触发的事件。这样做的好处是可以提高代码的可读性和可维护性,并且可以防止意外的事件触发。

示例:

<script>
export default {
  emits: ['update:modelValue', 'custom-event'],
  // ...
};
</script>

在这个例子中,emits 选项声明了组件会触发 update:modelValuecustom-event 事件。

4.4 emit 的使用场景

  • 数据同步: 使用 emit 可以实现父子组件之间的数据同步,例如 v-model 指令的实现。
  • 状态更新: 使用 emit 可以通知父组件更新状态,例如表单提交、列表项删除等。
  • 自定义事件: 使用 emit 可以触发自定义事件,实现更灵活的组件交互。

五、总结

slotsattrsemit 是 Vue 组件渲染上下文中的三个重要成员,它们分别负责内容分发、属性传递和事件触发。掌握了这三个工具,你就可以构建出更加灵活、可复用和易于维护的 Vue 组件。

特性 作用 内部实现 使用场景
slots 父组件向子组件传递内容 编译成渲染函数,存储在组件实例的 $slots 对象中。 布局组件(头部、内容、底部)、列表组件(自定义列表项渲染)、表单组件(可扩展的表单项)
attrs 传递未声明的属性到组件根元素,属性穿透 创建 attrs 对象,包含所有未声明的属性,在 render 函数中添加到根元素。可以通过 inheritAttrs: false 禁用属性继承,通过 $attrs 访问。 样式定制(添加 class、style)、事件处理(绑定 click、mouseover)、第三方库集成。
emit 子组件向父组件触发自定义事件 调用组件实例的 $emit 方法,查找父组件是否监听了对应的事件,如果监听了,则调用相应的事件处理函数。可以通过 emits 选项声明组件会触发的事件。 数据同步(v-model)、状态更新(表单提交、列表项删除)、自定义事件。

希望今天的讲座能帮助大家更好地理解 Vue 3 的渲染上下文。感谢大家的收看,下次再见!

发表回复

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