一、MVVM

Vue 的设计主要受到MVVM模型的启发,因此在官方文档中经常会使用 vm 这个变量名表示组件实例。

接下来给大家介绍一下MVVM,不过,在开始之前可以先给大家简单介绍下MVC设计模式,在MVC设计模式中:

  • M(Mode,模型):用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法,“ Model ” 有对数据直接访问的权力,例如对数据库的访问,“Model” 不依赖 “View” 和 “Controller”,也就是说, Model 不关心它会被如何显示或是如何被操作。但是 Model 中数据的变化一般会通过一种刷新机制被公布。为了实现这种机制,那些用于监视此 Model 的 View 必须事先在此 Model 上注册,从而,View 可以了解在数据 Model 上发生的改变。(比如:观察者模式
  • V(View,视图):能够实现数据有目的的显示(理论上,这不是必需的)。在 View 中一般没有程序上的逻辑。为了实现 View 上的刷新功能,View 需要访问它监视的数据模型(Model),因此应该事先在被它监视的数据那里注册。
  • C(Controller,控制器):负责转发请求,对请求进行处理。作为 “Modal” 和 “View”交互的桥梁,它处理事件并作出响应,“事件”包括用户的行为和数据 Model 上的改变。

简单理解就是,当数据发生变化,发送通知给控制器刷新视图,同理控制器监听用户操作,当需要修改数据时控制器会通知数据模型刷新数据,这里可以看出,视图和模型不直接交互,而是通过控制器实现对接的,那MVVM和MVC类似。

MVVM( Model-View-ViewModel ) 为一种设计模式。

mvvm.png

其中:

  • Model 表示数据模型;
  • View 表示视图;
  • ViewModel 负责监听Model中的数据改变并且控制视图的更新,处理用户交互操作,类似于MVC设计模式中的控制器,其本质为一个Vue的实例。

提示:ModelView 并无直接关联,而是通过 ViewModel 来进行联系的,ModelViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。

这种模式实现了 ModelView 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 DOM

二、Vue 响应式

1. 响应式属性

在 vue 中,可以通过双花括号来将数据绑定在视图上,比如:

  1. <script setup lang="ts">
  2. // -- 定义变量
  3. let count = 0;
  4. </script>
  5. <template>
  6. <!-- 绑定数据 -->
  7. <div>count:{{ count }}</div>
  8. </template>

页面输出:count:0,接下来我们在模板(template)中定义一个按钮尝试修改 count 的值,看看视图是否发生变化:

  1. <script setup lang="ts">
  2. // -- 定义变量
  3. let count = 0;
  4. // -- 事件处理函数
  5. const increment = () => {
  6. count++;
  7. };
  8. </script>
  9. <template>
  10. <!-- 绑定数据 -->
  11. <div>count:{{ count }}</div>
  12. <button type="button" @click="increment">increment</button>
  13. </template>

上述示例中,@click 表示为按钮 button 添加一个点击事件,事件处理函数为:increment,在事件处理函数中,我们让 count 变量自增,点击按钮,可以发现,视图并没有更新。这是因为我们定义的变量 count 并非是响应式的(尽管你可以将其呈现在视图上,但变量 count 并没有加入响应式系统中)。

接下来,我们看看在 vue3.x 中将变量加入响应式系统中的几种形式:

1.1. ref()

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

接下来我们改造一下示例,将 count 值通过 ref 包裹:

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. // -- 定义变量
  4. let count = ref(0);
  5. // -- 事件处理函数
  6. const increment = () => {
  7. count.value++;
  8. };
  9. </script>
  10. <template>
  11. <!-- 绑定数据 -->
  12. <div>count:{{ count }}</div>
  13. <button type="button" @click="increment">increment</button>
  14. </template>

点击按钮,可以发现,count 值成功更新。

提示:ref 一般包裹基本数据类型,如 字符串、数值、布尔类型等。如果你要将一个对象加入响应式,请使用 reactive

ref() 也可以用于获取单个 DOM元素,如下:

  1. <script setup lang="ts">
  2. import { onMounted, ref } from 'vue';
  3. const domRef = ref(null);
  4. onMounted(() => {
  5. console.log(domRef.value); // <div>Hello</div>
  6. })
  7. </script>
  8. <template>
  9. <!-- ref -->
  10. <div ref="domRef">Hello</div>
  11. </template>

1.2. reactive()

返回对象的响应式副本。

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. // -- 定义变量
  4. let obj = reactive({
  5. count: 0,
  6. });
  7. // -- 事件处理函数
  8. const increment = () => {
  9. obj.count++;
  10. };
  11. </script>
  12. <template>
  13. <!-- 绑定数据 -->
  14. <div>obj.count:{{ obj.count }}</div>
  15. <button type="button" @click="increment">increment</button>
  16. </template>

提示:reactive 一般包裹对象类型。

1.3. toRefs()

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property[ref](https://v3.cn.vuejs.org/api/refs-api.html#ref)

  1. <script setup lang="ts">
  2. import { reactive, toRefs } from 'vue';
  3. const state = reactive({
  4. name: 'Li-HONGYAO',
  5. age: 18,
  6. });
  7. const stateAsRefs = toRefs(state);
  8. /**
  9. stateAsRefs 的类型:
  10. {
  11. name: Ref<string>,
  12. age: Ref<number>
  13. }*/
  14. // ref 和原始 property 已经“链接”起来了
  15. state.age++;
  16. console.log(stateAsRefs.age.value); // 19
  17. stateAsRefs.age.value++;
  18. console.log(state.age); // 20
  19. </script>

小妙招:在一个页面中,通常会有多个状态(state),比如用户信息、登录状态等等,你可能会定义多个 ref 或者 reactive 变量来保存这些信息,在 vue2.x,属性一般统一定义在 data 属性中集中管理,如果你也想将一个页面的状态统定义在一个 state 变量中,可以这么做:

  1. const state = reactive({
  2. loginStatus: 0,
  3. user: {
  4. name: 'Li-HONGYAO',
  5. job: '前端工程师',
  6. address: '成都市高新区',
  7. },
  8. });

然后在模板中访问:

  1. <div>loginStatus:{{ state.loginStatus ? '已登录' : '未登录' }}</div>
  2. <div>Name:{{ state.user.name }}</div>
  3. <div>job:{{ state.user.job }}</div>
  4. <div>address:{{ state.user.job }}</div>

但是你可能会觉得,每次访问属性都需要通过 state.xxx,是否可以通过某种形式直接访问属性呢?答案是肯定有的,我们可以通过 toRefs 来改造。

  1. const { loginStatus, user } = toRefs(state);

然后就可以在模板中直接访问 loginStatususer 了。

2. 响应式原理

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据

2.1. @2.x

当一个Vue实例创建时,你可以把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,vue会遍历 data 选项的属性,并使用 Object.defineProperty 将它们全部转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

response-yuanli.jpg

深入理解:

  • 监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
  • 解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
  • 订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式
  • 订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

2.2. @3.x

Vue3.x 改用 Proxy 替代Object.defineProperty,因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy 只会代理对象的第一层,Vue3是怎样处理这个问题的呢?

判断当前 Reflect.get 的返回值是否为Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger

三、生命周期

每个 Vue 实例在被创建之前都要经过一系列的初始化过程,在这个过程中也会运行一些叫做 生命周期钩子 的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

1. @2.x

lifecycle.png

2. @3.x

被替换

  1. beforeCreate -> setup()
  2. created -> setup()

重命名

  1. beforeMount -> onBeforeMount
  2. mounted -> onMounted
  3. beforeUpdate -> onBeforeUpdate
  4. updated -> onUpdated
  5. beforeDestroy -> onBeforeUnmount
  6. destroyed -> onUnmounted
  7. errorCaptured -> onErrorCaptured

新增的

新增的以下2个方便调试 debug 的回调钩子:

  1. onRenderTracked
  2. onRenderTriggered

3. @代码示例

app.vue

  1. <!-- 脚本 -->
  2. <script setup lang="ts">
  3. import { onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated } from 'vue';
  4. console.log('__setup__');
  5. onBeforeMount(() => {
  6. console.log('__onBeforeMount__');
  7. });
  8. onMounted(() => {
  9. console.log('__onMounted__');
  10. });
  11. onBeforeUpdate(() => {
  12. console.log('__onBeforeUpdate__');
  13. });
  14. onUpdated(() => {
  15. console.log('__onUpdated__');
  16. });
  17. onBeforeUnmount(() => {
  18. console.log('__onBeforeUnmount__');
  19. });
  20. onUnmounted(() => {
  21. console.log('__onUnmounted__');
  22. });
  23. </script>
  24. <!-- 模板 -->
  25. <template>
  26. <div class="app">Hello, Vue3.x!</div>
  27. </template>
  28. <!-- 样式 -->
  29. <style scoped></style>