局部自定义指令
在optionsAPI中:
自定义一个v-focus
的局部指令
- 这个自定义指令实现很简单,只需要在组件选项中使用
directives
即可; - 他是一个对象,在对象中我们编写自定义指令的名称(这里不需要加v-)
- 自定义指令有一个生命周期,
是在组件挂载后调用的mounted
,我们可以在其中完成操作;
自定义自动聚焦
<input type = "text" v-focus>
<script>
export default{
directives:{
//和v-__后面的名字要求相同!
focus:{
//这个对象里面存放自定义指令的生命周期函数,当元素被挂载的时候,就会回调
mounted(el){
console.log('v-focus的元素被挂载了')
el?.focus()
}
}
}
}
</script>
在setup中:
<input type = "text" v-focus>
//通过vXxx驼峰写法来连接自定义指令
const vFocus = {
mounted(el){
el?.focus()
}
}
全局自定义指令
const app = createApp(App)
app.directive('focus',{
mounted(el){
el?.focus()
}
})
app.mount("#app")
全局自定义指令的抽取
由于自定义指令可能会存在很多,全部放在main.js中不太合适,所以进行抽取;
//focus.js
export default function directiveFocus(app){
app.directive({
mounted(el){
el?.focus()
}
})
}
------------------------------------------------------
//index.js
import directiveFocus from '...'
export default function useDirectives(app){
directiveFocus(app)
}
---------------------------------------------------------------------------------
import useDirectives from index.js
const app = createApp(App)
useDirectives(app)
app.mount('#app')
指令的生命周期
一个指令定义的对象,Vue提供了如下的几个钩子函数:
- created:在绑定元素的属性attribute或事件监听器被应用之前调用;
- beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
- mounted:在绑定元素的父组件被挂载后调用;
- beforeUpdate:在更新包含组件的VNode之前被调用;
- updated:在包含组件的VNode及其子组件的VNode更新后调用;
- beforeUnmount:在卸载绑定元素的父组件之前被调用;
- unmounted:当指令与元素接触绑定,且父组件已卸载时,只调用一次;
指令的高级用法:
指令参数-修饰符-值:
<h2 v-why:kobe.abc.cba="message">哈哈</h2>
const message = '你好啊,李银河'
//拿到自定义指令传入的数据
const vWhy = {
//bindings中存储所有数据,是一个对象,arg存储所有数据
mounted(el,bindings){
//将哈哈替换为你好啊,李银河
//el.textContent是哈哈,bindings.value拿到绑定指令的数据
el.textContent = bindings.value
}
}
- bindings.value拿到绑定指令的数据
- el.textContent拿到标签中间的数据
- bindings中还可以拿到修饰符:abc、cba,当然也能拿到值:value
日期格式化指令:
import dayjs from 'dayjs'
export default function directiveUnit(app){
app.directive('ftime',{
mounted(el,bingdings){
// 1.获取时间,并转换为毫秒
let timestamp = el.textContent
if(timestamp.length===10){
timestamp = timestamp*1000
}
// 2.获取传入的参数,来指定格式化格式
let value = bindings.value
//如果是空的
if(!value){
value = 'YYYY-MM-DD HH:mm:ss'
}
// 3.对时间进行格式化
const formatTime = dayjs(timestamp).format(value)
el.textContent = formatTime
}
})
}
-----------------------------------
<h2 v-ftime='YYYY-MM-DD'>{{1649843135}}</h2>
内置组件
<template>
<div class="my-app">
//让teleport中的内容(h2)挂载到body上去
<teleport to = "body">
<h2></h2>
</teleport>
</div>
</template>
和组件结合使用:
<template>
<div class="my-app">
<teleport to='body'>
<hello-world message="我是App中的message"/>
</teleport>
</div>
</template>
此时hello-world组件被挂载到body上
多个teleport
异步组件和Suspense
Suspense:一般用于异步组件
异步组件:会分包处理,该组件会放在单独的js文件中,需要在服务器中单独下载,有可能会存在渲染页面时,该文件还没有被下载出来;
//App.vue
<div class="app">
<suspense>
//suspense中存在2个插槽,default和fallback
<template #default>
<async-home/>
</template>
//如果async-home没被下载出来,先显示Loading应急组件
<template #fallback>
<async-home/>
</template>
</suspense>
</div>
const AsyncHome = defineAsyncComponent(()=>'./AsyncHome.vue')
认识h函数
Vue推荐我们:在绝大多数情况下,使用模板
来创建HTML,然后再某些特殊的场景,当我们真的需要JS完全编程
,这个时候template就不太灵活了,这个时候可以使用渲染函数
,他比模板更接近编译器;
因为本质上,template会被渲染成render函数,生成VNode;如果我们通过渲染函数
,直接就会生成render函数,更接近编译器;
- Vue在生成真实DOM之前,会将我们的节点转换成VNode(虚拟节点),而VNode组合在一块,会形成【树结构】,这个就是虚拟DOM(VDom);
- 事实上,我们编写的template的HTML最终也是使用【渲染函数】从而生成对应的VNode;
- 如果我们想要充分的利用JS编程能力,我们可以自己来编写【createVNode】函数,生成对应的VNode;
- 在template中有很多标签元素,每个元素会通过createVNode函数,创建一个VNode虚拟节点对象,根元素就是根VNode,子元素就是子VNode,形成一个树结构(VDom);最终生成真实DOM
我们现在自己编写createVNode,创建虚拟节点(VNode),那么我们可以通过h()函数
,用于创建VNode的一个函数;
其实更准备的命名是createVNode()函数
,只不过为了简便,Vue将其简化为h()函数
h()函数的使用
h()
函数可以接收3个参数:
- HTML标签名(String)、一个组件(Object)、一个异步组件或者一个函数式组件(必须的)
- 和attribute、prop或者事件相对应的对象,我们会在模板中使用(可选的)
- 子VNodes,使用
h()
构建,或使用字符串获取文本Vnode
或有插槽的对象(可选的)
render()函数
和h()函数
:render()函数
是大头,是写在组件里面的,只不过其中最核心的功能部分(创建VNode)是由h()函数
来完成实现的;
当我们去渲染一个组件的时候,就会调用render()函数,然后他又会通过调用h()函数,返回VNode对象,最终渲染成真实DOM生成在页面上:
<script>
import { h } from 'vue';
// 在OptionAPI中使用render()函数
export default {
render() {
// 创建一个div,类名为app,内容是一个数组,数组中又有很多子元素
return h('div', { class: "app" }, [
h('h2', { class: 'title' }, '我是标题'),
h('p', { class: 'content' }, '我是内容哈哈')
])
}
}
</script>
其中h()函数中也是可以来动态书写数据的:暂时先通过OptionApi来实现:计数器案例
<script>
import { h } from 'vue';
// 在OptionAPI中使用render()函数
export default {
data() {
return {
counter: 0
}
},
render() {
// 计数器案例
return h('div', { class: "app" }, [
h('h2', null, `当前计数:${this.counter}`),
// 这里不再是template模板,不能使用模板指令/语法糖
h('button', { onClick: this.increment }, '+1'),
h('button', { onClick: this.decrement }, '-1'),
])
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
}
</script>
渲染组件:
import Home from './Home.vue'
export default {
render() {
// 渲染home组件
h(Home)
},
}
在CompoisitionAPI中使用:
<script>
import { h, ref } from 'vue'
import Home from './Home.vue'
export default {
setup() {
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
// 此时return返回的不再是一个对象,而是一个函数:并且不是在模板中了,需要自己手动解包.value
return () => h('div', { class: "app" }, [
h('h2', null, `当前计数:${counter.value}`),
h('button', { onClick: increment }, '+1'),
h('button', { onClick: decrement }, '-1'),
// 渲染home组件
h(Home)
])
},
}
</script>
<style lang="less" scoped>
</style>
setup语法糖的写法:还是需要用到template
<template>
<render />
</template>
<script setup>
import { ref, h } from 'vue'
import home from './Home.vue'
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
// 此时return返回的不再是一个对象,而是一个函数:
const render = () => h('div', { class: "app" }, [
h('h2', null, `当前计数:${counter.value}`),
h('button', { onClick: increment }, '+1'),
h('button', { onClick: decrement }, '-1'),
// 渲染home组件
h(home)
])
</script>
jsx语法
上方的编写还是非常麻烦,通过jsx可以简化书写,jsx在react中发扬光大,我们在这里只稍微提一下:
<script lang="jsx">
export default {
data(){
return{
counter:1
}
},
methods:{
increment(){
this.counter++
}
}
render() {
// 不再使用h()函数,使用jsx
return (
<div class="app">
<h2>我是标题,哈哈</h2>
<p>我是内容,嘿嘿</p>
<span>当前计数{this.counter}</span>
<button onClick={this.increment}>+1</button>
</div>
)
}
}
</script>
<style lang="less" scoped>
</style>
只要在jsx中绑定数据,就一定会用{}
来绑定
安装插件:npm install @vitejs/plugin-vue-jsx -D
CompositionAPI的使用:
<template>
<jsx></jsx>
</template>
<script lang="jsx" setup>
import { ref } from 'vue'
const counter = ref(0)
const increment = () => {
counter.value++
}
const decrement = () => {
counter.value--
}
const jsx = () => {
<div class="app">
<h2>当前计数:{counter.value}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
}
</script>
<style lang="less" scoped>
</style>
动画
通过<transition>
标签包裹,被包裹的内容就会执行Vue提供的动画效果:v-enter-from、v-enter-to
等等;
<template>
<div class="home">
<button @click="isShow = !isShow">切换</button>
<transition>
<!-- 这个message会自动执行动画 -->
<h2 v-if="isShow">哈哈哈哈</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isShow = ref(false)
</script>
<style lang="less" scoped>
// 从哪里离开,一般情况下离开和显示是对应的,因为进入后的地方和准备离开的地方是同一个地方
.v-leave-from,
.v-enter-to {
// 变成什么状态
opacity: 1;
transform: scale(1);
}
// 离开到哪,和从哪里进入的是同一个地方
.v-leave-to,
.v-enter-from {
opacity: 0;
transform: scale(0.6);
}
// 进入/离开的过程是什么
.v-leave-active,
.v-enter-active {
// all就是让所有的动画都生效,如果指定opacity,那么只有opacity动画生效
transition: all 2s ease;
}
</style>
如果我们使用的是一个没有name的transition,那么所有的class是以v-
作为默认前缀
如果我们添加了一个name属性,比如<transition name = "why">
,那么所有的class会以why-
开头;
animation动画
我们在transition动画中,to定义结束动画状态,from定义初始动画状态;而animation是通过帧动画
来更好的书写动画效果;
编写好animation(也就是帧动画)时,会要求编写好名字,然后通过<transition>
标签,包裹要执行动画的部分,再通过transition中的.v-enter-leave
等方法来控制搭配【帧动画】;如果给transition标签
定义了name,则可以替换名字:.名字-enter-leave
<template>
<div class="home">
<button @click="isShow = !isShow">切换</button>
<transition name="xh">
<!-- 这个message会自动执行动画 -->
<h2 v-if="isShow">哈哈哈哈</h2>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isShow = ref(false)
</script>
<style lang="less" scoped>
.xh-leave-active{
animation: xhAnim 2s ease reverse;
}
.xh-enter-active {
// 指定对应动画的名字、时间、曲线、延迟
animation: xhAnim 2s ease;
}
@keyframes xhAnim {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>
同时设置过渡动画和帧动画(一般不设置)
其他补充
mode属性
通过设置out-in
执行完一个后,就离开,然后再执行另一个开始动画;
<template>
<div class="home">
<button @click="isShow = !isShow">切换</button>
<br>
<transition name="xh" mode="out-in">
<!-- 这个message会自动执行动画 -->
<h2 v-if="isShow">哈哈哈哈</h2>
<h2 v-else>呵呵呵</h2>
</transition>
</div>
</template>
动态组件的切换:
列表过渡
- 默认情况下,他不会渲染一个元素的包裹器,但是可以指定元素,并且以
tag attribute
进行渲染; - 并且不能使用mode模式
<template>
<div class="app">
<button @click="addNum">添加数字</button>
<button @click="removeNum">删除数字</button>
<button @click="shuffleNum">打乱数字</button>
<transition-group tag="div" name="why">
<template v-for="item in nums" :key="item">
<li>{{ item }}</li>
</template>
</transition-group>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { shuffle } from 'underscore'
const nums = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])
const addNum = () => {
nums.value.splice(randomIndex(), 0, nums.value.length)
}
const removeNum = () => {
nums.value.splice(randomIndex(), 1)
}
const shuffleNum = () => {
nums.value = shuffle(nums.value)
}
const randomIndex = () => {
console.log(Math.floor(Math.random() * nums.value.length))
return Math.floor(Math.random() * nums.value.length)
}
const afterEnter = () => {
console.log("after-enter")
}
</script>
<style scoped>
span {
margin-right: 10px;
display: inline-block;
}
.why-enter-from,
.why-leave-to {
opacity: 0;
transform: translateX(30px);
}
.why-move {
transition: all 2s ease;
}
.why-leave-active {
position: absolute;
}
.why-enter-active,
.why-leave-active {
transition: all 2s ease;
}
</style>
响应式原理
当数据发生变化的时候,其他地方用到该数据的时候,也会做出改变;
也就是将template中的数据重新执行一遍,本质就是通过重新执行render()函数,生成新的VNode
在开发中常见的是对象的响应式:
当第二个文件修改了name值,第一个文件中只要依赖name的,也会跟着重新执行一遍,如果没有依赖name则不需要重新执行,这就是响应式的基本原理;
const obj = {
name:"why",
age:18
}
第一个文件用到的代码
console.log(obj.name)
console.log(obj.age)
console.log(obj.age+100)
--------------------------------
第二个文件用到的代码
obj.name = "kobe"
所以到底哪些代码需要重新执行,是需要进行收集起来的,那么这就是响应式的其中一个功能;所以我们将需要重新执行的代码包裹在一个函数中,当我修改了值,就重新执行这个函数;
响应式依赖收集:
const obj = {
name: 'why',
age: 18
}
// 监听当依赖数据发生变化时,自身也要随之变化的函数的一个方法
const reactiveFns = [] //将所有的函数保存在数组中
function watchFn(fn) {
reactiveFns.push(fn)
// 然后传入进来的函数会先执行一次,和watchEffect类似
fn()
}
watchFn(function foo() {
console.log('foo', obj.name);
console.log('foo', obj.age);
console.log('foo function');
})
watchFn(function bar() {
console.log('bar', obj.name + 'hello');
console.log('bar', obj.age + '10');
console.log('bar function');
})
// 修改obj的属性
obj.name = 'kobe'
// 此时我们先假设我们知道依赖obj.name的函数有哪些,遍历出来后并再执行一次
reactiveFns.forEach(fn => {
fn()
})
这里我们先假设已经知道哪些需要变化的函数了,因此我们只将这些保存进reactiveFns中,然后再逐个遍历,并调用执行;
目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中会需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性,他们很多属性的变化都会有对应的响应式函数;
- 我们不可能在全局维护一大堆的数组来保存这些响应函数;
所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数,相当于替代了原来的简单reactiveFns的数组
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 数据一旦发生变化,notify就把所有的函数收集起来并执行
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'why',
age: 18
}
const dep = new Depend()
function watchFn(fn) {
dep.addDepend(fn)
fn()
}
watchFn(function foo() {
console.log('foo', obj.name);
console.log('foo', obj.age);
console.log('foo function');
})
watchFn(function bar() {
console.log('bar', obj.name + 'hello');
console.log('bar', obj.age + '10');
console.log('bar function');
})
console.log('name发生变化-----------');
obj.name = 'kobe'
dep.notify()
但是我们仍然还是手动通知notify()
,所以我们希望做出一个可以自动监听对象的方法:
我们可以采用2种方式:
- 通过
Object.defineProperty
的方式(vue2采用的方式) - 通过
new Proxy
的方式(vue3采用的方式)
第一种方案
一旦依赖发生变化,自动通知依赖发生变化:
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 数据一旦发生变化,notify就把所有的函数收集起来并执行
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'why',
age: 18
}
Object.keys(obj).forEach(key=>{
let value = obj[key]
Object.defineProperty(obj,key,{
set:function(newValue){
// 这里不能写obj[key]=newValue,一旦这样写,修改值后又会来调用set,会递归
value = newValue
dep.notify()
}
get:function(){
return value
}
})
})
const dep = new Depend()
function watchFn(fn) {
dep.addDepend(fn)
fn()
}
// 这个函数内部都是依赖obj的,只要有变化就要重新执行
watchFn(function foo() {
console.log('foo', obj.name);
console.log('foo', obj.age);
console.log('foo function');
})
watchFn(function bar(){
console.log("bar",obj.age+10)
console.log("bar function")
})
obj.name="kobe"
但是目前我们还存在一个问题,如果只改变obj中的其中一个内容,只让其中一个重新执行即可,而不是全部都执行一次;也就是说,不应该把所有依赖到数据的函数放在reactiveFns数组中;而是要动态决定变化的才放进去;
dep对象存放依赖数据的函数,以及通知操作
因此我们为对象中的每个属性,都创建一个dep对象
,如果属性发生改变,那么就找对应属性的dep对象
,就可以实现对改变的属性进行重新执行,但是由于存在过多的dep对象
,因此不好管理;因此我们将一个obj对象
对应一个map对象1
,而map对象1
中存放是映射关系(对象:key-value),比如name-->dep对象
、age-->dep对象
,除了obj对象还会存在很多的对象关系;
obj对象
也不可能存在一个,会有多个这样关系的对象,因此我们将最外层的对象也要设置成Map
管理;
Map用来将对象作为key
存放在对象中,这里我们不需要强引用,因为存在内存泄漏,所以这里只需要管理即可,使用WeakMap即可;
如果想要拿到obj对象中的name属性依赖,只需要:objMap.get(obj).get(name)
,一层一层通过key拿到对应的dep;因为最小的map中存放的是name:dep对象
的关系,然后通过dep.notify()
通知具体的依赖属性即可;
自动收集:只要用到依赖数据的属性,就应该自动存放在dep中,那么就可以在defineProperty中的get函数中拿到,因为只要依赖数据的地方,就会调用get函数,然后再get函数中拿到具体对象的key,然后通过key拿到dep对象;
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 数据一旦发生变化,notify就把所有的函数收集起来并执行
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
const obj = {
name: 'why',
age: 18
}
// 专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn){
reactiveFn = fn
fn()
reactiveFn = null
}
// 封装一个函数,通过obj的key,获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj,key){
// 1.根据obj对象,找到对应的map对象
let map = objMap.get(obj)
// 第一个可能拿不到:
if(!map){
// 这里key是字符串,不能用weakMap
map = new Map()
objMap.set(obj,map)
}
// 2.根据key,找到对应的dep对象
let dep = map.get(key)
if(!dep){
dep = new Depend()
map.set(key,dep)
}
return dep
}
Object.keys(obj).forEach(key=>{
let value = obj[key]
Object.defineProperty(obj,key,{
set:function(newValue){
value = newValue
// 拿到属于自己的dep对象
const dep = getDepend(obj,key)
dep.notify()
},
get:function(){
// 将依赖传递过去
const dep = getDepend(obj,key)
// 把函数加进去
dep.addDepend(reactiveFn)
return value
}
})
})
// 当我用到依赖的数据,就会调用get方法,然后生成对应的dep对象
// 当值修改后,就会执行对应的set方法,然后调用dep对象,并通知重新执行
watchFn(function foo(){
console.log("foo:",obj.name)
console.log("foo:",obj.age)
})
obj.name = "kobe"
业务代码:
// 如果对应的值发生改变,那么就执行对应的函数
watchFn(function(){
console.log(obj.name)
console.log(obj.age)
console.log(obj.age)
})
watchFn(function(){
console.log(obj.age)
})
watchFn(function(){
console.log(obj.name)
})
obj.name = "kobe"
但是如果在函数中使用了多次同一个属性,那么整个函数则会被重复执行;因为每多一次就会执行一次get函数;然后就会被添加进getDepend中;
class Depend {
constructor() {
// 写个set即可
this.reactiveFns = new Set()
}
addDepend(fn) {
if (fn) {
// 把push修改成add
this.reactiveFns.add(fn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
完善:对任意对象都可以进行依赖收集并响应式,将Object.defineProperty封装成一个函数
function reactive(obj){
Object.keys(obj).forEach(key=>{
let value = obj[key]
Object.defineProperty(obj,key,{
set:function(newValue){
value = newValue
// 拿到属于自己的dep对象
const dep = getDepend(obj,key)
dep.notify()
},
get:function(){
// 将依赖传递过去
const dep = getDepend(obj,key)
// 把函数加进去
dep.addDepend(reactiveFn)
return obj
}
})
})
}
const obj = reactive({
name:"why",
age:18
})
第二种方案
getDepend方法比较关键,一定要加以理解
class Depend {
constructor() {
this.reactiveFns = new Set()
}
addDepend(fn) {
if (fn) {
this.reactiveFns.add(fn)
}
}
depend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
fn()
reactiveFn = null
}
// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
// 1.根据对象obj, 找到对应的map对象
let map = objMap.get(obj)
if (!map) {
map = new Map()
objMap.set(obj, map)
}
// 2.根据key, 找到对应的depend对象
let dep = map.get(key)
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}
// 方式二: new Proxy() -> Vue3
function reactive(obj) {
const objProxy = new Proxy(obj, {
set: function(target, key, newValue, receiver) {
// target[key] = newValue
Reflect.set(target, key, newValue, receiver)
const dep = getDepend(target, key)
dep.notify()
},
get: function(target, key, receiver) {
const dep = getDepend(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
}
})
return objProxy
}
// ========================= 业务代码 ========================
const obj = reactive({
name: "why",
age: 18,
address: "广州市"
})
watchFn(function() {
console.log(obj.name)
console.log(obj.age)
console.log(obj.age)
})
// 修改name
console.log("--------------")
// obj.name = "kobe"
obj.age = 20
// obj.address = "上海市"
console.log("=============== user =================")
const user = reactive({
nickname: "abc",
level: 100
})
watchFn(function() {
console.log("nickname:", user.nickname)
console.log("level:", user.level)
})
user.nickname = "cba"