认识h函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然而一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用渲染函数,它比模板更接近编译器。
前面我们讲解过VNode和VDOM的改变,Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM)。事实上,我们之前编写的 template 中的HTML最终也是使用渲染函数生成对应的VNode,那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode。
那么我们应该怎么来做呢?使用 h() 函数:
h() 函数是一个用于创建 vnode 的一个函数,其实更准确的命名是createVNode() 函数,但是为了简便在Vue中将之简化为 h() 函数。
可能你会懵逼了,render()函数、h()函数,createVNode()函数什么关系?
render()函数是我们在组件中编写的一个选项,它要求我们返回一个VNode对象,我们利用h()函数来返回一个VNode对象,而h()函数更准确的命名是createVNode()函数。
h()函数如何使用呢?
h()函数如何使用呢?它接受三个参数:
注意:如果没有props,最好将null作为第二个参数传入,将children作为第三个参数传入,以防产生歧义。
h函数的基本使用
h函数可以在两个地方使用:
- render函数选项中;
- setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
<script>
// 先导入h函数
import { h } from 'vue';
export default {
//使用render函数后,上面的template就可以删掉了
render() {
// 参数一:h2标签 参数二:带有title的class 参数三:内容是Hello Render
return h("h2", {class: "title"}, "Hello Render")
}
}
</script>
<style scoped>
</style>
h函数计数器案例
<script>
import { h } from 'vue';
export default {
data() {
return {
counter: 0
}
},
render() {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${this.counter}`),
h("button", {
onClick: () => this.counter++
}, "+1"),
h("button", {
onClick: () => this.counter--
}, "-1"),
])
}
}
</script>
<style scoped>
</style>
如果使用setup,代码如下:
<script>
import { ref, h } from 'vue';
export default {
setup() {
// setup函数可以代替data选项
const counter = ref(0);
// setup函数还可以代替render函数
return () => {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", {
onClick: () => counter.value++
}, "+1"),
h("button", {
onClick: () => counter.value--
}, "-1"),
])
}
}
}
</script>
<style scoped>
</style>
h函数中组件和插槽的使用
前面我们使用h函数第一个参数传的是元素标签的名字,其实还可以传组件。
App.vue组件代码:
<script>
import { h } from 'vue';
import HelloWorld from './HelloWorld.vue';
export default {
render() {
return h("div", null, [
h(HelloWorld, null, {
default: props => h("span", null, `app传入到HelloWorld中的内容: ${props.name}`)
})
])
}
}
</script>
<style scoped>
</style>
HelloWorld.vue组件代码:
<script>
import { h } from "vue";
export default {
render() {
// HelloWorld组件也是通过render函数创建出来的
return h("div", null, [
h("h2", null, "Hello World"),
this.$slots.default ? this.$slots.default({name: "coderwhy"}): h("span", null, "我是HelloWorld的插槽默认值")
])
}
}
</script>
<style lang="scss" scoped>
</style>
jsx的babel配置
上面的代码显然难以阅读,如果我们又想充分利用JavaScript的编程能力又不想写上面那些代码,我们可以使用jsx,jsx和React很像。
如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持。jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的),对于Vue来说,我们只需要在Babel中配置对应的插件即可。
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D
在babel.config.js配置文件中配置插件:
编写jsx代码:
jsx计数器案例
<script>
import HelloWorld from './HelloWorld.vue';
export default {
data() {
return {
counter: 0
}
},
render() {
const increment = () => this.counter++;
const decrement = () => this.counter--;
return (
<div>
<h2>当前计数: {this.counter}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
// 还可以使用组件
<HelloWorld>
</HelloWorld>
</div>
)
}
}
</script>
<style lang="scss" scoped>
</style>
jsx组件的使用
认识自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令。(注意,在Vue中,代码的复用和抽象主要还是通过组件)。
自定义指令分为两种:
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点。
方式一:如果我们使用默认的实现方式;
方式二:自定义一个 v-focus 的局部指令;
方式三:自定义一个 v-focus 的全局指令;
实现方式一:聚焦的默认实现
实现方式二:局部自定义指令
自定义一个 v-focus 的局部指令,这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可,它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-),自定义指令有一个生命周期,是在组件挂载后调用 mounted,我们可以在其中完成操作。
<template>
<div>
<input type="text" v-focus>
</div>
</template>
<script>
export default {
// 局部指令
directives: {
focus: {
// 参数一:当前el元素
// 参数二:绑定的一个对象,传入的参数和修饰符就在这里面
// 参数三:虚拟节点
// 参数四:上一个虚拟节点
mounted(el, bindings, vnode, preVnode) {
//console.log("focus mounted");
el.focus();
}
}
}
}
</script>
<style scoped>
</style>
方式三:自定义全局指令
自定义一个全局的v-focus指令可以让我们在任何地方直接使用。
import { createApp } from 'vue'
const app = createApp(App);
app.directive("focus", {
mounted(el, bindings, vnode, preVnode) {
//console.log("focus mounted");
el.focus();
}
})
app.mount('#app');
指令的生命周期、参数、修饰符
一个指令定义的对象,Vue提供了如下几个钩子函数,在每个钩子函数中都有四个参数:el, bindings, vnode, preVnode 详情如下:
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
<template>
<div>
<button v-if="counter < 2" v-why.aaaa.bbbb="'coderwhy'" @click="increment">当前计数: {{counter}}</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
// 指令生命周期
directives: {
why: {
// 参数一:当前el元素
// 参数二:绑定的一个对象,传入的参数和修饰符就在这里面
// 参数三:虚拟节点
// 参数四:上一个虚拟节点
created(el, bindings, vnode, preVnode) {
console.log("why created", el, bindings, vnode, preVnode);
//获取传递的参数,也就是'coderwhy'
console.log(bindings.value);
//获取修饰符,modifiers是个对象 {aaa: true, bbb: true}
console.log(bindings.modifiers);
},
beforeMount() {
console.log("why beforeMount");
},
mounted() {
console.log("why mounted");
},
beforeUpdate() {
console.log("why beforeUpdate");
},
updated() {
console.log("why updated");
},
beforeUnmount() {
console.log("why beforeUnmount");
},
unmounted() {
console.log("why unmounted");
}
}
},
setup() {
const counter = ref(0);
const increment = () => counter.value++;
return {
counter,
increment
}
}
}
</script>
<style scoped>
</style>
自定义指令练习:时间戳的显示需求
在开发中,大多数情况下从服务器获取到的都是时间戳,我们需要将时间戳转换成具体格式化的时间来展示。在Vue2中我们可以通过过滤器来完成,Vue3中删除了过滤器,在Vue3中我们可以通过计算属性(computed)或者自定义一个方法(methods)来完成,其实我们还可以通过一个自定义的指令来完成。
我们来实现一个可以自动对时间格式化的指令v-format-time,这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可。
我们创建directives文件夹,format-time.js是每个指令的具体代码,index.js是注册每个指令,这样我们在main.js中直接导入,然后传入app即可,文件目录如下:
format-time.js代码如下:
// 需要通过npm install dayjs
import dayjs from 'dayjs';
export default function(app) {
app.directive("format-time", {
// 1. 先设置时间格式参数
created(el, bindings) {
// 设置默认的时间格式
// 这里我们把值设置到bindings上,目的就是为了在bindings里面能取到
// 如果我们不设置到bindings上就没法传给mounted了
bindings.formatString = "YYYY-MM-DD HH:mm:ss";
//如果自定义指令传递了时间格式参数,就使用传递的参数
if (bindings.value) {
bindings.formatString = bindings.value;
}
},
// 2. 再转换
mounted(el, bindings) {
//获取显示的文字
const textContent = el.textContent;
//转成int
let timestamp = parseInt(textContent);
//如果10位就是秒,转成毫秒
if (textContent.length === 10) {
timestamp = timestamp * 1000
}
//format后面的参数使用传递的参数
el.textContent = dayjs(timestamp).format(bindings.formatString);
}
})
}
index.js文件代码如下:
import registerFormatTime from './format-time';
export default function registerDirectives(app) {
registerFormatTime(app);
}
main.js文件代码如下:
import { createApp } from 'vue'
import App from './03_自定义指令/App.vue'
import registerDirectives from './directives'
const app = createApp(App);
registerDirectives(app);
app.mount('#app');
这样我们就将main.js里面注册全局组件的代码抽出来了,下次如果需要注册新的全局组件,只需在index.js里面添加一行代码即可。
认识Teleport
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用,那么组件A中template的元素,会被挂载到组件B中template的某个位置,最终我们的应用程序会形成一颗DOM树结构。但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置,比如移动到body元素上,或者我们有其他的div#app之外的元素上,这个时候我们就可以通过teleport来完成。
Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals,teleport翻译过来是心灵传输、远距离运输的意思,它有两个属性:
- to:指定将其中的内容移动到的目标元素,可以使用选择器;
- disabled:是否禁用 teleport 的功能;
我们来看下面代码的效果:
和组件结合使用
当然,teleport也可以和组件结合一起来使用,我们可以在 teleport 中使用组件,并且也可以给他传入一些数据。
多个teleport
如果我们将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并:
实现效果如下:
认识Vue插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
① 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
② 函数类型:一个function,这个函数会在安装插件时自动执行;
插件可以完成的功能没有限制,比如下面的几种都是可以的:
- 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
- 添加全局资源:指令/过滤器/过渡等;
- 通过全局 mixin 来添加一些组件选项;
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;
插件的编写方式
对象类型的写法,plugins_object.js文件代码:
export default {
install(app) {
// 全局配置添加一个name
app.config.globalProperties.$name = "coderwhy"
}
}
函数类型的写法,plugins_function.js文件代码:
export default function(app) {
console.log(app);
}
安装插件:
import { createApp } from 'vue'
import App from './03_自定义指令/App.vue'
const app = createApp(App);
//安装插件,其实内部就是调用install方法
app.use(pluginObject);
app.use(pluginFunction);
app.mount('#app');
使用插件:
如果在Vue2中,直接使用this.$xxx就可以拿到全局配置的属性了:
mounted() {
console.log(this.$name);
},
如果在Vue3中,因为在setup中我们不能使用this,所以我们可以通过如下方式获取this.$xxx:
import { getCurrentInstance } from "vue";
setup() {
const instance = getCurrentInstance();
console.log(instance.appContext.config.globalProperties.$name);
},
网友评论