回顾 Vue 基础

127 阅读13分钟

MV*的理解

1、概念

在计算机编程领域,MV*(也称为 MVC、MVP、MVVM 等)是一种用于组织和设计应用程序结构的模式。这些模式旨在实现应用程序的解耦、可维护性和可扩展性。MV 代表着 Model-View-(表示控制器或视图模型等其他组件)的缩写,其中可以根据具体的模式而变化

2、MVC(Model-View-Controller):

模型(Model):负责处理应用程序的数据逻辑,通常包含数据的获取、处理和存储。 视图(View):负责展示数据给用户,通常是用户界面的组件。 控制器(Controller):负责处理用户输入,并根据输入更新模型和视图。它充当了模型和视图之间的中介者。

3、MVP(Model-View-Presenter):

模型(Model):同样负责处理应用程序的数据逻辑,与 MVC 中的模型类似。 视图(View):负责展示数据给用户,但通常比 MVC 中的视图更为被动,不直接处理用户输入。 主持人(Presenter):充当了控制器的角色,处理用户输入并根据输入更新模型和视图。与控制器不同的是,它更紧密地与视图交互,可以通过接口直接与视图进行交互。

4、MVVM(Model-View-ViewModel):

模型(Model):同样负责处理应用程序的数据逻辑,与 MVC 和 MVP 中的模型类似。 视图(View):负责展示数据给用户,通常是用户界面的组件。 视图模型(ViewModel):是连接视图和模型的中介者。它从模型中获取数据,并将数据转换成视图所需的格式,同时处理用户输入,并将用户操作传递给模型。视图模型使得视图与模型的交互解耦,让视图能够更专注于展示数据

声明式及与传统 DOM 开发对比

1、什么叫声明式开发

声明式编程是一种编程范型,与命令式编程相对立。它描述目标性质,让计算机明白目标,而非流程。声明式编程不用告诉电脑问题领域,从而避免随之而来的副作用。而指令式编程专指需要用算法来明确的指出每一步该怎么做。

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统

2、与传统 DOM 开发对比

在 div 中写入你好 vue

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app"></div>
        <script>
            document.getElementById("app").innerHTML = "你好vue";
        </script>
    </body>
</html>

编写第一个 vue 代码并实现 hello word

<template>
    <div>
        {{ message }}
    </div>
</template>
<script>
export default {
    data() {
        return {
            message: "你好 vue",
        };
    },
};
</script>

Vue 基础语法

vue 语法分为选项式 API(Option api)和组合式 api(Composition Api),我们以选项式 Api 入门

基本构成

template、script、style 三部分构成。template 可以理解成编写 html 的地方,script 编写逻辑 js 的地方,style 编写样式的地方

Vue 的插值表达式

概念

vue 中,使用{{}}双花括号,在 html 标签的 '内容区域' 中表现数据,这个技术称为插值表达式。 表达式:变量、常量、算术运算符、比较运算符、逻辑运算符、三元运算符等等。我们通过把{{}}里面的内容称作组件的状态 Vue

<template>
    <div>
        {{ message }}
    </div>
</template>

如何使用定义状态并在 template 中显示

<template>
    <div>
        <!-- 花括号中可以直接展示这个状态 -->
        {{ message }}
    </div>
</template>
<script>
/**
 * 在script中使用export default 导出一个对象
 * 在对象里面定义一个函数 data
 * 在 data 函数里面return 一个 对象
 * 在 return 的这个对象里面 可以直接定义当前组件的状态
 */
export default {
    data() {
        return {
            // 定义的message状态
            message: "你好 vue",
        };
    },
};
</script>

如何展示当前的这个页面(组件)

在入口文件 main.js 中引入

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";

/**
 * 把引入的组件直接放到createApp中
 */
createApp(App).mount("#app");

条件渲染

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 的时候被渲染。可以和正常使用 if else 嵌套(不推荐)

<template>
    <div>
        <div v-if="isShow">出现吗</div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isShow: false,
        };
    },
};
</script>

v-show

v-show 和 v-if 的用法几乎一致(v-show 没有嵌套),不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 display 属性

<template>
    <div>
        <div v-show="isShow">显示吗</div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isShow: false,
        };
    },
};
</script>

v-if 与 v-show 如何选择

  • v-if 因为每次都会彻底删除 DOM 或者 不渲染 DOM, 所以适用于展示隐藏切换不频繁的时候
    • v-if 的值如果为 false, 那么 DOM 直接不加载
  • v-show 只是修改元素 DOM 的 display 样式, 所以一般适用于展示隐藏切换频繁的时候
    • v-show 的值如果为 false, 那么 DOM 还会加载 只不过 display 的值为 none

Vue 属性绑定

属性指的是 Html 元素的属性,例如 a 标签的 title 就是一个属性!

  • vue 如何动态绑定属性
    • Vue 绑定属性一般是在属性名后面加 : 也就是 v-bind(不推荐使用 v-bind),当使用 : 绑定 html 元素的属性后,属性就相当于参数,属性值为预期值,一般在实际应用中,: 绑定的属性值都是通过计算得来 或者 是一个通过判断时刻变化的值。该操作也叫动态属性在组件传值中经常使用。
<template>
    <div>
        <a :href="url">百度</a>
    </div>
</template>

<script>
export default {
    data() {
        return {
            url: "https://www.baidu.com",
        };
    },
};
</script>

Vue 处理样式

内联

<template>
    <div style="color:'red'">红色</div>
</template>

在 style 中编写

  1. 创建 xxx.css 文件
.red {
    color: red;
}
  1. 在文件中引入
<template>
    <div class="red">红色</div>
</template>
<script>
import "./learn-style.css";
</script>

样式污染

  • 产生原因
    • Vue 最终编译打包后都在一个 html 页面中,如果在两个组件中取一样类名分别引用在自身,那么后者会覆盖前者。默认情况下,只要导入了组件,不管组件有没有显示在页面中,组件的样式就会生效。也就是说并没有自己的局部作用域
  • 解决思路
    • 手动处理 (起不同的类名, 但项目大了之后就会导致类名很乱, 不利于团队协作)
    • CSS IN JS: 以 js 的方式处理 css (推荐)
  • CSS IN JS
    • CSS IN JS 是使用 JS 编写 CSS 的统称, 用来解决 CSS 样式冲突, 覆盖等问题
    • CSS IN JS 的具体实现有 50 多种
      • React 常用: CSS Modules、styled-components
      • Vue 常用: <style scoped> 、css modules
    • 推荐使用: <style scoped> (脚手架自动继承, 并且非常简单)

Scoped CSS

  • 基本使用

    在 style 标签上使用 scoped,当 <style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。

<template>
    <div class="ex">hi</div>
</template>
<style scoped>
.ex {
    color: red;
}
</style>
  • 原理解析
    • 每个 Vue 文件都将对应一个唯一的 id, 该 id 根据文件路径名和内容 hash 生成, 通过组合形成 scopeId
    • 编译 template 标签时, 会为每个标签添加了当前的 scopeId
      <div class="demo">test</div>
      // 会被编译成:
      <div class="demo" data-v-12e4e11e>test</div>
      
    • 编译 style 标签时, 会根据当前组件的 scopeId 通过属性选择器和组合选择器输出样式
      .demo {
          color: red;
      }
      // 会被编译成:
      .demo[data-v-12e4e11e] {
          color: red;
      }
      
  • 深层原理

    • vue-loader 通过生成 哈希 ID,根据 type 的不同, 调用不同的 loader 将 哈希 ID 分别注入到 DOM 和属性选择器中。实现 CSS 局部作用域的效果。CSS Scoped 可以算作为 Vue 定制的一个处理原生 CSS 作用域的解决方案
  • 混用本地和全局样式

    • 可以直接创建一个全局的 css 文件, 在入口文件处引入, 或者在单个组件内使用不加 scoped 的 style
    <style>
    /* 全局样式 */
    </style>
    
    <style scoped>
    /* 本地样式 */
    </style>
    
  • 样式穿透

    • 如果你的引入了第三方库,如果你想修改第三方库的样式,直接通过 dom 查找,修改样式是没有效果的。那么可以使用以下属性
      • >>>
      • /deep/
      • ::v-deep
    <style lang="scss" scoped>
    .box-card {
        >>> .el-card__body {
            padding: 10px;
        }
    }
    </style>
    <style lang="scss" scoped>
    .box-card {
        /deep/.el-card__body {
            padding: 10px;
        }
    }
    </style>
    <style lang="scss" scoped>
    .box-card {
        ::v-deep.el-card__body {
            padding: 10px;
        }
    }
    </style>
    
  • 注意

    1. 通过 v-html 创建的 DOM 内容不受 scoped 样式影响,但是你仍然可以通过深度作用选择器来为他们设置样式
    2. Scoped 样式不能代替 class。考虑到浏览器渲染各种 CSS 选择器的方式,当 p { color: red } 是 scoped 时 (即与特性选择器组合使用时) 会慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red },性能影响就会消除

Vue 中的事件

定义事件

Vue 元素的事件处理和 DOM 元素的很相似, 但是语法有一些区别 使用 @ 修饰符(v-on:的缩写) + 事件名的方式给 DOM 添加事件后面跟方法名, 方法名可以直接加括号(@click='add(可以书写实参)') 里面进行传参, 对应的事件处理函数必须在 methods 对象中定义

<template>
    <div>
        <!-- 在button上定义点击事件 -->
        <button @click="hello('传入的参数')">你好</button>
    </div>
</template>
<script>
export default {
    /**
     * methods 在vue定义 方法的属性对象
     * 所有的方法都必须在methods里面定义
     */
    methods: {
        hello(msg) {
            console.log("事件触发啦哈哈哈");
            console.log(msg);
        },
    },
};
</script>

事件修饰符

为了更好的处理时间, Vue3 提供了一些便利的事件修饰符, 事件修饰符可以用于改变默认事件行为, 限制事件触发条件等 如: .stop, .prevent, .capture, .self, .once ......

  • .stop

    • 阻止事件冒泡, 即停止事件在父元素中的传播
    <template>
        <div class="box" @click="handle2">
            <div class="box2" @click="handle"></div>
        </div>
    </template>
    
    <script>
    export default {
        methods: {
            handle() {
                console.log("触发");
            },
    
            handle2() {
                console.log("冒泡");
            },
        },
    };
    </script>
    
  • .prevent

    • 阻止事件的默认行为, 如提交表单或点击链接后的页面跳转
    <template>
        <!-- 只触发点击事件,不触发跳转 -->
        <a href="https://www.baidu.com" @click.prevent="handle">百度</a>
    </template>
    
    <script>
    export default {
        methods: {
            handle() {
                console.log("触发");
            },
        },
    };
    </script>
    
  • .once

    • 只触发一次事件处理方法, 之后解绑事件
    <template>
        <button @click.once="handle">点击一次就失效</button>
    </template>
    
    <script>
    export default {
        methods: {
            handle() {
                console.log("触发");
            },
        },
    };
    </script>
    

event 对象

  1. 默认传入获取 event
<template>
    <!-- 
        如果事件什么都不传、并且不写()
        那么事件处理函数会默认接收到event对象
   -->
    <button @click="handle">点击</button>
</template>

<script>
export default {
    methods: {
        handle(event) {
            console.log(event);
        },
    },
};
</script>
  1. 携带其他参数获取 event
<template>
    <!-- 
        在 template 里面使用 $event 获取当前事件的 event 对象
   -->
    <button @click="handle('第一个参数', $event)">点击</button>
</template>

<script>
export default {
    methods: {
        handle(msg, event) {
            console.log(event);
        },
    },
};
</script>

在函数内使用 this 获取当前 Vue 上下文

  • 可以直接使用 this.xx 使用 data 里定义的状态,或者使用 this.xx()调用 methods 里面定义的其他函数
  • 注意:this 指向问题
<template>
    <button @click="handle">点击</button>
</template>

<script>
export default {
    data() {
        return {
            num: 1,
        };
    },

    methods: {
        handle() {
            console.log(this.num);
            this.handle2();
        },
        handle2() {
            console.log("第二个方法");
        },
    },
};
</script>

Vue 定义组件状态

概念

存放当前组件状态的地方,组件所有的状态数据都可以放到 data 里面存储,在 data 里定义的数据具备响应式。在组件中 data 只能是函数。

定义状态并驱动视图 (点击按钮 num + 1)

<template>
    <div>
        {{ num }}
        <button @click="handle">num+1</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 1,
        };
    },

    methods: {
        handle() {
            // 可以直接使用this拿到当前组件的状态并修改,视图会自动重新渲染
            this.num += 1;
        },
    },
};
</script>

数据修改是同步还是异步

  1. 思考以下代码会打印什么内容 (同步更新数据, 异步更新 dom)
<template>
    <div>
        <button id="btn" @click="handle">{{ num }}</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 1,
        };
    },

    methods: {
        handle() {
            this.num += 1;
            console.log("num的值", this.num);
            console.log(
                "按钮里的数据",
                document.getElementById("btn").innerHTML
            );
        },
    },
};
</script>
  1. nextTick

    在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

<template>
    <div>
        <button id="btn" @click="handle">{{ num }}</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 1,
        };
    },

    methods: {
        async handle() {
            this.num += 1;
            console.log("num的值", this.num);
            await this.$nextTick();
            console.log(
                "按钮里的数据",
                document.getElementById("btn").innerHTML
            );
        },
    },
};
</script>
  1. 原理
    • Vue 是异步执行 dom 更新的,一旦观察到数据变化,Vue 就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 DOM 操作。而在下一个事件循环时,Vue 会清空队列,并进行必要的 DOM 更新。
    • 当你设置 this.num = '新的数据',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的 DOM 更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。
    • 为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)/this.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用
  2. 使用场景
    1. Vue 生命周期的 created() 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick() 的回调函数中
    2. 当项目中你想在改变 DOM 元素的数据后基于新的 DOM 做点什么,对新 DOM 一系列的 js 操作都需要放进 Vue.nextTick() 的回调函数中

Vue 渲染列表 + key 的作用

列表渲染

Vue 中使用 v-for 指令进行列表渲染

<template>
    <div>
        <!-- item 代表 当前循环的每一项 -->
        <!-- index 代表 当前循环的下标-->
        <!-- 注意:必须要加key-->
        <div v-for="(item, index) in arr" :key="index">{{ item }}</div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            arr: [1, 2, 3, 4],
        };
    },
};
</script>

为什么循环的时候需要加 key

  1. 作用
    • key 的作用主要是为了高效的更新虚拟 DOM,提高渲染性能。
    • key 属性可以避免数据混乱的情况出现。
  2. 原理
    • vue 实现了一套虚拟 DOM,使我们可以不直接操作 DOM 元素只操作数据,就可以重新渲染页面,而隐藏在背后的原理是高效的 Diff 算法
    • 当页面数据发生变化时,Diff 算法只会比较同一层级的节点;
    • 如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点后面的子节点;如果节点类型相同,则会重新设置该节点属性,从而实现节点更新
    • 使用 key 给每个节点做一个唯一标识,Diff 算法就可以正确识别此节点,"就地更新"找到正确的位置插入新的节点
  3. 注意
    • key 的值只能是字符串或数字类型
    • key 的值必须具有唯一性(即:key 的值不能重复)
    • 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
    • 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性,实际项目中如果没有 id,推荐使用 index)
    • 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)

为什么 index 不能当作 key

  • 因为 index 只是代表数组中的位置
  • 当我们删除了数组中位置靠前或者中间的某一个元素
  • 此时会出现数组塌陷的情况, 也就是被删除的元素后的所有元素的下标都发生了变化
  • 如果 key 值发生变化, 那么页面的标签就一定会被重新渲染
  • 所以我们只是删除了某一个对象, 但是会导致这个对象后续的所有标签重新渲染
<template>
    <div>
        <div v-for="(item, index) in infos" :key="index">
            {{ item.name }} --- <button @click="del(index)">删除</button>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            infos: [
                {
                    name: "张三",
                    age: 18,
                },
                {
                    name: "李四",
                    age: 20,
                },
                {
                    name: "王五",
                    age: 21,
                },
            ],
        };
    },

    methods: {
        del(index) {
            this.infos.splice(index, 1);
        },
    },
};
</script>

实现简易购物车