Vue 系列之:defineProps、defineEmits、...

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("子组件方法触发...");
}
const other = '其他'

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); // 第 15 行,报错

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

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

为什么第 15 行会报错?

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

为什么 child1.value.other 是 undefined?

因为子组件中没有通过 defineExpose 来暴露 other

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>

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

在这里插入图片描述

那么他们有什么区别?

区别是你可以在控制台的 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(/* 获取到的组件 */)
  })
})

ES6 模块动态导入也会返回一个 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

[səˈspens]

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>

备用组件 LoadingComponent:

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

在这里插入图片描述

5 秒后页面内容替换:

在这里插入图片描述


注意点:#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>

在这里插入图片描述


defineAsyncComponent 的实际作用:

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

路由懒加载:

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

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

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

defineOptions

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

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

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

注意:

可以在<script setup>之外使用<script>标签来配置选项,但是<script setup>和普通<script>中的 setup() 函数不能同时用来定义响应式数据。

例如:

<template>
  <div>
    {{ name }}
    {{ age }}
  </div>
</template>

<script setup>
import { ref } from 'vue';
const name = ref('张三');
</script>

<script>
import { ref } from 'vue';
export default {
  setup() {
    const age = ref(10);
    return { age };
  }
}
</script>

同时使用了<script setup>和 setup(),则页面不会按预期展示。

<template>
  <div>
    {{ name }}
    {{ age }}
  </div>
</template>

<script setup>
import { ref } from 'vue';
const name = ref('张三');
const age = ref(10);
</script>

<script>
export default {
  // 这里可以配置选项式 API 的内容,比如 props、emits、components 等
  props: {
    // 示例 props 配置
    someProp: String
  },
  emits: ['someEvent'],
  components: {
    // 示例组件配置
    // SomeComponent
  }
}
</script>

在这个示例里,<script setup>负责定义响应式数据和逻辑,普通<script>负责配置选项式 API 的内容。这样就能把两种语法风格结合起来使用。

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: {
        // ...
    }
}

这些就是选项对象。

Vue3中defineComponent 的作用详解

Vue 中的 defineComponent

Vue3源码解析-defineComponent

defineSlots

与 defineComponent 类似,辅助功能,用于类型检查和 IDE 的自动补全,主要用于 TypeScript 环境下。

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的使用

defineModel

仅在 3.4+ 中可用

v-model

Vue 系列之:自定义双向数据绑定

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

下面使用 defineModel 来实现双向数据绑定的例子:

父组件:

<!-- 父组件 -->
<template>
  <div>
    <p>Count in parent: {{ count }}</p>
    <Children v-model="count" />
  </div>
</template>

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

const count = ref(1)

watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal, typeof newVal) // number 类型
})  
</script>

子组件:

<!-- 原子组件代码 -->
<template>
    <div>
        <!-- 
        	这里不能直接 v-model="modelValue" 会报编译错误
			v-model cannot be used on a prop, because local prop bindings are not writable.
		 -->
        <el-select v-model="selectValue" @change="handleChange">
            <el-option label="选项1" :value="1" />
            <el-option label="选项2" :value="2" />
            <el-option label="选项3" :value="3" />
        </el-select>
    </div>
</template>

<script setup>
import { ref, defineProps, defineEmits } from 'vue'

const props = defineProps({
    modelValue: Number
})

const emit = defineEmits(['update:modelValue'])

const selectValue = ref(props.modelValue)

function handleChange() {
    console.log('选项变化了');
    emit('update:modelValue', selectValue.value);
}
</script>
<!-- 使用 defineModel 的子组件代码 -->
<template>
    <div>
        <el-select v-model="selectValue" @change="handleChange">
            <el-option label="选项1" :value="1" />
            <el-option label="选项2" :value="2" />
            <el-option label="选项3" :value="3" />
        </el-select>
    </div>
</template>

<script setup>
import { ref, defineProps, defineEmits } from 'vue'

// const props = defineProps({
//     modelValue: Number
// })

// const emit = defineEmits(['update:modelValue'])

// const selectValue = ref(props.modelValue)

// function handleChange() {
//     console.log('选项变化了');
//     emit('update:modelValue', selectValue.value);
// }

const selectValue = defineModel()

function handleChange() {
    console.log('选项变化了:', selectValue.value);
    // emit('update:modelValue', selectValue.value);
}
</script>

非常简单!

defineModel 就是封装了之前的实现过程:在子组件内定义了一个叫 selectValueref 变量(当然也可以取别的变量名)和名字叫 modelValue 的 props,并且 watch 了 props 中的 modelValue。当父组件改变 modelValue 的值后会同步更新 selectValue 变量的值;当子组件改变 selectValue 变量的值后会调用 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 })

setup() 和 <script setup> 的区别

编译

在编译时 setup() 难以进行深度静态分析, 因为它的返回值是动态的(比如返回的对象可能包含运行时才能确定的属性或方法)。

<script setup>是编译时语法糖,它的顶层绑定(变量、函数、import 等)是直接暴露给模板的,编译器可以明确知道哪些内容会被模板使用,从而进行更多优化,例如更好的 Tree-shaking:未在模板中使用的代码可以被标记并移除。

总结:

Vue 编译器可以对<script setup>语法糖内部的代码进行静态分析,从而进行更多的编译时优化,减少运行时的开销,提高组件的渲染性能。因此在性能优化上 setup() 不如<script setup>

上下文

setup() 函数通过参数 props 和 context 来访问组件的属性和上下文。

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

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

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

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('submit', 1200)
    }
    return { sendNum }
  }
}
</script>

<script setup>:

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

<script setup>使用 defineProps 和 defineEmits 宏来访问组件的属性和触发自定义事件,不需要手动接收 props 和 context。

return

setup() 函数是一个标准的组件选项(Component Option),由 Vue 运行时直接解析。需显式返回对象,其属性暴露给模板。

即:setup() 中的内容需要显式地 return 才能在模板中访问(属性和方法都需要 return):

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">
      测试
    </button>
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    const message = ref('Hello, Vue 3!');
    const changeMessage = () => {
      message.value = 'Message changed!';
    };
    return { message, changeMessage };
  }
};
</script>

例如上面这段代码,如果没有 return,则功能无法实现。

如果使用 <script setup> 则不需要 return:

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">
      测试
    </button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const message = ref('Hello, Vue 3!');
const changeMessage = () => {
  message.value = 'Message changed!';
};
</script>

这是因为:

编译器会对 <script setup>块进行静态分析和转换,生成等效的 setup() 函数。编译后的代码会提取顶层变量(包括 import 的组件),形成 setup() 的返回对象。

即:<script setup> 是 setup() 的语法糖,在 <script setup> 中定义的变量和方法会自动暴露给模板,无需手动 return。

expose

父组件:

<template>
  <div>
    <Children1 ref="child1" />
    <Children2 ref="child2" />
    <button @click="handleClick">按钮</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Children1 from './Children1.vue';
import Children2 from './Children2.vue';
const child1 = ref(null);
const child2 = ref(null);
const handleClick = () => {
  console.log("child1:", child1);
  console.log("child1 message:", child1.value.message);
  console.log("child1 obj:", child1.value.obj);
  console.log("child1 fn:", child1.value.fn);
  console.log("----------");
  console.log("child2:", child2);
  console.log("child2 message:", child2.value.message);
  console.log("child2 obj:", child2.value.obj);
  console.log("child2 fn:", child2.value.fn);
}
</script>

setup() 子组件:

<template>
    <div>
        setup() 子组件
    </div>
</template>

<script>
import { reactive, ref } from 'vue';
export default {
    setup() {
        const message = ref('Hello, Vue 3!');
        const obj = reactive({ name: '张三', age: 10 })
        const fn = () => {
            console.log("子组件方法");
        }
        return { message, obj }
    }
}
</script>

<script setup> 子组件:

<template>
    <div>
        <script setup> 子组件
    </div>
</template>

<script setup>
import { reactive, ref } from 'vue';
const message = ref('Hello, Vue 3!');
const obj = reactive({ name: '张三', age: 10 })
const fn = () => {
    console.log("子组件方法");
}
</script>

在这里插入图片描述

可以发现:setup() 会向父组件暴露所有 return 的属性和方法,而<script setup>就不会,<script 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 函数特点

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(props, context) 详细说明

执行顺序

setup() 是组件中使用组合式 API 的入口点,它在组件实例创建之前执行,在 beforeCreate 和 created 生命周期钩子之前调用。

注:有说法认为 Vue3 中没有 beforeCreate 和 created 钩子函数,这是不准确的。

  • 组合式 API 中确实没有 beforeCreate 和 created 钩子函数,他们的功能被 setup 取代;

  • 但是在选项式 API 中他们依然存在。

<template>
</template>

<script>
export default {
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    console.log("created");
  },
  setup() {
    console.log("setup");
  }
}
</script>

在这里插入图片描述
执行顺序:setup——beforeCreate——created

props 参数

特性:

  1. 是响应式的,包含组件接收的所有 prop,当父组件更新 props 时会自动更新

  2. 不能使用 ES6 解构,否则会失去响应性

  3. 如果需要解构,可以使用 toRefs 或 toRef 保持响应性

特性 1 举例:

<!--父组件-->
<template>
  <div>
    <Children name="张三" :age="10" />
  </div>
</template>

<script>
import Children from './Children.vue';
export default {
  components: { Children }
}
</script>
<!--子组件-->
<template>
    <div>
        <p>{{ name }}</p>
        <p>{{ age }}</p>
    </div>
</template>

<script>
export default {
    props: {
        name: String,
        // age: Number 没有接收 age 属性
    },
    setup(props) {
        console.log("props:", props);
        console.log("name:", props.name);
        console.log("age:", props.age);
    }
}
</script>

在这里插入图片描述
组件没有接收 age 属性,所以 props 参数中的 age 为 undefined。


特性 2、3 举例:

<!--父组件-->
<template>
  <div>
    <Children :name="name" :age="age" @change="handleChange" />
  </div>
</template>

<script>
import { ref } from 'vue';
import Children from './Children.vue';
export default {
  components: { Children },
  setup() {
    const name = ref('张三');
    const age = ref(10);
    const handleChange = () => {
      age.value++
    }

    return { name, age, handleChange };
  }
}
</script>
<!--子组件-->
<template>
    <div style="margin-left: 50px;">
        <p>{{ props.name }}</p>
        <p>{{ props.age }}</p>
        <p>{{ age }}</p>
        <button @click="handleClick">按钮
        </button>
    </div>
</template>

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

export default {
    props: {
        name: String,
        age: Number
    },
    setup(props, context) {
        const { age } = props
        const age1 = props.age
        const { age: age2 } = toRefs(props)
        const handleClick = () => {
            context.emit('change');
            setTimeout(() => {
                console.log("props.age:", props.age);
                console.log("age:", age);
                console.log("age1:", age1);
                console.log("age2.value:", age2.value);
                console.log("age2:", age2);
            })
        };
        return { props, age, handleClick }
    }
}
</script>

初始页面:
在这里插入图片描述
点击一次按钮后:
在这里插入图片描述
在这里插入图片描述
可以看到:

const { age } = propsconst age1 = props.age 丢失了响应性,

props.ageconst { age: age2 } = toRefs(props) 保留了响应性。

拓展:

使用 toRefs(props) 创建的 age2 是一个 ref 对象,它包含了多个内部属性:

属性类型说明
__v_isRefboolean标识这是一个 ref 对象,值为 true
_defaultValueany默认值
_keystring对应的 props 键名,这里是 “age”
_objectobject指向原始的 props 响应式对象
_valueany当前存储的 age 的值(与 value 相同)
depSet<ReactiveEffect>存储依赖该 ref 的副作用(effect)
valueany访问或修改

context 参数

context 是一个普通对象(非响应式),包含组件的三个属性:

  • attrs

    • 包含所有未在 props 中声明的 attribute

    • 相当于 Vue 2 中的 this.$attrs

    • 非响应式

    • 示例:

      setup(props, { attrs }) {
        console.log(attrs.class) // 访问 class 属性
      }
      
  • slots

    • 包含所有插槽内容的对象

    • 相当于 Vue 2 中的 this.$slots

    • 非响应式

    • 示例:

      setup(props, { slots }) {
        const defaultSlot = slots.default() // 获取默认插槽内容
        return () => h('div', defaultSlot)
      }
      
  • emit

    • 用于触发自定义事件的函数

    • 相当于 Vue 2 中的 this.$emit

    • 示例:

      setup(props, { emit }) {
        const handleClick = () => {
          emit('change', 'new value')
        }
        
        return {
          handleClick
        }
      }
      

当然也可以不使用解构写法:

setup(props, context) {
  console.log(context.attrs.class)
  const defaultSlot = context.slots.default()
  const handleClick = () => {
    context.emit('change', 'new value')
  }
}

返回值

setup() 可以返回一个对象,该对象的属性/函数将被暴露给模板使用,也可以返回一个渲染函数:

返回对象:

import { ref } from 'vue'

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

返回渲染函数:

import { h, ref } from 'vue'

setup() {
  const count = ref(0)
  
  return () => h('div', count.value)
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值