一、render函数
1.1 介绍
Vue推荐在绝大数情况下使用模板来创建HTML,然后一些特殊的场景,如果真的需要JavaScript的完全编程的能力,这个时候可以使用渲染函数,它比模板更接近编译器
-
Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM), 事实上,编写的 template 中的HTML最终也是使用渲染函数生成对应的VNode,如果想充分的利用JavaScript的编程能力,就可以自己来编写 createVNode 函数,生成对应的VNode
-
通过使用 h()函数:
- h() 函数是一个用于创建 vnode 的一个函数
- 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数
-
如何使用?
-
接收3个参数
-
注意事项:
- 如果没有props,那么通常可以将children作为第二个参数传入
- 如果会产生歧义,可以将
null作为第二个参数传入,将children作为第三个参数传入
-
1.2 h函数的基本使用
通过计数器的案例介绍
-
optionsApi使用
<script> import { h } from "vue"; export default { data() { return { counter: 0, }; }, render() { return h("div", { className: "app" }, [ h("h2", null, `${this.counter}`), h("button", { onClick: () => this.counter++ }, "+1"), h("button", { onClick: () => this.counter-- }, "-1"), ]); }, }; </script> -
compositionApi在setup中使用
<script> import { h, ref } from "vue"; export default { setup() { const counter = ref(0); const increment = () => { counter.value++; }; const decrement = () => { counter.value--; }; return () => { return h("div", null, [ h("h2", null, "setup写法"), h("h2", { className: "num" }, `${counter.value}`), h("button", { onClick: increment }, "+1"), h("button", { onClick: decrement }, "-1"), ]); }; }, }; </script> <style> .num { color: blueviolet; } </style> -
compositionApi script顶层setup
- 这种写法仍需要template
<template> <render /> </template> <script setup> import { h, ref } from "vue"; const counter = ref(0); const increment = () => { counter.value++; }; const decrement = () => { counter.value--; }; const render = () => { return h("div", null, [ h("h2", null, "顶层setup写法"), h("h2", { className: "num" }, `${counter.value}`), h("button", { onClick: increment }, "+1"), h("button", { onClick: decrement }, "-1"), ]); }; </script> <style> .num { color: blueviolet; } </style>
1.3 h函数和插槽的使用
-
子组件接收props和插槽
<script> import { h } from "vue"; export default { props: { message: "", }, setup(props, { attrs, slots, emit }) { console.log(props.message); const render = () => { return h("div", null, [ h("h2", null, "Hello World"), slots.default ? slots.default({ text: props.message }) : h("span", null, "我是插槽默认值"), ]); }; return render; }, }; </script> -
在父组件传入插槽
<script> import { h } from "vue"; import HelloWorld from "./HelloWorld.vue"; export default { setup() { return () => { return h("div", null, [ h( HelloWorld, // 传入属性 { message: "你好,世界!" }, // 如果是多个插槽,此处写一个数组 { default: (props) => h("span", null, `app传入到HelloWorld中的内容: ${props.text}`), } ), ]); }; }, }; </script>
二、jsx语法
- 如果写了很多渲染函数,可能会觉得很痛苦,那就试一下jsx语法吧
2.1 安装插件(vue-cli3默认是支持jsx格式)
-
如果希望在项目中使用jsx,就需要添加对jsx的支持:
-
通常会通过Babel来进行转换
-
对于Vue来说,只需要在Babel中配置对应的插件即可
-
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D -
在babel.config.js配置文件中配置插件:
-
-
vite 环境
- 安装插件:
npm install @vitejs/plugin-vue-jsx - D - 配置
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import jsx from '@vitejs/plugin-vue-jsx' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), jsx() ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) - 安装插件:
2.2 基本使用
计数器案例
-
optionsApi
<script lang="jsx"> export default { data() { return { counter: 0, }; }, render() { return ( <div class="app"> <h2>{this.counter}</h2> <button onClick={this.increment}>+1</button> <button onClick={this.decrement}>-1</button> </div> ); }, methods: { increment() { this.counter++; }, decrement() { this.counter--; }, }, }; </script> -
compositionApi
<template> <jsx /> </template> <script lang="jsx" setup> import { ref } from "vue"; import Home from "./Home.vue"; const counter = ref(0); const increment = () => { counter.value++; }; const decrement = () => { counter.value--; }; const jsx = () => { return ( <div class="app"> <h2>{counter.value}</h2> <button onClick={increment}>+1</button> <button onClick={decrement}>-1</button> <!-- 使用子组件 --> <Home /> </div> ); }; </script>
三、自定义指令
3.1 介绍
-
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
- 注意:在Vue中,代码的复用和抽象主要还是通过组件
- 通常在某些情况下,需要对DOM元素进行底层操作,这个时候就会用到自定义指令
-
自定义指令分为两种:
-
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用
-
自定义全局指令:app的 directive 方法,可以在任意组件中被使用
-
3.2 基本使用
此处做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
-
默认的实现方式
<template> <div> <input type="text" ref="inputRef" /> </div> </template> <script setup> import { ref, onMounted } from "vue"; const inputRef = ref(); onMounted(() => { inputRef.value?.focus(); }); </script>- 封装hooks
import { ref, onMounted } from 'vue'; export default function useInput() { const inputRef = ref() onMounted(() => { inputRef.value?.focus() }) return { inputRef } } -
自定义一个 v-focus 的局部指令
- 需要在组件选项中使用 directives 即可
- 它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-)
- 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,可以在其中完成操作
<template> <div> <input type="text" v-focus /> </div> </template> <script> export default { directives: { focus: { mounted(el) { el.focus(); }, }, }, }; </script>- setup顶层语法中使用
<script setup> const vFocus = { mounted(el) { el.focus(); }, }; </script> -
自定义一个 v-focus 的全局指令
const app = createApp(App); app.directive("focus", { mounted(el) { el.focus(); }, });
3.3 指令的生命周期
-
created:在绑定元素的 attribute 或事件监听器被应用之前调用
-
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用
-
mounted:在绑定元素的父组件被挂载后调用
-
beforeUpdate:在更新包含组件的 VNode 之前调用
-
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用
-
beforeUnmount:在卸载绑定元素的父组件之前调用
-
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次
<template> <div class="app"> <button @click="counter++">+1</button> <button @click="showTitle = false">隐藏</button> <h2 v-if="showTitle" class="title" v-test>当前计数: {{ counter }}</h2> </div> </template> <script setup> import { ref } from 'vue'; const counter = ref(0) const showTitle = ref(true) const vTest = { created() { console.log("created") }, beforeMount() { console.log("beforeMount") }, mounted() { console.log("mounted") }, beforeUpdate() { console.log("beforeUpdate") }, updated() { console.log("updated") }, beforeUnmount() { console.log("beforeUnmount") }, unmounted() { console.log("unmounted") } } </script>
3.4 指令的参数和修饰符
- 接受一些参数或者修饰符
- ddd是参数的名称
- abc,cba 是修饰符的名称
- 后面是传入的具体的值;
- 可以通过 bindings 获取到对应的内容
<template>
<div class="app">
<h2 class="title" v-test:ddd.abc.cba="message">哈哈哈</h2>
</div>
</template>
<script setup>
import { ref } from "vue";
const message = ref("Hello World");
const vTest = {
mounted(el, bindings) {
console.log(bindings);
el.textContent = bindings.value;
},
};
</script>
3.5 时间格式化指令(将时间戳进行格式化)
-
时间格式化指令
import dayjs from "dayjs"; export default function (app) { app.directive("ftime", { mounted(el, bindings) { // 1.获取时间, 并且转化成毫秒 let timestamp = el.textContent; if (timestamp.length === 10) { timestamp = timestamp * 1000; } // 转为数字类型(如果不是10位数) timestamp = Number(timestamp) // 2.获取传入的参数 let value = bindings.value; if (!value) { value = "YYYY-MM-DD HH:mm:ss"; } // 3.对时间进行格式化 const formatTime = dayjs(timestamp).format(value); el.textContent = formatTime; }, }); } -
使用
<template> <div class="app"> <h2 v-ftime="'YYYY/MM/DD'">{{ timestamp }}</h2> <h2 v-ftime>{{ 1551111166666 }}</h2> </div> </template> <script setup> const timestamp = 1231355453; </script>
四、安装插件
4.1 认识插件
-
我们肯定用到过vue-router、vuex或者pinia,这些都属于vue的插件
-
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行
- 函数类型:一个function,这个函数会在安装插件时自动执行
-
插件可以完成的功能没有限制,比如下面的几种都是可以的:
-
添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现
-
添加全局资源:指令/过滤器/过渡等
-
通过全局 mixin 来添加一些组件选项
-
一个库,提供自己的 API,同时提供上面提到的一个或多个功能
-
4.2 使用
-
对象语法
// 全局添加属性 export default { install(app) { app.config.globalProperties.$name = "test" } }- 引入,在main.js中使用app.use(),就能全局访问
<template> <div>hhh</div> </template> <script setup> // export default { // mounted() { // console.log(this.$name); // }, // }; import { getCurrentInstance } from "vue"; const instance = getCurrentInstance(); console.log(instance.appContext.config.globalProperties.$name); // test </script> -
函数语法
export default function (app) { // 添加组件或混入 // app.component() // app.mixin() console.log(app); } -
main.js中
app.use()会自动执行对象插件的install方法或者直接执行函数
五、内置组件
5.1 teleport
-
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
- 组件A中template的元素,会被挂载到组件B中template的某个位置,应用程序会形成一颗DOM树结构
-
某些情况下,我们会希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
-
比如移动到body元素上,或者我们有其他的div#app之外的元素上
-
这个时候我们就可以通过teleport来完成
-
-
Teleport是什么呢?
-
一个Vue提供的内置组件,teleport翻译过来是心灵传输、远距离运输的意思
-
它有两个属性:
-
to:指定将其中的内容移动到的目标元素,可以使用选择器
-
disabled:是否禁用 teleport 的功能
-
-
-
基本使用
<template> <div class="app"> <div class="hello"> <p class="content"> <!-- 挂在到body元素 --> <teleport to="body"> <hello-world /> </teleport> </p> </div> </div> </template> <script setup> import HelloWorld from "./HelloWorld.vue"; </script> -
挂在到某个id,多个teleport使用
- 将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并
<template> <div class="app"> <div class="hello"> <p class="content"> <teleport to="#abc"> <hello-world /> </teleport> </p> </div> <p class="content"> <teleport to="#abc"> <h2>App</h2> </teleport> </p> </div> </template> <script setup> import HelloWorld from "./HelloWorld.vue"; </script>
5.2 suspence(慎用)
Suspense显示的是一个实验性的特性,API随时可能会修改
-
Suspense是一个内置的全局组件(一般用在异步组件中),该组件有两个插槽:
-
default:如果default可以显示,那么显示default的内容
-
fallback:如果default无法显示,那么会显示fallback插槽的内容
-
-
使用
- 通常用于异步组件,在组件未加载时显式fallback插槽的内容,加载完成后则显示异步组件
<template> <div class="app"> <suspense> <template #default> <async-home /> </template> <template #fallback> <h2>loading</h2> </template> </suspense> </div> </template> <script setup> import { defineAsyncComponent } from "vue"; const AsyncHome = defineAsyncComponent(() => import("./AsyncHome.vue")); </script>