Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(二)——Vue2和Vue3的区别——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(三)——Vue3基础知识点(上)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(四)——Vue3基础知识点(中)——附沿途学习案例及项目实战代码
认识Mixin
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能;
一个Mixin对象可以包含任何组件选项;
当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中;
Mixin的基本使用
Mixin的合并规则
如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?
情况一:如果是data函数的返回值对象
返回值对象默认情况下会进行合并;
如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;
情况二:如何生命周期钩子函数
情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
比如都有methods选项,并且都定义了方法,那么它们都会生效;
但是如果对象的key相同,那么会取组件对象的键值对;
全局混入Mixin
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin:
全局的Mixin可以使用 应用app的方法 mixin 来完成注册;
一旦注册,那么全局混入的选项将会影响每一个组件;
const app = createApp(App)
app.mixin({
created() {
console.log("global mixin created")
}
})
app.mount("#app")
extends
另外一个类似于Mixin的方式是通过extends属性:
<template>
<div>
Home Page
<h2>{{title}}</h2>
<button @click="bar">按钮</button>
</div>
</template>
<script>
import BasePage from './BasePage.vue';
export default {
extends: [BasePage],
data() {
return {
content: "Hello Home"
}
}
}
</script>
<style scoped>
</style>
// BasePage.vue
<template>
<div>
<h2>哈哈哈哈啊</h2>
</div>
</template>
<script>
export default {
data() {
return {
title: "Hello Page"
}
},
methods: {
bar() {
console.log("base page bar");
}
}
}
</script>
<style scoped>
</style>
在开发中extends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API。
Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
Options API的一大特点就是在对应的属性中编写对应的功能模块;
比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;
大组件的逻辑分散
如果我们能将同一个逻辑关注点相关的代码收集在一起会更好。
这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
也有人把Vue CompositionAPI简称为VCA。
认识Composition API
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方;
在Vue组件中,这个位置就是 setup 函数;
setup其实就是组件的另外一个选项:
只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
比如methods、computed、watch、data、生命周期等等;
setup函数的参数
我们先来研究一个setup函数的参数,它主要有两个参数:
第一个参数:props
第二个参数:context
props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:
对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
并且在template中依然是可以正常去使用props中的属性,比如message;
如果我们在setup函数中想要使用props,那么不可以通过 this 去获取(后面我会讲到为什么);
因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可;
另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:
attrs:所有的非prop的attribute;
slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到);
emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
setup函数的返回值
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
setup的返回值可以在模板template中被使用;
也就是说我们可以通过setup的返回值来替代data选项;
甚至是我们可以返回一个执行函数来代替在methods中定义的方法:
setup() {
let counter = 100;
// 局部函数
const increment = () => {
counter++;
console.log(counter);
}
return {
title: "Hello Home",
counter,
increment
}
}
但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
答案是不可以;
这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
setup不可以使用this
官方关于this有这样一段描述(这段描述是我给官方提交了PR之后的一段描述):
表达的含义是this并没有指向当前组件实例;
并且在setup被调用之前,data、computed、methods等都没有被解析;
所以无法在setup中获取this;
在阅读源码的过程中,代码是按照如下顺序执行的:
调用 createComponentInstance 创建组件实例;
调用 setupComponent 初始化component内部的操作;
调用 setupStatefulComponent 初始化有状态的组件;
在 setupStatefulComponent 取出了 setup 函数;
通过callWithErrorHandling 的函数执行 setup;
从上面的代码我们可以看出, 组件的instance肯定是在执行 setup 函数之前就创建出来的
Reactive API
如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
const state = reactive({
name: "coderwhy",
counter: 100
})
那么这是什么原因呢?为什么就可以变成响应式的呢?
这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集;
当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
Ref API
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
这个时候Vue3给我们提供了另外一个API:ref API
ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
它内部的值是在ref的 value 属性中被维护的;
const message = ref("Hello World")
这里有两个注意事项:
在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
Ref自动解包
模板中的解包是浅层的解包,如果我们的代码是下面的方式:
如果我们将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包:
认识readonly
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
Vue3为我们提供了readonly的方法;
readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
在开发中常见的readonly方法会传入三个类型的参数:
类型一:普通对象;
类型二:reactive返回的对象;
类型三:ref的对象;
readonly的使用
在readonly的使用过程中,有如下规则:
readonly返回的对象都是不允许修改的;
但是经过readonly处理的原来的对象是允许被修改的;
比如 const info = readonly(obj),info对象是不允许被修改的;
当obj被修改时,readonly返回的info对象也会被修改;
但是我们不能去修改readonly返回的对象info;
其实本质上就是readonly返回的对象的setter方法被劫持了而已;
// readonly通常会传入三个类型的数据
// 1.传入一个普通对象
const info = {
name: "why",
age: 18
}
const state1 = readonly(info)
console.log(state1)
// 2.传入reactive对象
const state = reactive({
name: "why",
age: 18
})
const state2 = readonly(state);
// 3.传入ref对象
const nameRef = ref("why")
const state3 = readonly(nameRef)
readonly的应用
那么这个readonly有什么用呢?
Reactive判断的API
isProxy
isReactive
检查对象是否是由 reactive创建的响应式代理:
如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
isReadonly
toRaw
shallowReactive
shallowReadonly
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,都不再是响应式的:
const info = reactive({name: "why", age: 18});
const { name, age } = info
那么有没有办法让我们解构出来的属性是响应式的呢?
Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
那么我们再次进行结构出来的 name 和 age 本身都是 ref的;
// 当我们这样来做的时候,会返回两个ref对象,它们是响应式的
const { name, age } = toRefs(state);
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
toRef
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
setup() {
const info = reactive({name: "why", age: 18});
// 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
// let { name, age } = toRefs(info);
// 2.toRef: 对其中一个属性进行转换ref, 建立链接
let { name } = info;
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
}
return {
name,
age,
changeAge
}
}
ref其他的API
unref
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
如果参数是一个 ref,则返回内部值,否则返回参数本身;
这是 val = isRef(val) ? val.value : val 的语法糖函数;
isRef
shallowRef
triggerRef
setup() {
const info = shallowRef({name: "why"})
const changeInfo = () => {
info.value.name = "james";
triggerRef(info);
}
return {
info,
changeInfo
}
}
customRef
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
并且应该返回一个带有 get 和 set 的对象;
这里我们使用一个的案例:
customRef的案例
<template>
<div>
<input v-model="message"/>
<h2>{{message}}</h2>
</div>
</template>
<script>
import debounceRef from './hook/useDebounceRef';
export default {
setup() {
const message = debounceRef("Hello World");
return {
message
}
}
}
</script>
<style scoped>
</style>
// useDebounceRef.js
import { customRef } from 'vue';
// 自定义ref
export default function(value, delay = 300) {
let timer = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}
})
}
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
在前面的Options API中,我们是使用computed选项来完成的;
在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;
如何使用computed呢?
方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
// 1.用法一: 传入一个getter函数
// computed的返回值是一个ref对象
const fullName = computed(() => firstName.value + " " + lastName.value);
// 2.用法二: 传入一个对象, 对象包含getter/setter
const fullName = computed({
get: () => firstName.value + " " + lastName.value,
set(newValue) {
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
});
侦听数据的变化
在前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。
在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;
watchEffect用于自动收集响应式数据的依赖;
需要手动指定侦听的数据源;
watchEffect
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
我们来看一个案例:
首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
// watchEffect: 自动收集响应式的依赖
const name = ref("why");
const age = ref(18);
const changeName = () => name.value = "kobe"
const changeAge = () => age.value++
watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
watchEffect的停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
比如在上面的案例中,我们age达到20的时候就停止侦听:
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
watchEffect清除副作用
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用;
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
我们可以在传入的回调函数中,执行一些清楚工作;
const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
// 根据name和age两个变量发送网络请求
onInvalidate(() => {
// 在这个函数中清除额外的副作用
// request.cancel()
clearTimeout(timer);
console.log("onInvalidate");
})
console.log("name:", name.value, "age:", age.value);
});
setup中使用ref
在讲解 watchEffect执行时机之前,我们先补充一个知识:在setup中如何使用ref或者元素或者组件?
watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前:
我们会发现打印结果打印了两次:
这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null;
而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素;
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素呢?
这个时候我们需要改变副作用函数的执行时机;
它的默认值是pre,它会在元素 挂载 或者 更新 之前执行;
所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
我们可以设置副作用函数的执行时机:
let h2Elcontent = null;
watchEffect(() => {
h2ElContent = titleRef.value && titleRef.value.textContent;
console.log(h2ElContent, counter.value)
}, {
flush: "post"
})
flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
Watch的使用
watch的API完全等同于组件watch选项的Property:
watch需要侦听特定的数据源,并在回调函数中执行副作用;
默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
与watchEffect的比较,watch允许我们:
懒执行副作用(第一次不会直接执行);
更具体的说明当哪些状态发生变化时,触发侦听器的执行;
访问侦听状态变化前后的值;
侦听单个数据源
watch侦听函数的数据源有两种类型:
一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// watch(info, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
// 如果希望newValue和oldValue是一个普通的对象
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const name = ref("why");
// watch(name, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
// 1.定义可响应式的对象
const name = ref("why");
const age = ref(18)
// 2.侦听器watch
watch([name, age], (newValues, oldValues) => {
console.log(newValues, oldValues)
})
侦听响应式对象
如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
// 1.定义可响应式的对象
const info = reactive({name: "why", age: 18});
const name = ref("why");
// 2.侦听器watch
watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, oldName]) => {
console.log(newInfo, newName, oldInfo, oldName);
})
const changeData = () => {
info.name = "kobe";
}
watch的选项
如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true:
// 1.定义可响应式的对象
const info = reactive({
name: "why",
age: 18,
friend: {
name: "kobe"
}
});
// 2.侦听器watch
watch(() => ({...info}), (newInfo, oldInfo) => {
console.log(newInfo, oldInfo);
}, {
deep: true,
immediate: true
})
我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代 生命周期钩子。
那么setup中如何使用生命周期函数呢?
Provide函数
事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
我们可以通过 provide来提供数据:
可以通过 provide 方法来定义每个 Property;
provide可以传入两个参数:
name:提供的属性名称;
value:提供的属性值;
const name = "coderwhy";
let counter = 100;
provide("name", readonly(name));
provide("counter", readonly(counter));
Inject函数
在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
可以通过 inject 来注入需要的内容;
inject可以传入两个参数:
要 inject 的 property 的 name;
默认值;
const name = inject("name");
const counter = inject("counter");
数据的响应式
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 和 reactive
const name = ref("coderwhy");
let counter = ref(100);
provide("name", readonly(name));
provide("counter", readonly(counter));
修改响应式Property
如果我们需要修改可响应的数据,那么最好是在数据提供的位置来修改:
const changeInfo = () => {
info.name = "coderwhy"
}
provide("changeInfo", changeInfo)
useCounter
我们先来对之前的counter逻辑进行抽取:
import { ref, computed } from 'vue';
export default function() {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doubleCounter,
increment,
decrement
}
}
useTitle
我们编写一个修改title的Hook:
import { ref, watch } from 'vue';
export default function(title = "默认的title") {
const titleRef = ref(title);
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
return titleRef
}
useScrollPosition
我们来完成一个监听界面滚动位置的Hook:
import { ref } from 'vue';
export default function() {
const scrollX = ref(0);
const scrollY = ref(0);
document.addEventListener("scroll", () => {
scrollX.value = window.scrollX;
scrollY.value = window.scrollY;
});
return {
scrollX,
scrollY
}
}
useMousePosition
我们来完成一个监听鼠标位置的Hook:
import { ref } from 'vue';
export default function() {
const mouseX = ref(0);
const mouseY = ref(0);
window.addEventListener("mousemove", (event) => {
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});
return {
mouseX,
mouseY
}
}
useLocalStorage
我们来完成一个使用 localStorage 存储和获取数据的Hook:
import { ref, watch } from 'vue';
export default function(key, value) {
const data = ref(value);
if (value) {
window.localStorage.setItem(key, JSON.stringify(value));
} else {
data.value = JSON.parse(window.localStorage.getItem(key));
}
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
return data;
}
// 一个参数: 取值
// const data = useLocalStorage("name");
// // 二个参数: 保存值
// const data = useLocalStorage("name", "coderwhy");
// data.value = "kobe";
认识h函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
前面我们讲解过VNode和VDOM的改变:
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM);
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
那么我们应该怎么来做呢?使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;
h()函数 如何使用呢?
h()函数 如何使用呢?它接受三个参数:
注意事项:
如果没有props,那么通常可以将children作为第二个参数传入;
如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
h函数的基本使用
h函数可以在两个地方使用:
render函数选项中;
setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
<script>
import { h } from 'vue';
export default {
render() {
return h("h2", {class: "title"}, "Hello Render")
}
}
</script>
<script>
import { h } from 'vue';
export default {
setup() {
return () => h("h2", {class: "title"}, "Hello Render")
}
}
</script>
h函数计数器案例
// render函数实现计数器
<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>
// setup函数实现计数器
<script>
import { ref, h } from 'vue';
export default {
setup() {
const counter = ref(0);
return () => {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", {
onClick: () => counter.value++
}, "+1"),
h("button", {
onClick: () => counter.value--
}, "-1"),
])
}
}
}
</script>
函数组件和插槽的使用
// 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() {
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配置
如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持:
jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);
对于Vue来说,我们只需要在Babel中配置对应的插件即可;
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D
在babel.config.js配置文件中配置插件:
// babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
// HelloWorld.vue
<script>
export default {
render() {
return (
<div>
<h2>HelloWorld</h2>
</div>
)
}
}
</script>
<style scoped>
</style>
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>
jsx组件的使用
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
注意:在Vue中,代码的复用和抽象主要还是通过组件;
通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;
自定义指令分为两种:
自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
实现方式一:如果我们使用默认的实现方式;
实现方式二:自定义一个 v-focus 的局部指令;
实现方式三:自定义一个 v-focus 的全局指令
实现方式一:聚焦的默认实现
<template>
<div>
<input type="text" ref="input">
</div>
</template>
<script>
import { ref, onMounted } from "vue";
export default {
setup() {
const input = ref(null);
onMounted(() => {
input.value.focus();
})
return {
input
}
}
}
</script>
<style scoped>
</style>
实现方式二:局部自定义指令
实现方式二:自定义一个 v-focus 的局部指令
这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);
自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;
<template>
<div>
<input type="text" v-focus>
</div>
</template>
<script>
export default {
// 局部指令
directives: {
focus: {
mounted(el, bindings, vnode, preVnode) {
console.log("focus mounted");
el.focus();
}
}
}
}
</script>
<style scoped>
</style>
方式三:自定义全局指令
自定义一个全局的v-focus指令可以让我们在任何地方直接使用
app.directive("focus", {
mounted(el, bindings, vnode, preVnode) {
console.log("focus mounted");
el.focus();
}
})
指令的生命周期
一个指令定义的对象,Vue提供了如下的几个钩子函数:
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
指令的参数和修饰符
如果我们指令需要接受一些参数或者修饰符应该如何操作呢?
info是参数的名称;
aaa-bbb是修饰符的名称;
后面是传入的具体的值;
在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:
自定义指令练习
自定义指令案例:时间戳的显示需求:
在开发中,大多数情况下从服务器获取到的都是时间戳;
我们需要将时间戳转换成具体格式化的时间来展示;
在Vue2中我们可以通过过滤器来完成;
在Vue3中我们可以通过 计算属性(computed) 或者 自定义一个方法(methods) 来完成;
其实我们还可以通过一个自定义的指令来完成;
我们来实现一个可以自动对时间格式化的指令v-format-time:
时间格式化指令
// format-time.js
import dayjs from 'dayjs';
export default function(app) {
app.directive("format-time", {
created(el, bindings) {
bindings.formatString = "YYYY-MM-DD HH:mm:ss";
if (bindings.value) {
bindings.formatString = bindings.value;
}
},
mounted(el, bindings) {
const textContent = el.textContent;
let timestamp = parseInt(textContent);
if (textContent.length === 10) {
timestamp = timestamp * 1000
}
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');
// App.vue
<template>
<h2 v-format-time="'YYYY/MM/DD'">{{timestamp}}</h2>
<h2 v-format-time>{{timestamp}}</h2>
<h2 v-format-time>{{timestamp}}</h2>
<h2 v-format-time>{{timestamp}}</h2>
<h2 v-format-time>{{timestamp}}</h2>
<h2 v-format-time>{{timestamp}}</h2>
</template>
<script>
export default {
setup() {
const timestamp = 1624452193;
return {
timestamp
}
},
mounted() {
console.log("app mounted");
}
}
</script>
<style scoped>
</style>
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
那么组件A中template的元素,会被挂载到组件B中template的某个位置;
最终我们的应用程序会形成一颗DOM树结构;
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
比如移动到body元素上,或者我们有其他的div#app之外的元素上;
这个时候我们就可以通过teleport来完成;
Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals;
teleport翻译过来是心灵传输、远距离运输的意思;
它有两个属性:
to:指定将其中的内容移动到的目标元素,可以使用选择器;disabled:是否禁用 teleport 的功能;
我们来看下面代码的效果:
和组件结合使用
当然,teleport也可以和组件结合一起来使用:
多个teleport
如果我们将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并:
<template>
<div class="app">
<teleport to="#why">
<h2>当前计数</h2>
<button>+1</button>
<hello-world></hello-world>
</teleport>
<teleport to="#why">
<span>呵呵呵呵</span>
</teleport>
</div>
</template>
<script>
import { getCurrentInstance } from "vue";
import HelloWorld from './HelloWorld.vue';
export default {
components: {
HelloWorld
},
setup() {
const instance = getCurrentInstance();
console.log(instance.appContext.config.globalProperties.$name);
},
mounted() {
console.log(this.$name);
},
methods: {
foo() {
console.log(this.$name);
}
}
}
</script>
<style scoped>
</style>
实现效果如下:
认识Vue插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
函数类型:一个function,这个函数会在安装插件时自动执行;
插件可以完成的功能没有限制,比如下面的几种都是可以的:
添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
添加全局资源:指令/过滤器/过渡等;
通过全局 mixin 来添加一些组件选项;
一个库,提供自己的 API,同时提供上面提到的一个或多个功能
插件的编写方式
对象类型的写法
// plugins_object.js
export default {
install(app) {
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'
import pluginObject from './plugins/plugins_object'
import pluginFunction from './plugins/plugins_function'
const app = createApp(App);
app.use(pluginObject);
app.use(pluginFunction);
app.mount('#app');
真实的DOM渲染
我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?
虚拟DOM的优势
目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处:
首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:
因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这些,就变得非常的简单;
我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;
其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点
如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;
虚拟DOM的渲染过程
事实上Vue的源码包含三大核心:
Compiler模块:编译模板系统;
Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
Reactivity模块:响应式系统;
三大系统协同工作
三个系统之间如何协同工作呢:
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
渲染系统模块;
可响应式系统模块;
应用程序入口模块;
渲染系统实现
渲染系统,该模块主要包含三个功能:
功能一:h函数,用于返回一个VNode对象;
功能二:mount函数,用于将VNode挂载到DOM上;
功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数 – 生成VNode
h函数的实现:
const h = (tag, props, children) => {
// vnode -> javascript对象 -> {}
return {
tag,
props,
children
}
}
Mount函数 – 挂载VNode
mount函数的实现:
第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
第二步:处理props属性
如果以on开头,那么监听事件;
普通属性直接通过 setAttribute 添加即可;
第三步:处理子节点
如果是字符串节点,那么直接设置textContent;
如果是数组节点,那么遍历调用 mount 函数;
const mount = (vnode, container) => {
// vnode -> element
// 1.创建出真实的原生, 并且在vnode上保留el
const el = vnode.el = document.createElement(vnode.tag);
// 2.处理props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (key.startsWith("on")) { // 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value);
}
}
}
// 3.处理children
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach(item => {
mount(item, el);
})
}
}
// 4.将el挂载到container上
container.appendChild(el);
}
Patch函数 – 对比两个VNode
patch函数的实现,分为两种情况
n1和n2是不同类型的节点:
找到n1的el父节点,删除原来的n1节点的el;
挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
处理props的情况
先将新节点的props全部挂载到el上;
判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
处理children的情况
如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
如果新节点不同一个字符串类型:
旧节点是一个字符串类型
将el的textContent设置为空字符串;
就节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
旧节点也是一个数组类型
取出数组的最小长度;
遍历所有的节点,新节点和旧节点进行path操作;
如果新节点的length更长,那么剩余的新节点进行挂载操作;
如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2, n1ElParent);
} else {
// 1.取出element对象, 并且在n2中进行保存
const el = n2.el = n1.el;
// 2.处理props
const oldProps = n1.props || {};
const newProps = n2.props || {};
// 2.1.获取所有的newProps添加到el
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (newValue !== oldValue) {
if (key.startsWith("on")) { // 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue);
}
}
}
// 2.2.删除旧的props
for (const key in oldProps) {
if (key.startsWith("on")) { // 对事件监听的判断
const value = oldProps[key];
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 3.处理children
const oldChildren = n1.children || [];
const newChidlren = n2.children || [];
if (typeof newChidlren === "string") { // 情况一: newChildren本身是一个string
// 边界情况 (edge case)
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
el.textContent = newChidlren
}
} else {
el.innerHTML = newChidlren;
}
} else { // 情况二: newChildren本身是一个数组
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChidlren.forEach(item => {
mount(item, el);
})
} else {
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.前面有相同节点的原生进行patch操作
const commonLength = Math.min(oldChildren.length, newChidlren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i]);
}
// 2.newChildren.length > oldChildren.length
if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach(item => {
mount(item, el);
})
}
// 3.newChildren.length < oldChildren.length
if (newChidlren.length < oldChildren.length) {
oldChildren.slice(newChidlren.length).forEach(item => {
el.removeChild(item.el);
})
}
}
}
}
}
依赖收集系统
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
dep.notify();
响应式系统Vue2实现
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// vue2对raw进行数据劫持
function reactive(raw) {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key);
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend();
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
dep.notify();
}
}
})
})
return raw;
}
// 测试代码
const info = reactive({counter: 100, name: "why"});
const foo = reactive({height: 1.88});
// watchEffect1
watchEffect(function () {
console.log("effect1:", info.counter * 2, info.name);
})
// watchEffect2
watchEffect(function () {
console.log("effect2:", info.counter * info.counter);
})
// watchEffect3
watchEffect(function () {
console.log("effect3:", info.counter + 10, info.name);
})
watchEffect(function () {
console.log("effect4:", foo.height);
})
// info.counter++;
// info.name = "why";
foo.height = 2;
响应式系统Vue3实现
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => {
effect();
})
}
}
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap();
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2.取出具体的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target, key, newValue) {
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
}
})
}
为什么Vue3选择Proxy呢?
Object.definedProperty 是劫持对象的属性时,如果新增元素:
修改对象的不同:
Proxy 能观察的类型比 defineProperty 更丰富
has:in操作符的捕获器;
deleteProperty:delete 操作符的捕捉器;
等等其他操作;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
框架外层API设计
这样我们就知道了,从框架的层面来说,我们需要
有两部分内容:
createApp用于创建一个app对象;
该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
watchEffect(function() {
if (!isMounted) {
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {
const newVNode = rootComponent.render();
patch(oldVNode, newVNode);
oldVNode = newVNode;
}
})
}
}
}
源码阅读之createApp
源码阅读之挂载根组件
const app = {props: {message: String}
instance
// 1.处理props和attrs
instance.props
instance.attrs
// 2.处理slots
instance.slots
// 3.执行setup
const result = setup()
instance.setupState = proxyRefs(result);
// 4.编译template -> compile
<template> -> render函数
instance.render = Component.render = render函数
// 5.对vue2的options api进行知识
data/methods/computed/生命周期
组件化的初始化
Compile过程
Block Tree分析
生命周期回调
template中数据的使用顺序
路由其实是网络工程中的一个术语:
在架构一个网络时,非常重要的两个设备就是路由器和交换机。
当然,目前在我们生活中路由器也是越来越被大家所熟知,因为我们生活中都会用到路由器:
事实上,路由器主要维护的是一个映射表;
映射表会决定数据的流向;
路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
后端路由阶段;
前后端分离阶段;
单页面富应用(SPA);
后端路由阶段
早期的网站开发整个HTML页面是由服务器来渲染的.
但是, 一个网站, 这么多页面服务器如何处理呢?
一个页面有自己对应的网址, 也就是URL;
URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理;
Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
上面的这种操作, 就是后端路由:
当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
后端路由的缺点:
一种情况是整个页面的模块由后端人员来编写和维护的;
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码;
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情;
前后端分离阶段
前端渲染的理解:
每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染;
需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件;
同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了;
前后端分离阶段:
随着Ajax的出现, 有了前后端分离的开发模式;
后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中;
这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上;
并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可;
目前比较少的网站采用这种模式开发(jQuery开发模式);
URL的hash
前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="content">Default</div>
</div>
<script>
const contentEl = document.querySelector('.content');
window.addEventListener("hashchange", () => {
switch(location.hash) {
case "#/home":
contentEl.innerHTML = "Home";
break;
case "#/about":
contentEl.innerHTML = "About";
break;
default:
contentEl.innerHTML = "Default";
}
})
</script>
</body>
</html>
hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
HTML5的History
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
replaceState:替换原来的路径;
pushState:使用新的路径;
popState:路径的回退;
go:向前或向后改变路径;
forward:向前改变路径;
back:向后改变路径;
HTML5的History演练
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<a href="/home">home</a>
<a href="/about">about</a>
<div class="content">Default</div>
</div>
<script>
const contentEl = document.querySelector('.content');
const changeContent = () => {
console.log("-----");
switch(location.pathname) {
case "/home":
contentEl.innerHTML = "Home";
break;
case "/about":
contentEl.innerHTML = "About";
break;
default:
contentEl.innerHTML = "Default";
}
}
const aEls = document.getElementsByTagName("a");
for (let aEl of aEls) {
aEl.addEventListener("click", e => {
e.preventDefault();
const href = aEl.getAttribute("href");
// history.pushState({}, "", href);
history.replaceState({}, "", href);
changeContent();
})
}
window.addEventListener("popstate", changeContent)
</script>
</body>
</html>
目前前端流行的三大框架, 都有自己的路由实现:
Angular的ngRouter
React的ReactRouter
Vue的vue-router
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得非常容易。
vue-router是基于路由和组件的
路由用于设定访问路径, 将路径和组件映射起来.
在vue-router的单页面应用中, 页面的路径的改变就是组件的切换.
安装Vue Router:
npm install vue-router@4
路由的使用步骤
使用vue-router的步骤:
第一步:创建路由组件的组件;
第二步:配置路由映射: 组件和路径映射关系的routes数组;
第三步:通过createRouter创建路由对象,并且传入routes和history模式;
第四步:使用路由: 通过
和
;
路由的基本使用流程
路由的默认路径
我们这里还有一个不太好的实现:
默认情况下, 进入网站的首页, 我们希望
渲染首页的内容;
但是我们的实现中, 默认没有显示首页组件, 必须让用户点击才可以;
如何可以让路径默认跳到到首页, 并且
渲染首页组件呢?
const routes = [
{ path: "/", redirect: '/home' },
{ path: "/home", component: Home },
{ path: "/about", component: About },
]
我们在routes中又配置了一个映射:
path配置的是根路径: /
redirect是重定向, 也就是我们将根路径重定向到/home的路径下, 这样就可以得到我们想要的结果了.
history模式
另外一种选择的模式是history模式:
import { createRouter, createWebHistory } from 'vue-router'
// 创建一个路由对象router
const router = createRouter({
routes,
history: createWebHistory()
})
router-link
router-link事实上有很多属性可以配置:
to属性:
replace属性:
active-class属性:
exact-active-class属性:
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载:
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效;
也可以提高首屏的渲染效率;
其实这里还是我们前面讲到过的webpack的分包知识,而Vue Router默认就支持动态来导入组件:
这是因为component可以传入一个组件,也可以接收一个函数,该函数 需要放回一个Promise;
而import函数就是返回一个Promise;
const routes = [
{ path: "/", redirect: '/home' },
{ path: "/home", () => import("../pages/Home.vue") },
{ path: "/about", () => import("../pages/About.vue") },
]
打包效果分析
我们看一下打包后的效果:
我们会发现分包是没有一个很明确的名称的,其实webpack从3.x开始支持对分包进行命名(chunk name):
动态路由基本匹配
很多时候我们需要将给定匹配模式的路由映射到同一个组件:
例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的ID是不同的;
在Vue Router中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数;
{
path: "/user/:id",
component: () => import("../pages/User.vue")
},
在router-link中进行如下跳转:
<router-link to="/user/111">用户</router-link>
获取动态路由的值
那么在User中如何获取到对应的值呢?
在template中,直接通过 $route.params获取值;
在created中,通过 this.$route.params获取值;
在setup中,我们要使用 vue-router库给我们提供的一个hook useRoute;
匹配多个参数
NotFound
对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面
{
path: "/:pathMatch(.*)",
component: () => import("../pages/NotFound.vue")
}
我们可以通过 $route.params.pathMatch获取到传入的参数:
匹配规则加*
这里还有另外一种写法:
{
path: "/:pathMatch(.*)*",
component: () => import("../pages/NotFound.vue")
}
它们的区别在于解析的时候,是否解析 /:
路由的嵌套
什么是路由的嵌套呢?
目前我们匹配的Home、About、User等都属于底层路由,我们在它们之间可以来回进行切换;
但是呢,我们Home页面本身,也可能会在多个组件之间来回切换:
比如Home中包括Product、Message,它们可以在Home内部来回切换;
这个时候我们就需要使用嵌套路由,在Home中也使用 router-view 来占位之后需要渲染的组件;
路由的嵌套配置
{
path: "/home",
name: "home",
component: () => import(/* webpackChunkName: "home-chunk" */"../pages/Home.vue"),
children: [
{
path: "",
redirect: "/home/message"
},
{
path: "message",
component: () => import("../pages/HomeMessage.vue")
},
{
path: "shops",
component: () => import("../pages/HomeShops.vue")
}
]
},
代码的页面跳转
有时候我们希望通过代码来完成页面的跳转,比如点击的是一个按钮:
jumpToProfile() {
this.$router.push('/profile')
}
当然,我们也可以传入一个对象:
jumpToProfile() {
this.$router.push({
path: '/profile'
})
}
如果是在setup中编写的代码,那么我们可以通过 useRouter 来获取:
const router = useRouter()
const jumpToProfile = () => {
router.replace('/profile')
}
query方式的参数
我们也可以通过query的方式来传递参数:
jumpToProfile() {
this.$router.push({
path: '/profile',
query: { name: 'why', age: 18 }
})
}
在界面中通过 $route.query 来获取参数:
<h2>About: {{$route.query.name}}-{{$route.query.age}}</h2>
替换当前的位置
使用push的特点是压入一个新的页面,那么在用户点击返回时,上一个页面还可以回退,但是如果我们希望当前页面是一个替换操作,那么可以使用replace:
页面的前进后退
router的go方法:
router也有back:
router也有forward:
router-link的v-slot
在vue-router3.x的时候,router-link有一个tag属性,可以决定router-link到底渲染成什么元素:
但是在vue-router4.x开始,该属性被移除了;
而给我们提供了更加具有灵活性的v-slot的方式来定制渲染的内容;
v-slot如何使用呢?
首先,我们需要使用custom表示我们整个元素要自定义
其次,我们使用v-slot来作用域插槽来获取内部传给我们的值:
href:解析后的 URL;
route:解析后的规范化的route对象;
navigate:触发导航的函数;
isActive:是否匹配的状态;
isExactActive:是否是精准匹配的状态;
<!-- props: href 跳转的链接 -->
<!-- props: route对象 -->
<!-- props: navigate导航函数 -->
<!-- props: isActive 是否当前处于活跃的状态 -->
<!-- props: isExactActive 是否当前处于精确的活跃状态 -->
<router-link to="/home" v-slot="props" custom>
<button @click="props.navigate">{{props.href}}</button>
<button @click="props.navigate">哈哈哈</button>
<span :class="{'active': props.isActive}">{{props.isActive}}</span>
<span :class="{'active': props.isActive}">{{props.isExactActive}}</span>
<!-- <p>{{props.route}}</p> -->
</router-link>
router-view的v-slot
router-view也提供给我们一个插槽,可以用于
和
组件来包裹你的路由组件:
Component:要渲染的组件;
route:解析出的标准化路由对象;
<router-view v-slot="props">
<transition name="why">
<keep-alive>
<component :is="props.Component"></component>
</keep-alive>
</transition>
</router-view>
<style>
.why-active {
color: red;
}
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-active,
.why-leave-active {
transition: opacity 1s ease;
}
</style>
动态添加路由
某些情况下我们可能需要动态的来添加路由:
比如根据用户不同的权限,注册不同的路由;
这个时候我们可以使用一个方法 addRoute;
如果我们是为route添加一个children路由,那么可以传入对应的name:
// 动态添加路由
const categoryRoute = {
path: "/category",
component: () => import("../pages/Category.vue")
}
// 添加顶级路由对象
router.addRoute(categoryRoute);
// 添加二级路由对象
router.addRoute("home", {
path: "moment",
component: () => import("../pages/HomeMoment.vue")
})
动态删除路由
删除路由有以下三种方式:
方式一:添加一个name相同的路由;
方式二:通过removeRoute方法,传入路由的名称;
方式三:通过addRoute方法的返回值回调;
路由的其他方法补充:
router.hasRoute():检查路由是否存在。
router.getRoutes():获取一个包含所有路由记录的数组。
路由导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。
全局的前置守卫beforeEach是在导航触发时会被回调的:
它有两个参数:
to:即将进入的路由Route对象;
from:即将离开的路由Route对象;
它有返回值:
false:取消当前导航;
不返回或者undefined:进行默认导航;
返回一个路由地址:
可以是一个string类型的路径;
可以是一个对象,对象中包含path、query、params等信息;
可选的第三个参数:next
在Vue2中我们是通过next函数来决定如何进行跳转的;
但是在Vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next;
router.beforeEach((to, from) => {
console.log(to)
console.log(from)
return false
})
登录守卫功能
// 导航守卫beforeEach
let counter = 0;
// to: Route对象, 即将跳转到的Route对象
// from: Route对象,
/**
* 返回值问题:
* 1.false: 不进行导航
* 2.undefined或者不写返回值: 进行默认导航
* 3.字符串: 路径, 跳转到对应的路径中
* 4.对象: 类似于 router.push({path: "/login", query: ....})
*/
router.beforeEach((to, from) => {
console.log(`进行了${++counter}路由跳转`)
if (to.path !== "/login") {
const token = window.localStorage.getItem("token");
if (!token) {
return "/login"
}
}
})
<script>
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter();
const loginClick = () => {
window.localStorage.setItem("token", "why")
router.push({
path: "/home"
})
}
return {
loginClick
}
}
}
</script>
其他导航守卫
Vue还提供了很多的其他守卫函数,目的都是在某一个时刻给予我们回调,让我们可以更好的控制程序的流程或者功能:
我们一起来看一下完整的导航解析流程:
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫(2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
什么是状态管理
在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是 状态管理。
在前面我们是如何管理自己的状态呢?
在Vue开发中,我们使用组件化的开发方式;
而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state;
在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;
在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions;
复杂的状态管理
JavaScript开发的应用程序,已经变得越来越复杂了:
JavaScript需要管理的状态越来越多,越来越复杂;
这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等;
也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
多个视图依赖于同一状态;
来自不同视图的行为需要变更同一状态;
我们是否可以通过组件数据的传递来完成呢?
对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态;
但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的,比如兄弟组件如何共享数据呢?
Vuex的状态管理
管理不断变化的state本身是非常困难的:
状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
在这种模式下,我们的组件树构成了一个巨大的 “视图View”;
不管在树的哪个位置,任何组件都能获取状态或者触发行为;
通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;
这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想):
Vuex的状态管理
Vuex的安装
依然我们要使用vuex,首先第一步需要安装vuex:
npm install vuex@next
Vue devtool
vue其实提供了一个devtools,方便我们对组件或者vuex进行调试:
它有两种常见的安装方式:
方式一:通过chrome的商店;
方式二:手动下载代码,编译、安装;
方式一:通过Chrome商店安装:
方式二:手动下载代码,编译、安装
https://github.com/vuejs/devtools/tree/v6.0.0-beta.15下载代码;
执行 yarn install 安装相关的依赖;
执行 yarn run build 打包;
创建Store
每一个Vuex应用的核心就是store(仓库):
Vuex和单纯的全局对象有什么区别呢?
第一:Vuex的状态存储是响应式的
第二:你不能直接改变store中的状态
改变store中的状态的唯一途径就显示提交 (commit) mutation;
这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
使用步骤:
创建Store对象;
在app中通过插件安装;
组件中使用store
在组件中使用store,我们按照如下的方式:
在模板中使用;
在options api中使用,比如computed;
在setup中使用;
单一状态树
Vuex 使用单一状态树:
用一个对象就包含了全部的应用层级状;
采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源;
这也意味着,每个应用将仅仅包含一个 store 实例;
单状态树和模块化并不冲突,后面我们会讲到module的概念;
单一状态树的优势:
如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;
所以Vuex也使用了单一状态树来管理应用层级的全部状态;
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护;
组件获取状态
在前面我们已经学习过如何在组件中获取状态了。
当然,如果觉得那种方式有点繁琐(表达式过长),我们可以使用计算属性:
computed: {
counter() {
return this.$store.state.counter
}
}
但是,如果我们有很多个状态都需要获取话,可以使用mapState的辅助函数:
mapState的方式一:对象类型;
mapState的方式二:数组类型;
也可以使用展开运算符和来原有的computed混合在一起;
在setup中使用mapState
在setup中如果我们单个获取装是非常简单的:
通过useStore拿到store后去获取某个状态即可;
但是如果我们需要使用 mapState 的功能呢?
默认情况下,Vuex并没有提供非常方便的使用mapState的方式,这里我们进行了一个函数的封装:
import { computed } from 'vue'
import { mapState, useStore } from 'vuex'
export function useState(mapper) {
// 拿到store独享
const store = useStore()
// 获取到对应的对象的functions: {name: function, age: function}
const storeStateFns = mapState(mapper)
// 对数据进行转换
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
setup() {
const state = useState({
name: state => state.name,
age: state => state.age
})
return {
...state
}
}
getters的基本使用
某些属性我们可能需要警告变化后来使用,这个时候可以使用getters:
getters第二个参数
getters可以接收第二个参数:
getters: {
totalPrice(state, getters) {
let totalPrice = 0;
for (const book of state.books) {
totalPrice += book.count * book.price
}
return totalPrice + ',' + getters.myName
},
myName(state) {
return state.name
}
}
getters的返回函数
getters中的函数本身,可以返回一个函数,那么在使用的地方相当于可以调用这个函数:
getters: {
totalPrice(state, getters) {
return (price) => {
let totalPrice = 0;
for (const book of state.books) {
totalPrice += book.count * book.price
}
return totalPrice + ',' + getters.myName
}
},
myName(state) {
return state.name
}
}
mapGetters的辅助函数
这里我们也可以使用mapGetters的辅助函数。
computed: {
...mapGetters(["nameInfo", "ageInfo", "heightInfo"]),
...mapGetters({
sNameInfo: "nameInfo",
sAgeInfo: "ageInfo"
})
},
在setup中使用
import { computed } from 'vue'
import { mapGetters, useStore } from 'vuex'
export function useGetters(mapper) {
// 拿到store独享
const store = useStore()
// 获取到对应的对象的functions: {name: function, age: function}
const storeStateFns = mapGetters(mapper)
// 对数据进行转换
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
Mutation基本使用
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation:
mutations: {
increment(state) {
state.homeCounter++
}
},
Mutation携带数据
很多时候我们在提交mutation的时候,会携带一些数据,这个时候我们可以使用参数:
mutations: {
addNumber(state, payload) {
state.counter += payload
}
}
payload为对象类型
mutations: {
addNumber(state, payload) {
state.counter += payload.count
}
}
对象风格的提交方式
$store.commit({
type: "addNumber",
count: 100
})
Mutation常量类型
定义常量:mutation-type.js
export const INCREMENT_N = "increment_n"
定义mutation
[INCREMENT_N](state, payload) {
console.log(payload);
state.counter += payload.n
},
提交mutation
$store.commit({
type: ADD_NUMBER,
count: 100
})
mapMutations辅助函数
我们也可以借助于辅助函数,帮助我们快速映射到对应的方法中:
methods: {
...mapMutations(["increment", "decrement", INCREMENT_N]),
...mapMutations({
add: "increment"
})
},
在setup中使用也是一样的:
setup() {
const storeMutations = mapMutations(["increment", "decrement", INCREMENT_N])
return {
...storeMutations
}
}
mutation重要原则
一条重要的原则就是要记住 mutation 必须是同步函数
这是因为devtool工具会记录mutation的日记;
每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
但是在mutation中执行异步操作,就无法追踪到数据的变化;
所以Vuex的重要原则中要求 mutation必须是同步函数;
Action类似于mutation,不同在于:
Action提交的是mutation,而不是直接变更状态;
Action可以包含任意异步操作;
这里有一个非常重要的参数context:
context是一个和store实例均有相同方法和属性的context对象;
所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;
但是为什么它不是store对象呢?这个等到我们讲Modules时再具体来说;
// 1.参数问题
incrementAction(context, payload) {
console.log(payload)
setTimeout(() => {
context.commit('increment')
}, 1000);
},
actions的分发操作
如何使用action呢?进行action的分发:
add() {
this.$store.dispatch("increment")
}
同样的,它也可以携带我们的参数:
add() {
this.$store.dispatch("increment", {count: 100})
}
也可以以对象的形式进行分发:
add() {
this.$store.dispatch({
type: "increment",
count: 100
})
}
actions的辅助函数
action也有对应的辅助函数:
对象类型的写法;
数组类型的写法;
methods: {
...mapActions(["incrementAction", "decrementAction"]),
...mapActions({
add: "incrementAction",
sub: "decrementAction"
})
},
setup() {
const actions = mapActions(["incrementAction", "decrementAction"])
const actions2 = mapActions({
add: "incrementAction",
sub: "decrementAction"
})
return {
...actions,
...actions2
}
}
actions的异步操作
Action 通常是异步的,那么如何知道 action 什么时候结束呢?
actions: {
increment(context) {
return new Promise((resolve) => {
setTimeout(() => {
context.commit("increment")
resolve("异步完成")
}, 1000)
})
}
}
const store = useStore()
const increment = () => {
store.dispatch("increment").then(res => {
console.log(res, "异步完成")
})
}
module的基本使用
什么是Module?
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿;
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块;
module的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象:
getters: {
doubleHomeCounter(state, getters, rootState, rootGetters) {
return state.homeCounter * 2
},
otherGetter(state) {
return 100
}
},
mutations: {
increment(state) {
state.homeCounter++
},
changeName(state) {
state.name = "coderwhy"
}
},
actions: {
changeNameAction({state, commit, rootState}) {
commit("changeName", "kobe")
}
}
module的命名空间
默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的:
这样使得多个模块能够对同一个 action 或 mutation 作出响应;
Getter 同样也默认注册在全局命名空间;
如果我们希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:
const homeModule = {
namespaced: true,
state() {
return {
homeCounter: 100
}
},
getters: {
doubleHomeCounter(state, getters, rootState, rootGetters) {
return state.homeCounter * 2
},
otherGetter(state) {
return 100
}
},
mutations: {
increment(state) {
state.homeCounter++
}
},
actions: {
incrementAction({commit, dispatch, state, rootState, getters, rootGetters}) {
commit("increment")
commit("increment", null, {root: true})
}
}
}
export default homeModule
<template>
<div>
<h2>root:{{ $store.state.rootCounter }}</h2>
<h2>home:{{ $store.state.home.homeCounter }}</h2>
<h2>user:{{ $store.state.user.userCounter }}</h2>
<hr>
<h2>{{ $store.getters["home/doubleHomeCounter"] }}</h2>
<button @click="homeIncrement">home+1</button>
<button @click="homeIncrementAction">home+1</button>
</div>
</template>
<script>
export default {
methods: {
homeIncrement() {
this.$store.commit("home/increment")
},
homeIncrementAction() {
this.$store.dispatch("home/incrementAction")
}
}
}
</script>
<style scoped>
</style>
module修改或派发根组件
如果我们希望在action中修改root中的state,那么有如下的方式:
changeNameAction({commit, dispatch, state, rootState, getters, rootGetters}) {
commit("changeName", "kobe")
commit("changeRootName", null, {root: true})
// dispatch
dispatch("changeRootNameAction", null, {root: true})
}
module的辅助函数
如果辅助函数有三种使用方法:
方式一:通过完整的模块空间名称来查找;
方式二:第一个参数传入模块空间名称,后面写上要使用的属性;
方式三:通过 createNamespacedHelpers 生成一个模块的辅助函数;
第三种方式: createNamespacedHelpers
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home")
export default {
computed: {
// 1.写法一:
...mapState({
homeCounter: state => state.home.homeCounter
}),
...mapGetters({
doubleHomeCounter: "home/doubleHomeCounter"
})
// 2.写法二:
...mapState("home", ["homeCounter"]),
...mapGetters("home", ["doubleHomeCounter"])
// 3.写法三:
...mapState(["homeCounter"]),
...mapGetters(["doubleHomeCounter"])
},
methods: {
// 1.写法一:
...mapMutations({
increment: "home/increment"
}),
...mapActions({
incrementAction: "home/incrementAction"
}),
// 2.写法二
...mapMutations("home", ["increment"]),
...mapActions("home", ["incrementAction"]),
// 3.写法三:
...mapMutations(["increment"]),
...mapActions(["incrementAction"]),
},
}
对useState和useGetters修改
import { mapState, createNamespacedHelpers } from 'vuex'
import { useMapper } from './useMapper'
export function useState(moduleName, mapper) {
let mapperFn = mapState
if (typeof moduleName === 'string' && moduleName.length > 0) {
mapperFn = createNamespacedHelpers(moduleName).mapState
} else {
mapper = moduleName
}
return useMapper(mapper, mapperFn)
}
import { mapGetters, createNamespacedHelpers } from 'vuex'
import { useMapper } from './useMapper'
export function useGetters(moduleName, mapper) {
let mapperFn = mapGetters
if (typeof moduleName === 'string' && moduleName.length > 0) {
mapperFn = createNamespacedHelpers(moduleName).mapGetters
} else {
mapper = moduleName
}
return useMapper(mapper, mapperFn)
}
Vue3+TypeScript从入门到进阶(六)——TypeScript知识点——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(七)——项目实战——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(八)——项目打包和自动化部署——附沿途学习案例及项目实战代码
https://gitee.com/wu_yuxin/vue3-learning.git
https://gitee.com/wu_yuxin/vue3-ts-cms.git
基本用法
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称之为解构(Destructuring)
// 以前为变量赋值,只能直接指定值
var a = 1;
var b = 2;
var c = 3;
// ES6允许写成这样
var [a,b,c] = [1,2,3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的例子:
let [foo,[[bar],baz]] = [1,[[2],3]];
foo // 1
bar // 2
baz // 3
let [,,third] = ["foo","bar","baz"];
third // "baz"
let [head,...tail] = [1,2,3,4];
head // 1
tail // [2,3,4]
let [x,y,...z] = ['a'];
x // "a"
y // undefined
z // []
默认值
解构赋值允许制定默认值
var [foo = true] = [];
foo // true
[x,y='b'] = ['a'];
// x='a', y='b'
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x=1] = [undefined];
x //1
var [x=1] = [null];
x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
function f(){
console.log('aaa');
}
let [x=f()] = [1];
上面的代码中,因为x能取到值,所以函数f()根本不会执行。上面的代码其实等价于下面的代码:
let x;
if([1][0] === undefined){
x = f();
}else{
x = [1][0];
}
默认值可以引用解构赋值的其他变量,但该变量必须已经声明:
let [x=1,y=x] = [];
// x=1; y=1
let [x=1,y=x] = [2];
// x=2; y=2
let [x=1,y=x] = [1,2];
// x=1; y=2
let [x=y,y=1] = []; // ReferenceError
上面最后一个表达式,因为x用到默认值是y时,y还没有声明。
1、最简单的案例
看下面的案例
let person = {
name: 'yhb',
age: 20
}
/*
注意:下面虽然看起来是创建了一个对象,对象中有两个属性 name 和 age
但是:其实是声明了两个变量
name:等于对象person 中的name属性的值
age:等于对象person 中的 age属性的值
*/
let { name, age } = person
console.log(name,age)
如上面注释中所说,声明了变量 name和age,然后分别从对象person中寻找与变量同名的属性,并将属性的值赋值给变量
所以,这里的关键,就是首先要知道对象中都有哪些属性,然后再使用字面量的方式声明与其同名的变量
2、属性不存在怎么办
如果不小心声明了一个对象中不存在的属性怎么办?
或者,实际情况下,可能是我们就是想再声明一个变量,但是这个变量也不需要从对象中获取值,这个时候,此变量的值就是 undefined
let person = {
name: 'yhb',
age: 20
}
let { name, age,address } = person
console.log(name,age,address)
此时,可以给变量加入一个默认值
let { name, age,address='北京' } = person
3、属性太受欢迎怎么办
当前声明了 name 和 age 变量,其值就是person对象中name和age属性的值,如果还有其他变量也想获取这两个属性的值怎么办?
let { name, age, address = '北京' } = person
console.log(name, age, address)
let { name, age } = person
console.log(name, age)
上面的方法肯定不行,会提示定义了重复的变量 name 和 age
那怎么办呢?
难道只能放弃结构赋值,使用老旧的方式吗?
let l_name=person.name
let l_age=person.age
console.log(l_name,l_age)
其实不然!
let {name:l_name,age:l_age}=person
console.log(l_name,l_age)
说明:
声明变量 l_name 并从对象person中获取name属性的值赋予此变量
声明变量 l_age, 并从对象person中获取age属性的值赋予此变量
这里的重点是下面这行代码
let {name:l_name,age:l_age}=person
按照创建对象字面量的逻辑,name 为键,l_name 为值。但注意,这里是声明变量,并不是创建对象字面量,所以争取的解读应该是
声明变量 l_name,并从person 对象中找到与 name 同名的属性,然后将此属性的值赋值给变量 l_name
所以,我们最后输出的是变量 l_name和l_age
console.log(l_name,l_age)
当然这种状态下,也是可以给变量赋予默认值的
let { name:l_name, age:l_age, address:l_address='北京' }=person
4、嵌套对象如何解构赋值
let person = {
name: 'yhb',
age: 20,
address: {
province: '河北省',
city: '保定'
}
}
// 从对象 person 中找到 address 属性,并将值赋给变量 address
let {address}=person
// 从对象 address 中找到 province 属性,并将值赋给变量 province
let {province}=address
console.log(province)
上面代码一层层的进行结构赋值,也可以简写为如下形式
let {address:{province}}=person
从peson 对象中找到 address 属性,取出其值赋值给冒号前面的变量 address,然后再将 变量address 的值赋值给 冒号 后面的变量 {province},相当于下面的写法
let {province}=address
1、字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
三个连续的点具有两个含义:展开运算符(spread operator)和剩余运算符(rest operator)。
展开运算符
展开运算符允许迭代器在接收器内部分别展开或扩展。迭代器和接收器可以是任何可以循环的对象,例如数组、对象、集合、映射等。你可以把一个容器的每个部分分别放入另一个容器。
const newArray = ['first', ...anotherArray];
剩余参数
剩余参数语法允许我们将无限数量的参数表示为数组。命名参数的位置可以在剩余参数之前。
const func = (first, second, ...rest) => {};
用例
定义是非常有用的,但是很难仅从定义中理解概念。我认为用日常用例会加强对定义的理解。
复制数组
当我们需要修改一个数组,但又不想改变原始数组(其他人可能会使用它)时,就必须复制它。
const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits]; // ['apple', 'orange', 'banana']
console.log(fruits === fruitsCopied); // false
// 老方法
fruits.map(fruit => fruit);
它正在选择数组中的每个元素,并将每个元素放在新的数组结构中。我们也可以使用 map 操作符实现数组的复制并进行身份映射。
唯一数组
如果我们想从数组中筛选出重复的元素,那么最简单的解决方案是什么?
Set 对象仅存储唯一的元素,并且可以用数组填充。它也是可迭代的,因此我们可以将其展开到新的数组中,并且得到的数组中的值是唯一的。
const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)]; // ['apple', 'orange', 'banana']
// old way
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
串联数组
可以用 concat 方法连接两个独立的数组,但是为什么不再次使用展开运算符呢?
const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables]; // ['apple', 'orange', 'banana', 'carrot']
const fruitsAndVegetables = ['carrot', ...fruits]; // ['carrot', 'apple', 'orange', 'banana']
// 老方法
const fruitsAndVegetables = fruits.concat(vegetables);
fruits.unshift('carrot');
将参数作为数组进行传递
当传递参数时,展开运算符能够使我们的代码更具可读性。在 ES6 之前,我们必须将该函数应用于 arguments。现在我们可以将参数展开到函数中,从而使代码更简洁。
const mixer = (x, y, z) => console.log(x, y, z);
const fruits = ['apple', 'orange', 'banana'];
mixer(...fruits); // 'apple', 'orange', 'banana'
// 老方法
mixer.apply(null, fruits);
数组切片
使用 slice 方法切片更加直接,但是如果需要的话,展开运算符也可以做到。但是必须一个个地去命名其余的元素,所以从大数组中进行切片的话,这不是个好方法。
const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits; // ['orange', 'banana']
// 老方法
const remainingFruits = fruits.slice(1);
将参数转换为数组
Javascript 中的参数是类似数组的对象。你可以用索引来访问它,但是不能调用像 map、filter 这样的数组方法。参数是一个可迭代的对象,那么我们做些什么呢?在它们前面放三个点,然后作为数组去访问!
const mixer = (...args) => console.log(args);
mixer('apple'); // ['apple']
将 NodeList 转换为数组
参数就像从 querySelectorAll 函数返回的 NodeList 一样。它们的行为也有点像数组,只是没有对应的方法。
[...document.querySelectorAll('div')];
// 老方法
Array.prototype.slice.call(document.querySelectorAll('div'));
复制对象
最后,我们介绍对象操作。复制的工作方式与数组相同。在以前它可以通过 Object.assign 和一个空的对象常量来实现。
const todo = { name: 'Clean the dishes' };
const todoCopied = { ...todo }; // { name: 'Clean the dishes' }
console.log(todo === todoCopied); // false
// 老方法
Object.assign({}, todo);
合并对象
合并的唯一区别是具有相同键的属性将被覆盖。最右边的属性具有最高优先级。
const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo }; // { name: 'Ironing', completed: false }
// 老方法
Object.assign({}, todo, state, nextTodo);
需要注意的是,合并仅在层次结构的第一级上创建副本。层次结构中的更深层次将是相同的引用。
将字符串拆分为字符
最后是字符串。你可以用展开运算符把字符串拆分为字符。当然,如果你用空字符串调用 split 方法也是一样的。
const country = 'USA';
console.log([...country]); // ['U', 'S', 'A']
// 老方法
country.split('');
在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了
Webpack从入门到进阶(一)—附沿路学习案例代码
Webpack从入门到进阶(二)—附沿路学习案例代码
Webpack从入门到进阶(三)—附沿路学习案例代码
数据可视化-echarts入门、常见图表案例、超详细配置解析及项目案例
Vue项目开发-仿蘑菇街电商APP
Vue 知识点汇总(上)–附案例代码及项目地址
Vue 知识点汇总(下)–附案例代码及项目地址
JavaScript面向对象编程浅析
JavaScript设计模式浅析
SingleSpa及qiankun入门、源码分析及案例