Skip to content

前言

以下文章将讲解对 vue 的基本使用以及各种高级特性还有周边插件 vuexvue-router ,融合大量案例 🌰 和动图 🕹️ 进行展示。可以把它当成是 vue 的入门宝库,有不懂的语法知识点时或许在这里可以寻找到你的答案并且通过例子运用起来。

废话不多说,下面来开始探索 vue 的奥秘吧 🙆

一、vue 的使用

1、vue-cli

vue 项目是基于 vue-cli 脚手架搭建的项目。当我们要创建一个项目时,首先要先全局安装 vue-cli 脚手架,命令行为:

bash
npm i -g @vue/cli
npm i -g @vue/cli

在搭建完成项目以后,我们需要来了解 src 目录下各个文件夹和文件的用法。

bash
├── assets 放置静态资源
├── components 放组件
├── router 定义路由的相关配置
├── views 视图
├── views 视图
├── app.vue 应用主组件
├── main.js 入口文件
├── assets 放置静态资源
├── components 放组件
├── router 定义路由的相关配置
├── views 视图
├── views 视图
├── app.vue 应用主组件
├── main.js 入口文件

2、基本使用

(1)模板(插值,指令)

1)插值、表达式

html
<template>
  <div>
    <p>文本插值 {{message}}</p>
    <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        message: 'I am Monday.',
        flag: true,
      };
    },
  };
</script>
//浏览器显示结果 // 文本插值 I am Monday. // JS 表达式 yes
<template>
  <div>
    <p>文本插值 {{message}}</p>
    <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        message: 'I am Monday.',
        flag: true,
      };
    },
  };
</script>
//浏览器显示结果 // 文本插值 I am Monday. // JS 表达式 yes

2)动态属性

html
<template>
  <div>
    <p :id="dynamicId">动态属性:{{dynamicId}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        dynamicId: `id-${Date.now()}`,
      };
    },
  };
</script>

//浏览器显示结果 //动态属性:id-1622339576875
<template>
  <div>
    <p :id="dynamicId">动态属性:{{dynamicId}}</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        dynamicId: `id-${Date.now()}`,
      };
    },
  };
</script>

//浏览器显示结果 //动态属性:id-1622339576875

3)v-html 指令

html
<template>
  <div>
    <p v-html="rawHtml">
      <span>有 xss 风险</span>
      <span>【注意】使用 v-html 之后,将会覆盖子元素</span>
    </p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
      };
    },
  };
</script>

//浏览器显示结果 //指令 - 原始 html 加粗 斜体
<template>
  <div>
    <p v-html="rawHtml">
      <span>有 xss 风险</span>
      <span>【注意】使用 v-html 之后,将会覆盖子元素</span>
    </p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
      };
    },
  };
</script>

//浏览器显示结果 //指令 - 原始 html 加粗 斜体

值得注意的是, v-html 指令会有 xss 风险,且会覆盖子组件,要谨慎使用!!

(2)computed 和 watch

1)computed

computed 有缓存data 不变则不会重新计算。如下代码所示:

html
<template>
  <div>
    <p>num {{num}}</p>
    <p>double1 {{double1}}</p>
    //用v-model一定要有get和set,否则会报错
    <input v-model="double2" />
  </div>
</template>

<script>
  export default {
    data() {
      return {
        num: 20,
      };
    },
    computed: {
      double1() {
        return this.num * 2;
      },
      double2: {
        // 获取值
        get() {
          return this.num * 2;
        },
        // 设置值
        set(val) {
          this.num = val / 2;
        },
      },
    },
  };
</script>
<template>
  <div>
    <p>num {{num}}</p>
    <p>double1 {{double1}}</p>
    //用v-model一定要有get和set,否则会报错
    <input v-model="double2" />
  </div>
</template>

<script>
  export default {
    data() {
      return {
        num: 20,
      };
    },
    computed: {
      double1() {
        return this.num * 2;
      },
      double2: {
        // 获取值
        get() {
          return this.num * 2;
        },
        // 设置值
        set(val) {
          this.num = val / 2;
        },
      },
    },
  };
</script>

此时浏览器的打印结果如图所示。

computed

大家可以看到,当 num 的值如果一直是 20 的值时,那么 double1double2get() 方法是不会重新计算的,它会被缓存下来,达到提高运算性能的效果。

2)watch

  • watch 监听基本数据类型时,可正常拿到 oldValval 的值。
  • watch 如果监听引用数据类型时,需要进行深度监听,且拿不到 oldVal 。因为指针相同,监听时,指针已经指向了新的 val

如下代码所示:

html
<template>
  <div>
    <input v-model="name" />
    <input v-model="info.city" />
  </div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'Monday',
        info: {
          city: 'FuZhou',
        },
      };
    },
    watch: {
      name(oldVal, val) {
        // eslint-disable-next-line
        console.log('watch name', oldVal, val); // 值类型,即基本数据类型,可正常拿到 oldVal 和 val
      },
      info: {
        handler(oldVal, val) {
          // eslint-disable-next-line
          console.log('watch info', oldVal, val); // 引用数据类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
        },
        deep: true, // 深度监听
      },
    },
  };
</script>
<template>
  <div>
    <input v-model="name" />
    <input v-model="info.city" />
  </div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'Monday',
        info: {
          city: 'FuZhou',
        },
      };
    },
    watch: {
      name(oldVal, val) {
        // eslint-disable-next-line
        console.log('watch name', oldVal, val); // 值类型,即基本数据类型,可正常拿到 oldVal 和 val
      },
      info: {
        handler(oldVal, val) {
          // eslint-disable-next-line
          console.log('watch info', oldVal, val); // 引用数据类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
        },
        deep: true, // 深度监听
      },
    },
  };
</script>

(3)class 和 style

  • 使用动态属性,即 v-bind 绑定;

  • 使用驼峰式写法。

如下代码所示:

html
<template>
  <div>
    <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
    <p :class="[black, yellow]">使用 class (数组)</p>
    <p :style="styleData">使用 style</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        isBlack: true,
        isYellow: true,

        black: 'black',
        yellow: 'yellow',

        styleData: {
          fontSize: '40px', // 转换为驼峰式
          color: 'red',
          backgroundColor: '#ccc', // 转换为驼峰式
        },
      };
    },
  };
</script>

<style scoped>
  .black {
    background-color: #999;
  }
  .yellow {
    color: yellow;
  }
</style>
<template>
  <div>
    <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
    <p :class="[black, yellow]">使用 class (数组)</p>
    <p :style="styleData">使用 style</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        isBlack: true,
        isYellow: true,

        black: 'black',
        yellow: 'yellow',

        styleData: {
          fontSize: '40px', // 转换为驼峰式
          color: 'red',
          backgroundColor: '#ccc', // 转换为驼峰式
        },
      };
    },
  };
</script>

<style scoped>
  .black {
    background-color: #999;
  }
  .yellow {
    color: yellow;
  }
</style>

此时浏览器的显示效果如下。

class和style

(4)条件

  • v-ifv-else 的用法:可使用变量,也可以使用 === 表达式。
  • v-ifv-show 的区别?
    • v-if 会根据条件对元素进行渲染的控制,此处的控制渲染指的是将元素添加到 dom 中或移除 dom ,所以会存在 dom 的增删。假设页面初始化时,条件判断结果为 false ,则不会将该元素添加到 dom 当中。
    • v-showv-if 不一样的是,不管条件是 true 还是 false ,都会将对应的元素添加到 dom 中,在条件为 false 时将元素的 css 属性display设置为none,所以我们只是肉眼看到元素从页面中消失了,但是它还存在于 dom 当中。
  • v-ifv-show 的使用场景?
    • v-if 适用于条件变化频率不高的时候,这样不会频繁的去对 DOM 进行增删操作;
    • v-show 适用于需要做频繁切换的时候,比如说 A 和 B 两个元素,需要一会显示 A ,一会显示 B ,这样就算比较频繁。如果去操作 v-if ,让 DOM 疯狂的增添和销毁是会非常耗费性能的,所以这个时候就应该用 v-showdisplay 属性原本就已经存放在 DOM 当中,只是对 display 属性进行修改修做即可。

具体使用方式如下代码所示:

html
<template>
  <div>
    <p v-if="type === 'a'">A</p>
    <p v-else-if="type === 'b'">B</p>
    <p v-else>other</p>

    <p v-show="type === 'a'">A by v-show</p>
    <p v-show="type === 'b'">B by v-show</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        type: 'a',
      };
    },
  };
</script>
<template>
  <div>
    <p v-if="type === 'a'">A</p>
    <p v-else-if="type === 'b'">B</p>
    <p v-else>other</p>

    <p v-show="type === 'a'">A by v-show</p>
    <p v-show="type === 'b'">B by v-show</p>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        type: 'a',
      };
    },
  };
</script>

(5)循环(列表)渲染

  • vue 中如何遍历对象? —— 使用 v-for

  • v-forv-if 不能一起使用。

  • key 的重要性。在对数据进行 v-for 遍历时,需要加上 key 值来保证数据的唯一性。同时, key 不能乱写,一般不要使用 random 或者 index ,而是绑定一个与该绑定数据相关的唯一值来确定。如果用 random 或者 index 时,在对数据进行删除或增添操作时,有可能会出现数据错乱等问题出现。

  • key 的作用。在对节点进行 diff 的过程中,判断两个节点是否为相同节点的一个很重要的条件就是 key 值是否相等,如果 key 值相等,则说明两个节点是相同节点,所以会尽可能的复用原有的 DOM 节点,减少不必要的 DOM 操作,提升系统性能。

下面用一段代码演示 v-for 遍历数组和遍历对象时的效果。

html
<template>
  <div>
    <p>遍历数组</p>
    <ul>
      <li v-for="(item, index) in listArr" :key="item.id">
        {{index}} - {{item.id}} - {{item.title}}
      </li>
    </ul>

    <p>遍历对象</p>
    <ul>
      <li v-for="(val, key, index) in listObj" :key="key">
        {{index}} - {{key}} - {{val.title}}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        listArr: [
          { id: 'a', title: '今天周一' }, // 在数据结构中,最好有 id ,目的是为了方便使用 key
          { id: 'b', title: '今天周二' },
          { id: 'c', title: '今天周三' },
        ],
        listObj: {
          a: { title: '今天周一' },
          b: { title: '今天周二' },
          c: { title: '今天周三' },
        },
      };
    },
  };
</script>
<template>
  <div>
    <p>遍历数组</p>
    <ul>
      <li v-for="(item, index) in listArr" :key="item.id">
        {{index}} - {{item.id}} - {{item.title}}
      </li>
    </ul>

    <p>遍历对象</p>
    <ul>
      <li v-for="(val, key, index) in listObj" :key="key">
        {{index}} - {{key}} - {{val.title}}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        listArr: [
          { id: 'a', title: '今天周一' }, // 在数据结构中,最好有 id ,目的是为了方便使用 key
          { id: 'b', title: '今天周二' },
          { id: 'c', title: '今天周三' },
        ],
        listObj: {
          a: { title: '今天周一' },
          b: { title: '今天周二' },
          c: { title: '今天周三' },
        },
      };
    },
  };
</script>

此时浏览器的显示效果如下所示:v-for

(6)事件

1)event 参数,自定义参数

下面通过绑定一个两个按钮事件,来观察 vue 中的事件,参数时怎么进行传递的,事件又会被绑定到哪里去?

html
<template>
  <div>
    <!-- <p>{{num}}</p> -->
    <!-- event参数 -->
    <button @click="increment1">+1</button>
    <!-- 自定义参数 -->
    <button @click="increment2(2, $event)">+2</button>
    <div>递增值:{{num}}</div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        num: 0,
      };
    },
    methods: {
      increment1(event) {
        console.log('event', event, event.__proto__.constructor); // 是原生的 event 对象
        console.log(event.target); //vue中的event,放在什么元素下,就会被挂载在什么元素下
        console.log(event.currentTarget); // 注意,事件是被注册到当前元素的,和 React 不一样
        this.num++;

        // 1. event 是原生的
        // 2. 事件被挂载到当前元素
        // 和 DOM 事件一样
      },
      increment2(val, event) {
        console.log(event.target);
        this.num = this.num + val;
      },
    },
  };
</script>
<template>
  <div>
    <!-- <p>{{num}}</p> -->
    <!-- event参数 -->
    <button @click="increment1">+1</button>
    <!-- 自定义参数 -->
    <button @click="increment2(2, $event)">+2</button>
    <div>递增值:{{num}}</div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        num: 0,
      };
    },
    methods: {
      increment1(event) {
        console.log('event', event, event.__proto__.constructor); // 是原生的 event 对象
        console.log(event.target); //vue中的event,放在什么元素下,就会被挂载在什么元素下
        console.log(event.currentTarget); // 注意,事件是被注册到当前元素的,和 React 不一样
        this.num++;

        // 1. event 是原生的
        // 2. 事件被挂载到当前元素
        // 和 DOM 事件一样
      },
      increment2(val, event) {
        console.log(event.target);
        this.num = this.num + val;
      },
    },
  };
</script>

大家可以看到,上面的 increment1 方法,没有传递参数,则它可以直接使用 event 来触发当前对象;而 increment2 方法中,通过传递了一个 2 的参数,我们可以通过 $event 的方式,把 2 传递进来,之后同样用 event 触发原生对象。

同时,当触发到当前元素时,事件会被挂载到当前元素,和 DOM 事件一样。

此时浏览器的打印效果如下:

$event

2)自定义事件

上面的演示中, vue 事件是直接挂载到 vue 上面进行监听的。而下面的代码中,我们可以看到,如果我们需要自定义一个事件,而不进行直接挂载,则可以通过 addEventListener 来进行注册。

但值得注意的是,用 vue 绑定的事件,组件销毁时会自动进行解绑。但是呢,如果是自己定义的事件需要自己再进行手动销毁!!这一点需要特别注意,不然写代码过程很容易因为没有销毁热导致后面引发的一系列 bug 出现。

js
<template>
    <div>

    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 但如果是,自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>
<template>
    <div>

    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 但如果是,自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>

3)事件修饰符

对于 vue 来说,经常用到的修饰符有下面6 种事件修饰符3 种按键修饰符

html
<!-- 事件修饰符 -->

<!-- 阻止单击事件继续传播 -->
<a v-on:stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 事件修饰符 -->

<!-- 阻止单击事件继续传播 -->
<a v-on:stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
html
<!-- 按键修饰符 -->

<!-- 即使Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有ctrl被按下时候才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click,exact="onClick">A</button>
<!-- 按键修饰符 -->

<!-- 即使Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有ctrl被按下时候才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click,exact="onClick">A</button>

(7)表单

在 vue 项目中,主要使用 v-model 指令在表单 inputtextareacheckboxradioselect 等元素上创建双向数据绑定

对于 v-model 来说,常见的修饰符有 trimlazynumbertrim 表示去除输入内容前后的空格, lazy 表示需要按回车键数据才会进行创建, number 表示把用户输入的值转换为 number 类型,如果先输入字符,则整串数据都会被当成字符串 string 类型看待,如果先输入数字,后输入字母,则后面的字母无法输入到输入框当中。

接下来我们用代码演示一遍常见的表单。

html
<template>
  <div>
    <p>输入框: {{name}} {{age}}</p>
    <!-- .trim去除前后空格 -->
    <input type="text" v-model.trim="name" />
    <!-- .lazy需要按回车键数据才会起作用 -->
    <input type="text" v-model.lazy="name" />
    <!-- .number表示把用户输入的值转为number类型 -->
    <input type="text" v-model.number="age" />
    <hr />
    <p>多行文本: {{intro}}</p>
    <textarea v-model="intro"></textarea>
    <!-- 注意,<textarea>{{intro}}</textarea> 是不允许的!!! -->
    <hr />
    <p>复选框 {{checked}}</p>
    <input type="checkbox" v-model="checked" />
    <hr />
    <p>多个复选框 {{checkedNames}}</p>
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames" />
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
    <label for="mike">Mike</label>
    <hr />
    <p>单选 {{gender}}</p>
    <input type="radio" id="male" value="male" v-model="gender" />
    <label for="male">男</label>
    <input type="radio" id="female" value="female" v-model="gender" />
    <label for="female">女</label>
    <hr />
    <p>下拉列表选择 {{selected}}</p>
    <select v-model="selected">
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <hr />
    <p>下拉列表选择(多选) {{selectedList}}</p>
    <select v-model="selectedList" multiple>
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'Monday',
        age: 18,
        intro: '自我介绍',

        checked: true,
        checkedNames: [],

        gender: 'female',

        selected: '',
        selectedList: [],
      };
    },
  };
</script>
<template>
  <div>
    <p>输入框: {{name}} {{age}}</p>
    <!-- .trim去除前后空格 -->
    <input type="text" v-model.trim="name" />
    <!-- .lazy需要按回车键数据才会起作用 -->
    <input type="text" v-model.lazy="name" />
    <!-- .number表示把用户输入的值转为number类型 -->
    <input type="text" v-model.number="age" />
    <hr />
    <p>多行文本: {{intro}}</p>
    <textarea v-model="intro"></textarea>
    <!-- 注意,<textarea>{{intro}}</textarea> 是不允许的!!! -->
    <hr />
    <p>复选框 {{checked}}</p>
    <input type="checkbox" v-model="checked" />
    <hr />
    <p>多个复选框 {{checkedNames}}</p>
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames" />
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
    <label for="mike">Mike</label>
    <hr />
    <p>单选 {{gender}}</p>
    <input type="radio" id="male" value="male" v-model="gender" />
    <label for="male">男</label>
    <input type="radio" id="female" value="female" v-model="gender" />
    <label for="female">女</label>
    <hr />
    <p>下拉列表选择 {{selected}}</p>
    <select v-model="selected">
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <hr />
    <p>下拉列表选择(多选) {{selectedList}}</p>
    <select v-model="selectedList" multiple>
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        name: 'Monday',
        age: 18,
        intro: '自我介绍',

        checked: true,
        checkedNames: [],

        gender: 'female',

        selected: '',
        selectedList: [],
      };
    },
  };
</script>

此时浏览器的显示效果如下:

表单

3、组件

(1)props 和$emit(适合父子组件间的通信)

传递形式通信方式
$emit子组件向父组件传递数据
props父组件的数据需要通过 props 把数据传给子组件,props 的取值可以是数组也可以是对象

(2)组件间通信 - 自定义事件

除了父子组件通信外,一般还有兄弟间的组件通信。对于兄弟间的组件通信来说, 兄弟1 可以通过 event.$on 来绑定自定义事件,绑定完成之后,需要再 beforeDestroy 的生命周期里面用 event.$off及时销毁自定义事件, 防止内存泄漏发生。之后 兄弟2 通过 event.$emit 来调用 兄弟1 的自定义事件。

下面我们结合父子组件通信和兄弟组件通信的知识点,来实现一个添加和删除元素的功能。具体代码如下:

父组件 index.vue:

html
<template>
  <div>
    <!-- 当前使用是父组件,背后的组件是子组件,即input.vue和List.vue是子组件 -->
    <input @add="addHandler" />
    <List :list="list" @delete="deleteHandler" />
  </div>
</template>

<script>
  import Input from './Input';
  import List from './List';

  export default {
    components: {
      Input,
      List,
    },
    data() {
      return {
        list: [
          {
            id: 'id-1',
            title: '标题1',
          },
          {
            id: 'id-2',
            title: '标题2',
          },
        ],
      };
    },
    methods: {
      addHandler(title) {
        this.list.push({
          id: `id-${Date.now()}`,
          title,
        });
      },
      deleteHandler(id) {
        // 过滤出不等于当前删除内容的元素
        this.list = this.list.filter((item) => item.id !== id);
      },
    },
  };
</script>
<template>
  <div>
    <!-- 当前使用是父组件,背后的组件是子组件,即input.vue和List.vue是子组件 -->
    <input @add="addHandler" />
    <List :list="list" @delete="deleteHandler" />
  </div>
</template>

<script>
  import Input from './Input';
  import List from './List';

  export default {
    components: {
      Input,
      List,
    },
    data() {
      return {
        list: [
          {
            id: 'id-1',
            title: '标题1',
          },
          {
            id: 'id-2',
            title: '标题2',
          },
        ],
      };
    },
    methods: {
      addHandler(title) {
        this.list.push({
          id: `id-${Date.now()}`,
          title,
        });
      },
      deleteHandler(id) {
        // 过滤出不等于当前删除内容的元素
        this.list = this.list.filter((item) => item.id !== id);
      },
    },
  };
</script>

子组件 List.vue:

html
<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item.id">
        {{item.title}}

        <button @click="deleteItem(item.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script>
  import event from './event'

  export default {
      // props: ['list']
      props: {
          // prop 类型和默认值
          list: {
              type: Array,
              default() {
                  return []
              }
          }
      },
      data() {
          return {

          }
      },
      methods: {
          deleteItem(id) {
              this.$emit('delete', id)
          },
          addTitleHandler(title) {
              console.log('on add title', title)
          }
      },
      created() {
      },
      mounted() {
          // 绑定自定义事件
          //event是通过实例化一个vue对象,来调用vue中本身具有的自定义事件能力
          event.$on('onAddTitle', this.addTitleHandler)
      }
      beforeDestroy() {
          // 及时销毁,否则可能造成内存泄露
          event.$off('onAddTitle', this.addTitleHandler)
      }
  }
</script>
<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item.id">
        {{item.title}}

        <button @click="deleteItem(item.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script>
  import event from './event'

  export default {
      // props: ['list']
      props: {
          // prop 类型和默认值
          list: {
              type: Array,
              default() {
                  return []
              }
          }
      },
      data() {
          return {

          }
      },
      methods: {
          deleteItem(id) {
              this.$emit('delete', id)
          },
          addTitleHandler(title) {
              console.log('on add title', title)
          }
      },
      created() {
      },
      mounted() {
          // 绑定自定义事件
          //event是通过实例化一个vue对象,来调用vue中本身具有的自定义事件能力
          event.$on('onAddTitle', this.addTitleHandler)
      }
      beforeDestroy() {
          // 及时销毁,否则可能造成内存泄露
          event.$off('onAddTitle', this.addTitleHandler)
      }
  }
</script>

子组件 Input.vue:

html
<template>
  <div>
    <input type="text" v-model="title" />
    <button @click="addTitle">add</button>
  </div>
</template>

<script>
  import event from './event';

  export default {
    data() {
      return {
        title: '',
      };
    },
    methods: {
      addTitle() {
        // 调用父组件的事件
        this.$emit('add', this.title);

        // 调用自定义事件,调用List.vue中自定义的事件
        event.$emit('onAddTitle', this.title);

        this.title = '';
      },
    },
  };
</script>
<template>
  <div>
    <input type="text" v-model="title" />
    <button @click="addTitle">add</button>
  </div>
</template>

<script>
  import event from './event';

  export default {
    data() {
      return {
        title: '',
      };
    },
    methods: {
      addTitle() {
        // 调用父组件的事件
        this.$emit('add', this.title);

        // 调用自定义事件,调用List.vue中自定义的事件
        event.$emit('onAddTitle', this.title);

        this.title = '';
      },
    },
  };
</script>

event.vue:

js
import Vue from 'vue';

export default new Vue();
import Vue from 'vue';

export default new Vue();

此时浏览器的显示效果如下:

props-$emit

(3)组件生命周期

1)组件生命周期(单个组件)

一般来说,组件生命周期的执行顺序为:挂载阶段 → 更新阶段 → 销毁阶段。下面给出常用组件生命周期的解析。

生命周期钩子介绍
beforeCreate在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
created页面还没有渲染,但是 vue 的实例已经初始化结束。
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted页面已经渲染完毕。
beforeUpdate数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
updated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
activatedkeep-alive 组件激活时调用。
deactivatedkeep-alive 组件停用时调用。
beforeDestroy实例销毁之前调用。在这一步,实例仍然完全可用。常用场景有: 自定义事件的绑定要解除、setTimeout 等定时任务需要销毁、自己绑定的 window 或者 document 事件需要销毁。
destroyedVue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

2)组件生命周期(父子组件)

加载渲染过程

bash
  父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

子组件更新过程

bash
  父beforeUpdate->子beforeUpdate->子updated->父updated
  父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程

bash
  父beforeUpdate->父updated
  父beforeUpdate->父updated

销毁过程

bash
  父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
  父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

二、vue 的高级特性

vue 的高级特性主要有以下 6 种:

  • 自定义 v-model
  • $nextTick
  • slot
  • 动态、异步组件
  • keep-alive
  • mixin

下面对这 6 种高级特性进行一一讲解。

1、自定义 v-model

在文章的前半部分我们有讲到了 v-model 在表单中的应用,那么接下来我们将动手来实现一个 v-model

第一步,我们先定义一个子组件,名字叫 CustomVModel.vue ,具体代码如下:

html
<template>
  <!-- $emit是子组件往父组件传递数据 -->
  <input
    type="text"
    :value="text1"
    @input="$emit('change1', $event.target.value)"
  />
  <!--
        1. 上面的 input 使用了 :value 来绑定数据,而不是使用 v-model
        2. 上面的 change1 和 model.event 要对应起来
        3. 上面的 text1 与下面props的 text1 属性对应起来
    -->
</template>

<script>
  export default {
    model: {
      prop: 'text1', // 对应下面 props 的 text1
      event: 'change1',
    },
    props: {
      text1: String,
      default() {
        return '';
      },
    },
  };
</script>
<template>
  <!-- $emit是子组件往父组件传递数据 -->
  <input
    type="text"
    :value="text1"
    @input="$emit('change1', $event.target.value)"
  />
  <!--
        1. 上面的 input 使用了 :value 来绑定数据,而不是使用 v-model
        2. 上面的 change1 和 model.event 要对应起来
        3. 上面的 text1 与下面props的 text1 属性对应起来
    -->
</template>

<script>
  export default {
    model: {
      prop: 'text1', // 对应下面 props 的 text1
      event: 'change1',
    },
    props: {
      text1: String,
      default() {
        return '';
      },
    },
  };
</script>

第二步,我们在父组件中使用上面的这个子组件:

html
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <!-- 自定义 v-model -->
    <p>{{name}}</p>
    <CustomVModel v-model="name" />
  </div>
</template>

<script>
  import CustomVModel from './CustomVModel';

  export default {
    components: {
      CustomVModel,
    },
    data() {
      return {
        name: 'Monday',
      };
    },
  };
</script>
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <!-- 自定义 v-model -->
    <p>{{name}}</p>
    <CustomVModel v-model="name" />
  </div>
</template>

<script>
  import CustomVModel from './CustomVModel';

  export default {
    components: {
      CustomVModel,
    },
    data() {
      return {
        name: 'Monday',
      };
    },
  };
</script>

通过上面的代码我们可以发现,通过绑定 value 属性和 input 事件这两个语法糖,最终实现数据的双向绑定。

此时我们看下浏览器的显示效果。

自定义v-model

通过上图我们自己发现,结果跟实际的 v-model 结果是一样的。至此,我们就实现了自定义的 v-model ,以此来操作数据的双向绑定

2、$nextTick

(1)知识点

先来了解三个知识点:

  • Vue异步渲染
  • data 发生改变之后, DOM 不会立刻进行渲染;
  • $nextTick 会在 DOM 渲染之后被触发,以获取最新 DOM 节点。

(2)例子展示 🌰

假设我们现在要实现一个功能,当我们点击按钮时,打印出列表的项数。这个时候我们大多人可能会这么操作。

html
<template>
  <div id="app">
    <!-- ref的设置时为了方便后续可以用来:取节点的DOM元素 -->
    <ul ref="ul1">
      <li v-for="(item, index) in list" :key="index">{{item}}</li>
    </ul>
    <button @click="addItem">添加一项</button>
  </div>
</template>

<script>
  export default {
    name: 'app',
    data() {
      return {
        list: ['a', 'b', 'c'],
      };
    },
    methods: {
      addItem() {
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);

        // 获取 DOM 元素
        const ulElem = this.$refs.ul1;
        console.log(ulElem.childNodes.length);
      },
    },
  };
</script>
<template>
  <div id="app">
    <!-- ref的设置时为了方便后续可以用来:取节点的DOM元素 -->
    <ul ref="ul1">
      <li v-for="(item, index) in list" :key="index">{{item}}</li>
    </ul>
    <button @click="addItem">添加一项</button>
  </div>
</template>

<script>
  export default {
    name: 'app',
    data() {
      return {
        list: ['a', 'b', 'c'],
      };
    },
    methods: {
      addItem() {
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);

        // 获取 DOM 元素
        const ulElem = this.$refs.ul1;
        console.log(ulElem.childNodes.length);
      },
    },
  };
</script>

此时浏览器的显示效果如下。

nextTick

细心的小伙伴已经发现,浏览器并没有按照我们所想的打印。当页面上的列表显示 6项 内容时,此时控制台只打印 3项 ;当显示 9项 时,此时控制台直接只打印 6项

那这究竟时为什么呢?

其实,当我们点击的那一刻, data 发生变化,但是 DOM 并不会立刻进行渲染。所以等到我们点击完成的时候,获取的元素还是原来触发的内容,而不会增添上新的内容。

那我们所期望的是,当点击之后立刻触发 DOM 渲染并拿到最新的值。这个时候就需要用到 nextTick 。具体代码如下:

html
<script>
  export default {
    name: 'app',
    data() {
      return {
        list: ['a', 'b', 'c'],
      };
    },
    methods: {
      addItem() {
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);

        // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调,
        //    即NextTick函数会在多次data修改完并且全部DOM渲染完再触发,仅在最后触发一次
        // 2. 页面渲染时会将 data 的修改做整合
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1;
          console.log(ulElem.childNodes.length);
        });
      },
    },
  };
</script>
<script>
  export default {
    name: 'app',
    data() {
      return {
        list: ['a', 'b', 'c'],
      };
    },
    methods: {
      addItem() {
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);
        this.list.push(`${Date.now()}`);

        // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调,
        //    即NextTick函数会在多次data修改完并且全部DOM渲染完再触发,仅在最后触发一次
        // 2. 页面渲染时会将 data 的修改做整合
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1;
          console.log(ulElem.childNodes.length);
        });
      },
    },
  };
</script>

我们通过给获取 DOM 元素的代码外面再嵌套一层 $nextTick 函数,来达到我们想要的效果。在此过程中,当我们点击结束后,data 的值发生变化,此时 $nextTick 会等待 DOM 全部渲染完成之后再进行回调。

最终浏览器的打印效果如下:

nextTick2

3、slot 插槽

(1)插槽的作用

让用户可以拓展组件,去更好地复用组件和对其做定制化处理

(2)三种插槽类型

1)普通 slot 插槽

slot 的基本使用方法是,父组件往子组件中插入一段内容。

我们来演示一下。

假设我们现在有一个子组件,名字叫 SlotDemo.vue,它的代码如下。

html
<template>
  <a :href="url">
    <slot></slot>
  </a>
</template>

<script>
  export default {
    props: ['url'],
    data() {
      return {};
    },
  };
</script>
<template>
  <a :href="url">
    <slot></slot>
  </a>
</template>

<script>
  export default {
    props: ['url'],
    data() {
      return {};
    },
  };
</script>

我们可能希望这个 <a> 标签内绝大多数情况下都渲染文本 Sunday ,但是有时候却希望渲染文本不是 Sunday ,而是其他内容,那该怎么实现呢?

我们就可以将 Sunday 作为后备内容,这个时候把它放在 <slot> 标签内:

html
<a :href="url">
  <slot>Sunday</slot>
</a>
<a :href="url">
  <slot>Sunday</slot>
</a>

现在,我定义一个父级组件,名字叫 index.vue ,并且在父级组件中引用上面的 SlotDemo 组件,具体代码如下:

vue
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <SlotDemo :url="website.url"></SlotDemo>
  </div>
</template>

<script>
import SlotDemo from './SlotDemo';

export default {
  components: {
    SlotDemo,
  },
  data() {
    return {
      name: 'Monday',
      website: {
        url: 'https://blog.csdn.net/weixin_44803753',
        title: 'Monday',
        subTitle: '穿梭于前端开发的学习永动机',
      },
    };
  },
};
</script>
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <SlotDemo :url="website.url"></SlotDemo>
  </div>
</template>

<script>
import SlotDemo from './SlotDemo';

export default {
  components: {
    SlotDemo,
  },
  data() {
    return {
      name: 'Monday',
      website: {
        url: 'https://blog.csdn.net/weixin_44803753',
        title: 'Monday',
        subTitle: '穿梭于前端开发的学习永动机',
      },
    };
  },
};
</script>

这个时候后备内容 Sunday 就会被渲染出来。此时浏览器显示效果如下:

后备内容Sunday

那么如果想要把这个后备内容渲染成我们想要的内容,而不是 Sunday 呢,这个时候我们应该这么做:

vue
<SlotDemo :url="website.url"> {{website.title}} </SlotDemo>
<SlotDemo :url="website.url"> {{website.title}} </SlotDemo>

我们可以通过在父组件 index.vue 中引用的子组件 SlotDemo.vue 里面,插入我们想要的内容。这个时候子组件 SlotDemo.vue后备内容 Sunday 就会渲染成我们提供的新内容,以此来达到我们想要的目的。

此时浏览器的打印效果如下:

后备内容Monday

2)作用域插槽 slot-scope

vue 官方的说法给到的解释是:

  • 父组件模板的所有东西都会在父级作用域内编译;
  • 子组件模板的所有东西都会在子级作用域内编译。

这样看起来好像有点难懂,接下来我们用一个例子来了解作用域插槽想要解决的问题究竟是什么!

首先,我们先定义一个子组件,名字叫 ScopedSlotDemo.vue具体代码如下所示:

vue
<template>
  <a>
    <slot>
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://tinymce.ax-z.cn/',
        title: 'tinymce',
        subTitle: '一款易用、且功能强大的富文本编辑器',
      },
    };
  },
};
// 通过以上代码可以分析,这个时候我们想要把 `slot` 里面想要呈现的内容绑定下方的 `data` 数据,于是通过 `{{ website.subTitle }}` 引用。
</script>
<template>
  <a>
    <slot>
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://tinymce.ax-z.cn/',
        title: 'tinymce',
        subTitle: '一款易用、且功能强大的富文本编辑器',
      },
    };
  },
};
// 通过以上代码可以分析,这个时候我们想要把 `slot` 里面想要呈现的内容绑定下方的 `data` 数据,于是通过 `{{ website.subTitle }}` 引用。
</script>

接下来我们定义一个父组件,名字叫 index.vue ,父组件代码如下:

vue
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />
    <ScopedSlotDemo> {{ website.title }} </ScopedSlotDemo>
  </div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo';
export default {
  components: {
    ScopedSlotDemo,
  },
  data() {
    return {
      name: 'Monday',
    };
  },
};
</script>
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />
    <ScopedSlotDemo> {{ website.title }} </ScopedSlotDemo>
  </div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo';
export default {
  components: {
    ScopedSlotDemo,
  },
  data() {
    return {
      name: 'Monday',
    };
  },
};
</script>

我们需要在父级作用域 index.vue 中为我们要绑定的元素设置一个 v-slot ,这个 v-slot 用来定义我们提供的插槽 prop 的名字。

index.vue:

vue
<ScopedSlotDemo>
  <template v-slot="slotProps"> {{slotProps.slotData.title}} </template>
</ScopedSlotDemo>
<ScopedSlotDemo>
  <template v-slot="slotProps"> {{slotProps.slotData.title}} </template>
</ScopedSlotDemo>

那么父级定义完只是第一步,接下来我们要让子级 ScopedSlotDemo.vue 的插槽内容在父级当中可用,于是我们需要在子级的插槽中绑定一个自定义事件,并且指向我们需要的数据,如下代码所示:

ScopedSlotDemo.vue:

vue
<slot v-bind:slotData="website"> {{website.subTitle}} </slot>
<slot v-bind:slotData="website"> {{website.subTitle}} </slot>

从上面我们可以看到,我们通过绑定一个自定义事件 v-bind:slotData ,并把它指向我们想要的数据 website 。指向之后,我们在父级的插槽当中,通过 slopProps.slotData ,就可以访问到自己子级 website 的数据,这样,就达到最终我们想要的效果。

最终浏览器访问结果:

作用域插槽成功访问子级数据


最后贴上完整的父子级代码,大家可以根据以上讲解进行演示。

子级 ScopedSlotDemo.vue:

vue
<template>
  <a>
    <slot>
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://tinymce.ax-z.cn/',
        title: 'tinymce',
        subTitle: '一款易用、且功能强大的富文本编辑器',
      },
    };
  },
};
</script>
<template>
  <a>
    <slot>
      {{ website.subTitle }}
      <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
    </slot>
  </a>
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      website: {
        url: 'http://tinymce.ax-z.cn/',
        title: 'tinymce',
        subTitle: '一款易用、且功能强大的富文本编辑器',
      },
    };
  },
};
</script>

父级 index.vue:

vue
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <ScopedSlotDemo>
      <template v-slot="slotProps"> {{ slotProps.slotData.title }} </template>
    </ScopedSlotDemo>
  </div>
</template>

<script>
import ScopedSlotDemo from './ScopedSlotDemo';

export default {
  components: {
    ScopedSlotDemo,
  },
  data() {
    return {
      name: 'Monday',
      website: {
        url: 'https://blog.csdn.net/weixin_44803753',
        title: 'Monday',
        subTitle: '穿梭于前端开发的学习永动机',
      },
    };
  },
};
</script>
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <ScopedSlotDemo>
      <template v-slot="slotProps"> {{ slotProps.slotData.title }} </template>
    </ScopedSlotDemo>
  </div>
</template>

<script>
import ScopedSlotDemo from './ScopedSlotDemo';

export default {
  components: {
    ScopedSlotDemo,
  },
  data() {
    return {
      name: 'Monday',
      website: {
        url: 'https://blog.csdn.net/weixin_44803753',
        title: 'Monday',
        subTitle: '穿梭于前端开发的学习永动机',
      },
    };
  },
};
</script>

3)具名插槽

具名插槽的定义: 将父组件中的内容插入指定的子组件位置中。

接下来来看一下具名插槽是怎么使用的?

有时候我们一个组件里需要多个插槽。比如,现在我们有一个子组件,名字叫 named.vue ,具体代码如下:

vue
<template>
  <div>
    <header>
      <!-- 我们希望把标题放这里 -->
    </header>

    <main>
      <!-- 我们希望把主体内容放这里 -->
    </main>

    <footer>
      <!-- 我们希望把页脚放这里 -->
    </footer>
  </div>
</template>
<template>
  <div>
    <header>
      <!-- 我们希望把标题放这里 -->
    </header>

    <main>
      <!-- 我们希望把主体内容放这里 -->
    </main>

    <footer>
      <!-- 我们希望把页脚放这里 -->
    </footer>
  </div>
</template>

大家可以看到,上面的代码中,我们希望在 headermainfooter 当中放入对应模块的内容进去,这个时候就需要使用多个插槽来解决。


那怎么处理呢? 对于这样的情况, <slot> 元素有一个特殊的属性: name ,这个属性可以用来定义额外的插槽。我们通过使用 name 属性来对子组件 named.vue 的插槽加上属性名。具体做法如下:

vue
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>

    <main>
      <!-- 如果一个<slot>不带name属性的话,那么它的name默认为default -->
      <slot></slot>
    </main>

    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>

    <main>
      <!-- 如果一个<slot>不带name属性的话,那么它的name默认为default -->
      <slot></slot>
    </main>

    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

值得注意的是,如果一个 <slot> 不带 name 属性的话,那么它的 name 默认为 default


子级组件插入完插槽之后,接下来我们来定义一个父组件 index.vue 。在向子级组件的具名插槽提供内容的时候,我们可以在父级组件的 <template> 元素上使用 v-slot 指令,并以参数的形式传递具名插槽的名称。

index.vue:

vue
<template>
  <div>
    <template v-slot:header>
      <h1>这里是页面标题</h1>
    </template>

    <p>页面主题内容第一页</p>
    <p>页面主题内容第二页</p>

    <template v-slot:footer>
      <p>这里是页脚</p>
    </template>
  </div>
</template>
<template>
  <div>
    <template v-slot:header>
      <h1>这里是页面标题</h1>
    </template>

    <p>页面主题内容第一页</p>
    <p>页面主题内容第二页</p>

    <template v-slot:footer>
      <p>这里是页脚</p>
    </template>
  </div>
</template>

现在 <template> 元素中的所有内容都将会被传入到子组件相应的插槽当中,且任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容。

如果你希望更加明确一点的话,那就把主体内容包裹上一个插槽,并设置 name="default"

vue
<template v-slot:default>
  <p>页面主题内容第一页</p>
  <p>页面主题内容第二页</p>
</template>
<template v-slot:default>
  <p>页面主题内容第一页</p>
  <p>页面主题内容第二页</p>
</template>

这样看起来或许会更优雅一点。


最终打印结果如下:

这里是页面标题

页面主题内容第一页
页面主题内容第二页

这里是页脚
这里是页面标题

页面主题内容第一页
页面主题内容第二页

这里是页脚

讲完具名插槽,我们来对它的使用方法进行一个总结归纳。

具名插槽的使用语法:

  • 子组件定义 slot 时,在标签上加上 name='xxx' 属性,不写 name 则默认 name=default
  • 父组件将想插入的部分最外部的 div 上加上 v-slot="xxx" 属性, xxx 对应 name 的值。
  • v-slot: 可以简写为 # ,如 v-slot:header 相当于 #header

都说一个萝卜一个坑,具名插槽通过将父组件中的内容一个一个对应地插入到指定的子组件位置当中,把坑填上了,萝卜也就长出来了。

4、动态、异步组件

(1)动态组件

1)举个例子 🌰

比如我们在做某个新闻网站的文章详情页,那这个时候文章详情页显示的内容是非常多样化的。有可能是文本、视频和图片类型,也有可能是文本和图片类型,这个时候我么就没有办法确定这个组件想要的是什么类型的内容,于是我们据需要用到动态组件。

2)动态组件的用法和应用场景

  • 用法: :is = "component-name" 用法;
  • 应用场景: 需要根据 数据 进行动态渲染的场景,即组件类型不确定的场景。

3)演示

演示 1:

我们定义一个子组件,名字叫 Dynamic.vue ,具体代码如下:

vue
<template>
  <div>
    <p>动态组件子组件</p>
  </div>
</template>

<script>

export default {
    data() {
        reuturn{

        }
    }
}
</script>
<template>
  <div>
    <p>动态组件子组件</p>
  </div>
</template>

<script>

export default {
    data() {
        reuturn{

        }
    }
}
</script>

接下来定义一个父组件,名字叫 index.vue 。我们希望地引入子组件 Dynamic.vue 。因此,我们可以这样做:

vue
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <!-- 动态组件 -->
    <component :is="DynamicName" />
  </div>
</template>

<script>
import Dynamic from './Dynamic';
export default {
  components: {
    Dynamic,
  },
  data() {
    return {
      DynamicName: 'Dynamic',
    };
  },
};
</script>
<template>
  <div>
    <p>vue 高级特性</p>
    <hr />

    <!-- 动态组件 -->
    <component :is="DynamicName" />
  </div>
</template>

<script>
import Dynamic from './Dynamic';
export default {
  components: {
    Dynamic,
  },
  data() {
    return {
      DynamicName: 'Dynamic',
    };
  },
};
</script>

这个时候我们可以发现,通过在 data 中绑定组件的名字,之后在视图上用 is="ComponentName" 的形式进行引用,达到对组件进行动态绑定的效果。


演示 2:

假设我们要给某一个页面上的一组数据进行动态绑定,这个时候我们可以这么做:

vue
<template>
  <div>
    <!-- 动态组件 -->
    <div v-for="(val, key) in newsData" :key="key">
      <component :is="val.type" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newsData: {
        1: {
          type: 'text',
        },
        2: {
          type: 'text',
        },
        3: {
          type: 'image',
        },
      },
    };
  },
};
</script>
<template>
  <div>
    <!-- 动态组件 -->
    <div v-for="(val, key) in newsData" :key="key">
      <component :is="val.type" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newsData: {
        1: {
          type: 'text',
        },
        2: {
          type: 'text',
        },
        3: {
          type: 'image',
        },
      },
    };
  },
};
</script>

(2)异步组件

平常我们在开发中,通常用的是 import()函数 来引入一个组件,但是 import 虽然好用,可是大家有没有想过,如果当我们要引入一个比较大的组件时,也用 import 去引入的话,它会进行同步加载操作,这样子对性能优化用户体验来说是非常不友好的。

因此,我们要引入异步组件来解决 import 同步加载的问题。

我们来举个例子:

vue
<template>
  <div>
    <!-- 异步组件 -->
    <FormDemo v-if="showFormDemo" />
    <button @click="showFormDemo = true">show form demo</button>
  </div>
</template>

<script>
export default {
  components: {
    FormDemo: () => import('./FormDemo'),
  },
  data() {
    return {
      showFormDemo: false,
    };
  },
};
</script>
<template>
  <div>
    <!-- 异步组件 -->
    <FormDemo v-if="showFormDemo" />
    <button @click="showFormDemo = true">show form demo</button>
  </div>
</template>

<script>
export default {
  components: {
    FormDemo: () => import('./FormDemo'),
  },
  data() {
    return {
      showFormDemo: false,
    };
  },
};
</script>

通过上面的代码,我们可以了解到,通过控制 showFormDemo 的布尔值,来决定 FormDemo 组件在什么时候引入。因此,在 button 点击之前, FormDemo 的判断条件 showFormDemofalse ,这个时候组件不会被加载。只有当 button 点击之后, FormDemo 的判断条件 showFormDemo 变为 true,这个时候才按需将组件的内容加载出来。

通过以上分析,大家应该可以了解到, import 是同步加载,不管页面需不需要这个组件,都会在页面加载时就加载出来。

而如果将组件直接注册在 components 中,通过布尔值数据来控制组件是否加载,而不会从一开始就加载,这样的方式来加载组件,从性能上会好非常多。


因此,我们对异步组件做出以下总结:

  • 不适合用 import() 函数;
  • 按需加载,异步方式加载大组件;
  • 异步加载,即什么时候用什么时候加载,什么时候不用,就永远不会加载。

5、keep-alive

(1)定义和应用场景

keep-alive ,从字面意思来讲就是,保持它存活着,保持它不死亡。

我们先抛出 keep-alive定义和应用场景:

  • keep-alive 是一个缓存组件;
  • 应用于频繁切换的场景,用 keep-alive 就不需要重复渲染;
  • keep-alivevue 中常见的性能优化方法。

(2)举例 🌰

了解完定义和应用场景,我们来开始举例子看看这个缓存组件,到底是何方神圣,让人们在开发时都对之有所热爱。

首先,我们先来定义一个父组件,名字叫 keepAlive.vue ,具体代码如下:

keepAlive.vue:

vue
<template>
  <div>
    <button @click="changeState('A')">button A</button>
    <button @click="changeState('B')">button B</button>
    <button @click="changeState('C')">button C</button>

    <KeepAliveStageA v-if="state === 'A'" />
    <KeepAliveStageB v-if="state === 'B'" />
    <KeepAliveStageC v-if="state === 'C'" />
  </div>
</template>

<script>
import KeepAliveStageA from './KeepAliveStateA';
import KeepAliveStageB from './KeepAliveStateB';
import KeepAliveStageC from './KeepAliveStateC';

export default {
  components: {
    KeepAliveStageA,
    KeepAliveStageB,
    KeepAliveStageC,
  },
  data() {
    return {
      state: 'A',
    };
  },
  methods: {
    changeState(state) {
      this.state = state;
    },
  },
};
</script>
<template>
  <div>
    <button @click="changeState('A')">button A</button>
    <button @click="changeState('B')">button B</button>
    <button @click="changeState('C')">button C</button>

    <KeepAliveStageA v-if="state === 'A'" />
    <KeepAliveStageB v-if="state === 'B'" />
    <KeepAliveStageC v-if="state === 'C'" />
  </div>
</template>

<script>
import KeepAliveStageA from './KeepAliveStateA';
import KeepAliveStageB from './KeepAliveStateB';
import KeepAliveStageC from './KeepAliveStateC';

export default {
  components: {
    KeepAliveStageA,
    KeepAliveStageB,
    KeepAliveStageC,
  },
  data() {
    return {
      state: 'A',
    };
  },
  methods: {
    changeState(state) {
      this.state = state;
    },
  },
};
</script>

我们再来定义三个子组件,名字分别为 keepAliveStateA.vuekeepAliveStateB.vuekeepAliveStateC.vue 。 具体代码如下:

keepAliveStateA.vue:

vue
<template>
  <p>state A</p>
</template>

<script>
export default {
  mounted() {
    console.log('A mounted');
  },
  destroyed() {
    console.log('A destroyed');
  },
};
</script>
<template>
  <p>state A</p>
</template>

<script>
export default {
  mounted() {
    console.log('A mounted');
  },
  destroyed() {
    console.log('A destroyed');
  },
};
</script>

keepAliveStateB.vue:

vue
<template>
  <p>state B</p>
</template>

<script>
export default {
  mounted() {
    console.log('B mounted');
  },
  destroyed() {
    console.log('B destroyed');
  },
};
</script>
<template>
  <p>state B</p>
</template>

<script>
export default {
  mounted() {
    console.log('B mounted');
  },
  destroyed() {
    console.log('B destroyed');
  },
};
</script>

keepAliveStateC.vue:

vue
<template>
  <p>state C</p>
</template>

<script>
export default {
  mounted() {
    console.log('C mounted');
  },
  destroyed() {
    console.log('C destroyed');
  },
};
</script>
<template>
  <p>state C</p>
</template>

<script>
export default {
  mounted() {
    console.log('C mounted');
  },
  destroyed() {
    console.log('C destroyed');
  },
};
</script>

此时我们来看下浏览器的运行效果:

没有keep-alive组件时

通过以上动图可以发现,我们初始化时是状态 A ,之后当我们每次点击按钮时,编译器都会对这个组件进行销毁,再创建我们点击的新组件。这样反复的创建销毁,创建销毁,对用户的体验似乎是不太好的。因为用户每点击一个东西就得重新下载,如果遇到代码量很多的页面呢?那用户可能会从此把这个网站拉入黑名单了。

因此,我们可以使用 vue 中的 keep-alive ,来解决这个问题。我们想要达到的目的很简单,就是把用户每次点击后的内容给缓存下来,之后用户每次点击时就可以直接从缓存中拉取资源直接显示,而无需重新加载。

那么,怎么做呢?

很简单,我们只需要在父组件 keepAlive.vue 中引入的三个子组件外面,再包上一个缓存组件 keep-alive , 这样,就能达到我们想要的目的了。

vue
<keep-alive>
  <KeepAliveStageA v-if="state === 'A'" />
  <KeepAliveStageB v-if="state === 'B'" />
  <KeepAliveStageC v-if="state === 'C'" />
</keep-alive>
<keep-alive>
  <KeepAliveStageA v-if="state === 'A'" />
  <KeepAliveStageB v-if="state === 'B'" />
  <KeepAliveStageC v-if="state === 'C'" />
</keep-alive>

我们来看下此时浏览器的打印效果:

有keep-alive组件时

我们可以发现,有 keep-alive 时,浏览器直接把子组件的内容缓存下来了,当我们再次点击它时,就不会再进行重复的创建和销毁,直接从缓存中拉取资源,并加载到我们的页面中来。

6、mixin

(1)mixin 是什么

我们先来了解下 mixin 的定义:

  • 多个组件有相同的逻辑,抽离出来,当作 mixin 来使用。
  • mixin 并不是完美的解决方案,会存在一些小问题的出现。
  • Vue 3 提出的 Composition API 旨在解决这些问题。这里不对 Vue 3Composition API 进行细讲,将在后面的文章中做介绍。

(2)mixin 的问题

mixin 虽说可以用来抽离相同逻辑的组件,但是总会存在各种各样的问题。比如:

  • 变量来源不明确,不利于阅读。
  • 多个 mixin 可能会造成命名冲突
  • mixin 和组件可能出现多对多的关系,复杂度较高。

(3)举个栗子 🌰

我们先来定义一个组件,名字叫 MixinDemo.vue ,具体代码如下:

vue
<template>
  <div>
    <p>{{ name }}: {{ intro }}: {{ city }}</p>
    <button @click="showName">显示姓名</button>
  </div>
</template>

<script>
import myMixin from './mixin';

export default {
  mixins: [myMixin], // 可以添加多个,会自动合并起来
  data() {
    return {
      name: 'Monday',
      intro: '热爱前端的学习永动机',
    };
  },
  methods: {},
  mounted() {
    console.log('component mounted', this.name);
  },
};
</script>
<template>
  <div>
    <p>{{ name }}: {{ intro }}: {{ city }}</p>
    <button @click="showName">显示姓名</button>
  </div>
</template>

<script>
import myMixin from './mixin';

export default {
  mixins: [myMixin], // 可以添加多个,会自动合并起来
  data() {
    return {
      name: 'Monday',
      intro: '热爱前端的学习永动机',
    };
  },
  methods: {},
  mounted() {
    console.log('component mounted', this.name);
  },
};
</script>

再定义一个 js 文件,名字叫 mixin.js具体代码如下:

js
export default {
  data() {
    return {
      city: '广东',
    };
  },
  methods: {
    showName() {
      console.log(this.name);
    },
  },
  mounted() {
    console.log('mixin mounted', this.name);
  },
};
export default {
  data() {
    return {
      city: '广东',
    };
  },
  methods: {
    showName() {
      console.log(this.name);
    },
  },
  mounted() {
    console.log('mixin mounted', this.name);
  },
};

此时浏览器的演示效果如下:

mixin

我们来解析下上面这波操作。假设此时 js 文件的代码是多个组件中抽离出来的比较相似的代码部分,那我们我们对它进行抽离后并注册到 MixinDemo.vue 当中,用 mixins: [myMixin] 的方式进行引入。此时,我们在 MixinDemo.vue 组件中就可以访问到 mixin.js 里面的内容,这样就完成了一波逻辑复用。

mixin 看着是挺好用,但是它会存在各种各样的问题发生。这就谈到我们第二点中所说的。

我们来对问题进行一一剖析。

第一个,变量来源不明确,不利于阅读。 比如我们的第一块代码,里面有一个按钮绑定了 showName 的方法,此时 showName 的方法放在了 mixin 里面,而没有放在该页面中。假如我们定义了多个 mixin,那我们得对每个 mixin 一个一个地去找 showName 这个方法,因为我们也不知道 showName 来自于哪里,只能这样干。那这样子的话得有多麻烦,非常的影响开发效率。

第二个,多个 mixin 可能会造成命名冲突。 比如说当出现多个 mixin 时,里面的 data 绑定的属性一模一样,那这个时候就有可能出现覆盖的情况,也很不利于我们找错。

第三个, mixin 和组件可能出现多对多的关系,复杂度较高。 我们在开发过程中,有可能会遇到一个组件引入多个 mixin ,也有可能多个组件引入一个 mixin ,这样就是一个多对多的关系。这有一天如果某个组件想要改个属性值,但是它只能动 mixin 。这一动,其他组件就都被影响到了,很容易导致了剪不断理还乱的尴尬局面。

所以, mixin 有它的好处所在,也有它的问题所在。而 vue3 中的 composition API 就旨在解决这个问题。这里先给大家抛个概念, composition API 将在后面的文章中进行讲解。

三、vue 的周边插件:vuex 和 vue-router

1、vuex

(1)vuex 基本概念

  • state:单一状态树,用一个对象来包含全部的应用层级状态
  • getters: vuex 允许我们在 store 中定义 getter ,就像计算属性一样, getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变时,才会被重新计算。
  • action:异步逻辑封装;
  • mutation:更改状态的唯一方法,并且这个过程是同步的

(2)用于 vue 组件中的 API

  • dispatch
  • commit
  • mapState
  • mapGetters
  • mapActions
  • mapMutations

(3)图例

vuex

对于 vuex 来说,先理解上面这几个概念,后面还会有文章讲解关于 vuex 的实际应用。

2、vue-router

(1)路由模式

1)hash 模式(默认)

hash 模式,如 http://abc.com/#/user/10 。早期的前端路由就是基于 location.hash 来实现的。其实原理很简单, location.hash 的值就是 url# 后面的内容。比如下面这个网站,它的 location.hash 就是 #abc

js
https://www.baidu.com#abc
https://www.baidu.com#abc

2)H5 history 模式

history 模式,如 http://abc.com/user/20 ;与 hash 模式不同的是,这种模式需要 server 端支持,因此无特殊需求可选择前者

HTML5 提供了 History API 来实现 URL 的变化。其中最主要的 API 有以下两个。 history.pushState()history.replaceState() 。这两个 API 可以在不刷新的情况下,操作浏览器的历史记录。唯一不同的是,前者是新增一个记录,后者是直接替换当前的历史记录如下所示:

js
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

同时,对于 history 来说,如果刷新时,服务器没有响应的资源,会报错 404

(2)路由配置

1)动态路由

动态路径参数,以 冒号 : 开头进行路由配置。如下所示:

const User = {
    //获取参数如 3 5
    template: '<div>User {{$route.params.id}}</div>'
}

const router = new VueRouter({
    routes:[
        //动态路径参数 以冒号开头。能命中`/user/3` `/user/5` 等格式的路由
        {path: 'user/:id', component: User}
    ]
})
const User = {
    //获取参数如 3 5
    template: '<div>User {{$route.params.id}}</div>'
}

const router = new VueRouter({
    routes:[
        //动态路径参数 以冒号开头。能命中`/user/3` `/user/5` 等格式的路由
        {path: 'user/:id', component: User}
    ]
})

下面我们再来了解关于路由 vue-router 的传参方式。具体如下:

第一种: Params 传参。 params 传参具有以下特点:

  • 只能使用 name 来指定目标路由,不能使用 path
  • 参数不会显示在路径上;
  • 浏览器强制刷新时参数会被清空
//传递参数
this.$router.push({
    name: Home,
    params: {
        number: 1,
        code: '999'
    }
});

// 接收参数
const p = this.$route.params.code;
//传递参数
this.$router.push({
    name: Home,
    params: {
        number: 1,
        code: '999'
    }
});

// 接收参数
const p = this.$route.params.code;

第二种: Query 传参。 query 传参具有以下特点:

  • 参数会显示在路径上,刷新时不会被清空;
  • 使用 path 来指定目标路由。
//传递参数
this.$router.push({
    path: '/home/index',
    params: {
        number: 1,
        code: '999'
    }
});

// 接收参数
const p = this.$route.query.number;
//传递参数
this.$router.push({
    path: '/home/index',
    params: {
        number: 1,
        code: '999'
    }
});

// 接收参数
const p = this.$route.query.number;

2)懒加载

懒加载即对应一个 import() 函数,然后去导入一个组件。跟我们前面主题二的第 4 小点讲过的异步组件类似,当我们去访问一级二级等各种级别的的路由时,就会异步的去把对应的组件加载出来。不会出现一下子全部同时加载的问题,而是该加载时再加载,可以理解为我需要你的时候你再出现就好了示例代码如下所示:

js
export default new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./../compoents/Navigator'),
    },
    {
      path: '/detail',
      component: () => import('./../components/Detail'),
    },
  ],
});
export default new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./../compoents/Navigator'),
    },
    {
      path: '/detail',
      component: () => import('./../components/Detail'),
    },
  ],
});

3、导航钩子

vue-router 主要有三种导航钩子,分别是:全局导航钩子单独路由独享钩子组件内导航钩子。下面将依据这三种钩子分别展开讲解。

(1)全局导航钩子

全局导航钩子又分为两种:分别是全局前置首卫全局后置钩子。它们俩各自的用法如下 👇

1)全局前置守卫

全局前置守卫,即 beforeEach其用法如下:

const router = new VueRouter({});
router.beforeEach((to, from, next) => {
    // to do something
})
const router = new VueRouter({});
router.beforeEach((to, from, next) => {
    // to do something
})

下面对其中所涉及到的参数进行解析,具体如下:

  • to: Route 👉 代表要进入的目标,它是一个路由对象。
  • from: Route 👉 代表当前正要离开的路由,也是一个路由对象;
  • next: Function 👉 必须需要调用的方法,其具体执行的效果则依赖 next 方法调用的参数;
  • next() 👉 进入管道中的下一个钩子,如果全部的钩子执行完了,则导航的状态就是 confirmed ,表示确认的
  • next(false) 👉 终端当前的导航。如浏览器 url 改变,那么 url 将会充值到 from 路由对应的地址。
  • next('/') || next({path: '/'}) 👉 表示跳转到一个不同的地址,即当前导航终端将执行新的导航。

2)全局后置钩子

全局后置钩子,即 afterEach其用法如下:

const router = new VueRouter({});
router.afterEach((to, from) => {
    // to do something
})
const router = new VueRouter({});
router.afterEach((to, from) => {
    // to do something
})

值得注意的是,后置钩子并没有 next 函数,也不会改变导航本身。

(2)单独路由独享钩子

单独路由独享钩子,即 beforeEnter其用法如下:

const router = new VueRouter({
    routes: [
        {
            path: './home',
            component: Home,
            beforeEnter: (to, from, next) => {
                // to do something
                // 参数的用法与brforeEach一致
            }
        }
    ]
})
const router = new VueRouter({
    routes: [
        {
            path: './home',
            component: Home,
            beforeEnter: (to, from, next) => {
                // to do something
                // 参数的用法与brforeEach一致
            }
        }
    ]
})

值得注意的是, beforeEnter 中各参数的用法与上述中的全局前置守卫的用法一致。

(3)组件内导航钩子

组件内导航钩子,即 beforeRouteEnter其用法如下:

js
const Home = {
  template: `<div></div>`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件对应路由被confirm前被调用
    next((vm) => {
      // 不能获取组件实例 this,因为当守卫执行之前,组件的实例还没有被创建
      // 通过 vm 访问组件实例
    });
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是在该组件被复用时才调用
    // 例如:对于一个动态参数的路径 /home/:id,那么就是在 '/home/1' 和 '/home/2' 之间跳转的时候
    // 由于会渲染同样的 Home 组件,因此组件实例会被复用,而这个钩子就会在这个情况下被调用
    // 可以访问组件实例 this
  },
  beforeRouteLeave(to, from, next) {
    // 调用时间:导航离开该组件的对应路由时
    // 可以访问组件实例 this
  },
};
const Home = {
  template: `<div></div>`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件对应路由被confirm前被调用
    next((vm) => {
      // 不能获取组件实例 this,因为当守卫执行之前,组件的实例还没有被创建
      // 通过 vm 访问组件实例
    });
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是在该组件被复用时才调用
    // 例如:对于一个动态参数的路径 /home/:id,那么就是在 '/home/1' 和 '/home/2' 之间跳转的时候
    // 由于会渲染同样的 Home 组件,因此组件实例会被复用,而这个钩子就会在这个情况下被调用
    // 可以访问组件实例 this
  },
  beforeRouteLeave(to, from, next) {
    // 调用时间:导航离开该组件的对应路由时
    // 可以访问组件实例 this
  },
};

四、结束语

以上文章总结了 vue 的基本使用以及 vue 的高级特性,还有概括了 vue 的周边插件 vuexvue-router 的一些常见知识,融合大量案例进行讲解。

前端在做 vue 的项目中,总是脱离不开以上文章所涉及到的内容,唯一的区别在于用的多和用的少的问题。

希望通过以上文章的讲解,小伙伴们能有所收获 🥂

Released under the MIT License.