Java 程序员的 Vue 指南 - Vue 万字速览(01)
Vue的基本原理
当一个 Vue 组件被创建时,Vue 会查看你在 data 里定义的所有数据。在 Vue 2 中,它使用 Object.defineProperty(Vue 3 改用 Proxy)把这些数据变成“可监听”的——也就是说,Vue 能知道这些数据什么时候被读取,什么时候被修改。
同时,Vue 会为每个组件创建一个“观察者”(watcher),这个观察者的作用就像是一个记事员:当组件第一次显示(渲染)时,它会一边画界面,一边记下“我这里用到了哪些数据”。
之后,一旦这些数据发生了变化(比如你修改了一个变量),Vue 就能通过监听机制立刻知道:“哦,这个数据变了!”然后它会通知对应的观察者:“赶紧重新渲染一下组件!”
于是,组件就会自动更新界面,反映出最新的数据。整个过程是自动的,你只需要改数据,界面就会跟着变。

构建vue实例
Vue 2 中如何构建 Vue 实例
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>我的第一个vue程序</title><!-- 引入 Vue 2 的 CDN --><script src="https://cdn.staticfile.net/vue/2.2.2/vue.min.js"></script>
</head>
<body><!-- 定义一个 DOM 容器,作为 Vue 应用的挂载点 --><div id="root"><h1>{{ message }}</h1></div><script>// 创建 Vue 实例var vm = new Vue({el: "#root",           // 指定挂载到哪个元素上data: {                // 数据对象message: "Hello Vue"}})</script>
</body>
</html>
那么什么是DOM呢?DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
Vue 3 中如何构建 Vue 实例
<script>// 使用 vue3 构建应用程序// 方式二// 获得vue的应用实例const app = Vue.createApp({data() {return {message: "Hello Vue"}}})// 将实例挂载到容器上// 获得了根组件实例const vm = app.mount("#root")
</script>
在Vue3中,使用虚拟DOM是Vue的优点之一。因为DOM的操作是非常损耗性能的,不再使用原生的DOM操作节点,这样极大解放DOM操作,虽然操作的还是DOM,但是换了一种形式。相较于React而言,同样是操作虚拟DOM,Vue的性能依旧有优势。
那么虚拟DOM又是什么呢?本质上讲就是一个JS对象,通过对象的方式来表示DOM结构。有了虚拟DOM就可以更好的跨平台,因为页面的状态抽象为了JS对象,配合不同的渲染工具就可以实现跨平台渲染。同时还可以提高渲染性能,通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数。另外现代前端框架的一个要求就是无需手动操作DOM,省略手动DOM操作可以大大提高开发效率。
存储和显示vue数据
存储数据的实现
想要存储数据,可以通过data选项 + 键值对的方式实现。
显示数据的实现
- 使用插值表达式 {{}} 
使用插值表达式,可以data中的键值对数据以文本的形式插入模板。双大括号标签会被替换为data中对应属性的值。属性值更改页面也会同步更新。
这里有一个例子,比如 {{ name }} 就被替换为了刘备:

代码的运行结果如下:

但是我们可以观察发现,人民币符号没有正确显示。这是因为双大括号会将数据解释为纯文本,而不是HTML。¥是一个HTML特殊符号编码,不会被正确解析成¥。
- 解释插值表达式的HTML解析缺陷 - 使用 v-html 的数值指令进行绑定 
在Vue中,指令是带有特殊前缀 "v- " 的标签属性。v-html的作用是将表达式的值作为HTML进行解析。代码改造为:
<h1>薪水: <span v-html="salary"></span></h1>v-html的底层原理是:先移除节点下的所有节点,调用 HTML 方法,通过 addProp 添加 innerHTML 属性,本质还是设置 innerHTML 为 v-html 的值。
我们还想把博客地址改为超链接,初步的设想是这样的:
<h1>博客: <a href={{url}}>{{ url }}</a></h1>但是超链接并不能被访问,这是因为双大括号不能在标签的属性中使用:

- 解释插值表达式不能在标签属性中使用缺陷 - 使用 v-bind 的数值指令进行数据绑定 
v-bind 的作用是将data中的属性值与模板元素的属性值进行绑定。使用如下:
<h1>博客: <a v-bind:href="url">{{ url }}</a></h1>还可以简写,就是把 v-bind 省略掉:

我们想要的正确结果如下:

我们还想把日期进行格式化:

- 通过自定义方法实现日期格式化,通过字符串的 split 与 join 方法实现简单的日期格式转换: 
<script>const app = Vue.createApp({data() {return {name: "刘刘备",sex: "男",birthday: "1961.6.1",salary: "¥ 3000.00",url: "http://liubei.com"};},// 日期格式化方法// 先用"."分割,再用"-"拼接methods: {formatBirthday(bir) {return bir.split(".").join("-");}}});const vm = app.mount("#root");
</script>
<h1>生日: {{ formatBirthday(birthday) }}</h1>首先我们定义了一个格式化方法 formatBirthday(bir) ,该方法执行了两个字符串操作:
- split(".") :将输入的字符串 bir 按照 . 进行分割,生成一个数组。 - 例如:"1961.6.1".split(".") → ["1961", "6", "1"] 
 
- join("-"):将数组中的元素用 - 连接成一个新的字符串。 - 例如:["1961", "6", "1"].join("-") → "1961-6-1" 
 
然后在 HTML 模板中调用:
Vue 会调用 formatBirthday 方法,传入 birthday 的值。
扩展:data为什么是一个函数而不是对象?
可以看一下下面的这个例子:
<!DOCTYPE html>
<html>
<head><title>data 作为对象:多个组件实例共享同一数据(错误)</title><meta charset="UTF-8"><script src="https://cdn.staticfile.net/vue/3.3.4/vue.global.min.js"></script>
</head>
<body><div id="app"><counter></counter><counter></counter><counter></counter></div><script>const { createApp } = Vue;// ❌ 错误:data 使用了对象字面量const sharedData = { count: 0 }; // 共享的数据对象const Counter = {template: `<div><p>计数: {{ count }}</p><button @click="increment">+1</button></div>`,// ❌ data 返回的是同一个对象引用data() {return sharedData; // 所有实例都引用同一个对象!},methods: {increment() {this.count++;}}};const app = createApp({});app.component('counter', Counter);app.mount('#app');</script>
</body>
</html>在这个例子中,三个 <counter> 组件显示的 count 值完全同步,点击任意一个按钮所有三个组件的数字会一起增加。这是因为他们的data都返回了同一个sharedData对象的引用,造成了状态污染。
这就说明,如果data是一个对象,所有的组件实例会共享同一个数据对象,一个实例修改数据,其他的实例也会跟着变。
正确的应该是这样:
        // 定义一个 Counter 组件const Counter = {template: `<div><p>计数: {{ count }}</p><button @click="increment">+1</button></div>`,// ✅ data 是一个函数,每次实例化时返回一个新的对象data() {return {count: 0}},methods: {increment() {this.count++;}}};为什么使用函数在每次实例化时都会返回一个新对象呢?这是因为采用函数形式定义,在 initData 时会将其作为工厂函数返回全新 data 对象,能够有效规避多实例之间的状态污染问题。
生命周期函数
mounted函数
首先看一个例子,以解释钩子函数:
<!DOCTYPE html>
<html>
<head><title>案例:数字从1到10重复变化</title><meta charset="UTF-8"><script src="https://cdn.staticfile.net/vue/3.3.4/vue.global.min.js"></script>
</head>
<body><div id="root"><h1>{{ num }}</h1></div><script>const app = Vue.createApp({data() {return {num: 0}},// 当vue完成模板解析,把初始的DOM元素放入页面后调用// 这个过程就叫挂载完毕mounted() {setInterval(() => {if(this.num === 10) {this.num = 0;}this.num++;}, 1000);}});const vm = app.mount("#root");// setInterval(() => {//     if(vm.num == 10) {//         vm.num = 0;//     }//     vm.num++;// }, 500);</script>
</body>
</html>
综上所述,mounted 钩子的作用是:标志组件已经挂载到页面上,DOM已经可用。Vue完成模板的解析,并把初始的真实DOM元素放入页面后调用。而且也是个回调函数,会被自动调用。
Vue的生命周期
Vue的生命周期本质上就是一系列函数,也可以叫生命周期回调/钩子函数。
总览
| 生命周期阶段 | 钩子函数与作用 | 
| 初始化阶段 | beforeCreate: 无法访问data中的数据、methods中的方法 created: 可以访问data中的数据、methods中的方法;初始化data、methods等 | 
| 挂载阶段 | beforeMount: 页面中的DOM未经过Vue编译 mounted: 页面中的DOM经过Vue编译;将虚拟DOM转为真实DOM插入页面 | 
| 更新阶段 | beforeUpdate: 数据是新的,但页面是旧的 updated: 数据是新的,页面是新的 | 
| 卸载阶段 | beforeUnmount: Vue实例准备卸载,但DOM元素仍然存在 unmounted: Vue实例已卸载,相关的DOM元素被销毁 | 
Vue2 与 Vue3 的表述是有差异的,可以看下表:
| Vue 2 生命周期钩子 | Vue 3 对应写法 | 
| beforeCreate | setup(() => {}) | 
| created | setup(() => {}) | 
| beforeMount | onBeforeMount(() => {}) | 
| mounted | onMounted(() => {}) | 
| beforeUpdate | onBeforeUpdate(() => {}) | 
| updated | onUpdated(() => {}) | 
| beforeDestroy | onBeforeUnmount(() => {}) | 
| destroyed | onUnmounted(() => {}) | 
| activated | onActivated(() => {}) | 
| deactivated | onDeactivated(() => {}) | 
| errorCaptured | onErrorCaptured(() => {}) | 
初始化与挂载

更新与卸载

created 与 mounted 的区别
| 钩子函数 | 调用时机 | 主要特点与常见用途 | 
| created | 在模板渲染成 HTML 之前调用 | 数据已初始化:可以访问和操作 data、methods 等。 DOM 未生成:无法操作 DOM 元素。 常用用途:发起异步请求获取数据、初始化组件内部状态。 | 
| mounted | 在模板渲染成 HTML 之后调用 | DOM 已挂载:可以直接操作 DOM 节点。 页面已初始化:确认整个组件已被渲染到页面中。 常用用途:集成需要 DOM 的第三方库、执行 DOM 操作、获取渲染后的元素尺寸。 | 
例子
在这个例子中,代码中写上 debugger,就相当于打断点。
<!DOCTYPE html>
<html>
<head><title>Vue Example</title><script src="https://cdn.staticfile.net/vue/3.3.4/vue.global.min.js"></script>
</head>
<body><div id="root"><h1>{{ name }}说:"{{ say() }}"</h1></div><script>const app = Vue.createApp({data() {return {name: "刘备",sex: "男",age: 35}},methods: {say() {return "Hello";}},beforeCreate() {console.log("beforeCreate");console.log(this);debugger;},created() {console.log("created");// console.log(this);// debugger;},beforeMount() {console.log("beforeMount");// console.log(this);// debugger;},mounted() {console.log("mounted");// console.log(this);// debugger;},beforeUpdate() {console.log("beforeUpdate");// console.log(this);// debugger;},update() {console.log("update");// console.log(this);// debugger;},beforeUnmount() {console.log("beforeUnmount");// console.log(this);// debugger;},unmounted() {console.log("unmounted");// console.log(this);// debugger;}});const vm = app.mount("#root");</script>
</body>
</html>将 beforeCreate(),created(),beforeMount() 的断点加上,可以看到页面的元素是无法显示的。但是 mounted() 的断点加上后,就能显示了。这就说明模板中的 html 渲染到了 html 页面中。
断点打到 beforeUpdate,可以看到数据是新的,页面是旧的;打到 updated 后,数据与页面都是新的。

断点打到 beforeUnmount,可以看到卸载前页面的DOM还在,卸载完后,页面上的DOM就没了。

使用指令进行条件渲染
v-show
v-show 可以根据一个条件来渲染数据。指令内容可以是以下三种:
- 布尔值 
- 返回布尔值的表达式 
- 返回布尔值的 data 数据 
v-show的底层原理是通过改变元素样式 display,实现元素的显示与隐藏。详细来讲就是 v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点属性中修改 show 属性值,也就是常说的display。其中 vnode(虚拟节点)是Vue中用来描述真实DOM节点的JavaScript对象,而 render(渲染)是指将vnode 转换为真实DOM节点并插入页面的过程。

下面是一个例子:
<div id="root"><!-- v-show指令方式一:根据布尔值,切换元素的显示与隐藏 --><!-- <h1 v-show="true">{{ name }}</h1> --><!-- v-show指令方式二:返回布尔值表达式,切换元素的显示与隐藏 --><!-- <h1 v-show="1 === 2">{{name}}</h1> --><h1 v-show="1 === 1">{{name}}</h1><!-- <h1 v-show="false">{{ name }}</h1> --><!-- v-show指令方式三:返回布尔值的data数据,切换元素的显示与隐藏 --><!-- <h1 v-show="isShow">{{ name }}</h1> --></div><script>const app = Vue.createApp({data() {return {// isShow: true,name: '张飞'}}});const vm = app.mount("#root");</script>通过控制台可以看到,当 isShow 切换为 false 后,display 的值变为了 none:


v-if
首先将之前的例子进行改造,可以发现两者作用都是差不多的;相比v-show,也没有出现在了真实的DOM结构当中。
<h1 v-if="isShow">{{ name }}</h1>
v-if 的底层原理是:调用 addIfCondition 方法,生成 vnode 时会忽略对应节点,render 时就不会渲染。简要来说就是根据条件的真假,决定要不要渲染这个元素。

v-if 与 v-show 的区别
- 从手段来看:v-if 是动态的向 DOM 树中添加或删除 DOM 元素,v-show 是通过设置 DOM 元素的 display 样式属性控制显隐 
- 从编译过程来看:v-if 切换有一个局部编译与卸载的过程,切换过程中合适的销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换 
- 从编译条件来看:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留 
- 从性能消耗来看:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗 
- 从使用场景来看:v-if 适合运营条件不大可能改变;v-show 适合频繁切换 

同时从Vue性能优化的角度来讲,在更多的情况下应该使用v-if替代v-show。
v-else
会意一下就知道怎么用了。
<body><div id="root"><h1>{{ name }}的成绩是{{ score }}</h1><h1 v-if="score >= 90">优秀</h1><h1 v-else-if="score >= 80">良好</h1><h1 v-else-if="score >= 70">中等</h1><h1 v-else-if="score >= 60">及格</h1><h1 v-else>不及格</h1></div><script>const app = Vue.createApp({data() {return {name: '张飞',score: 100}}});const vm = app.mount("#root");</script>
</body>
<template>标签的使用
如果想让重复条件多次出现,可以这么写:
<div id="root"><h1>如果张飞的成绩在90以上,则把名字显示三遍</h1><div v-if="score >= 90"><h2>{{ name }}</h2><h2>{{ name }}</h2><h2>{{ name }}</h2></div>
</div>但是会破坏原有的DOM结构:

但是如果改成这样(就是使用<template>标签):
<div id="root"><h1>如果张飞的成绩在90以上,则把名字显示三遍</h1><template v-if="score >= 90"><h2>{{ name }}</h2><h2>{{ name }}</h2><h2>{{ name }}</h2></template>
</div>div就不会被渲染,可以看作是一个空白标签:

<template>是一个不可见的包装器元素,最后渲染的结果并不会包含这个元素。<template>不能使用v-show。
那为什么我们要避免破坏原有的DOM结构呢?
- 插入额外的div可能会破坏CSS选择器的层级关系,比如: 
/* 原始CSS设计 */
.container > .content {padding: 20px;background: #f5f5f5;
}
<div class="container"><div v-if="showContent">  <!-- 这个多余的div --><div class="content">实际内容</div></div>
</div>那么在渲染时:
<div class="container"><div> <!-- 破坏层级!.content不再是.container的直接子元素 --><div class="content">实际内容</div></div>
</div>- 不必要的DOM节点会增加内存占用,影响浏览器渲染性能 
