Vue3(二):defineProps、defineEmits、...

441 阅读12分钟

defineProps

用于接收父组件传递的属性值。

父组件:

<!-- 父组件 -->
<template>
  <Child1 str="字符串" :num="num" />
  -----------------
  <Child2 str="字符串" :num="num" />
</template>

<script setup>
import { ref } from 'vue';
import Child1 from './views/Child1.vue';
import Child2 from './views/Child2.vue';

const num = ref(18)

setInterval(() => {
  num.value++
}, 1000);
</script>

子组件1:

<!-- Child1 -->
<template>
    <div>
        <p>{{ str }}</p>
        <!-- 两种写法都可以 -->
        <p>{{ props.str }}</p>
        <p>{{ num }}</p>
        <p>{{ obj }}</p>
        <p>{{ fun }}</p>
        
        <!-- 会报错 -->
        <!-- v-model cannot be used on a prop, because local prop bindings are not writable. -->
        <!-- <input v-model="str" /> -->
        
        <!-- 在 input 框中输入值并不会引起 str 的变化 -->
        <input v-model="newStr" />
    </div>
</template>

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

const props = defineProps({
    str: String,
    /**
     * 通过 defineProps 定义的 props 是响应式的。
     * 这意味着当父组件传递给子组件的 prop 值发生变化时,子组件中的相应 prop 也会自动更新,
     * 并且任何依赖于这些 props 的计算属性、侦听器(watchers)或模板都会更新。
     */
    num: {
        type: Number,
        default: 0
    },
    obj: {
        type: Object,
        default: () => {
            return {}
        }
    },
    fun: {
        type: Function,
        default: null
    }
});

console.log("child1 created props:", props, typeof props); // Proxy 对象
console.log("child1 created str:", props.str, typeof props.str); // 字符串
console.log("child1 created num:", props.num);
console.log("child1 created obj:", props.obj, typeof props.obj); // object
console.log("child1 created fun:", props.fun); // object

const newStr = ref(props.str)
console.log("child2 newStr:", newStr.value)
</script>

子组件2:

<!-- Child2 -->
<template>
    <div>
        <p>{{ str }}</p>
        <!-- 这里就不能这些写了,会报错 -->
        <!-- <p>{{ props.str }}</p> -->
        <p>{{ num }}</p>
        <p>{{ obj }}</p>
        <p>{{ fun }}</p>
    </div>
</template>

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

const { str, num, obj, fun } = defineProps({
    str: String,
    num: {
        type: Number,
        default: 0
    },
    obj: {
        type: Object,
        default: () => {
            return {}
        }
    },
    fun: {
        type: Function,
        default: null
    }
});

console.log("child2 created str:", str);
console.log("child2 created num:", num);
console.log("child2 created obj:", obj);
console.log("child2 created fun:", fun);

/**
 * 在 Vue 3 中,通过 defineProps 定义的 props 是只读的,不能直接修改。
 * 这是为了确保数据流保持单向,即父组件传递给子组件的数据不会被子组件意外地改变。
 * 非要修改只能使用计算属性或者创建一个响应式变量
 */
// str = '改变字符串' // 会报错

const changeStr1 = computed(() => {
    return `使用计算属性改变${str}`
})
console.log("child2 changeStr1:", changeStr1.value);

const changeStr2 = ref(str)
changeStr2.value = `使用响应式变量改变${str}`
console.log("child2 changeStr2:", changeStr2.value);
</script>

defineProps() 返回的是一个 Proxy 对象,它既不是 ref 对象,也不是 reactive 对象。

defineProps() 返回的对象的属性值是普通数据类型或普通对象,也不是 ref/reactive 对象。

defineEmits

用在子组件中。表面上看它的作用似乎是用于子组件调用父组件的方法。

更准确的说法是用来定义子组件可以发出的事件,父组件可以通过监听这些事件来响应子组件的行为。

它实际上是在告诉父组件:当‘我’使用 emit 的时候,你父组件需要做出相应的响应,你想怎么响应是你父组件自己的事情。

<!-- 子组件 -->
<template>
    <button @click="handleClick">子组件按钮</button>
</template>

<script setup>
// 定义可以发出的事件
const emit = defineEmits(['update'])

function handleClick() {
    // 发出 'update' 事件给父组件
    emit('update', '我是参数')
}
</script>

<!-- 父组件 -->
<template>
  <!-- 监听子组件的 'update' 事件 -->
  <Child1 @update="handleUpdate" />
</template>

<script setup>
import Child1 from './views/Child1.vue';

function handleUpdate(params) {
  // 父组件做出响应:打印参数值
  console.log("params:", params);
}
</script>

defineExpose

用在子组件中,用于暴露子组件实例的方法和数据。它可以让父组件通过 ref 获取子组件的特定方法或数据。

<!-- 子组件 -->
<template>
    子组件
</template>

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

const str = ref('子组件字符串')
const fun = function () {
    console.log("子组件方法触发...");
}

defineExpose({
    str, // ES6 简化写法
    num: 18,
    fun: fun
})
</script>

<!-- 父组件 -->
<template>
  <Child1 ref="child1" />
  <button @click="handleClick">
    父组件按钮
  </button>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import Child1 from './views/Child1.vue';

const child1 = ref(null)

console.log("created str:", child1.value.str); // 报错

onMounted(() => {
  console.log("mounted str:", child1.value.str); // 子组件字符串
})


function handleClick() {
  console.log("str:", child1.value); // 子组件字符串
  console.log("num:", child1.num); // 18
  child1.value.fun() // 子组件方法触发...
}
</script>

为什么第 15 行会报错?

因为在 setup 函数执行时,子组件还没有被挂载到 DOM 上,组件的实例也没有准备好。因此,此时 child1.valuenull

defineOptions

在 Vue 3.3 及之后的版本中,defineOptions 是一个新引入的宏(macro),它允许开发者在 <script setup> 语法糖中声明组件的选项(options)。

这个特性解决了之前需要额外编写一个非 setup 的 <script> 标签来配置选项的问题。

// 设置组件名并禁止属性继承
defineOptions({
  name: 'MyComponent', // 组件名称
  inheritAttrs: false        // 禁止属性继承
});

官网中给出的信息很少,目前只知道有这 2 个选项。

defineComponent

Vue3中defineComponent 的作用详解

Vue 中的 defineComponent

Vue3源码解析-defineComponent

从 API 名称来看,意思是定义一个组件,是 Vue 3 中引入的一个辅助函数,主要用于 TypeScript 项目中。它允许你在定义组件选项时获得更好的类型推断和 IDE(如 VSCode)中的自动补全功能。通过使用 defineComponent,IDE 能够识别这是一个 Vue 组件,并据此提供 Vue 特有的 API 提示和类型检查。

什么意思呢?

在 Vue2 中,我们会习惯这样写:

export default {
    //...
}

这个时候,对于开发工具而言,{} 只是一个普通的 Object 对象,开发工具不会对一个普通对象做任何特殊的处理。

但是增加一层 defineComponet 的话:

export default defineComponent({
    //...
})

你实际上就是在告诉开发工具,我使用的是 Vue3,你需要给我一些 Vue3 相关的自动提示。这样在你写代码的时候,开发工具会给出更多的一些自动提示帮你补全代码。

核心源码:

var Vue = (function (exports) {
    // 定义组件
    function defineComponent(options) {
        return isFunction(options) ? { setup: options, name: options.name } : options;
    }
    exports.defineComponent = defineComponent;  
}({}));

参数 options 是一个选项对象或 setup 函数。

什么是选项对象?

Vue2 中写的:

export default {
    data() {
        // ...
    },
    methods: {
        // ...
    }
}

这些就是选项对象。

defineCustomElement

defineCustomElement 作用是定义一个自定义元素。在 Vue 中我们可以自己写 .vue 文件封装组件,那么它存在的意义是什么呢?

在 Vue 3 中,defineCustomElement 是一种特殊的 API,它允许你将 Vue 组件转换为 Web Components,这样它们就能在任何现代浏览器中作为原生的自定义 HTML 元素使用,而不仅仅是在 Vue 应用中使用。与常规的 Vue 组件不同,使用 defineCustomElement 创建的组件可以独立于 Vue 环境运行,也可以在非 Vue 项目中使用。

这个方法接收的参数和 defineComponent 完全相同。但它会返回一个继承自 HTMLElement 的自定义元素构造器:

import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // 这里是同平常一样的 Vue 组件选项
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`]
})

// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签都会被升级
customElements.define('my-vue-element', MyVueElement)

有兴趣的可以看看 Web Component 的相关知识 【zh-CN】。可以将 Web Components 简单理解为一个自定义的 HTML 标签。

用的很少,就不做具体研究。

Vue 与 Web Components

Vue3中defineCustomElement的使用

defineAsyncComponent

异步组件。可以理解为延迟加载或按需加载。比如当一个大型页面中包含很多个子组件,如果一次性加载所有内容势必会导致页面渲染速度过慢,这时候就可以对那些不需要第一时间就加载的、或者需要经过某些操作后才加载的子组件使用异步组件。

同步组件写法:

<!-- App.vue -->
<template>
  <button @click="handleClick">加载子组件</button>
  <SyncComponent v-if="show" />
</template>

<script setup>
import { ref } from 'vue';
import SyncComponent from "./views/Child1.vue";

const show = ref(false)

function handleClick() {
  show.value = true
}
</script>

异步组件写法:

<!-- App.vue -->
<template>
  <button @click="handleClick">加载子组件</button>
  <AsyncComponent v-if="show" />
</template>

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

const AsyncComponent = defineAsyncComponent(() => import('./views/Child1.vue'))
const show = ref(false)

function handleClick() {
  show.value = true
}

</script>

检查控制台发现:无论同步组件还是异步组件,页面元素都没有渲染子组件内容。

image.png

那么他们有什么区别?

区别是你可以在控制台的 Network 中发现:

  • 同步组件会在页面初始化时一次性加载 App.vue 和 Child1.vue

  • 异步组件在页面初始化时只加 App.vue,当点击按钮时再加载 Child1.vue。


知道了 defineAsyncComponent 的作用,下面详细介绍 defineAsyncComponent 的用法:

defineAsyncComponent 方法接收一个返回 Promise 的加载函数:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})

ES 模块动态导入也会返回一个 Promise,所以也可以这样写:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

异步组件会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。


异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent 也支持在高级选项中处理这些状态:

import LoadingComponent from './views/Loading.vue';
import ErrorComponent from './views/Error.vue';

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./views/Child1.vue'),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 2000,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  
  // 如果提供了一个 timeout 时间限制,并超时了,
  // 也会显示加载失败后展示的组件,默认值是:Infinity
  timeout: 3000
})

注意:delay: 2000 并不是指等待 2s 后才开始加载异步组件,而是指在异步组件开始加载后,等待 2s 再显示 loadingComponent。

当使用服务器端渲染时还可以配置:在空闲时进行激活、在可见时激活、自定义策略等等...这里不做拓展,详情可以直接访问官网。


Suspense

Suspense 是一个包裹异步组件的容器组件,用来处理异步组件加载期间的 UI 状态。

Suspense 组件有两个插槽:

  • #default:默认插槽,这个插槽用于放置异步组件。当异步组件加载完成后,#default 插槽中的内容将被渲染。

  • #fallback:备用插槽,当异步组件正在加载时,#fallback 插槽中的内容会被渲染。

父组件:

<!-- 父组件 -->
<template>
  <div>我是父组件内容</div>
  <Suspense>
    <AsyncComponent />
    <template #fallback>
      <!-- <h1>正在加载中...</h1> -->
      <LoadingComponent />
    </template>
  </Suspense>
</template>

<script setup>
import LoadingComponent from "./views/LoadingComponent.vue";
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(function () {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(import('./views/AsyncComponent.vue'))
    }, 5000);
  });
})
</script>

异步子组件:

<!-- 异步子组件 -->
<template>
    <div>我是异步子组件</div>
</template>

<script setup>
import { onMounted } from 'vue';
onMounted(() => {
    console.log("子组件 onMounted 执行");

})
</script>

备用组件:

<!-- 备用组件 -->
<template>
    <div>
        加载中...
    </div>
</template>

image.png

5 秒后页面内容替换:

image.png


注意点:#default 和 #fallback 两个插槽都只允许一个直接子节点。

<template #fallback> Loading... </template>
<template #fallback> 
    <LoadingComponent /> 
</template>
<template #fallback> 
    <h1>正在加载中...</h1>
</template>

都是可以的,但是

<template #fallback>
    哈哈
    <h1>正在加载中...</h1>
</template>

就会报错。因为它有两个直接子节点。

Suspense 组件会触发三个事件:pendingfallbackresolve

  • pending 事件是在进入挂起状态时触发。

  • fallback 事件是在 #fallback 插槽的内容显示时触发。

  • resolve 事件是在 #default 插槽完成获取新内容时触发。

<template>
  <div>我是父组件内容</div>
  <Suspense 
      @pending="handlePending" 
      @fallback="handleFallback" 
      @resolve="handleResolve"
  >
    <AsyncComponent />
    <template #fallback>
      <h1>正在加载中...</h1>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(function () {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(import('./views/AsyncComponent.vue'))
    }, 5000);
  });
})

let num = 1

setInterval(() => {
  console.log(num++);
}, 1000)

function handlePending() {
  console.log("pending...");
}
function handleFallback() {
  console.log("fallback...");
}
function handleResolve() {
  console.log("resolve...");
}
</script>

image.png


defineAsyncComponent 的实际作用:

手摸手教你利用defineAsyncComponent实现长页面按需加载组件

路由懒加载:

const routes = [
  {
    path: '/dashboard',
    component: defineAsyncComponent(() => import('./views/Dashboard.vue'))
  },
  {
    path: '/profile',
    component: defineAsyncComponent(() => import('./views/Profile.vue'))
  }
];

通过 Vue Router 的懒加载机制,只有在用户访问特定路由时,相关页面组件才会被加载。

拓展:CommonJS 的 require() 也可以实现路由懒加载。

defineSlots

与 defineComponent 类似,辅助功能,给开发工具提供类型推导和提示,没有实际作用。

下面说一说 Vue3 中 slot 的用法。

默认内容

子组件:

<!-- 子组件 -->
<template>
    <div>
        <slot>我是子组件的默认内容</slot>
    </div>
</template>

父组件:

<!-- 父组件 -->
<template>
  <Child1>
  </Child1>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

默认内容就是指当父组件没有提供内容时会展示插槽的默认内容:

image.png

如果父组件提供了内容,默认内容就会被替换:

<!-- 父组件 -->
<template>
  <Child1>
    父组件提供了内容
  </Child1>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

image.png

具名插槽

子组件:

<!-- 子组件 -->
<template>
    <div>
        <slot name="header"></slot>
    </div>
    <div>
        <slot></slot>
    </div>
    <div>
        <slot name="footer"></slot>
    </div>
</template>

<style scoped>
div {
    height: 50px;
}
div:nth-child(1) {
    background-color: pink;
}
div:nth-child(2) {
    background-color: white;
}
div:nth-child(3) {
    background-color: yellow;
}
</style>

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <div>我是内容1</div>
      <template v-slot:header>
        我是头部
      </template>
      <template #footer>
        我是底部
      </template>
      我是内容2
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

image.png

  • 子组件使用 <slot name="xxx"></slot> 进行占位

  • 父组件可以使用 <template v-slot:xxx>内容</template> 进行填充也可以使用 <template #xxx>内容</template> 填充,#xxx 就是 v-slot:xxx 的简写。

  • 所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容

  • v-slot can only be used on components or <template> tags

注意点:

上面的示例中没有显式地定义默认插槽,如果显式地定义了默认插槽,那么就不能在插槽以外地地方添加内容

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <!-- 显式地定义了默认插槽 -->
      <template #default>
        我是内容
      </template>
      <template v-slot:header>
        我是头部
      </template>
      <template #footer>
        我是底部
      </template>
      我是插槽以外地内容
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

就比如这个例子,会导致编译报错。

条件插槽

<!-- 子组件 -->
<template>
    <div v-if="$slots.header">
        <slot name="header"></slot>
    </div>
    <div v-if="$slots.default">
        <slot></slot>
    </div>
    <div v-if="$slots.footer">
        <slot name="footer"></slot>
    </div>
</template>

动态插槽名

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <template #default>
        我是内容
      </template>
      <template v-slot:[name1]>
        我是头部
      </template>
      <template #[name2.name]>
        我是底部
      </template>
    </Child1>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import Child1 from './views/Child1.vue'

const name1 = ref('header')
const name2 = reactive({ name: 'footer' })
</script>

作用域插槽

默认插槽

子组件:

<!-- 子组件 -->
<template>
    <div>
        <slot :text="text" :count="1"></slot>
    </div>
</template>

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

const text = ref('默认文本')
</script>

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <template v-slot="slotProps">
        {{ slotProps }}
      </template>
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'

// console.log("slotProps:", slotProps); 无法打印,报错 slotProps is not defined
</script>

1735297184553.png

可以在 v-slot 中使用解构:

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <template v-slot="{ text, count }">
        {{ text }}---{{ count }}
      </template>
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

image.png

当只有单个插槽时,v-slot 可以直接写在组件标签上(单个具名插槽也可以):

<!-- 父组件 -->
<template>
  <div>
    <Child1 v-slot="slotProps">
      <!-- 这里不能再用 template 标签包裹了 -->
      {{ slotProps }}
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。也就是说如果你在上面的子组件中这样写:

<!-- 子组件 -->
<template>
    <div>
        <slot name="张三" :text="text" :count="1"></slot>
    </div>
</template>

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

const text = ref('默认文本')
</script>

页面将不会正常渲染。


具名插槽

子组件:

<!-- 子组件 -->
<template>
    <div>
        <slot name="header" text="头部文本" :count="1"></slot>
        <slot name="footer" text="底部文本" :count="3"></slot>
    </div>
</template>

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <template #header="headerProps">
        {{ headerProps }}
      </template>
      <template v-slot:footer="footerProps">
        {{ footerProps }}
      </template>
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

image.png


同时使用默认和具名

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。

子组件:

<!-- 子组件 -->
<template>
    <div>
        <slot name="header" text="头部文本" :count="1"></slot>
        <slot text="默认文本" :count="2"></slot>
        <slot name="footer" text="底部文本" :count="3"></slot>
    </div>
</template>

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Child1>
      <template #default="defaultProps">
        {{ defaultProps }}
      </template>
      <template #header="headerProps">
        {{ headerProps }}
      </template>
      <template v-slot:footer="footerProps">
        {{ footerProps }}
      </template>
    </Child1>
  </div>
</template>

<script setup>
import Child1 from './views/Child1.vue'
</script>

注意:具名插槽是有作用域的,也就是说,你不能在 #default 插槽中获取 headerProps 的内容。

defineModel

仅在 3.4+ 中可用

v-model

Vue2 自定义双向数据绑定

默认情况

Vue3 使用 v-model 实现自定义双向数据绑定时默认使用 modelValue 作为属性名,使用 update:modelValue 作为事件名。

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model="count"></Children>
    <p>----------</p>
    <p>
      父: {{ count }}
      <button @click="handleAdd">父按钮</button>
    </p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import Children from './views/Child1.vue'

const count = ref(1)

function handleAdd() {
  count.value++;
}

watch(count, (newVal, oldVal) => {
  console.log("newVal:", newVal, typeof newVal);
  console.log("oldVal:", oldVal, typeof oldVal);
})
</script>

子组件:

<!-- 子组件 -->
<template>
    <div>
        子: <input :value="modelValue" @input="handleInput" />
        <button @click="handleAdd">子按钮</button>
    </div>
</template>

<script setup>
const prop = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);

function handleInput(e) {
    console.log(e, typeof e.target.value); // string
    // 与 Vue2 不同,这里不再需要转成 number 类型
    emit('update:modelValue', e.target.value)
}
function handleAdd() {
    prop.modelValue++;
    // 与 Vue2 相同,依然无法直接操作 prop 中的内容。
    // [Vue warn] Set operation on key "modelValue" failed: target is readonly.
}
</script>

注意点:

由于父组件中的 count 是 number 类型,e.target.value 是 string 类型,在 Vue2 中如果不手动处理会报类型错误,但是在 Vue 3 中,v-model 的类型推导并不会强制要求子组件传递的类型与父组件绑定的数据类型严格匹配。Vue3 主要依赖于 TypeScript 类型检查工具(如果使用 TypeScript 的话)来提供类型安全,而不会在运行时报错。

父组件中的 watch 打印的类型最开始是 number,而后会变成 string

自定义属性和事件

属性名可以自定义,但是事件名要遵循 update:<propName> 的模式,即必须以 update: 为前缀。

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model:aa="count"></Children>
    <p>----------</p>
    <p>
      父: {{ count }}
      <button @click="handleAdd">父按钮</button>
    </p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import Children from './views/Child1.vue'

const count = ref(1)

function handleAdd() {
  count.value++;
}
</script>

子组件:

<!-- 子组件 -->
<template>
    <div>
        子: <input :value="aa" @input="handleInput" />
    </div>
</template>

<script setup>
defineProps(['aa']);
const emit = defineEmits(['update:aa']);

function handleInput(e) {
    console.log(e, typeof e.target.value);
    emit('update:aa', e.target.value)
}
</script>

<style scoped></style>

父组件使用 v-model:<propName> 来绑定自定义的属性;

子组件使用 update:<propName> 来指定自定义的事件。

支持绑定多个属性

Vue3 允许组件有多个 v-model,Vue2 只能有一个 v-model

父组件:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model:name="myName" v-model:age="myAge"></Children>
    <p>----------</p>
    <p>
      父: 
      姓名:{{ myName }}
      年龄:{{ myAge }}
      <button @click="handleAdd">长大一岁</button>
    </p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Children from './views/Child1.vue'

const myName = ref('张三')
const myAge = ref(1)

function handleAdd() {
  myAge.value++;
}
</script>

子组件:

<!-- 子组件 -->
<template>
    <div>
        子:
        姓名:<input :value="name" @input="nameInput" />
        年龄:<input :value="age" @input="ageInput" />
    </div>
</template>

<script setup>
const prop = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);

function nameInput(e) {
    emit('update:name', e.target.value)
}
function ageInput(e) {
    emit('update:age', e.target.value)
}
</script>

v-model 修饰符

处理 v-model 修饰符

具体内容有时间再写

defineModel 就是简化 v-model 实现过程

下面使用 defineModel 来实现上面小节中的默认情况中的例子:

父组件代码不变:

<!-- 父组件 -->
<template>
  <div>
    <Children v-model="count"></Children>
    <p>----------</p>
    <p>
      父: {{ count }}
      <button @click="handleAdd">父按钮</button>
    </p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
import Children from './views/Child1.vue'

const count = ref(1)

function handleAdd() {
  count.value++;
}
</script>

子组件:

<!-- 子组件 -->
<template>
    <div>
        子: <input v-model="model" />
        <button @click="handleAdd">子按钮</button>
    </div>
</template>

<script setup>
const model = defineModel()
function handleAdd() {
    model.value++
}
</script>

非常简单!甚至 handleAdd 中的方法都不报错了!

defineModel 就是封装了之前的实现过程:在子组件内定义了一个叫 modelref 变量(当然也可以取别的变量名)和名字叫 modelValue 的 props,并且 watch 了 props 中的 modelValue。当父组件改变 modelValue 的值后会同步更新 model 变量的值;当子组件改变 model 变量的值后会调用 update:modelValue 事件,父组件收到这个事件后就会更新父组件中对应的变量值。

defineModel 中的 type 和 default

默认情况就使用:

const model = defineModel();

如果想定义类型:

const model = defineModel({ type: String })

类型 + 默认值:

const model = defineModel({ type: String, default: "张三" });

自定义属性名:

<!-- 父组件 -->
<Children v-model:aa="count"></Children>
// 子组件
const model = defineModel('aa', { type: String, default: "张三" })

绑定多个属性:

<!-- 父组件 -->
<Children v-model:name="myName" v-model:age="myAge"></Children>
// 子组件
const model1 = defineModel('name')
const model2 = defineModel('age', { type: Number, default: 8 })

什么是单向数据流

官网介绍:

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告。

白话就是:

通过 props 将父组件的变量传递给子组件,在子组件中是没有权限去修改父组件传递过来的变量。只能通过 emit 抛出事件给父组件,让父组件在事件回调中去修改 props 传递的变量,然后通过 props 将更新后的变量传递给子组件。在这一过程中数据的流动是单向的,由父组件传递给子组件,只有父组件有数据的更改权,子组件不可直接更改数据。

setup() 和 <script setup> 的区别

参考资料

参考资料

参考资料

区别

setup()

  • template 模版经过编译,成了 render 函数

  • setup() 经过编译,什么也没有改变

<script setup>

  • template 模版经过编译,成了 render 函数

  • <script setup> 编译成了 setup 函数去执行,最终通过 expose 导出。所以 <script setup> 就是一个语法糖,本质还是 setup 函数,只不过里面自动做了一个 expose

expose

用于声明当组件实例被父组件通过模板引用访问时暴露的公共属性。

默认情况下 setup 函数,会将组件实例向父组件暴露所有的实例属性,这可能不是我们希望看到的,因为组件可能会有一些应该保持私有的内部属性/方法。而 expose 就是限定子组件只能向外暴露哪些属性/方法。

所以区别来了:

  • setup() 会将组件的属性/实例、以及一些隐藏的属性/方法,部暴露出去

  • 而 <script setup> 则什么都没有暴露出去,想要暴露就需要使用 defineExpose 方法手动暴露,defineExpose 最终也会编译成 expose。

举个例子说明:

// 父组件中
<template>
    <button @click="handleClick">获取子组件信息</button>
    <child ref="child" />
<template>

<script>
export default {
    methods: {
      handleClick() {
        this.$refs.child.getInfo();
      }
    }
}

</script>

在 Vue2 中这是一个父组件调用子组件的方法。

移植到 Vue3 的 setup() 中可以正常执行,但是到 <script setup> 中就会报错。

就是因为在 <script setup> 中,子组件没有对外暴露 getInfo(),父组件获取不到该方法。

总结: setup 函数和 setup 语法糖最大的区别是:

setup 函数会对外暴露组件内所有的实例、属性、方法等等...

setup 语法糖只会对外暴露手动 defineExpose 的内容。


其他的一些使用细节上的区别:

注册组件:

setup():

<script>
import Hello from '@/components/HelloWorld'
export default {
  components: {
    Hello
  }
}
</script>

<script setup>:

不需要手动注册

<script setup>
import Hello from '@/components/HelloWorld'
</script>

自定义指令:

setup():

<template>
    <h1 v-onceClick>使用了setup函数</h1>
</template>
<script>
 
export default {
  directives: {
    onceClick: {
      mounted (el, binding, vnode) {
        console.log(el)
      }
    }
  },
 
}
</script>

<script setup>:

不需要显式注册,但他们必须遵循 vNameOfDirective 这样的命名规范。

<template>
    <h1 v-once-Directive>使用了script setup</h1>
</template>
<script setup>
const vOnceDirective = {
  beforeMount: (el) => {
    console.log(el)
  }
}
</script>

子组件接收父组件传递的值:

setup():

<script>
export default {
  props: {
    num: {
      type: Number,
      default: 1
    }
  },
  setup (props) {
    console.log(props)
  }
}
</script>

<script setup>:

<script setup>
import { defineProps } from 'vue'
const props = defineProps({
  num: {
    type: Number,
    default: 1
  }
})
</script>

子组件给父组件传值:

setup():

<script>
export default {
  setup (props, context) {
    const sendNum = () => {
      context.emit('sendNum', 1200)
    }
    return { sendNum }
  }
}
</script>

<script setup>:

<script setup>
import { defineProps, defineEmits } from 'vue'
const emit = defineEmits(['submit'])
const sendNum = () => {
  emit('submit', 1000)
}
</script>

setup 函数特点

1、setup 函数有两个参数:setup(props, context)

  • props 就是 Vue2 中组件中的 props,指父组件传递来的参数

  • context 有三个属性 attrs slots emit 分别对应 Vue2 中的 attrs 属性、slots 插槽、$emit 事件

2、setup 函数是 Composition API(组合API)的入口

3、在 setup 函数中定义的变量和方法最后都是需要 return 出去的,不然无法在模板中使用

4、setup 函数在 beforeCreate 钩子函数之前执行

export default {
  setup() {
    console.log("setup");
  },
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    console.log("created");
  },
  mounted() {
    console.log("mounted");
  }
}

// setup
// beforeCreate
// created
// mounted

为什么 setup() 需要 return 才能在模板中使用,而 <script setup> 不需要return?

setup() 会返回一个对象,这个对象中的属性和方法会暴露给模板。<script setup> 本质上是 setup() 的一个语法糖,会自动 return 其中所有的变量和方法,无需我们手动处理。