Java 程序员的 Vue 指南 - Vue 万字速览(01)

Vue的基本原理

当一个 Vue 组件被创建时,Vue 会查看你在 data 里定义的所有数据。在 Vue 2 中,它使用 Object.defineProperty(Vue 3 改用 Proxy)把这些数据变成"可监听"的------也就是说,Vue 能知道这些数据什么时候被读取,什么时候被修改。

同时,Vue 会为每个组件创建一个"观察者"(watcher),这个观察者的作用就像是一个记事员:当组件第一次显示(渲染)时,它会一边画界面,一边记下"我这里用到了哪些数据"。

之后,一旦这些数据发生了变化(比如你修改了一个变量),Vue 就能通过监听机制立刻知道:"哦,这个数据变了!"然后它会通知对应的观察者:"赶紧重新渲染一下组件!"

于是,组件就会自动更新界面,反映出最新的数据。整个过程是自动的,你只需要改数据,界面就会跟着变。

构建vue实例

Vue 2 中如何构建 Vue 实例

html 复制代码
<!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 实例

html 复制代码
<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。&yen是一个HTML特殊符号编码,不会被正确解析成¥。

  • 解释插值表达式的HTML解析缺陷 - 使用 v-html的数值指令进行绑定

在Vue中,指令是带有特殊前缀 "v- " 的标签属性。v-html的作用是将表达式的值作为 HTML 进行解析。代码改造为:

html 复制代码
<h1>薪水: <span v-html="salary"></span></h1>

v-html的底层原理是:先移除节点下的所有节点,调用 HTML 方法,通过 addProp 添加 innerHTML 属性,本质还是设置 innerHTML 为 v-html 的值

我们还想把博客地址改为超链接,初步的设想是这样的:

html 复制代码
<h1>博客: <a href={{url}}>{{ url }}</a></h1>

但是超链接并不能被访问,这是因为双大括号不能在标签的属性中使用

  • 解释插值表达式不能在标签属性中使用缺陷 - 使用 v-bind的数值指令进行数据绑定

v-bind 的作用是将data中的属性值与模板元素的属性值进行绑定。使用如下:

html 复制代码
<h1>博客: <a v-bind:href="url">{{ url }}</a></h1>

还可以简写,就是把 v-bind 省略掉:

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

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

  • 通过自定义方法实现日期格式化,通过字符串的 split 与 join 方法实现简单的日期格式转换:
html 复制代码
<script>
        const app = Vue.createApp({
            data() {
                return {
                    name: "刘刘备",
                    sex: "男",
                    birthday: "1961.6.1",
                    salary: "&yen 3000.00",
                    url: "http://liubei.com"
                };
            },
            // 日期格式化方法
            // 先用"."分割,再用"-"拼接
            methods: {
                formatBirthday(bir) {
                    return bir.split(".").join("-");
                }
            }
        });
        const vm = app.mount("#root");
</script>
html 复制代码
<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为什么是一个函数而不是对象?++

可以看一下下面的这个例子:

html 复制代码
<!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是一个对象,所有的组件实例会共享同一个数据对象,一个实例修改数据,其他的实例也会跟着变。

正确的应该是这样:

html 复制代码
        // 定义一个 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函数

首先看一个例子,以解释钩子函数:

html 复制代码
<!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,就相当于打断点。

html 复制代码
<!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节点并插入页面的过程。

下面是一个例子:

html 复制代码
<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 结构当中。

html 复制代码
<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

会意一下就知道怎么用了。

html 复制代码
<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>标签的使用

如果想让重复条件多次出现,可以这么写:

html 复制代码
<div id="root">
        <h1>如果张飞的成绩在90以上,则把名字显示三遍</h1>
        <div v-if="score >= 90">
            <h2>{{ name }}</h2>
            <h2>{{ name }}</h2>
            <h2>{{ name }}</h2>
        </div>
</div>

但是会破坏原有的DOM结构:

但是如果改成这样(就是使用<template>标签):

html 复制代码
<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选择器的层级关系,比如:
html 复制代码
/* 原始CSS设计 */
.container > .content {
  padding: 20px;
  background: #f5f5f5;
}
html 复制代码
<div class="container">
    <div v-if="showContent">  <!-- 这个多余的div -->
        <div class="content">实际内容</div>
    </div>
</div>

那么在渲染时:

html 复制代码
<div class="container">
    <div> <!-- 破坏层级!.content不再是.container的直接子元素 -->
        <div class="content">实际内容</div>
    </div>
</div>
  • 不必要的DOM节点会增加内存占用,影响浏览器渲染性能
相关推荐
小p7 小时前
react学习2:react中常用的hooks
前端·react.js
Xiaouuuuua7 小时前
2026年计算机毕业设计项目合集
前端·vue.js·课程设计
IT_陈寒7 小时前
React 18并发模式实战:3个优化技巧让你的应用性能提升50%
前端·人工智能·后端
@大迁世界7 小时前
我用 Rust 重写了一个 Java 微服务,然后丢了工作
java·开发语言·后端·微服务·rust
用户761736354017 小时前
CSS重点知识-样式计算
前端
yoyoma7 小时前
object 、 map 、weakmap区别
前端·javascript
himobrinehacken7 小时前
c语言宏注意事项
c语言·开发语言
shawn_yang7 小时前
实现公历和农历日期选择组件(用于选择出生日期)
vue.js·vant
shyshi7 小时前
vercel 部署 node 服务和解决 vercel 不可访问的问题
前端·javascript