Vue 系列之:基础知识
什么是 MVVM
MVVM(Model-View-ViewModel)一种软件设计模式,旨在将应用程序的数据模型(Model)与视图层(View)分离,并通过 ViewModel 来实现它们之间的通信。降低了代码的耦合度。
Model 代表数据模型,是应用程序中用于处理数据的部分。在 Vue.js 中,Model 通常指的是组件的 data
函数返回的对象,或者 Vue 实例的 data
属性。
View 是用户界面,是用户与应用程序进行交互的部分。在 Vue.js 中,View 通常指的是 Vue 组件的模板(template),它使用声明式的语法来描述如何渲染数据。Vue 的模板语法允许开发者以声明的方式将 DOM 绑定到底层 Vue 实例的数据上。
ViewModel 是 MVVM 模式的核心,它作为 View 和 Model 之间的桥梁,负责将 Model 的数据同步到 View 显示出来,以及将用户对 View 的操作反映到 Model 上。
Vue 实例通过其响应式系统(基于 ES6 的 Proxy 或 Object.defineProperty 实现)来监听 Model 的变化,并自动更新 DOM(View)
Vue 钩子函数
钩子函数(hook)是在系统在进行消息的传递处理时候利用钩子机制截取消息,可以对一些特定的消息进行处理。
为什么要叫钩子?
钩子就是用来挂的嘛,在线程执行的过程中,我们想要在步骤1和步骤2直接挂一个函数,什么东西可以挂呢?钩子!所以就有钩子函数 这个叫法了。
Vue 八大生命周期钩子函数:
Vue2 | Vue3 | 调用时间 |
---|---|---|
beforeCreate | setup | vue 实例初始化之前调用 |
created | setup | vue 实例初始化之后调用 |
beforeMount | onBeforeMount | 挂载到 DOM 树之前调用 |
mounted | onMounted | 挂载到 DOM 树之后调用 |
beforeUpdate | onBeforeUpdate | 数据更新之前调用 |
updated | onUpdated | 数据更新之后调用 |
beforeDestroy | onBeforeUnmount | vue实例销毁之前调用 |
destroyed | onUnmounted | vue实例销毁之后调用 |
Vue.js 官方文档
Vue 实例
每个 Vue 应用都是通过用 Vue 构造函数创建一个新的 Vue 实例开始的
// vue2
var vm = new Vue({
// 选项
})
// vue3
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
Vue 实例和 Vue 生命周期有什么关系?
在 Vue 中,当创建一个组件或 Vue 实例时,它会经历一系列初始化步骤,这就是所谓的生命周期。
Vue 生命周期描述了 Vue 实例从创建到销毁的整个过程。
Vue2 实例方法
Vue3 应用实例
Vue3 组件实例
Vue.prototype
用途:
-
Vue.prototype 是 Vue 构造函数的原型对象,用于向所有 Vue 实例添加共享的方法和属性。
-
通过在 Vue.prototype 上定义方法或属性,可以确保这些方法或属性在所有 Vue 实例中都是可用的。
用法:
-
添加全局方法:
Vue.prototype.$myMethod = function() { ... };
-
添加全局属性:
Vue.prototype.$myProperty = 'some value';
为什么要以 $ 符号开头?
这是 Vue 中的一个简单约定,为了避免和组件中定义的数据、方法、计算属性产生冲突。
Vue.prototype 加一个变量,只是给每个组件加了一个属性,这个属性的值并不具有全局性。
例如:
// main.js
Vue.prototype.$test = "测试"
//test1.vue
mounted() {
this.$test = "哈哈"
console.log(this.$test) // 哈哈
this.$router.push("/test2")
}
//test2.vue
mounted() {
console.log(this.$test) // 测试
}
如果要实现全局变量的功能,需要把属性变为引用类型
// main.js
Vue.prototype.$test = { name: "测试" }
//test1.vue
mounted() {
this.$test.name= "哈哈"
console.log(this.$test) // 哈哈
this.$router.push("/test2")
}
//test2.vue
mounted() {
console.log(this.$test) // 哈哈
}
created 和 mounted 区别
-
created:在模板渲染成 html 前调用,通常初始化某些属性值,然后再渲染成视图。
-
mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。
例如 chart.js 的使用
var ctx = document.getElementById(id)
,这个就一定要等 html 渲染完成后才可以完成,这就要用 mounted
computed 和 watch 区别
computed 计算属性:
-
只有当计算属性所依赖的响应式数据发生变化时,计算属性才会重新求值。如果多次访问计算属性,但依赖项没有变化,那么它将返回之前的计算结果,而不是重新计算。
-
在模板中可以直接使用计算属性
-
计算属性总是返回一个值
<div>{{test}}</div>
data() {
return {
name: '张三',
age: 10
}
},
computed: {
test() {
return `姓名${this.name},年龄${this.age}`;
}
}
watch 监听器:
-
watch 中的回调函数可以执行异步操作,而 computed 则不可以。
-
没有缓存,每次触发时重新计算
-
接收两个参数(newValue, oldValue)
data() {
return {
user: {
name: "张三",
age: 10,
},
};
},
watch: {
user: {
handler(newVal, oldVal) {
console.log("用户信息发生变化:", newVal);
},
deep: true, // 深度监听
immediate: true, // 立即触发
},
}
总结:
-
当多个属性通过计算影响一个属性的时候,建议用 computed
-
当一个值发生变化之后,会引起一系列的操作,这种情况就适合用 watch
computed 如何传参
<div :title="text('123')"></div>
computed: {
text(){
return function (params) {
console.log(params) // 123
return params
}
}
}
执行顺序
父子组件生命周期执行顺序
-
页面初始化时:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted-> 父 mounted
-
页面销毁时:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed-> 父 destroyed
页面跳转的生命周期执行顺序
-
旧页面跳转到新页面:
新页面 created > 新 beforeMount > 旧 beforeDestroy > 旧 destroyed > 新 mounted
computed 、watch、created 、mounted 的先后顺序
-
immediate [ɪˈmiːdiət] 为 false 时: created => computed => mounted => watch
-
immediate 为 true 时: watch =>created=> computed => mounted
$nextTick
原理
Vue 采用异步更新策略,当你修改响应式数据时,Vue 不会立即更新 DOM,而是将这些 DOM 更新操作放入一个队列中,等到下一个事件循环时,再批量执行这些更新操作。$nextTick 的作用就是将回调函数(即传递给 $nextTick 的函数)添加到这个队列的末尾,确保在 DOM 更新完成后再执行回调。
批量的意思是指:当有多个 DOM 更新操作时,Vue 不会立即逐个地去更新 DOM,而是将这些更新操作收集起来,在下次事件循环时一次性处理,而不是循环依次更新。
在 Vue 里,数据是响应式的。当一个响应式数据发生变化时,与之绑定的 DOM 就需要更新。要是每次数据变化都立即更新 DOM,会带来大量的重排和重绘操作,严重影响性能。重排和重绘是浏览器渲染页面时开销较大的操作,频繁进行会导致页面卡顿。
JavaScript 的事件循环机制
实现细节
Vue 会根据不同的环境,选择不同的异步执行方法,优先顺序如下:
-
Promise.then:现代浏览器支持的异步方法,优先使用。
-
MutationObserver:用于监听 DOM 变化的 API,在不支持 Promise 的环境中使用。
-
setImmediate:IE 浏览器和 Node.js 环境支持的异步方法。
-
setTimeout:作为最后的兜底方案,所有环境都支持。
使用场景
-
当你修改了数据后,需要立即操作更新后的 DOM 元素时,必须使用 $nextTick。
-
当你修改了数据后,需要立即获取更新后的组件状态(如计算属性、子组件的状态等),必须使用 $nextTick。
-
当你使用 v-if 或 v-for 动态渲染 DOM 元素后,需要立即操作这些元素时,必须使用 $nextTick。
-
当你在 watch 监听器中监听数据变化,并需要操作 DOM 时,必须使用 $nextTick 确保 DOM 已经更新。
总结起来就是在操作执行完成时需要理解操作 DOM 或子组件的,都要使用 $nextTick
@click.native
-
@click:主要用于监听 Vue 组件内部自定义的点击事件,适用于普通 HTML 元素和自定义组件。当用于自定义组件时,需要在组件内部通过 $emit 触发自定义的 click 事件,外部才能监听到。
-
@click.native:用于监听原生 DOM 元素的点击事件,即使是在自定义组件上使用,它监听的也是组件根元素的原生点击事件,而不是自定义事件。对于普通 HTML 元素,@click.native 和 @click 效果相同。
意思就是:如果你想在组件内的子组件上绑定一个点击事件,如果你使用的是 @click,如果你的子组件内部没有通过 $emit 触发自定义的 click 事件,那么 @click 无效。
例如:
<!--子组件-->
<template>
<div class="children">
<div class="my-class">子组件内容</div>
</div>
</template>
<!--父组件-->
<template>
<div id="app">
<Children @click="handleClick"></Children>
</div>
</template>
<script>
import Children from "./Children.vue";
export default {
components: { Children },
methods: {
handleClick(e) {
console.log(e);
}
}
}
</script>
此时 handleClick 是不会触发的,除非把子组件改成:
<template>
<div class="children" @click="handleChildClick">
<div class="my-class">子组件内容</div>
</div>
</template>
<script>
export default {
methods: {
handleChildClick() {
// 通过 $emit 触发父组件的 click 事件
this.$emit('click', '这是子组件触发的事件');
}
}
}
</script>
但是如果你使用的是 @click.native 就不会有这个问题,因为它监听的是组件根元素的原生点击事件,而不是自定义事件。
再举个例子加深理解:
<el-card @click="handleClick1"></el-card>
<el-card @click.native="handleClick2"></el-card>
<script>
methods: {
handleClick1() {
// 不会触发,因为 element-ui 的 el-card 组件没有定义 click 事件
},
handleClick2() {
// 可以触发,因为是原生点击事件
}
}
</script>
你以为你已经掌握了全部吗?不,对于 Vue2 来说,确实是这样,但是对于 Vue3 来说,情况又有不同:
<!--子组件-->
<template>
<div class="children">
<div class="my-class">子组件内容</div>
</div>
</template>
<!--父组件-->
<template>
<div id="app">
<Children @click="handleClick"></Children>
</div>
</template>
<script setup>
import Children from "./Children.vue";
const handleClick = e => {
console.log(e);
}
</script>
发现也可以触发!为什么?
因为在 Vue 3 里,当父组件向子组件传递事件监听器时,如果子组件没有通过 emits 选项显式声明这些事件,那么这些事件监听器会被自动绑定到子组件的根元素上,这就是所谓的事件继承特性。
上面的例子就是因为子组件没有显式定义 emits 选项,父组件中的 @click 会被当作原生 DOM 事件绑定到子组件的根元素上。
@click.stop
阻止事件冒泡,即阻止事件向父级元素传递。
<div id="app">
<div v-on:click="parentClick">
<button v-on:click="childClick">阻止单击事件继续传递</button>
</div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
name: "Vue.js"
},
methods: {
parentClick: function () {
alert("parentClick");
},
childClick: function () {
alert("childClick");
},
}
});
</script>
<!--点击按钮,将会先弹出 "childClick", 再弹出 "parentClick"-->
<div id="app">
<div v-on:click="parentClick">
<button v-on:click.stop="childClick">阻止单击事件继续传播</button>
</div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
name: "Vue.js"
},
methods: {
parentClick: function () {
alert("parentClick");
},
childClick: function () {
alert("childClick");
},
}
});
</script>
<!--点击按钮,只弹出 "childClick"-->
使用场景:点击子元素区域的的时候,不触发父级元素的点击事件。
@click.prevent
阻止事件的默认行为。
<!--点击链接时,阻止 a 标签跳转,仅执行函数 test4-->
<a href="http://www.baidu.com" @click.prevent="test4">百度一下</a>
<!--点击按钮时,阻止表单默认提交行为 action,仅执行函数 test5-->
<form action="/xxx" @submit.prevent="test5">
<input type="submit" value="注册">
</form>
@keyup.enter
按键修饰符。
<!--按下键盘 enter 健时,执行方法 test7-->
<input type="text" @keyup.enter="test7">
directive 自定义指令
什么是自定义指令?
vue2.js 官方链接
vue3.js 官方链接
Vue 的自定义指令允许你对 DOM 进行低级别的操作。自定义指令通过 Vue 的全局方法 Vue.directive()
或组件的 directives
选项来注册。注册之后,你可以在 Vue 模板中通过 v-
前缀加上指令名来使用这个指令。
低级别操作是指直接通过 JavaScript 对 DOM 树中节点的进行增删改查,以及对节点的属性、样式、事件等进行操作。
全局自定义指令
Vue2:
// 在main.js中
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
Vue3:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
局部组件指令
Vue2:
created() {},
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
Vue3:
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。
<template>
<input v-focus />
</template>
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
或:
<template>
<input v-focus />
</template>
<script>
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
</script>
钩子函数
Vue2:
函数 | 描述 |
---|---|
bind | 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 |
inserted | 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 |
update | 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。 |
componentUpdated | 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 |
unbind | 只调用一次,指令与元素解绑时调用。 |
Vue3:
函数 | 描述 |
---|---|
created | 在绑定元素的 attribute 前或事件监听器应用前调用 |
beforeMount | 在元素被插入到 DOM 前调用 |
mounted | 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用 |
beforeUpdate | 绑定元素的父组件更新前调用 |
updated | 在绑定元素的父组件及他自己的所有子节点都更新后调用 |
beforeUnmount | 绑定元素的父组件卸载前调用 |
unmounted | 绑定元素的父组件卸载后调用 |
钩子函数参数:
-
el:指令绑定到的元素。这可以用于直接操作 DOM。
-
binding:一个对象,包含指令值(value)、旧的指令值(oldValue)、参数(arg)等等
-
vnode
-
oldVnode
原理
当 Vue 组件的模板被编译时,Vue 会识别出模板中所有的 v-
前缀指令,这会包含 Vue 的内置指令和自定义指令。对于自定义指令,Vue 会根据注册的钩子函数生成相应的指令对象,这些对象包含了指令的名称、参数、表达式等信息。然后会将这些信息存储在之后生成的 VNode 对象中。最后在生成真实 DOM 的时候,Vue 会在相应的 DOM 元素上调用自定义指令的钩子函数。
v-model 的原理
v-model
是 Vue.js 中的一个语法糖,它内部实际上是使用了 v-bind
(:)和 v-on
(@)两个指令。
-
v-bind:实现数据到 DOM 的绑定
-
修改 data 中的属性值会体现在页面上
-
原理主要是基于 Vue 的响应式系统,当数据变化时触发相应的 setter,来触发渲染过程,以更新 DOM
-
-
v-on:实现 DOM 到数据的绑定
-
在页面上修改的属性值会作用到 data 中
-
原理是监听 DOM 元素上指定的事件,当事件发生时,执行相应的 JavaScript 代码或方法,从而更新属性值。
-
Vue 中使用 CSS 变量
<template>
<!-- 方式一:
在根或父或当前节点使用 --varName 来定义变量
然后当前节点使用 var(--varName) 来使用变量 -->
<div class="parent">
<!-- 方式二:可以在 data 中定义变量,在 template 中直接使用 -->
<span :style="{ color: myColor1 }">parent</span>
<!-- 方式三:data 中定义变量,css 中使用 v-bind 绑定变量 -->
<div class="child1">child1</div>
<!-- 方式四:通过 style module -->
<div :class="classModule.child2">child2</div>
<!-- 方式五:不好理解,不知道这样写的意义在哪里 -->
<div class="child3" :style="{ '--my-color': myColor3 }">child3</div>
</div>
</template>
<script lang="ts" setup>
const myColor1 = 'red'; // 方式二
const myColor2 = 'yellow'; // 方式三
const myColor3 = 'blue'; // 方式五
</script>
<style>
.parent {
/* 方式一 */
--parent-bg-color: skyblue;
/* 方式一 */
background-color: var(--parent-bg-color);
}
.child1 {
/* 方式三 */
color: v-bind(myColor2);
}
.child3 {
/* 方式五 */
color: var(--my-color);
}
</style>
<style module="classModule">
/* 方式四 */
.child2 {
color: green;
}
</style>
Vue 项目结构
文件名 | 注释 |
---|---|
build | 项目构建(webpack)相关代码 |
config | 项目配置 |
dev.env.js | 开发环境变量 |
prod.env.js | 生产环境变量 |
test.env.js | 测试环境变量 |
index.js | 项目配置文件 |
package.json | npm 包配置文件,npm 脚本 |
src | 日常开发主要在这个文件夹 |
asset | 放置静态资源,会被 webpack 构建 |
main.js | 项目入口文件 |
components | 公共组件 |
App.vue | 根组件 |
pages | 页面文件 |
api | 接口文件 |
router | 路由文件 |
static | 纯静态资源,不会被 webpack 构建 |
index.html | 首页入口文件 |
README.md | 项目的说明文档 |
dist | 打包之后的文件 |