一、简介
Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码
二、Vue2和Vue3区别
Vue3+TypeScript从入门到进阶(二)——Vue2和Vue3的区别——附沿途学习案例及项目实战代码
三、Vue知识点学习
Vue3+TypeScript从入门到进阶(三)——Vue3基础知识点(上)——附沿途学习案例及项目实战代码
Vue3+TypeScript从入门到进阶(四)——Vue3基础知识点(中)——附沿途学习案例及项目实战代码
九、Composition API
1、Mixin和extends
认识Mixin
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
Mixin的基本使用
Mixin的合并规则
如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?
情况一:如果是data函数的返回值对象
情况二:如何生命周期钩子函数
情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
全局混入Mixin
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的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>
<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。
2、Composition API
Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
但是这种代码有一个很大的弊端:
-
当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中; -
当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散; -
尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
大组件的逻辑分散
如果我们能将同一个逻辑关注点相关的代码收集在一起会更好。
这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
也有人把Vue CompositionAPI简称为VCA。
认识Composition API
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
setup其实就是组件的另外一个选项:
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既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
甚至是我们可以返回一个执行函数来代替在methods中定义的方法:
setup() {
let counter = 100;
const increment = () => {
counter++;
console.log(counter);
}
return {
title: "Hello Home",
counter,
increment
}
}
但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
setup不可以使用this
官方关于this有这样一段描述(这段描述是我给官方提交了PR之后的一段描述):
在阅读源码的过程中,代码是按照如下顺序执行的:
-
调用 createComponentInstance 创建组件实例; -
调用 setupComponent 初始化component内部的操作; -
调用 setupStatefulComponent 初始化有状态的组件; -
在 setupStatefulComponent 取出了 setup 函数; -
通过callWithErrorHandling 的函数执行 setup;
从上面的代码我们可以看出, 组件的instance肯定是在执行 setup 函数之前就创建出来的
3、Reactive API、Ref API和readonly
Reactive API
如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
const state = reactive({
name: "coderwhy",
counter: 100
})
那么这是什么原因呢?为什么就可以变成响应式的呢?
-
这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集; -
当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面); -
事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
Ref API
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
- 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;
这个时候Vue3给我们提供了另外一个API:ref API
const message = ref("Hello World")
这里有两个注意事项:
Ref自动解包
模板中的解包是浅层的解包,如果我们的代码是下面的方式:
如果我们将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包:
认识readonly
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
在开发中常见的readonly方法会传入三个类型的参数:
-
类型一:普通对象; -
类型二:reactive返回的对象; -
类型三:ref的对象;
readonly的使用
在readonly的使用过程中,有如下规则:
-
readonly返回的对象都是不允许修改的; -
但是经过readonly处理的原来的对象是允许被修改的; 比如 const info = readonly(obj),info对象是不允许被修改的; 当obj被修改时,readonly返回的info对象也会被修改; 但是我们不能去修改readonly返回的对象info; -
其实本质上就是readonly返回的对象的setter方法被劫持了而已;
const info = {
name: "why",
age: 18
}
const state1 = readonly(info)
console.log(state1)
const state = reactive({
name: "why",
age: 18
})
const state2 = readonly(state);
const nameRef = ref("why")
const state3 = readonly(nameRef)
readonly的应用
那么这个readonly有什么用呢?
- 在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用readonly了;
Reactive判断的API
isProxy
- 检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
isReadonly
- 检查对象是否是由 readonly 创建的只读代理。
toRaw
- 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
- 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
- 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
4、toRefs、toRef和ref其他的API
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,都不再是响应式的:
const info = reactive({name: "why", age: 18});
const { name, age } = info
那么有没有办法让我们解构出来的属性是响应式的呢?
const { name, age } = toRefs(state);
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
toRef
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
setup() {
const info = reactive({name: "why", age: 18});
let { name } = info;
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
}
return {
name,
age,
changeAge
}
}
ref其他的API
unref
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
isRef
shallowRef
triggerRef
- 手动触发和 shallowRef 相关联的副作用:
setup() {
const info = shallowRef({name: "why"})
const changeInfo = () => {
info.value.name = "james";
triggerRef(info);
}
return {
info,
changeInfo
}
}
customRef
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
这里我们使用一个的案例:
- 对双向绑定的属性进行debounce(节流)的操作;
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>
import { customRef } from 'vue';
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);
}
}
})
}
5、computed、watch和watchEffect
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
如何使用computed呢?
const fullName = computed(() => firstName.value + " " + lastName.value);
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。
我们来看一个案例:
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)
onInvalidate(() => {
clearTimeout(timer);
console.log("onInvalidate");
})
console.log("name:", name.value, "age:", age.value);
});
setup中使用ref
在讲解 watchEffect执行时机之前,我们先补充一个知识:在setup中如何使用ref或者元素或者组件?
- 其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前:
- 如果我们希望在副作用函数中获取到元素,是否可行呢?
我们会发现打印结果打印了两次:
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素呢?
我们可以设置副作用函数的执行时机:
let h2Elcontent = null;
watchEffect(() => {
h2ElContent = titleRef.value && titleRef.value.textContent;
console.log(h2ElContent, counter.value)
}, {
flush: "post"
})
flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
Watch的使用
watch的API完全等同于组件watch选项的Property:
与watchEffect的比较,watch允许我们:
侦听单个数据源
watch侦听函数的数据源有两种类型:
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
const name = ref("why");
const age = ref(18)
watch([name, age], (newValues, oldValues) => {
console.log(newValues, oldValues)
})
侦听响应式对象
如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
const info = reactive({name: "why", age: 18});
const name = ref("why");
watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, oldName]) => {
console.log(newInfo, newName, oldInfo, oldName);
})
const changeData = () => {
info.name = "kobe";
}
watch的选项
如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true:
const info = reactive({
name: "why",
age: 18,
friend: {
name: "kobe"
}
});
watch(() => ({...info}), (newInfo, oldInfo) => {
console.log(newInfo, oldInfo);
}, {
deep: true,
immediate: true
})
6、生命周期钩子
我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代 生命周期钩子。
那么setup中如何使用生命周期函数呢?
- 可以使用直接导入的 onX 函数注册生命周期钩子;
7、Provide函数和Inject函数
Provide函数
事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
我们可以通过 provide来提供数据:
const name = "coderwhy";
let counter = 100;
provide("name", readonly(name));
provide("counter", readonly(counter));
Inject函数
在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
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)
8、compositionAPI练习
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;
}
9、h函数
认识h函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
-
前面我们讲解过VNode和VDOM的改变: -
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM); -
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode; -
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
那么我们应该怎么来做呢?使用 h()函数:
h()函数 如何使用呢?
h()函数 如何使用呢?它接受三个参数:
注意事项:
h函数的基本使用
h函数可以在两个地方使用:
<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函数计数器案例
<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>
<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>
函数组件和插槽的使用
<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>
<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>
10、jsx
jsx的babel配置
如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持:
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D
在babel.config.js配置文件中配置插件:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
<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组件的使用
11、自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
自定义指令分为两种:
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
实现方式一:聚焦的默认实现
<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 获取到对应的内容:
自定义指令练习
自定义指令案例:时间戳的显示需求:
我们来实现一个可以自动对时间格式化的指令v-format-time:
- 这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可;
时间格式化指令
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);
}
})
}
import registerFormatTime from './format-time';
export default function registerDirectives(app) {
registerFormatTime(app);
}
import { createApp } from 'vue'
import App from './03_自定义指令/App.vue'
import registerDirectives from './directives'
const app = createApp(App);
registerDirectives(app);
app.mount('#app');
<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>
12、Teleport
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:
Teleport是什么呢?
我们来看下面代码的效果:
和组件结合使用
当然,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>
实现效果如下:
13、Vue插件
认识Vue插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
插件可以完成的功能没有限制,比如下面的几种都是可以的:
插件的编写方式
对象类型的写法
export default {
install(app) {
app.config.globalProperties.$name = "coderwhy"
}
}
函数类型的写法
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');
十、Vue3源码学习
1、真实DOM和虚拟DOM
真实的DOM渲染
我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?
虚拟DOM的优势
目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处:
首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:
其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点
虚拟DOM的渲染过程
2、Vue三大核心系统
事实上Vue的源码包含三大核心:
三大系统协同工作
三个系统之间如何协同工作呢:
3、实现Mini-Vue
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
-
渲染系统模块; -
可响应式系统模块; -
应用程序入口模块;
渲染系统实现
渲染系统,该模块主要包含三个功能:
h函数 – 生成VNode
h函数的实现:
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
Mount函数 – 挂载VNode
mount函数的实现:
第一步:根据tag,创建HTML元素,并且存储到vnode的el中;
第二步:处理props属性
第三步:处理子节点
const mount = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.tag);
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);
}
}
}
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach(item => {
mount(item, el);
})
}
}
container.appendChild(el);
}
Patch函数 – 对比两个VNode
patch函数的实现,分为两种情况
n1和n2是不同类型的节点:
-
找到n1的el父节点,删除原来的n1节点的el; -
挂载n2节点到n1的el父节点上;
n1和n2节点是相同的节点:
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement;
n1ElParent.removeChild(n1.el);
mount(n2, n1ElParent);
} else {
const el = n2.el = n1.el;
const oldProps = n1.props || {};
const newProps = n2.props || {};
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);
}
}
}
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);
}
}
const oldChildren = n1.children || [];
const newChidlren = n2.children || [];
if (typeof newChidlren === "string") {
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
el.textContent = newChidlren
}
} else {
el.innerHTML = newChidlren;
}
} else {
if (typeof oldChildren === "string") {
el.innerHTML = "";
newChidlren.forEach(item => {
mount(item, el);
})
} else {
const commonLength = Math.min(oldChildren.length, newChidlren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i]);
}
if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach(item => {
mount(item, el);
})
}
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;
}
const targetMap = new WeakMap();
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
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});
watchEffect(function () {
console.log("effect1:", info.counter * 2, info.name);
})
watchEffect(function () {
console.log("effect2:", info.counter * info.counter);
})
watchEffect(function () {
console.log("effect3:", info.counter + 10, info.name);
})
watchEffect(function () {
console.log("effect4:", foo.height);
})
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;
}
const targetMap = new WeakMap();
function getDep(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
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 是劫持对象的属性时,如果新增元素:
- 那么Vue2需要再次 调用definedProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
框架外层API设计
这样我们就知道了,从框架的层面来说,我们需要
有两部分内容:
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;
}
})
}
}
}
4、源码阅读
源码阅读之createApp
源码阅读之挂载根组件
const app = {props: {message: String}
instance
instance.props
instance.attrs
instance.slots
const result = setup()
instance.setupState = proxyRefs(result);
<template> -> render函数
instance.render = Component.render = render函数
data/methods/computed/生命周期
组件化的初始化
Compile过程
Block Tree分析
生命周期回调
template中数据的使用顺序
十一、VueRouter路由使用
1、认识前端路由
路由其实是网络工程中的一个术语:
路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
-
后端路由阶段; -
前后端分离阶段; -
单页面富应用(SPA);
后端路由阶段
早期的网站开发整个HTML页面是由服务器来渲染的.
- 服务器直接生产渲染好对应的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.replaceState({}, "", href);
changeContent();
})
}
window.addEventListener("popstate", changeContent)
</script>
</body>
</html>
2、vue-router
目前前端流行的三大框架, 都有自己的路由实现:
-
Angular的ngRouter -
React的ReactRouter -
Vue的vue-router
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得非常容易。
- 目前Vue路由最新的版本是4.x版本,我们上课会基于最新的版本讲解。
vue-router是基于路由和组件的
安装Vue Router:
npm install vue-router@4
路由的使用步骤
使用vue-router的步骤:
-
第一步:创建路由组件的组件; -
第二步:配置路由映射: 组件和路径映射关系的routes数组; -
第三步:通过createRouter创建路由对象,并且传入routes和history模式; -
第四步:使用路由: 通过<router-link> 和<router-view> ;
路由的基本使用流程
路由的默认路径
我们这里还有一个不太好的实现:
-
默认情况下, 进入网站的首页, 我们希望<router-view> 渲染首页的内容; -
但是我们的实现中, 默认没有显示首页组件, 必须让用户点击才可以;
如何可以让路径默认跳到到首页, 并且<router-view> 渲染首页组件呢?
const routes = [
{ path: "/", redirect: '/home' },
{ path: "/home", component: Home },
{ path: "/about", component: About },
]
我们在routes中又配置了一个映射:
history模式
另外一种选择的模式是history模式:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
routes,
history: createWebHistory()
})
router-link
router-link事实上有很多属性可以配置:
to属性:
replace属性:
- 设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push();
active-class属性:
- 设置激活a元素后应用的class,默认是router-link-active
exact-active-class属性:
- 链接精准激活时,应用于渲染的 的 class,默认是router-link-exact-active;
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载:
其实这里还是我们前面讲到过的webpack的分包知识,而Vue Router默认就支持动态来导入组件:
const routes = [
{ path: "/", redirect: '/home' },
{ path: "/home", () => import("../pages/Home.vue") },
{ path: "/about", () => import("../pages/About.vue") },
]
打包效果分析
我们看一下打包后的效果:
我们会发现分包是没有一个很明确的名称的,其实webpack从3.x开始支持对分包进行命名(chunk name):
动态路由基本匹配
很多时候我们需要将给定匹配模式的路由映射到同一个组件:
{
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;
- 该Hook会返回一个Route对象,对象中保存着当前路由相关的值;
匹配多个参数
NotFound
对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面
- 比如NotFound的错误页面中,这个时候我们可编写一个动态路由用于匹配所有的页面;
{
path: "/:pathMatch(.*)",
component: () => import("../pages/NotFound.vue")
}
我们可以通过 $route.params.pathMatch获取到传入的参数:
匹配规则加*
这里还有另外一种写法:
- 注意:我在/: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("../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:
- 通过调用 history.back() 回溯历史。相当于 router.go(-1);
router也有forward:
- 通过调用 history.forward() 在历史中前进。相当于 router.go(1);
router-link的v-slot
在vue-router3.x的时候,router-link有一个tag属性,可以决定router-link到底渲染成什么元素:
v-slot如何使用呢?
首先,我们需要使用custom表示我们整个元素要自定义
- 如果不写,那么自定义的内容会被包裹在一个 a 元素中;
其次,我们使用v-slot来作用域插槽来获取内部传给我们的值:
<!-- 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也提供给我们一个插槽,可以用于 <transition> 和 <keep-alive> 组件来包裹你的路由组件:
-
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")
})
动态删除路由
删除路由有以下三种方式:
路由的其他方法补充:
路由导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。
全局的前置守卫beforeEach是在导航触发时会被回调的:
它有两个参数:
-
to:即将进入的路由Route对象; -
from:即将离开的路由Route对象;
它有返回值:
可选的第三个参数:next
router.beforeEach((to, from) => {
console.log(to)
console.log(from)
return false
})
登录守卫功能
let counter = 0;
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还提供了很多的其他守卫函数,目的都是在某一个时刻给予我们回调,让我们可以更好的控制程序的流程或者功能:
- https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html
我们一起来看一下完整的导航解析流程:
-
导航被触发。 -
在失活的组件里调用 beforeRouteLeave 守卫。 -
调用全局的 beforeEach 守卫。 -
在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。 -
在路由配置里调用 beforeEnter。 -
解析异步路由组件。 -
在被激活的组件里调用 beforeRouteEnter。 -
调用全局的 beforeResolve 守卫(2.5+)。 -
导航被确认。 -
调用全局的 afterEach 钩子。 -
触发 DOM 更新。 -
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
十二、Vuex的状态管理
1、Vuex介绍和安装
什么是状态管理
在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是 状态管理。
在前面我们是如何管理自己的状态呢?
-
在Vue开发中,我们使用组件化的开发方式; -
而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state; -
在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View; -
在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions;
复杂的状态管理
JavaScript开发的应用程序,已经变得越来越复杂了:
-
JavaScript需要管理的状态越来越多,越来越复杂; -
这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等; -
也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
-
多个视图依赖于同一状态; -
来自不同视图的行为需要变更同一状态;
我们是否可以通过组件数据的传递来完成呢?
Vuex的状态管理
管理不断变化的state本身是非常困难的:
因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
-
在这种模式下,我们的组件树构成了一个巨大的 “视图View”; -
不管在树的哪个位置,任何组件都能获取状态或者触发行为; -
通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;
这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想):
Vuex的状态管理
Vuex的安装
依然我们要使用vuex,首先第一步需要安装vuex:
- 我们这里使用的是vuex4.x,安装的时候需要添加 next 指定版本;
npm install vuex@next
Vue devtool
vue其实提供了一个devtools,方便我们对组件或者vuex进行调试:
- 我们需要安装beta版本支持vue3,目前是6.0.0 beta15;
它有两种常见的安装方式:
-
方式一:通过chrome的商店; -
方式二:手动下载代码,编译、安装;
方式一:通过Chrome商店安装:
- 由于某些原因我们可能不能正常登录Chrome商店,所以可以选择第二种;
方式二:手动下载代码,编译、安装
2、Store
创建Store
每一个Vuex应用的核心就是store(仓库):
- store本质上是一个容器,它包含着你的应用中大部分的状态(state);
Vuex和单纯的全局对象有什么区别呢?
第一:Vuex的状态存储是响应式的
- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
第二:你不能直接改变store中的状态
使用步骤:
组件中使用store
在组件中使用store,我们按照如下的方式:
单一状态树
Vuex 使用单一状态树:
-
用一个对象就包含了全部的应用层级状; -
采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源; -
这也意味着,每个应用将仅仅包含一个 store 实例; -
单状态树和模块化并不冲突,后面我们会讲到module的概念;
单一状态树的优势:
-
如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难; -
所以Vuex也使用了单一状态树来管理应用层级的全部状态; -
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护;
3、mapState
组件获取状态
在前面我们已经学习过如何在组件中获取状态了。
当然,如果觉得那种方式有点繁琐(表达式过长),我们可以使用计算属性:
computed: {
counter() {
return this.$store.state.counter
}
}
但是,如果我们有很多个状态都需要获取话,可以使用mapState的辅助函数:
在setup中使用mapState
在setup中如果我们单个获取装是非常简单的:
默认情况下,Vuex并没有提供非常方便的使用mapState的方式,这里我们进行了一个函数的封装:
import { computed } from 'vue'
import { mapState, useStore } from 'vuex'
export function useState(mapper) {
const store = useStore()
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
}
}
4、getters
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) {
const store = useStore()
const storeStateFns = mapGetters(mapper)
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
5、Mutation
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必须是同步函数;
6、actions
Action类似于mutation,不同在于:
这里有一个非常重要的参数context:
-
context是一个和store实例均有相同方法和属性的context对象; -
所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters; -
但是为什么它不是store对象呢?这个等到我们讲Modules时再具体来说;
incrementAction(context, payload) {
console.log(payload)
setTimeout(() => {
context.commit('increment')
}, 1000);
},
actions的分发操作
如何使用action呢?进行action的分发:
- 分发使用的是 store 上的dispatch函数;
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 什么时候结束呢?
- 我们可以通过让action返回Promise,在Promise的then中来处理完成后的操作;
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, "异步完成")
})
}
7、module
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仍然是注册在全局的命名空间中的:
如果我们希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:
- 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;
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("changeRootNameAction", null, {root: true})
}
module的辅助函数
如果辅助函数有三种使用方法:
第三种方式: createNamespacedHelpers
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home")
export default {
computed: {
...mapState({
homeCounter: state => state.home.homeCounter
}),
...mapGetters({
doubleHomeCounter: "home/doubleHomeCounter"
})
...mapState("home", ["homeCounter"]),
...mapGetters("home", ["doubleHomeCounter"])
...mapState(["homeCounter"]),
...mapGetters(["doubleHomeCounter"])
},
methods: {
...mapMutations({
increment: "home/increment"
}),
...mapActions({
incrementAction: "home/incrementAction"
}),
...mapMutations("home", ["increment"]),
...mapActions("home", ["incrementAction"]),
...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)
}
四、TypeScript知识点
Vue3+TypeScript从入门到进阶(六)——TypeScript知识点——附沿途学习案例及项目实战代码
五、项目实战
Vue3+TypeScript从入门到进阶(七)——项目实战——附沿途学习案例及项目实战代码
六、项目打包和自动化部署
Vue3+TypeScript从入门到进阶(八)——项目打包和自动化部署——附沿途学习案例及项目实战代码
七、沿途学习代码地址及案例地址
1、沿途学习代码地址
https://gitee.com/wu_yuxin/vue3-learning.git
2、项目案例地址
https://gitee.com/wu_yuxin/vue3-ts-cms.git
八、知识拓展
1、ES6数组与对象的解构赋值详解
数组的解构赋值
基本用法
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称之为解构(Destructuring)
var a = 1;
var b = 2;
var c = 3;
var [a,b,c] = [1,2,3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的例子:
let [foo,[[bar],baz]] = [1,[[2],3]];
foo
bar
baz
let [,,third] = ["foo","bar","baz"];
third
let [head,...tail] = [1,2,3,4];
head
tail
let [x,y,...z] = ['a'];
x
y
z
默认值
解构赋值允许制定默认值
var [foo = true] = [];
foo
[x,y='b'] = ['a'];
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x=1] = [undefined];
x
var [x=1] = [null];
x
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
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] = [];
let [x=1,y=x] = [2];
let [x=1,y=x] = [1,2];
let [x=y,y=1] = [];
上面最后一个表达式,因为x用到默认值是y时,y还没有声明。
对象的解构赋值
1、最简单的案例
看下面的案例
let person = {
name: 'yhb',
age: 20
}
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: '保定'
}
}
let {address}=person
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
b
c
d
e
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len
2、JavaScript的 …(展开运算符)
三个连续的点具有两个含义:展开运算符(spread operator)和剩余运算符(rest operator)。
展开运算符
展开运算符允许迭代器在接收器内部分别展开或扩展。迭代器和接收器可以是任何可以循环的对象,例如数组、对象、集合、映射等。你可以把一个容器的每个部分分别放入另一个容器。
const newArray = ['first', ...anotherArray];
剩余参数
剩余参数语法允许我们将无限数量的参数表示为数组。命名参数的位置可以在剩余参数之前。
const func = (first, second, ...rest) => {};
用例
定义是非常有用的,但是很难仅从定义中理解概念。我认为用日常用例会加强对定义的理解。
复制数组
当我们需要修改一个数组,但又不想改变原始数组(其他人可能会使用它)时,就必须复制它。
const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits];
console.log(fruits === fruitsCopied);
fruits.map(fruit => fruit);
它正在选择数组中的每个元素,并将每个元素放在新的数组结构中。我们也可以使用 map 操作符实现数组的复制并进行身份映射。
唯一数组
如果我们想从数组中筛选出重复的元素,那么最简单的解决方案是什么?
Set 对象仅存储唯一的元素,并且可以用数组填充。它也是可迭代的,因此我们可以将其展开到新的数组中,并且得到的数组中的值是唯一的。
const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)];
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
串联数组 可以用 concat 方法连接两个独立的数组,但是为什么不再次使用展开运算符呢?
const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables];
const fruitsAndVegetables = ['carrot', ...fruits];
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);
mixer.apply(null, fruits);
数组切片
使用 slice 方法切片更加直接,但是如果需要的话,展开运算符也可以做到。但是必须一个个地去命名其余的元素,所以从大数组中进行切片的话,这不是个好方法。
const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits;
const remainingFruits = fruits.slice(1);
将参数转换为数组 Javascript 中的参数是类似数组的对象。你可以用索引来访问它,但是不能调用像 map、filter 这样的数组方法。参数是一个可迭代的对象,那么我们做些什么呢?在它们前面放三个点,然后作为数组去访问!
const mixer = (...args) => console.log(args);
mixer('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 };
console.log(todo === todoCopied);
Object.assign({}, todo);
合并对象 合并的唯一区别是具有相同键的属性将被覆盖。最右边的属性具有最高优先级。
const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo };
Object.assign({}, todo, state, nextTodo);
需要注意的是,合并仅在层次结构的第一级上创建副本。层次结构中的更深层次将是相同的引用。
将字符串拆分为字符 最后是字符串。你可以用展开运算符把字符串拆分为字符。当然,如果你用空字符串调用 split 方法也是一样的。
const country = 'USA';
console.log([...country]);
country.split('');
3、export ‘defineEmit’ (imported as ‘defineEmit’) was not found in ‘vue’
在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了
九、其他知识学习
1、Webpack学习
Webpack从入门到进阶(一)—附沿路学习案例代码
Webpack从入门到进阶(二)—附沿路学习案例代码
Webpack从入门到进阶(三)—附沿路学习案例代码
2、数据可视化-echarts
数据可视化-echarts入门、常见图表案例、超详细配置解析及项目案例
3、Vue2学习
Vue项目开发-仿蘑菇街电商APP
Vue 知识点汇总(上)–附案例代码及项目地址
Vue 知识点汇总(下)–附案例代码及项目地址
4、JavaScript面向对象和设计模式
JavaScript面向对象编程浅析
JavaScript设计模式浅析
5、微前端学习
SingleSpa及qiankun入门、源码分析及案例
|