<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="referrer" content="no-referrer" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- javascript -->
    <script type="module" src="./index.js"></script>
    <!-- 笔记中所有的代码都在 ./index.js 中, 然后用 vite 启动在浏览器中查看效果 -->
    <script
      src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.js"
      type="application/javascript"
    ></script>
  </body>
</html>在线文档 
Vue.js 2.x
Vue.js 3.x (推荐)
vue3 在线体验
模板 
v-bind : 
const app = new Vue({
  el: '#app',
  // v-bind 的缩写是 :
  // 一旦绑定后, vue 就会将msg替换成 data 中 msg 变量的值
  // :style 绑定样式 :calss 绑定css类名
  template: `
  <div>
    <p v-bind:data-a="msg">绑定普通属性</p>
    <p :data-b="msg">:缩写</p>
    <p :[attrName]="msg">属性名是变量, 如果是 falsy 无法绑定成功</p>
    <hr>
    <p :style="styleStr">字符串绑定style</p>
    <p :style="{color: '#f00'}">对象语法绑定style</p>
    <hr>
    <p :class="['text-red', 'bg-black']">数组绑定className</p>
    <p :class="{'text-red': true, 'bg-black': false}">对象绑定className</p>
  </div>
  `,
  data: () => ({
    styleStr: 'border: 1px solid #f00',
    attrName: 'data-c',
    msg: 'hello vue.js',
  }),
});
// 绑定 style
// 1. 可以拼接写行内样式字符串
// 2. 开始用对象的语法 {属性:"值"}
// 绑定 className
// 1. 使用数组语法, 将数组中所有的值都绑定到元素上
// 2. 使用对象语法, 只有属性值为 truely 才能绑定到元素上
// 绑定 className插值指令: v-text/v-html 
const app = new Vue({
  el: '#app',
  template: `
  <div>
    <p v-text="str">1234</p>
    <p v-html="str">1234</p>
  </div>
  `,
  data: () => ({
    str: '<span style="color:#f00;">text text text text text text text</span>',
  }),
});
// v-text 不会解析 HTML 字符串: 类似 el.textContent = str
// v-html 会解析 HTML 字符串: 类似 el.innerHTML = str双向绑定: v-model 
双向绑定就是当视图中的数据变化, data 中的数据也会发生变化, 如果 data 中的数据变化, 视图也会及时更新 官方文档
const app = new Vue({
  el: '#app',
  template: `
  <div>
    <input v-model="email" type="text" />
    <textarea v-model="intro"></textarea>
  </div>
  `,
  data: () => ({
    email: '',
    intro: '',
  }),
});
// 不能表单控件 vue.js 会使用不同的事件
// 同时忽略 value 和 selected 和 checked 等属性
// 这些可以在文档中找到条件渲染: v-if/v-show 
'use strict';
const app = new Vue({
  el: '#app',
  template: `
  <div>
    <p v-if="show">渲染</p>
    <p v-if="hide">不会渲染</p>
    <p v-show="show">显示出来</p>
    <p v-show="hide">不显示但是会渲染</p>
  </div>
  `,
  data: () => ({
    show: true, // 显示
    hide: false, // 隐藏
  }),
});
// v-if: 控制是否渲染(是否会创建dom元素)
// v-show: 控制是否显示(只是控制 display 属性的值,无论显示都会创建dom元素)
// 如果要频繁的控制是否需要显示, 建议使用 v-show, 因为他并不会销毁dom元素
// 如果只是控制一次, 不需要频繁的切换, 建议使用 v-if列表渲染: v-for 
v-for: 用于遍历数据, 然后渲染, 也就是所谓的: 列表渲染官方文档
const app = new Vue({
  el: '#app',
  template: `
  <div>
    <ul class="navbar-container">
      <li v-for="item of navs" :key="item.id" class="navbar-item"> 
        <a :href="item.href">{{ item.text }}</a>
      </li>
    </ul>
  </div>
  `,
  data: () => ({
    navs: [
      { id: 101, text: 'Vue.js', link: 'https://github.com/vuejs/vue' },
      {
        id: 102,
        text: 'VueRouter',
        link: 'https://github.com/vuejs/vue-router',
      },
      { id: 103, text: 'Vuex', link: 'https://github.com/vuejs/vuex' },
    ],
  }),
});
// 为什么要绑定 key 属性?
// 建议查看: https://vue3js.cn/interview/vue/key.html组件化 
组件注册 
注册组件和组件名的一些注意点 官方文档
// 注册全局组件(所有组件都可以使用): Vue.component(组件名, 组件)
// 注册局部组件(在哪个组件中注册就可以在哪个组件中使用): 在组件内部使用 components 选项
const Navs = {
  name: 'navbar',
  template: `<ul>
    <li>首页</li>
    <li>发现</li>
    <li>商城</li>
    <li>我的</li>
  </ul>`,
};
Vue.component('global-header', {
  template: `<div>
    <p>全局 header 组件</p>
    <navs />
  </div>`,
  // 只有这个注册了 Navs, 只有这个组件可以使用,
  // 在 global-footer 和 app 根组件中都无法使用
  components: { Navs },
});
Vue.component('global-footer', {
  template: `<div>全局 footer 组件</div>`,
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <global-header/>
    <global-footer/>
  </div>`,
});porps 
// 使用 props 的好处:
// 一个共用的模板/样式传入不同的数据,就能渲染出不同的东西, 高度复用
// 单向数据流:
// 子组件(navs), 无法改变父组件的数据, 利于调试
// 数据只能 父组件 -> 子组件(无法修改) 这样在开发是就知道数据在哪里修改的
const Navs = {
  template: `<ul>
    <li v-for="item of courseList" :key="item.id">
      {{ item.text }}
    </li>
  </ul>`,
  props: {
    courseList: {
      type: Array,
      required: true,
    },
  },
};
Vue.component('navs', Navs);
Vue.component('global-header', {
  template: `<div>
    <p>全局 header 组件</p>
    <navs :course-list="courseList" />
  </div>`,
  data: () => ({
    courseList: [
      { id: 101, text: 'js' },
      { id: 102, text: 'vue.js' },
    ],
  }),
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <global-header/>
    <navs :course-list="courses" />
  </div>`,
  data: () => ({
    courses: [
      { id: 201, text: 'css' },
      { id: 202, text: 'tialwind' },
    ],
  }),
});data 
定义响应式数据, 必须是一个函数, 不能是一个对象? 否则就报错, 为什么这么设计 查看
简而言之: 因为函数有单独的作用域, 不会导致组件互相影响, 比如: 在 app 根组件中, 用了两个 navs 组件(例如:n1, n2), 现在点击 n1 组件会导致数据变化, 如果是对象, 那么两个 navs 组件(n1, n2)都会发生变化, 这就不对了, 我只想要被点击 navs 组件(n1) 发生变化, n2 没被点击就不发生变化
// ${msg} 是解析变量
// {{msg}} 是插值表达式
let msg = 'hello';
Vue.component('navs', {
  template: `<div>
    <p>非响应式数据: ${msg}</p>
    <p>响应式数据: {{reactiveMsg}}</p>
    <button @click="f1">修改 msg</button>
  </div>`,
  data: () => ({
    reactiveMsg: 'world',
  }),
  // data: {
  //   reactiveMsg: "world",
  // },
  methods: {
    f1() {
      msg = msg.toUpperCase();
      this.reactiveMsg = this.reactiveMsg.toUpperCase();
    },
  },
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <navs />
    <hr/>
    <navs />
    <hr/>
    <navs />
  </div>`,
});methods 
事件监听函数
// @ 接收事件(包括原生的事件 & 自定义事件): 绑定处理函数
// this.$emit: 向父组件发送一个自定义事件
Vue.component('navs', {
  template: `<div>
    <button @click="handleClick">点击</button>
  </div>`,
  methods: {
    handleClick() {
      console.log('子组件被点击了, 向父组件发送一个自定义事件');
      // $emit(事件名, 携带的数据)
      // 携带的数据可以在父组件中接收到
      this.$emit('nav-click', { a: 1 });
    },
  },
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <button @click="f1">App</button>
    <navs @nav-click="f2" />
  </div>`,
  methods: {
    f1() {
      console.log('原生事件');
    },
    f2(e) {
      console.log('子组件发送的自定义事件');
      console.info(e); // 子组件传递的数据
    },
  },
});watch 
监听数据变化
const app = new Vue({
  el: '#app',
  template: `<div>
    <p>{{msg}}</p>
    <button @click="updateMsg">修改数据</button>
  </div>`,
  data: () => ({
    msg: 'hello world',
    info: {
      email: '',
    },
  }),
  methods: {
    updateMsg() {
      const randStr = Math.random().toString(16).substring(2);
      this.msg = randStr;
      this.info.email = randStr + '@foxmail.com';
    },
  },
  watch: {
    // 监听 msg 属性变化
    msg(newval, oldVal) {
      // newval: 修改后的值 oldval: 修改前的值
      console.log('newval, oldVal: ', { newval, oldVal });
    },
    // 监听 info 对象的 email 属性变化, 'info.email'
    'info.email'(newval, oldVal) {
      // 数据改变就会执行, 不管是否渲染到 模板中
      console.info('数据改变了');
    },
  },
});computed 
计算属性, 相对于 methods 来说具有缓存的作用
const app = new Vue({
  el: '#app',
  template: `<div>
    <p>计算属性: {{ fullName }}</p>
    <button @click="updateData">修改数据</button>
    <button @click="setFullName">设置计算属性</button>
  </div>`,
  data: () => ({
    firstName: 'hello',
    lastName: 'world',
  }),
  methods: {
    updateData() {
      this.firstName = 'Hello';
    },
    setFullName() {
      this.fullName += '( computed )';
    },
  },
  computed: {
    fullName: {
      get() {
        // 获取计算属性的时候调用: {{ fullName }}
        // 或者 依赖的数据更新就调用(firstName/lastName)
        console.info('getter');
        return this.firstName + '-' + this.lastName;
      },
      set(v) {
        // 设置 计算属性的时候会调用, this.fullName = xxx
        console.info('setter:', v);
      },
    },
  },
});lifecycle 生命周期(2.x 版本) 

- beforeCreate: 组件创建之前
- created: 组件创建之后
- beforeMount: 组件挂载之前(vnode 已经替换完成, 但是还没有渲染到页面上)
- mounted: 组件挂载之后, 可以获取到 dom
- beforeUpdate: 组件数据变化, 更新之前
- updated: 组件数据变化, 更新之后
- beforeDestroy: 组件销毁之前
- destroyed: 组件销毁之后
Vue.component('navs', {
  props: ['msg'],
  template: `<div> {{msg}} </div>`,
  beforeCreate() {
    console.info('beforeCreate');
  },
  created() {
    console.info('created');
  },
  beforeMount() {
    console.info('beforeMount');
  },
  mounted() {
    console.info('mounted');
  },
  beforeUpdate() {
    console.log('beforeUpdate');
  },
  updated() {
    console.log('updated');
  },
  beforeDestroy() {
    console.log('beforeDestroy');
  },
  destroyed() {
    console.log('destroyed');
  },
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <div v-if="show">
      <navs :msg="msg" />
    </div>
    <button @click="toggle">切换状态</button>
    <button @click="changeData">修改数据</button>
  </div>`,
  data: () => ({
    msg: 'message',
    show: true,
  }),
  methods: {
    changeData() {
      this.msg = Math.random().toString(16).substring(2);
    },
    toggle() {
      this.show = !this.show;
    },
  },
});lifecycle 生命周期(3.x 版本 hooks) 
vue3.x 这些 hooks 只能在 setup 函数中调用

const { reactive, onBeforeMount, onMounted, ojnBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } = Vue;
const Home = {
  template: `<div>
    <p>{{state.msg}}</p>
    <button @click="updateData">修改数据</button>
  </div>`,
  setup() {
    const state = reactive({
      msg: 'hello',
    });
    onBeforeMount(() => {
      console.log('beforeMount');
    });
    onMounted(() => {
      console.log('mounted');
    });
    onBeforeUpdate(() => {
      console.log('beforeUpdate');
    });
    onUpdated(() => {
      console.log('updated');
    });
    onBeforeUnmount(() => {
      console.log('beforeUnmount');
    });
    onUnmounted(() => {
      console.log('unmounted');
    });
    function updateData() {
      state.msg = Math.random().toString(16).substring(2);
    }
    return {
      state,
      updateData,
    };
  },
};
const App = {
  template: `<div>
    <div v-if="state.show">
      <home />
    </div>
    <button @click="toggle">切换组件是否渲染</button>
  </div>`,
  components: { Home },
  setup() {
    const state = reactive({
      show: true,
    });
    function toggle() {
      state.show = !state.show;
    }
    return {
      state,
      toggle,
    };
  },
};
Vue.createApp(App).mount('#app');插槽 
// 具名插槽: 有名字的插槽
// 默认插槽: 默认的插槽, 不需要指定插槽名称
Vue.component('base-layout', {
  template: `<div>
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>`,
});
const app = new Vue({
  el: '#app',
  template: `<div>
    <base-layout>
      <template v-slot:header> 这是头部 </template>
      <p>中间内容部分1</p>
      <p>中间内容部分2</p>
      <p>中间内容部分3</p>
      <template v-slot:footer> footer 底部 </template>
    </base-layout>
  </div>`,
  data: () => ({
    msg1: '<div>hello</div>',
    msg2: '<a href="https://github.com">github</a>',
  }),
  methods: {
    toggle() {
      this.show = !this.show;
    },
  },
});组件通信 
props + emit 
主要用于父子组件传值, 非常方便, 但是同级组件传值就维护非常麻烦
父组件给子组件传递数据(size 和 color): props
<template>
  <div>
    <h2>Props + Emit</h2>
    <div class="btns">
      <x-button @xbtn-click="handleClick" size="small">小按钮</x-button>
      <x-button @xbtn-click="handleClick" size="big">大按钮</x-button>
      <x-button @xbtn-click="handleClick" color="danger">中按钮</x-button>
      <x-button @xbtn-click="handleClick" color="info">中按钮</x-button>
      <x-button @xbtn-click="handleClick" color="warning">中按钮</x-button>
      <x-button @xbtn-click="handleClick" color="primary">中按钮</x-button>
      <x-button @xbtn-click="handleClick" color="success">中按钮</x-button>
    </div>
  </div>
</template>
<script>
  import XButton from './XButton.vue';
  export default {
    components: { XButton },
    methods: {
      handleClick(e) {
        console.info(e.target);
      },
    },
  };
</script>
<style lang="scss" scoped>
  .btns button {
    margin-right: 20px;
  }
</style>组建给父组件传递数据 $this.emit 发射自定义事件
在子组件中不要直接修改 props 数据, 这会破坏单向数据流的规范, 导致维护时, 不知道数据在哪个子组件中修改的, 而且, 直接修改 props vue 是会报错的
<template>
  <button :class="classNames" @click="emitClick">
    <slot>XButton</slot>
  </button>
</template>
<script>
  export default {
    name: 'XButton',
    props: [
      'size', // 控制按钮大小
      'color', // 控制按钮颜色
    ],
    methods: {
      emitClick(event) {
        this.$emit('xbtn-click', event);
      },
    },
    computed: {
      classNames() {
        const allowColors = ['info', 'danger', 'warning', 'primary', 'success'];
        const allowSizes = ['big', 'middle', 'small'];
        const color = allowColors.includes(this.color) ? this.color : 'info';
        const size = allowSizes.includes(this.size) ? this.size : 'middle';
        return [color, size];
      },
    },
  };
</script>
<style lang="scss" scoped>
  button {
    margin: 0;
    padding: 0;
    border: none;
    color: #fff;
    border-radius: 2px;
    cursor: pointer;
  }
  .big {
    padding: 20px 45px;
  }
  .middle {
    padding: 10px 25px;
  }
  .small {
    padding: 5px 15px;
  }
  .primary {
    background: #3f9eff;
  }
  .danger {
    background: #f42c2e;
  }
  .info {
    background: #909399;
  }
  .warning {
    background: #feb03b;
  }
  .success {
    background: #67c23a;
  }
</style>vuex / pinia 
官方推荐使用
- vuex: https://vuex.vuejs.org/zh/
- pinia: https://pinia.vuejs.org/introduction.html
- pinia 中文: https://pinia.web3doc.top/introduction.html
provider + inject 
弊端很大, 知道在哪注入的, 不知道数据在哪改的, 不推荐使用, 建议使用 Vuex
而且注入的数据默认不是响应式的

eventBus 
主要依靠发布订阅模式来实现, 弊端也很大, 因为所有的事件都是在全局下的

递归组件 
一般用于组件不确定数据到底有多少层级, 比如无限分类的树形结构数据
<template>
  <div>
    <h2>MenuTree</h2>
    <menu-tree :data="menus" />
  </div>
</template>
<script>
  import MenuTree from './MenuTree.vue';
  export default {
    components: { MenuTree },
    data: () => ({
      menus: [
        {
          id: 1,
          title: '菜单-1',
        },
        {
          id: 2,
          title: '菜单-2',
          children: [
            {
              id: 21,
              title: '菜单-2-1',
            },
            {
              id: 22,
              title: '菜单-2-2',
              children: [
                {
                  id: 221,
                  title: '菜单-2-2-1',
                },
                {
                  id: 222,
                  title: '菜单-2-2-2',
                },
                {
                  id: 223,
                  title: '菜单-2-2-3',
                },
              ],
            },
            {
              id: 23,
              title: '菜单-2-3',
            },
          ],
        },
        {
          id: 3,
          title: '菜单-3',
        },
      ],
    }),
  };
</script><template>
  <div class="menu-tree-container">
    <div class="menu" v-for="menu of data" :key="menu.id">
      <!-- 普通item -->
      <div class="menu-item">
        <span> {{ menu.title }} </span>
        <span v-if="menu.children" class="icon">></span>
      </div>
      <!-- 有子选项的 item, 递归的渲染所有子选项 -->
      <div v-if="menu.children" class="sub-menus">
        <menu-tree :data="menu.children" />
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'MenuTree', // 必须指定name, 否则无法在本组件 template 中使用 <menu-tree> 组件
    props: ['data'],
  };
</script>
<style lang="scss" scoped>
  .menu {
    width: 200px;
    background: #000;
    text-align: left;
    color: #fff;
    position: relative;
    padding: 0 10px;
    box-sizing: border-box;
    .menu-item {
      margin: 0;
      padding: 0;
      line-height: 50px;
      position: relative;
      .icon {
        position: absolute;
        top: 0;
        right: 0;
      }
    }
    .sub-menus {
      position: absolute;
      top: 0;
      left: 100%;
    }
  }
</style>
动态组件 
动态组件: 就是由内置的 Component 组件来渲染, 而不是直接渲染, 在运行时可能改变的组件, 非常类似 v-if 的效果
组件缓存: 就是切换组件时, 不会直接销毁组件, 而是缓存组件的vnode, 类似 v-show 的效果, 但是实现的原理不一样
动态组件 Component 
<template>
  <div>
    <h2>动态组件 & 异步组件</h2>
    <div class="tab-container">
      <ul class="tabs">
        <li
          class="tab-item"
          v-for="tab of tabs"
          :key="tab.componentName"
          :class="{
            'active-tab': currentTab.componentName === tab.componentName,
          }"
          @click="changeTab(tab)"
        >
          {{ tab.title }}
        </li>
      </ul>
      <div class="tab-content">
        <component :is="currentTab" />
        <!-- component 和 v-if 的效果非常相似 -->
        <!-- <account-login v-if="currentTab === 'account-login'" /> -->
        <!-- <qrcode-login  v-if="currentTab === 'qrcode-login'" /> -->
        <!-- <mobile-login  v-if="currentTab === 'mobile-login'" /> -->
      </div>
    </div>
  </div>
</template>
<script>
  const AccountLogin = {
    name: 'account-login',
    template: `<h2>账号密码登录内容部分</h2>`,
  };
  const QrcodeLogin = {
    name: 'qrcode-login',
    template: `<h2>扫码登录内容部分</h2>`,
  };
  const MobileLogin = {
    name: 'mobile-login',
    template: `<h2>手机验证码登录内容部分</h2>`,
  };
  export default {
    components: {
      AccountLogin,
      QrcodeLogin,
      MobileLogin,
    },
    data: () => ({
      tabs: [
        { title: '密码登录', componentName: 'account-login' },
        { title: '扫码登录', componentName: 'qrcode-login' },
        { title: '验证码登录', componentName: 'mobile-login' },
      ],
      currentTab: 'account-login',
    }),
    methods: {
      changeTab(tab) {
        this.currentTab = tab.componentName;
      },
    },
  };
</script>
<style lang="scss" scoped>
  .tabs {
    display: flex;
    &,
    .tab-item {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    .tab-item {
      margin-right: 20px;
      cursor: pointer;
    }
  }
</style>组件缓存 Component & keep-alive 
<!-- ... -->
<div class="tab-content">
  <keep-alive>
    <component :is="currentTab" />
  </keep-alive>
  <!-- component 和 v-show 的效果非常相似 -->
  <!-- <account-login v-show="currentTab === 'account-login'" /> -->
  <!-- <qrcode-login  v-show="currentTab === 'qrcode-login'" /> -->
  <!-- <mobile-login  v-show="currentTab === 'mobile-login'" /> -->
</div>
<!-- ... -->
异步组件 
所谓异步组件就是利用 ESModule 可以动态加载的特性, 在代码运行选择是否要加载组件, 而不是在加载的时候, 直接加载
异步组件不会直接打包到 main.js 中, 而是会单独打包, 因为需要在运行时单独加载
被异步加载的组件 XButton.vue 
<!-- XButton.vue -->
<template>
  <button :class="classNames" @click="emitClick">
    <slot>XButton</slot>
  </button>
</template>
<script>
  export default {
    name: 'XButton',
    props: [
      'size', // 控制按钮大小
      'color', // 控制按钮颜色
    ],
    methods: {
      emitClick(event) {
        this.$emit('xbtn-click', event);
      },
    },
    computed: {
      classNames() {
        const allowColors = ['info', 'danger', 'warning', 'primary', 'success'];
        const allowSizes = ['big', 'middle', 'small'];
        const color = allowColors.includes(this.color) ? this.color : 'info';
        const size = allowSizes.includes(this.size) ? this.size : 'middle';
        return [color, size];
      },
    },
  };
</script>
<style lang="scss" scoped>
  button {
    margin: 0;
    padding: 0;
    border: none;
    color: #fff;
    border-radius: 2px;
    cursor: pointer;
  }
  .big {
    padding: 20px 45px;
  }
  .middle {
    padding: 10px 25px;
  }
  .small {
    padding: 5px 15px;
  }
  .primary {
    background: #3f9eff;
  }
  .danger {
    background: #f42c2e;
  }
  .info {
    background: #909399;
  }
  .warning {
    background: #feb03b;
  }
  .success {
    background: #67c23a;
  }
</style>路由 
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/async',
    name: 'AsyncComponent',
    component: () => import(/* webpackChunkName: "async-comp" */ '../views/async/index.vue'),
    // 这个注释 /* webpackChunkName: "xbutton" */
    // 是用来指定 webpack 打包的时候, 将这个.vue 文件打包后输出的名字 xbutton.js
    // 异步加载: 只有 vue-router 导航到当前页面的时候才会加载 xbutton.js,
    // 在其他页面的时候不会加载这个js文件
  },
];Vue2异步组件 
一个也页面中, 某个组件异步加载
<template>
  <div>
    <h2>Vue 2 Async Component</h2>
    <x-button></x-button>
  </div>
</template>
<script>
  export default {
    components: {
      // 这个注释 /* webpackChunkName: "xbutton" */
      // 是用来指定 webpack 打包的时候, 将这个.vue 文件打包后输出的名字 xbutton.js
      // 异步加载: 只有 vue-router 导航到当前页面的时候才会加载 xbutton.js,
      // 在其他页面的时候不会加载这个js文件
      'x-button': () => import(/* webpackChunkName: "xbutton" */ './XButton.vue'),
    },
  };
</script>Vue3异步组件 
<template>
  <div>
    <h2>Vue 3 Async Component</h2>
    <x-button></x-button>
  </div>
</template>
<script setup>
  import { defineAsyncComponent } from 'vue';
  const XButton = defineAsyncComponent({
    loader: () => import(/* webpackChunkName: "xbutton" */ './XButton.vue'),
    // loadingComponent: loadingComponent,
    // errorComponent: errorComponent,
    delay: 100,
    timeout: 3000,
    suspensible: false,
    onError(error, retry, fail, attempts) {
      if (error.message.match(/fetch/) && attempts <= 3) {
        retry();
      } else {
        fail();
      }
    },
  });
</script>静态加载和异步加载的实现原理 
- 利用 link 标签异步加载(不会阻塞) js 文件(异步组件打包后的js文件)
- 当路由匹配到对应组件的时候, 如果这个组件是异步的, 会再次请求. 此时:因为(link prefetch)请求过一次了, 默认情况下会有缓存, 所以速度就会特别快, 而且还不影响正常使用


为什么要使用异步加载?
- 因为所有的文件都会打包成js, 打包后的文件会放到一个js文件中, 同步文件非常多, 就会导致这个文件特别大, 就导致加载速度慢
- 使用异步组件可以将一部分暂时用不到的一部加载, 等需要的时候在请求一次就行了
为什么要请求两次?
- 第一次加载是为了速度, link 标签异步请求不会阻塞, 第二次请求的时候就直接使用缓存了, 速度非常快
- 第二次加载是为了确保正确的加载出来, 如果禁用了浏览器缓存, 第二次请求就可以确保数据能够正常的加载出来