本文主要记录了关于 Vue2 的相关笔记,主要记录 Vue2 的一些基础概念与知识,帮助理解 Vue3。

Vue 的核心特性

数据驱动(MVVM)

数据驱动(MVVM):MVVM表示的是Model-View-ViewModel

MVVM 模型由以下三个部分组成:

  • View:表示当前页面所渲染的 DOM 结构,负责将数据模型转化为 UI 展现出来。View 层通常是由 HTMLCSS 来编写的,也可以使用模板语言或组件化的方式来构建。
  • Model:表示当前页面渲染时所依赖的数据源,可以在 Model 层中定义数据修改和操作的业务逻辑。Model 层通常是由 Javascript 对象或数组来表示的,也可以使用 Vuex 等状态管理工具来管理。
  • ViewModel:表示 Vue 的实例,它是 MVVM 的核心。ViewModel 负责连接 ViewModel,保证视图和数据的一致性,这种轻量级的架构让前端开发更加高效、便捷。ViewModel 层通常是由 Vue 的选项和方法来定义的,也可以使用组合式 API 等新特性来增强。

MVVM 模型的工作原理是这样的:

  • 当用户在 View 层进行操作时,如点击按钮,输入文本等,ViewModel 层会监听到这些事件,并根据相应的逻辑来修改 Model 层的数据。
  • Model 层的数据发生变化时,ViewModel 层会通过双向数据绑定的机制,自动将数据的变化反映到 View 层,更新视图的内容。
  • 这样,View 层和 Model 层之间的同步工作完全是自动的,无需人为干预,开发者只需关注业务逻辑,无需手动操作 DOM,复杂的数据状态维护交给 MVVM 统一来管理。

组件化

  1. 什么是组件化
    一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件。
  2. 组件化的优势

    • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现。
    • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单。
    • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。

指令系统

指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
常用的指令

  • 条件渲染指令 v-if
  • 列表渲染指令 v-for
  • 属性绑定指令 v-bind
  • 事件绑定指令 v-on
  • 双向数据绑定指令 v-model

SPA

SPA 是单页面应用的缩写,它是一种前端开发的模式,主要特点是在浏览器中只加载一个 HTML 文件,然后通过 JavaScript 动态地更新页面内容,实现无刷新的用户体验。SPA的优点是页面切换快,用户体验好,适合开发复杂的交互式应用。SPA 的缺点是首屏加载速度慢,不利于 SEO,需要额外的技术来解决这些问题。

访问页面时的应用初始化即为直接通过url访问的解决方案。
If the URL doesn't match any static assets, it should serve the same index.html page that your app lives in. Beautiful, again! —- 摘自 vue-router 官方文档。

  • Hash模式

    1. 定义路由,定义路由缓存对象。
    2. 定义路由内容访问函数,如已做路由缓存,则直接返回结果,否则模拟异步请求。
    3. 定义更新页面的函数。
    4. 监听window对象的hashchange事件,当url改变的时候,请求路由,更新页面。
    5. 注意在一开始的时候也需要执行一次函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    <!DOCTYPE html>
    <html lang="zh-CN">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hash 模式</title>
      </head>
      <body>
        <header>
          <nav>
            <ul>
              <li><a href="#/">首页</a></li>
              <li><a href="#/about">关于</a></li>
              <li><a href="#/contact">联系我们</a></li>
            </ul>
          </nav>
        </header>
        <div id="content"></div>
      </body>
      <script>
        // 响应的路由及内容
        const routes = {
          "/": "这是首页",
          "/about": "这是关于页面",
          "/contact": "这是联系我们页面",
        };
        // 缓存路由对象
        const cache = {};
        // 获取需要展示的页面内容
        const getContent = (route) => {
          if (cache[route]) {
            return Promise.resolve(cache[route]);
          } else {
            // 模拟异步请求
            return new Promise((resolve, reject) => {
              setTimeout(() => {
                if (routes[route]) {
                  cache[route] = routes[route];
                  resolve(routes[route]);
                } else {
                  reject("页面不存在");
                }
              }, 500);
            });
          }
        };
        // 更新页面内容
        const updateContent = (content) => {
          const app = document.querySelector("#content");
          app.innerHTML = content;
        };
        // 当哈希值发生改变,即 haschange 事件触发的时候,进行处理
        const handleNav = () => {
          console.log("hashchange事件触发了");
          // 获取当前的路径,注意需要去除 # 号
          const route = window.location.hash.slice(1);
          // 获取路由信息并尝试展示
          getContent(route).then(
            (value) => {
              updateContent(value);
            },
            (err) => {
              updateContent(err);
            }
          );
        };
        window.onhashchange = handleNav;
        handleNav();
      </script>
    </html>
  • History模式

    前三步与hash模式基本相同,主要记录后面不一样的地方。

    1. 对于History模式需要监听的是window对象的popstate事件,此事件只能由浏览器记录的前进后退,和a标签的锚点触发,在这个时间上绑定更新页面的函数。
    2. 此外需要阻止超链接事件的点击的默认事件,阻止其跳转,并使用pushStatereplaceState更新url记录,并更新页面内容。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    <!DOCTYPE html>
    <html lang="zh-CN">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hash 模式</title>
      </head>
      <body>
        <header>
          <nav>
            <ul>
              <li><a href="/">首页</a></li>
              <li><a href="/about/">关于</a></li>
              <li><a href="/contact/">联系我们</a></li>
            </ul>
          </nav>
        </header>
        <div id="content"></div>
      </body>
      <script>
        // 响应的路由及内容
        const routes = {
          "/": "这是首页",
          "/about/": "这是关于页面",
          "/contact/": "这是联系我们页面",
        };
        // 缓存路由对象
        const cache = {};
        // 获取需要展示的页面内容
        const getContent = (route) => {
          if (cache[route]) {
            return Promise.resolve(cache[route]);
          } else {
            // 模拟异步请求
            return new Promise((resolve, reject) => {
              setTimeout(() => {
                if (routes[route]) {
                  cache[route] = routes[route];
                  resolve(routes[route]);
                } else {
                  reject("页面不存在");
                }
              }, 500);
            });
          }
        };
        // 更新页面内容
        const updateContent = (content) => {
          const app = document.querySelector("#content");
          app.innerHTML = content;
        };
        // popstate 事件触发的时候,进行处理
        const handleNav = (pathname = window.location.pathname) => {
          // 为空则为 '/'
          const route = pathname || "/";
          // 获取路由信息并尝试展示
          getContent(route).then(
            (value) => {
              updateContent(value);
            },
            (err) => {
              updateContent(err);
            }
          );
        };
        window.onpopstate = handleNav;
    
        // 拦截连接跳转事件
        const navLinks = document.querySelectorAll("nav a");
        navLinks.forEach((item) => {
          item.addEventListener("click", (e) => {
            e.preventDefault();
            const pathname = item.getAttribute("href");
            history.pushState(null, null, pathname);
            handleNav(pathname);
          });
        });
        handleNav();
      </script>
    </html>

SPA 首屏加载慢处理方案

  • 减小入口文件体积。常用的方案为路由懒加载

    1
    2
    3
    4
    5
    routes:[
      path: 'Blogs',
      name: 'ShowBlogs',
      component: () => import('./components/ShowBlogs.vue')
    ]
  • 静态资源本地缓存
    后端手段

    • http常见缓存手段,如Cache-ControlEtagLast-Modified等响应头对静态资源进行缓存。
    • 采用Service Worker离线缓存,代表方案PWA

    前端手段

    • 合理利用localStorage
  • UI框架按需加载与避免组件重复打包。核心为去除冗余的部分。

  • 图片与文件资源的压缩。
  • SSR:改善首屏加载时间与SEO的通用解决方案。

Vue 中的 data 属性

Vue中,组件中的data函数只能为一个函数而不能为一个对象,是因为组件是可以复用的,如果data是一个对象,那么所有复用的组件实例会共享同一个data对象,导致数据相互影响,无法保证组件的独立性和一致性。而如果data是一个函数,那么每个组件实例都会返回一个全新的data对象,保证了数据的隔离和安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const app = new Vue({
  el: "#app",
  // 对象格式
  data: {
    foo: "foo",
  },
  // 函数格式
  data() {
    return {
      foo: "foo",
    };
  },
});
// 组件形式
const school = Vue.extend({
  template: `
      <div class="demo">
        <h2>学校名称:{{schoolName}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="showName">点我提示学校名</button>	
      </div>
			`,
  // el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。
  data() {
    return {
      schoolName: "sss",
      address: "ssss",
    };
  },
  methods: {
    showName() {
      alert(this.schoolName);
    },
  },
});

Vue2 响应式的缺陷

Vue2 响应式的缺陷主要有以下几点:

  • 无法检测到对象属性的添加或删除,只能检测到已存在的属性的变化。这是因为Vue2使用了Object.defineProperty()方法来实现数据的劫持和监听,这个方法只能对对象的已有属性进行拦截,无法对新添加或删除的属性进行响应。
  • 无法检测到数组的变动,除非使用 Vue 提供的特殊方法,如push(), pop(), splice()等。这是因为 Vue2 对数组的处理是通过重写数组的原型方法来实现的,如果直接修改数组的长度或索引,Vue2无法捕获到这些变化。
  • 无法检测到嵌套对象的变化,需要手动调用 Vue.set()Vue.delete()方法。这是因为 Vue2 对嵌套对象的处理是通过递归遍历的方式来实现的,如果对象的层级过深,Vue2 无法对所有的属性进行监听,需要手动触发更新。
  • 需要对每个属性进行遍历和劫持,消耗了一定的性能和内存。这是因为 Vue2 对数据的响应式处理是通过遍历对象的所有属性,并为每个属性创建一个订阅者(Watcher)来实现的,这样会增加初始化的时间和内存的占用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 无法检测到对象属性的添加或删除的例子
var vm = new Vue({
  data: {
    a: 1,
  },
});
// `vm.a` 是响应式的

vm.b = 2;
// `vm.b` 是非响应式的

delete vm.a;
// `vm.a` 仍然是响应式的,无法删除

// 无法检测到数组的变动的例子
var vm = new Vue({
  data: {
    items: ["a", "b", "c"],
  },
});
vm.items[1] = "x"; // 不是响应性的
vm.items.length = 2; // 不是响应性的

// 无法检测到嵌套对象的变化的例子
var vm = new Vue({
  data: {
    user: {
      name: "Alice",
    },
  },
});
vm.user.age = 18; // 不是响应性的

Vue 的 nextTick 函数

vue 组件的生命周期与 JS 浏览器的事件循环有一定的关系,主要体现在以下几个方面:

  • vue 组件的生命周期是指组件从创建到销毁的过程中,经历的一系列阶段,每个阶段都有对应的钩子函数,可以在这些函数中执行一些自定义的逻辑。
  • JS 浏览器的事件循环是指浏览器在执行 JS 代码时,采用的一种机制,可以将同步任务和异步任务分别放入不同的队列中,按照一定的顺序执行。
  • vue 组件的生命周期钩子函数是同步任务,它们会按照组件的创建顺序依次执行,直到所有组件都完成相应的阶段。
  • JS 浏览器的事件循环会在每个宏任务(macro task)执行完毕后,检查微任务(micro task)队列,如果有未执行的微任务,就会依次执行它们,直到微任务队列为空。
  • vue 组件的更新过程是一个微任务,它会在组件的数据发生变化后,异步地将变化的数据应用到 DOM 上,这样可以避免多次不必要的 DOM 操作。
  • vue 组件的生命周期钩子函数和组件的更新过程之间存在一定的先后顺序,具体如下:
    • 当组件的数据发生变化时,会触发 beforeUpdate 钩子函数,这是一个同步任务,会立即执行。
    • 然后,组件的更新过程会被推入微任务队列,等待执行。
    • 接着,如果当前宏任务执行完毕,事件循环会检查微任务队列,发现组件的更新过程,就会执行它,将变化的数据应用到 DOM 上。
    • 最后,组件的更新过程执行完毕后,会触发 updated 钩子函数,这也是一个同步任务,会立即执行。

vue 组合式 apinextTick 是一个微任务,它的作用是在下一次 DOM 更新循环结束之后执行延迟回调。它可以用来等待 DOM 更新完成后再进行一些操作,比如获取更新后的 DOM 元素的属性值等。
vue 组合式 apinextTick 的实现原理是:首先检测当前环境是否支持 Promise,如果支持,就使用 Promise.resolve().then()创建一个微任务;如果不支持,就使用 setTimeout(fn, 0)创建一个宏任务。但是,由于 setTimeout 的最小延迟时间在不同浏览器中有差异,可能导致 nextTick 的回调执行顺序不一致,所以 vue 还使用了一个内部的队列来维护 nextTick 的回调函数,确保它们按照注册的顺序执行。

配合源码的简单实现理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 定义一个异步队列
const queue = [];
// 定义一个执行队列的函数
let runQueue = () => {
  // 遍历并执行队列中的函数
  for (let fn of queue) {
    fn();
  }
  // 清空队列
  queue.length = 0;
};
// 定义一个调度函数,根据环境选择合适的异步方式
let nextTick = () => {
  // 如果支持Promise,则使用Promise.then()
  if (typeof Promise !== "undefined") {
    return Promise.resolve().then(runQueue);
  }
  // 如果支持MutationObserver,则使用它
  if (typeof MutationObserver !== "undefined") {
    let counter = 1;
    let observer = new MutationObserver(runQueue);
    let textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true,
    });
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
    return;
  }
  // 如果支持setImmediate,则使用它
  if (typeof setImmediate !== "undefined") {
    return setImmediate(runQueue);
  }
  // 最后使用setTimeout
  setTimeout(runQueue, 0);
};
// 定义一个暴露给外部的nextTick函数
export function nextTick(fn) {
  // 将传入的函数推入队列
  queue.push(fn);
  // 调用调度函数
  nextTick();
}
查看在 Vue3 中 nextTick 的使用

vue3 组合式 api 中的 setup 语法糖中,你可以从 vue 模块中导入 nextTick 函数,然后在 setup 函数中使用它。你有两种方式来使用 nextTick

  • 方式一:传入一个回调函数作为参数,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script setup>
    import { ref, nextTick } from "vue";
    
    const msg = ref("Hello");
    
    const changeMsg = () => {
      msg.value = "World";
      nextTick(() => {
        console.log(msg.value); // 'World'
      });
    };
    </script>
  • 方式二:不传入任何参数,得到一个返回 promise 的函数,然后使用 async/await 语法,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <script setup>
    import { ref, nextTick } from "vue";
    
    const msg = ref("Hello");
    
    const changeMsg = async () => {
      msg.value = "World";
      await nextTick();
      console.log(msg.value); // 'World'
    };
    </script>