render函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM)
我们编写的 template 中的HTML 最终也是使用**渲染函数(render函数)**生成对应的VNode
如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode,
也就是自己编写render函数,直接使用JavaScript的方式编写模板。
此时因为render函数需要返回的是vnode,所以我们需要借助另一个函数h() 函数,
h() 函数是一个用于创建 vnode 的一个函数
其实更准确的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数
h函数接收三个参数:
- 元素或组件
- 元素或组件上的属性 --- 可选的
- 子元素,子组件,数组(多个子元素的时候) --- 可选的
注意事项:
- 如果没有props,那么通常可以将children作为第二个参数传入但是不推荐
- 容易产生歧义,推荐将
null或{}作为第二个参数传入,将children作为第三个参数传入
<script>
import { h } from 'vue'
export default {
// template中的代码会被编译为render函数
// 而render其实是options api中的一个选项
// 如果我们自己编写了render函数,那么我们就不需要在编写template模板
// 如果同时存在template和render函数,那么vue编译器会去读取template 忽略render函数
render() {
// render函数需要返回vnode
// 我们无法直接编写vnode, 所以存在h函数(createVNode)函数
// 来为我们生成对应的vnode
return h('h2', { title: 'title' }, 'Hello Vue')
}
}
</script>
<!--
=> <h2 title="title">Hello Vue</h2>
-->
<script>
import { h } from 'vue'
import Cpn from './components/Cpn.vue'
export default {
render() {
return h(Cpn, { title: 'title' })
}
}
</script>
<script>
import { h } from 'vue'
import Cpn from './components/Cpn.vue'
export default {
// render函数是和data,methods,setup同级的一个选项
render() {
return h('div', { title: 'title' }, [
'Hello Vue',
h('p', {}, 'Hello Render'),
Cpn.render()
])
}
}
</script>
计数器案例
写法1
<script>
import { h } from 'vue'
export default {
name: 'App',
data() {
return {
count: 0
}
},
render() {
return h('div', {}, [
h('p', {}, `count:${this.count}`),
h('button', {
onClick: () => this.count++
}, '+1'),
h('button', {
onClick: () => this.count--
}, '-1')
])
}
}
</script>
写法2
<script>
import { ref, h } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count
}
},
render() {
// 在render函数中,this有被正常绑定,所以可以使用this
// 在这里使用ref对象的时候,会自动解包
return h('div', {}, [
h('p', {}, `count:${this.count}`),
h('button', {
onClick: () => this.count++
}, '+1'),
h('button', {
onClick: () => this.count--
}, '-1')
])
}
}
</script>
写法3
<script>
import { ref, h } from 'vue'
export default {
setup() {
const count = ref(0)
// return 可以返回一个render函数
// 这里使用ref对象的时候,并不会自动解包
return () => {
return h('div', {}, [
h('p', {}, `count:${count.value}`),
h('button', {
onClick: () => count.value++
}, '+1'),
h('button', {
onClick: () => count.value--
}, '-1')
])
}
}
}
</script>
插槽的使用
父组件
<script>
import { h } from 'vue'
import Cpn from './components/Cpn.vue'
export default {
setup() {
return () => h(Cpn, {}, {
default(props) {
return h('h2', {}, props)
}
})
}
}
</script>
子组件
<script>
import { h } from 'vue'
export default {
setup(props, { slots }) {
return () => h('div', {}, slots.default?.('something in Cpn') || h('h2', {}, 'default value'))
}
}
</script>
父组件
<script>
import { h } from 'vue'
import Home from './components/Home.vue'
export default {
setup() {
return () => {
// 如果是组件,第一个传递组件对象即可
return h(Home, null, {
default(props) {
return `my name is ${props}`
}
})
}
}
}
</script>
子组件
<script>
export default {
// 不使用的变量,可以使用下划线做占位符
setup(_, { slots }) {
const name = 'Klaus'
return () => {
return slots.default ? slots.default(name) : 'default value'
}
}
}
</script>
JSX
render函数使用h函数的可读性是非常差的,所以在实际编写的时候,我们使用的一般是JSX代码,
然后通过babel来将我们的jsx代码转换为h函数的形式
计数器
<!--
如果是vite环境 需要安装@vitejs/plugin-vue-jsx并进行对应的配置
并为script添加属性 lang="jsx"或lang="tsx"
-->
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return () => {
return (
<>
{/* JSX不会自动包裹fragment,所以需要手动添加 */}
<h2>count: {count.value}</h2>
<button onClick={ () => count.value++ }>+1</button>
<button onClick={ () => count.value-- }>-1</button>
</>
)
}
}
}
</script>
使用组件
<script>
import Cpn from './components/Cpn.vue'
export default {
setup() {
return () => {
return (
<>
<Cpn />
</>
)
}
}
}
</script>
传递props
传递者
<script>
import Cpn from './components/Cpn.vue'
export default {
setup() {
return () => {
return (
<>
<Cpn msg="msg" />
</>
)
}
}
}
</script>
调用者
<script>
export default {
// 传入的props依旧需要在这里进行声明,以便于区分props和no-props attribute
props: ['msg'],
setup(props) {
console.log(props.msg) // => 'msg'
return () => {
return <h2>default value</h2>
}
}
}
</script>
使用插槽
插槽使用者
<script>
import Cpn from './components/Cpn.vue'
export default {
setup() {
return () => {
return (
<>
<Cpn>
{/* 插槽以对象的形式进行传递 */}
{
{
default(props) {
return <h2>{ props }</h2>
}
}
}
</Cpn>
</>
)
}
}
}
</script>
插槽提供者
<script>
export default {
setup(props, { slots }) {
return () => {
return slots.default?.('Hello World') || <h2>default value</h2>
}
}
}
</script>
自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等
除了使用这些指令之外,Vue 也允许我们来自定义自己的指令
- 注意:在Vue中,代码的复用和抽象主要还是通过组件
- 通常在某些情况下,你需要
对DOM元素进行底层操作,这个时候就会用到自定义指令
自定义指令分为两种:
- 自定义局部指令: 组件中通过 directives 选项,只能在当前组件中使用
- 自定义全局指令: app的 directive 方法,可以在任意组件中被使用
局部指令
<template>
<div>
<input type="text" v-focus>
</div>
</template>
<script>
export default {
name: 'App',
// directives是一个对象,可以挂载多个指令
directives: {
// key是指令的名称, 不需要添加v-前缀
// value是一个配置对象,其中的值主要都是一些指令对应的生命周期函数
focus: {
mounted(el) {
el.focus()
}
}
}
}
</script>
全局指令
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
script setup中的自定义指令
<template>
<div>
<input type="text" v-focus>
</div>
</template>
<script setup>
// 在vue script setup中 自定义局部指令只需要定义一个变量
// 该变量使用v开头的小驼峰命名 以区分普通变量即可
const vFocus = {
mounted(el) {
el.focus()
}
}
</script>
生命周期
| 函数 | 说明 |
|---|---|
| created | 在绑定元素的 attribute 或事件监听器被应用之前调用 元素创建完毕,属性和事件没有绑定上之前 一般用于进行初始化操作 |
| beforeMount | 当指令第一次绑定到元素并且在挂载父组件之前调用 |
| mounted | 在绑定元素的父组件被挂载后调用 |
| beforeUpdate | 在更新包含组件的 VNode 之前调用 |
| updated | 在包含组件的 VNode 及其子组件的 VNode 更新后调用 |
| beforeUnmount | 在卸载绑定元素的父组件之前调用 |
| unmounted | 当指令与元素解除绑定且父组件已卸载时,只调用一次 |
每一个生命周期钩子都有下面四个参数
| 名称 | 说明 |
|---|---|
| el | 绑定的元素 |
| bindings | 绑定信息,存储了修饰符和参数 |
| vnode | 当前的vnode |
| preVnode | 更新之前的vnode |
参数和修饰符
<template>
<div>
<!--
.once --- modifier --- 修饰符
param --- 参数
-->
<input type="text" v-focus.once="'param'">
</div>
</template>
<script>
export default {
name: 'App',
directives: {
focus: {
mounted(el, bindings) {
el.focus()
console.log(bindings.value) // => param
console.log(bindings.modifiers) // => { once: true }
}
}
}
}
</script>
案例
格式化时间
定义 --- src/driectives/formatTime.js
import dayjs from 'dayjs'
// 注册指令所需要使用的app由外部传入
export default function(app) {
// 指令名使用 format-time 和 formatTime 皆可
// 推荐使用format-time
app.directive('format-time', {
created (el, bindings) {
// 将formatStr在这里初始化是为了避免多次调用执行的时候
// 可以都得到正确的初始化
// 如果在函数外部定义的话,指令在多次调用的时候
// 初始化值可能会被污染
bindings.formatStr = 'YYYY-MM-DD HH:mm:ss'
if (bindings.value) {
bindings.formatStr = bindings.value
}
},
mounted (el) {
// textContent的类型是string
const timeStamp = Number(el.textContent)
el.textContent = timeStamp.length === 10
? dayjs.unix(timeStamp).format( bindings.formatStr)
: dayjs(timeStamp).format( bindings.formatStr)
}
})
}
统一暴露 --- src/driectives/index.js
import registerFormatTime from './formatTime'
// 统一暴露出一个函数,目的是为了获取注册所必须的app对象
export default function(app) {
registerFormatTime(app)
}
全局注册 --- main.js
import { createApp } from 'vue'
import App from './App.vue'
import registerDriectives from './driectives'
const app = createApp(App)
// 全局注册指令
registerDriectives(app)
app.mount('#app')
使用
<!--
指令中的value默认会被解析为变量,
所以如果需要传递字符串,需要手动添加引号
-->
<p v-format-time="'YYYY/MM/DD'">1629561162</p>
Teleport
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
- 那么组件A中template的元素,会被挂载到组件B中template的某个位置
- 最终我们的应用程序会形成一颗DOM树结构
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
-
比如移动到body元素上,或者我们有其他的div#app之外的元素上;
-
这个时候我们就可以通过teleport来完成
Teleport是什么
- 它是一个Vue提供的内置组件,类似于react的Portals
- teleport翻译过来是心灵传输、远距离运输的意思
<template>
<div>
<!--
这里的元素将不在被挂载到div#app上,而是会被挂载到div#foo上
使用to重新定义挂载点
-->
<teleport to="#foo">
<h2>Hello Teleport</h2>
</teleport>
</div>
</template>
<template>
<div>
<teleport to="#foo">
<h2>Hello Teleport</h2>
</teleport>
<!--
多个teleport同时存在的时候
并不会产生覆盖,而是会进行合并
-->
<teleport to="#foo">
<Cpn />
</teleport>
</div>
</template>
插件
通常我们**向Vue全局添加一些功能时,会采用插件的模式**,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行
- 函数类型:一个function,这个函数会在安装插件时自动执行
插件可以完成的功能没有限制,
- 可以是添加全局方法或者 property
- 可以是 添加全局资源,如指令/过滤器/过渡等
- 通过全局 mixin 来添加一些组件选项
- 将某一个库中的API挂载到全局上进行使用
定义 --- plugins/addGlobalVariable.js
对象方式
export default {
// 在调用函数的时候,会将app对象作为参数传入
install(app) {
// 全局的方法或变量一般以$开头,以区分全局变量或局部变量
app.config.globalProperties.$name = 'Klaus'
}
}
函数方式
export default function(app) {
app.config.globalProperties.$name = 'Klaus'
}
注册插件
import { createApp } from 'vue'
import App from './App.vue'
import addGlobalVariable from './plugins/addGlobalVariable'
const app = createApp(App)
// 注册插件
app.use(addGlobalVariable)
app.mount('#app')
使用 --- vue2
mounted() {
console.log(this.$name)
}
使用 --- vue3
import { getCurrentInstance } from 'vue'
setup() {
// getCurrentInstance().appContext ==> 获取到的就是app实例对象
const name = getCurrentInstance().appContext.config.globalProperties.$name
console.log(name)
}
案例
格式化时间
实现方式一 --- 挂载到全局属性
实现
import dayjs from 'dayjs'
export default {
install(app) {
app.config.globalProperties.$formatTime =
(timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') =>
timeStamp.toString().length === 10 ?
dayjs.unix(timeStamp).format(formatStr)
: dayjs(timeStamp).format(formatStr)
}
}
挂载
import { createApp } from 'vue'
import App from './App.vue'
import formatTime from './plugins/formatTime'
const app = createApp(App)
app.use(formatTime)
app.mount('#app')
使用
import { getCurrentInstance } from 'vue'
setup() {
const { $formatTime } = getCurrentInstance().appContext.config.globalProperties
console.log($formatTime(1629564731))
}
实现方式二 -- 使用provide全局传递
实现
import dayjs from 'dayjs'
export default {
install(app) {
app.provide('$formatTime', (timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') =>
timeStamp.toString().length === 10 ?
dayjs.unix(timeStamp).format(formatStr)
: dayjs(timeStamp).format(formatStr))
}
}
挂载
import { createApp } from 'vue'
import App from './App.vue'
import formatTime from './plugins/formatTime'
const app = createApp(App)
app.use(formatTime)
app.mount('#app')
使用
import { inject } from 'vue'
export default {
name: 'App',
setup() {
const $formatTime = inject('$formatTime')
console.log($formatTime(1629564731))
}
}
实现方式三 -- 使用全局mixin
定义
import dayjs from 'dayjs'
export default {
install(app) {
app.mixin({
methods: {
formatTime(timeStamp = Date.now() , formatStr = 'YYYY-MM-DD HH:mm:ss') {
return timeStamp.toString().length === 10 ?
dayjs.unix(timeStamp).format(formatStr)
: dayjs(timeStamp).format(formatStr)
}
}
})
}
}
使用
<template>
<div>
</div>
</template>
<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
name: 'App',
setup() {
// 只有在mounted方法中,定义在data和methods中的属性才会被挂载
onMounted(() => {
const instance = getCurrentInstance()
// 在组件实例的ctx上有一个方法可以获取到methods中定义的方法
console.log(instance.ctx.formatTime(1629564731))
// 也可以在data中获取在data中定义的数据
// console.log(instance.data.name)
})
}
}
</script>