hexon
发布于 2025-08-22 / 17 阅读
0

二、Vue核心语法

本文将全面介绍Vue中常用的语法,主要包括模板语法、计算属性、监听、绑定动态样式、条件渲染指令、列表渲染指令、事件处理、收集表单数据、Vue实例的生命周期、过渡与动画、内置指令。这些内容是Vue开发中最常用,也必要掌握的内容。本文目前依然会使用Vue2就有的Options API(选项式API)进行学习,对于Vue3推出的Composition API(组合式API)将在后面学习。

另外,要强调的是文章中的代码我省去了HTML的重复声明,只保留了body里面的代码。

模板语法

个人觉得这个模板就是Vue提供的一种类似于HTML的语法,降低直接使用JSX编写代码的难度,相当于中间多了一层转换。以下是官网介绍:

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。

理解编译时优化:

  • 模板:编译器可以识别静态节点(无动态绑定的部分),将其提升到渲染函数外部,避免重复创建。

  • JSX:由于 JSX 是动态 JavaScript 代码,编译器难以区分静态/动态内容,所有节点通常会在每次渲染时重新创建。

Vue的模板是可以采取更激进的优化手段的,这样在一定程度上可以把优化做的更自动更好。而React的设计理念不同,它强调灵活性不使用模板,但它也有自己的优化手段。

插值语法

插值语法用来向标签体中插入一个动态的值。插值语法的结构很固定,用双大括号包含一个JavaScript表达式,即{{JavaScript表达式}}。值得一提的是,插值语法只可以作为一个标签的标签体文本或标签体文体的一部分,不能作为标签的一个属性值。双大括号中可以包含任意JavaScript表达式,可以是一个常量、一个变量、或者一个变量对象的方法调用,甚至可以是一个三目表达式

但需要注意的是,模板中的变量读取数据的来源都是配置指定的data对象。后续还会讲解其他的数据来源,目前我们只需要理解为data对象。

下面是一些使用示例:

<div id="app">
  <p>{{123}}</p>
  <p>{{msg}}</p>
  <p>{{msg.toUpperCase()}}</p>
  <p>
    {{score < 60 ? '考试不及格' : '及格'}}
  </p>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        msg: 'Welcome to Vue',
        score: 59,
      }
    },
  }).mount('#app')
</script>

受限的全局访问

模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 MathDate

没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。

指令语法

指令(Directive)是带有 "v-" 前缀的Vue自定义标签属性,其属性一般是一个JavaScript表达式。Vue中包含了一些不同功能的指令,比如v-bind用来给标签指定动态属性值,v-on用来给标签绑定事件监听,v-if和v-show用来控制是否显示。但需要注意的是,不管是什么功能的指令,它们操作的目标都是指令属性所在的标签

下面以v-bind和v-on为例来演示指令语法的使用,这里先将这两个指令的语法进行展示:

  • v-bind:属性名 = "JavaScript表达式"

  • v-on:事件名 = "方法名表达式"

<div id="app">
  <a v-bind:href="url">去学习IT技术</a>
  <br />
  <button v-on:click="confirm">确认一下</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        url: 'http://www.atguigu.com',
      }
    },

    // methods中配置的方法,模板中可以直接调用
    methods: {
      confirm() {
        if (window.confirm('确定要来学习吗?')) {
          window.location.href = 'https://www.prajnalab.com/'
        }
      },
    },
  }).mount('#app')
</script>

其实 Vue 中允许将 "v-bind:属性名" 简化为 ":属性名","v-on:事件名" 简化为 "@事件名" 的形式,并且在实际项目中,前端工程师基本上会使用这个 "语法糖" 进行开发。

例如,可以将上面的代码简化为下面的代码:

<div id="app">
  <a :href="url">去学习IT技术</a>
  <br />
  <button @click="confirm">确认一下</button>
</div>

Vue 3.4版本以后支持

如果 attribute 的名称与绑定的 JavaScript 变量的名称相同,那么可以进一步简化语法,省略 attribute 值:

<!-- 与 :id="id" 相同 -->
<div :id></div>

<!-- 这也同样有效 -->
<div v-bind:id></div>

https://cn.vuejs.org/guide/essentials/template-syntax.html#same-name-shorthand

这与在 JavaScript 中声明对象时使用的属性简写语法类似。请注意,这是一个只在 Vue 3.4 及以上版本中可用的特性。

data和methods配置项

在通过 createApp 创建应用对象时,需要为其指定一个配置对象,这个配置对象中有两个基本的配置项,分别是 data 和 methods。下面我们就对这两个配置项进行具体讲解。

首先来讲解 data 配置项。它的值必须是函数,且返回一个包含 n 个可变属性的对象,我们一般称此对象为 data 对象。此对象中的属性,我们常称为 data 属性。在模板中可以读取任意 data 属性进行动态显示。然后再来看 methods 配置项。它是一个包含 n 个方法的对象。在方法中用户可以通过 this.xxx 来读取或者更新 data 对象中对应的属性。当更新了对应的 data 属性后,页面会自动更新。

有一点需要特别说明,data 函数和 methods 方法中的 this,本质上是一个代理(Proxy)对象。它代理了 data 对象中所有属性的读/写操作。也就是说,用户可以用过 this 来读取或更新 data 对象中的属性。在 methods 对象中定义的所有方法最终也会被添加到代理对象中,同理,也可以在方法中通过 this 来更新 data 属性,从而触发页面自动更新。至于模板中的表达式读取变量或函数,本质上也是从代理对象上查找的。

下面通过一段代码对 data 函数和 methods 对象进行具体演示:

<div id="app">
  <h2>count: {{count}}</h2>
  <button @click="updateCount">更新</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  const app = createApp({
    // data函数返回data对象, data对象中的所有属性在代理对象上都会有对应的名称的属性
    data() {
      console.log(this)
      return {
        count: 0,
      }
    },

    // methods中的所有方法,都会成为代理对象的方法
    methods: {
      updateCount() {
        this.count += 1
      },
    },
  })
  // console.log(app)

  const vm = app.mount('#app')
  // 通过代理对象vm读取定义在data对象中的count属性
  console.log(vm.count)

  // 2秒后通过代理对象vm直接更新data中的count属性,界面会自动更新
  setTimeout(() => {
    vm.count += 2
  }, 2000)

  // 4秒后通过代理对象vm调用methods中定义的方法来更新count,界面会自动更新
  setTimeout(() => {
    vm.updateCount()
  }, 4000)
</script>

观察打印出的代理对象,可以看到 data 中的属性和 methods 中的方法确实是在代理对象上。

计算属性

在模板表达式中使用表达式是非常便利的,但是表达式更适用于简单运算,如果使用表达式来处理复杂的逻辑,则会让模板变得非常臃肿且难以维护。Vue 为了帮助用户处理复杂的逻辑,提供了计算属性和监听,本节将对计算属性进行讲解释。

计算属性的基本使用

在之前的案例中,通过三目表达式动态判断 data 中 score 的值,从而决定显示的文本。这个动态判断的过程可以称为计算。这里将之前的三目表达式模板编码单列出来,对其进行分析。

<p>{{score < 60 ? '考试不及格' : '及格'}} </p>

上面的模板看起来有些复杂,可读性较差。而对于这段代码来说,更重要的是,如果页面中有多处需要同样的计算,这个计算的模板就需要重复编写多遍。

为了解决这一问题,Vue 框架专门设计了一个重要语法:计算属性。当模板显示的某个数据需要通过已有数据进行一定的逻辑计算才能确定时,就可以选择用计算属性语法来实现。先来看利用计算属性语法重构后的代码:

<div id="app">
  <!-- 读取data数据显示 -->
  <h2>测评得分: {{score}}</h2>
  <!-- 读取计算属性数据显示 -->
  <h3>测评结果: {{resultText}}</h3>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 59,
      }
    },

    // 所有计算属性都定义在computed配置对象中
    computed: {
      // 一个函数类型的计算属性,返回计算的结果
      resultText() {
        console.log('computed resultText()')
        // 根据score进行计算,产生模板需要显示的结果
        const result =
          this.score < 60 ? '考试不及格' : '及格'
        // 返回结果数据
        return result
      },
    },
  }).mount('#app')
</script>

可以明显的看出,在重构的代码中多出了一个配置对象 computed。实际上这就是 Vue 中计算属性定义的位置,整个代理对象需要的计算属性都需要定义在 computed 配置对象中。

"测评得分" 的数据就是在读取 data 数据,这并没有什么特别的。"测评结果" 需要根据 "测评得分" 来显示对应的结果,该结果需要根据 score 的数据来决定,这满足了计算属性的条件,因此使用了计算属性来实现。这里在 computed 配置对象中定义了名称为 resultText 的计算属性,它本质上就是一个函数,内部可以根据已有的 data 数据进行计算,产生要显示的结果数据并返回它。需要的注意的是,计算属性函数中的 this 就是当前组件对象,通过它可以直接访问 data 中的数据。在模板中可以读取计算属性函数 resultText,当初始化显示时,系统会自动调用这个计算属性函数得到返回值并显示到页面上。

注意:计算属性看上去是一个函数,但是在模板中不需要显式调用,计算属性本质上是一个 getter 函数!如果用()调用会报错!!!

如果更新了计算属性依赖的数据会怎么样呢?

比如我们更新了 data 中的 score,那么计算属性函数 resultText 会重新执行计算并返回新的结果值,对应的页面元素会自动更新。下面通过代码对其进行测试。

<div id="app">
  <!-- 读取data数据显示 -->
  <h2>测评得分: {{score}}</h2>
  <!-- 读取计算属性数据显示 -->
  <h3>测评结果: {{resultText}}</h3>
  <button @click="reExam">学习一段时间后重新测评</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 59,
      }
    },

    computed: {
      resultText() {
        console.log('computed resultText()')
        const result = this.score < 60 ? '考试不及格' : '及格'
        return result
      },
    },

    methods: {
      // 更新data中的score
      reExam() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

点击按钮后,h2标签中内容也会被更新。同时计算属性函数 resultText 重新执行返回新的值,h3标签也显示为最新的计算属性值。这里需要注意的是,每次更新依赖数据,计算函数都会重新执行计算

如果更新的值与原值一样,计算属性是不会重新计算的(有缓存)。

计算属性和method方法

计算属性的效果还可以通过在模板中调用 method 方法实现,它的实现结果与计算属性是一样的。

methods代码片段如下。

methods: {
  getResultText() {
    console.log('method getResultText()')
    const result = this.score < 60 ? '考试不及格' : '及格'
    return result
  },
}

模板代码片段如下。

<!-- 调用method方法显示  3处 -->
<h4>测评结果(method实现): {{ getResultText() }}</h4>

无论是初始化显示还是更新显示,其效果跟计算属性的效果都是一样的。尽管如此,我们在开发时还是会选择使用计算属性来实现。这是因为计算属性会将计算属性函数返回的结果数据进行缓存处理,如果结果数据需要在页面中显示多次,那么计算属性函数只会执行1次,但 method 方法会对应执行多次。这样对比下来,计算属性的效率明显要比方法高。

下面修改一下模板,对计算属性和 method 方法再进行测试。

<div id="app">
  <!-- 读取data数据显示 -->
  <h2>测评得分: {{ score }}</h2>

  <!-- 读取计算属性数据显示 3处 -->
  <h3>测评结果(computed实现): {{ resultText }}</h3>
  <h3>测评结果(computed实现): {{ resultText }}</h3>
  <h3>测评结果(computed实现): {{ resultText }}</h3>

  <!-- 调用method方法显示  3处 -->
  <h4>测评结果(method实现): {{ getResultText() }}</h4>
  <h4>测评结果(method实现): {{ getResultText() }}</h4>
  <h4>测评结果(method实现): {{ getResultText() }}</h4>

  <button @click="reExam">学习一段时间后重新测评</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 59,
      }
    },

    computed: {
      resultText() {
        console.log('computed resultText()')
        const result = this.score < 60 ? '考试不及格' : '及格'
        return result
      },
    },

    methods: {
      getResultText() {
        console.log('method getResultText()')
        const result = this.score < 60 ? '考试不及格' : '及格'
        return result
      },
      // 更新data中的score
      reExam() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

当页面初始化显示时,计算属性函数只执行了1次,而method方法执行了3次。点击按钮更新,计算属性函数只执行了1次,而method方法执行了3次。因此当有可能存在结果需要显示多次的情况时,明显应该选择计算属性,因为其效率更高,但如果确定只显示一次,则 method 方法也是一个可以接受的选择

计算属性的 setter

计算属性在默认情况下仅能通过计算属性函数得出结果。当开发者尝试修改一个计算属性时,会收到一个运行警告提示。我们来看一个页面效果:

现有3个需求,具体如下。

① 姓名由 "姓-名" 组成,姓名的初始显示为 "A-B"。

② 当改变姓或名时,姓名能自动同步变化。

③ 姓和名能实时与姓名同步。

下面对数据进行分析和设计。可以将 "姓" 和 "名" 设计为2个data数据,我们可以利用 v-model 实现双向数据绑定,但因为 "姓名" 是由 "姓" 和 "名" 动态确定的,所以我们可以将 "姓名" 设计为计算属性,同样用 v-model 绑定到 input 标签上。具体实现代码如下:

<div id="app">
  <p>姓: <input type="text" v-model="firstName" /></p>
  <p>名: <input type="text" v-model="lastName" /></p>
  <p>
    姓名: <input type="text" placeholder="格式: 姓-名" v-model="fullName" />
  </p>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        firstName: 'A',
        lastName: 'B',
      }
    },

    computed: {
      fullName() {
        return this.firstName + '-' + this.lastName
      }
    }
  }).mount('#app')
</script>

在上面的代码中,我们在 data 对象中定义了 firstName 和 lastName 两个计算属性,在 computed 配置中定义了计算属性 fullName,并通过 v-model 将其绑定到对应的 input 标签上。初始显示实现了需求①中 "姓名" 的显示要求;当修改 "姓" 或 "名" 的内容时,"姓名" 也会自动同步变化,这样也实现了需求②中 "姓名" 同步变化的要求。

那么问题来了,当我们在输入框(input)中改变 "姓名" 的内容时,v-model 会自动将输入值赋给计算属性 fullName,Vue 框架会抛出警告提示。

警告提示表示写操作失败,因为计算属性 fullName 是只读的,也就是只能计算返回一个值。那么应该如何设置计算属性值呢?答案是:可以通过同时提供 getter 和 setter 的计算属性来实现。下面修改一下计算属性 fullName 的实现,代码如下。

<div id="app">
  <p>姓: <input type="text" v-model="firstName" /></p>
  <p>名: <input type="text" v-model="lastName" /></p>
  <p>
    姓名: <input type="text" placeholder="格式: 姓-名" v-model="fullName" />
  </p>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        firstName: 'A',
        lastName: 'B',
      }
    },

    computed: {
      fullName: {
        get() {
          console.log('fullName, get()');
          return this.firstName + '-' + this.lastName
        },
        set(value) {
          console.log('fullName set()', value);
          const names = value.split('-')
          this.firstName = names[0]
          this.lastName = names[1]
        }
      }
    }
  }).mount('#app')
</script>

此时的计算属性 fullName 是一个对象,包含 get 方法(常称为 getter)和 set 方法(常称为 setter)。前面写的计算属性函数的功能等同于get方法,当它在初始化时会执行一次,并且任意依赖数据发生变化时会再次执行,也就实现了 "姓名" 的初始动态显示与修改 "姓" 或 "名" 的内容时会同步更新显示的功能。而 set 方法则是在修改 fullName 属性值后,会自动执行。也就是说,当修改 "姓名" 的内容时,计算属性的 set 方法就会自动执行,在 set 方法中接收 fullName 指定的最新值,并分隔出两个值的数组分别去更新 firstName 和 lastName,这样就实现了需求③中的要求。

以下内容是我看最新官方的关于计算属性的补充:

获取上一个值(Vue3.4+)

如果需要,可以通过访问计算属性的 getter 的第二个参数来获取计算属性返回的上一个值。

export default {
  data() {
    return {
      count: 2
    }
  },
  computed: {
    // 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
    // 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
    // 直到 count 的值再次小于或等于 3 为止。
    alwaysSmall(_, previous) {
      if (this.count <= 3) {
        return this.count
      }

      return previous
    }
  }
}

如果你正在使用可写的计算属性的话:

export default {
  data() {
    return {
      count: 2
    }
  },
  computed: {
    alwaysSmall: {
      get(_, previous) {
        if (this.count <= 3) {
          return this.count
        }

        return previous;
      },
      set(newValue) {
        this.count = newValue * 2
      }
    }
  }
}

最佳实践

Getter 不应有副作用

计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。

避免直接修改计算属性值

从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算

监听

计算属性可以根据已有的一个或多个数据,通过同步计算返回一个新的用于显示的数据。在计算数据的依赖数据发生变化时,计算属性函数会重新执行并返回一个新的值进行显示。但有的时候,在依赖数据发生变化时,我们需要让其产生一些 "副作用"。例如,直接更新DOM,或者执行异步操作后,更新其他数据。此时我们可以使用Vue的监听语法来实现。

监听的基本使用

在选项式API中,我们可以用过watch选项配置一个函数来监听某个响应式属性的变化。监听回调函数默认在数据发生变化时回调,且接收新值和旧值两个参数。其语法格式如下。

watch: {
  xxx(newVal, oldVal) {
    // 处理 xxx 数据发生变化后的逻辑
  }
}

下面我们利用监听来实现如下图所示的页面效果。

现有两个需求,具体如下。

① 输入标题,实时同步显示到页面标题上。

② 输入标题,实时 AJAX 请求获取一个最匹配的问题并显示到输入框下面。

下面我们对需求进行分析:输入的标题可以通过 v-model 收集 data 对象中的 title 属性上,同时用 watch 选项来监听 title 属性的变化,在监听回调函数中将最新的值设置给文档的标题。同时发送 AJAX 请求,根据 title 属性获取一个最匹配的问题并设置给 data 对象中的 question 属性,显示到输入框下面。

实现代码如下所示。

<div id="app">
  标题:
  <input
    type="text"
    placeholder="输入内容同步到标题"
    v-model="title"
  /><br />
  问题: <span>{{ question }}</span>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        title: '',
        question: '',
      }
    },

    watch: {
      title(newVal, oldVal) {
        console.log(newVal, oldVal)
        // 直接更新DOM
        document.title = newVal
        // 模拟请求异步获取对应的答案
        setTimeout(() => {
          const question = `${newVal}最匹配的问题?`
          this.question = question
        }, 1000)
      },
    },
  }).mount('#app')
</script>

运行代码后,在输入框中输入 Vue,会发现1秒后显示问题。

其实 watch 选项不仅可以监听 data 对象中外部的属性,还可以监听其内部的属性。现有一个 person 对象,内部有 name 和 age 两个属性,我们使用 watch 选项对 name 属性进行监听。其语法格式如下。

data() {
  return {
    person: {
      name: 'tom',
      age: 12
    }
  }
}

watch: {
  // 监听 data 中 person 对象的 name 属性的变化
  'person.name': function (newVal, oldVal) {
    // 处理 person 对象中 name 属性变化后的逻辑
  }
}

即时回调与深度监听

watch 回调默认是在数据发生变化时自动调用,如果我们需要在初始化时就执行一次监听的回调,那么要如何实现呢?

Vue 的 watch 语法也支持这种场景需求,但是用法有些许差别,watch 的属性不能再是一个函数了,需要是一个配置对象,并通过 handler 配置来指定监听回调函数。同时我们可以通过配置 immediate 为 true 来指定监听回调函数在初始化过程中执行第1次,当然在被监听数据发生改变时也会执行。

代码如下。

<div id="app">
  <h3>name: {{ person.name }}</h3>
  <h3>likes: {{ person.likes }}</h3>
  <button @click="updateP">更新人员信息</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        person: {
          name: 'tom',
          likes: ['football', 'basketball'],
        },
      }
    },

    watch: {
      person: {
        handler(newVal, oldVal) {
          // 模拟异步将人的信息提交给后台
          setTimeout(() => {
            alert('向后台提交人员信息' + JSON.stringify(newVal))
          }, 1000)
        },
        immediate: true, // 标识立即执行: 也就是初始时会执行第一次
      },
    },

    methods: {
      updateP() {
        // 更新person: 指定一个新的人对象
        this.person = {
          name: this.person.name + '--',
          likes: [...this.person.likes, 'qqsg'],
        }
      },
    },
  }).mount('#app')
</script>

运行代码后,页面上展示了 person 中的 name 属性和 likes 属性。

由于我们配置了 immediate 为true,因此 watch 的回调在初始化时会执行 1 次。我们又通过定时器模拟异步向后台提交请求,即提交当前 person 的信息数据。点击 "更新人员信息" 按钮,直接将当前 person 更新为一个新的人员对象,监听的回调也会执行,用来模拟异步向后台提交请求,发送最新的人员信息。

补充:

回调函数的初次执行就发生在 created 钩子之前。Vue 此时已经处理了 datacomputedmethods 选项,所以这些属性在第一次调用时就是可用的。

watch 默认是浅层监听,如果只是更新 person 对象中的内部属性,比如 name 属性,那么 watch 的回调就不会执行

updateP () {
  // 更新 person 对象中的 name 属性
  this.person.name += '--'
}

点击 "更新人员信息" 按钮后,页面上的 name 属性值会发生改变,但是不会触发 watch 中的异步操作,因此页面上不会弹出警告提示。

那么如何才能深度监听到对象或数组内任意数据的变化呢?其实可以通过将 deep 配置为 true 来实现。也就是说,deep 的默认值为 false,因此之前 watch 监听不到内部数据的修改。现在修改一下 watch 的配置,具体如下。

watch: {
  person: {
    handler(newVal, oldVal) {
        // 模拟异步将人的信息提交给后台
        setTimeout(() => {
          alert('向后台提交人员信息' + JSON.stringify(newVal))
        }, 1000)
      },
      immediate: true, // 标识立即执行: 也就是初始时会执行第一次
      deep :true,  // 深度监听
  }
}

此时我们再次重复当前的操作,可以发现除了页面发生变化,1秒后还弹出了警告提示,证明此时已经监听到内部数据了。

其实,无论是更新 person 对象的 name 属性,还是更新 person 对象中的 likes 属性,监听的回调都会调用,会将最新的人员信息提交给后台,如下代码所示。

updateP () {
  // 更新 person 对象中的 likes 属性
  this.person.likes.push('qqsg')
}

重复点击 "更新人员信息" 按钮,这次 likes 属性的数组中新增了一项 "qqsg",这个修改同时被监听到了,也会异步向后台提交数据。

补充:

在 Vue 3.5+ 中,deep 选项还可以是一个数字,表示最大遍历深度——即 Vue 应该遍历对象嵌套属性的级数。

谨慎使用

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

一次性监听

  • 仅支持 3.4 及以上版本

每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。

export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // 当 `source` 变化时,仅触发一次
      },
      once: true
    }
  }
}

另外,监听器也叫侦听器,官网介绍了其他高级内容,这与后面的组合式API有关,请参考:

https://cn.vuejs.org/guide/essentials/watchers.html

绑定动态样式

在项目开发中,动态的页面中除了要绑定动态的数据,还要绑定动态的样式。在模板中,给标签绑定动态样式的方式有两种,分别是 class 绑定和 style 绑定。在 HTML 中这两者的值都是字符串类型,而在 Vue 中自然也可以绑定动态的字符串值,但频繁地使用连接字符串对于开发者来说,不仅操作冗余枯燥,同时也很容易出现错误。

为此,Vue 专门增强了 class 和 style 的绑定语法,Vue不仅可以支持动态字符串,还可以指定对象和数组类型值。本节主要讲解 Vue 绑定动态样式的两种方式。

class绑定

class绑定就是通过 "v-bind:class=表达式" 来绑定动态类名样式的,当然我们一般会使用 ":class="表达式"" 的简写方式。表达式的值支持字符串、对象和数组3种类型,而这3种类型的写法的使用场景也不尽相同,本节将带领读者依次学习和使用。

我们首先明确这3种类型的写法的使用场景:

写法

使用场景

字符串写法

样式类名不确定,需要动态指定

对象写法

要绑定的样式的个数确定、名字也确定,但要动态地决定是否使用

数组写法

要绑定的样式的个数不确定、名字也不确定

需要注意的是,在一个标签上静态 class 与动态 class 可以同时存在,最终编译后,Vue 会将动态 class 与静态 class 合并,并指定为标签对象的 class 样式。

来看下面的代码。

<!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" />
    <title>class绑定</title>
    <style>
      .basic {
        width: 200px;
        height: 100px;
        line-height: 100px;
        text-align: center;
        border: 1px solid black;
        margin-bottom: 10px;
      }

      .normal {
        background-color: skyblue;
      }
      .happy {
        background-color: rgba(255, 255, 0, 0.644);
      }
      .sad {
        background-color: gray;
      }

      .qqsg1 {
        background-color: yellowgreen;
      }
      .qqsg2 {
        font-size: 30px;
      }
      .qqsg3 {
        border-radius: 20px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <h2>测试class绑定(点击更新)</h2>
      <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
      <div class="basic" :class="classStr" @click="updateClass1">哈哈哈1</div>
      <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
      <div class="basic" :class="classObj" @click="updateClass2">哈哈哈2</div>

      <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
      <div class="basic" :class="classArr" @click="updateClass3">哈哈哈3</div>
    </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            classStr: 'normal',
            classObj: {
              qqsg1: true,
              qqsg2: false,
            },
            classArr: ['qqsg1', 'qqsg2', 'qqsg3'],
          }
        },

        methods: {
          // 更新动态的字符串类型class
          updateClass1() {
            const arr = ['happy', 'sad', 'normal']
            const index = Math.floor(Math.random() * 3)
            this.classStr = arr[index]
          },

          // 更新动态的对象类型class
          updateClass2() {
            this.classObj.qqsg1 = !this.classObj.qqsg1
            this.classObj.qqsg2 = !this.classObj.qqsg2
          },

          // 更新动态的数组类型class
          updateClass3() {
            this.classArr.pop()
          },
        },
      }).mount('#app')
    </script>
  </body>
</html>

上面代码中,每个div元素都有一个静态 class "basic"。

  • 当点击第1个div元素时,它的在 normal、sad 和 happy 这3个类名中随机切换,显示的样式也会随之更新。

  • 当点击第2个div元素时,它的在 qqsg1 和 qqsg2 这2个类名中来回切换,示的样式也会随之更新。

  • 当点击第3个div元素时,第1次点击类名中减少 qqsg3,第2次点击类名中减少 qqsg2,第3次点击类名中减少 qqsg1。此时再点击div元素,页面不会发生变化,只会应用静态class的效果,因为此时已经没有动态的类名了。

还可以绑定计算属性,这个在某些场景下面是很有用的!

style绑定

与 class 绑定类似,style绑定同样是通过 "v-bind:style="表达式"" 来绑定动态 style 样式的,当然我们一般会使用 ":style="表达式"" 的简写方式。表达式的值支持字符串、对象和数组3种类型,但是在实际开发中,开发者一般使用对象类型的写法。下面我们来编码测试一下。

<!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" />
    <title>style绑定</title>
    <style>
      .basic {
        width: 200px;
        height: 100px;
        line-height: 100px;
        text-align: center;
        border: 1px solid black;
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <h2>测试style绑定(点击更新)</h2>
      <div class="basic" :style="styleStr" @click="updateStyle1">哈哈哈4</div>
      <div class="basic" :style="styleObj" @click="updateStyle2">哈哈哈5</div>
      <div class="basic" :style="styleArr" @click="updateStyle3">哈哈哈6</div>
    </div>

    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            styleStr: 'font-size: 30px; color: red',
            styleObj: {
              fontSize: '40px',
              color: 'green',
            },
            styleArr: [
              {
                fontSize: '40px',
                color: 'blue',
              },
              {
                backgroundColor: 'gray',
              },
            ],
          }
        },

        methods: {
          // 更新动态的字符串类型style
          updateStyle1() {
            this.styleStr = 'font-size: 20px; color: blue'
          },
          // 更新动态的对象类型style
          updateStyle2() {
            this.styleObj.fontSize = '50px'
            this.styleObj.color = 'blue'
          },
          // 更新动态的数组类型style
          updateStyle3() {
            this.styleArr[0].color = 'red'
          },
        },
      }).mount('#app')
    </script>
  </body>
</html>

这个代码与之前实现的效果类似,在此不再赘述。但要注意的是,这个数组里面放的是对象,不再是字符串。

条件渲染指令

在实际项目开发中,有两种场景非常常见:第1种是局部页面需要在满足某种条件后显示;第2种是在多个不同的页面效果之间进行切换显示。无论哪一种场景,都只有在满足某种条件后才能渲染显示。在 Vue 中将其称为条件渲染,可以通过 v-if 相关指令或 v-show 指令实现。

v-if 相关指令

v-if 指令用于控制元素的显示或者隐藏,其相关指令包含 v-if、v-else 和 v-else-if。v-if 与 v-else-if 指令的表达式类型需要是布尔类型,当结果为 true 时,也就是满足条件后,才显示对应的元素;v-else 指令不需要指定表达式,只需要指定属性名,当元素前面的 v-if 和 v-else-if 指令都不满足条件时才会显示。

如果只有一个页面的条件渲染,则推荐使用 v-if 指令来渲染;如果有两个页面的条件渲染,则推荐配置使用 v-if 与 v-else 指令来渲染;如果超过两个页面的条件渲染,则推荐配合使用 v-if、v-else-if 和 v-else 指令来渲染。

先来看一个代码。

<div id="app">
  <h1>你的入学测试得分: {{ score }}</h1>
  <button @click="examAgin">学习后再复试一次</button>
  <button @click="examAgin" v-if="score<60">学习后再复试一次</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 55,
      }
    },
    methods: {
      examAgin() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

我们使用 v-if 实现了,只有当入学测试得分小于60分时按钮才显示。

现在添加一个新的需求:当入学测试得分大于或等于60分时,显示绿色的h3文本提示;当入学测试得分小于60分时,显示红色的h4文本提示。对于这个需求就可以配合使用 v-if 和 v-else 实。不过需要注意的是, v-else 指令不需要指定表达式。

<div id="app">
  <h3 v-if="score>=60" style="color: green">欢迎来学习!</h3>
  <h4 v-else style="color: red">很遗憾, 你的面试没有通过...</h4>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 55,
      }
    },
    methods: {
      examAgin() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

现将需求再次更改:当入学测试得分大于或等于60分时,显示绿色的h2文本提示;当入学测试得分小于50分时,显示红色的h3文本提示;当入学测试得分在50~60分(包括50分)时,显示黄色的h4文本提示。此时可以配置使用 v-if、v-else-if 和 v-else 指令来实现。需要注意的是,只有当 v-else-if 指令指定的表达式为true时,对应的标签才会显示。

<div id="app">
  <h2 v-if="score>=60" style="color: green">欢迎来学习!</h2>
  <h3 v-else-if="score<50" style="color: red">
    很遗憾, 你的入学测试没有通过,可以考虑UI或测试
  </h3>
  <h4 v-else style="color: yellow">此次测试没有完全通过, 可选择复试</h4>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 55,
      }
    },
    methods: {
      examAgin() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

前面都是对一个标签进行条件渲染,那么如果有多个标签要怎么处理呢?

我们可以用 template 标签来包含要实现条件渲染的多个标签,并在 template 标签上使用 v-if 指令来判断元素是否显示。需要注意的是,template 标签只在模板中存在,并不会产生任何 HTML 标签,具体如下。

<div id="app">
  <h1>你的入学测试得分: {{score}}</h1>
  <button @click="examAgin">学习后再复试一次</button>
  <template v-if="score>=60">
    <button>去找咨询老师办理入学</button>
    <button>去找技术老师深入交流技术疑问</button>
    <button>去找就业老师了解就业信息</button>
  </template>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        score: 55,
      }
    },
    methods: {
      examAgin() {
        this.score += 10
      },
    },
  }).mount('#app')
</script>

可以调试下页面元素,发现没有 template 标签,验证了 "template 标签只在模板中存在,并不会产生任何 HTML 标签" 的说法。

v-show 指令

在 Vue 中,v-show 指令也可以实现条件渲染。当 v-show 指令指定的表达式的值为 true 时,对应的标签才会显示。

下面我们将 v-if 指令中涉及的案例使用 v-show 指令来实现。

代码片段1如下。

<button @click="examAgin" v-show="score<60">学习后再复试一次</button>

代码版本2如下。

<div id="app">
  <h2 v-show="score>=60" style="color: green">欢迎来学习!</h2>
  <h3 v-show="score<60" style="color: red">
    很遗憾, 你的入学测试没有通过...
  </h3>
</div>

代码3片段如下。

<div id="app">
  <h2 v-show="score>=60" style="color: green">欢迎来学习!</h2>
  <h3 v-show="score<50" style="color: red">
    很遗憾, 你的入学测试没有通过,可以考虑UI或测试
  </h3>
  <h4 v-show="score<60 && score>=50" style="color: yellow">
    此次测试没有完全通过, 可选择复试
  </h4>
</div>

需要注意的是, v-show 指令是不能与 v-else 和 v-else-if 指令配合使用的。当需要在多个标签之间切换显示时,要使用多个 v-show 指令来实现。特别注意,v-show 指令不能用在 template 标签上

比较 v-if 和 v-show 指令

通过前面的学习我们知道,v-if 和 v-show 指令实现的功能是一致的,但实际上,二者内部的实现机制截然不同,这一点是在理论面试时经常被提问的问题

在初始化解析显示时,如果表达式的值为 true,那么二者的内部处理相同;如果表达式的值为 false,则内部处理完全不同。v-if 指令对应的模板标签结构不会被解析,也就不会产生对象的 HTML 标签结构;而 v-show 指令则会解析模板标签结构,生成 HTML 标签结构,只不过它会通过指定display为none的样式来隐藏标签结构

在更新数据后,表达式的值变为 true,需要显示标签结构。v-if 指令的标签会新建HTML标签结构并显示,而 v-show 指令只需要去除 display 为none的样式让标签结构重新显示出来。

当再次更新数据后,表达式的值变为 false,需要隐藏标签结构。v-if 指令的标签会新建HTML标签结构并显示,而 v-show 指令只需要去除display的none的样式让标签结构重新显示出来。

当再次更新数据后,表达式的值变为 false,需要隐藏标签结构。v-if 指令的标签直接被删除,内存中不再有对应的DOM结构;v-show 指令的标签通过指定display为none样式来隐藏标签结构。

通过下方代码进行验证:

 <div id="app">
  <h1 v-if="isShow">IT教育1</h1>
  <h2 v-show="isShow">IT教育2</h2>
  <button @click="isShow=!isShow">切换标识</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        isShow: false,
      }
    },
  }).mount('#app')
</script>

通过点击 "切换标识" 按钮,观察页面DOM结构的变化,很容易看出两者的区别。

最后简单总结一下 v-if 与 v-show 指令的区别,在隐藏时,v-if 指令会删除标签,而 v-show 指令只是通过样式控制标签不显示;再次显示时,v-if 指令需要重新创建标签,而 v-show 指令只需要更新一下样式就可以显示。

对于使用场景的选择,如果条件渲染的条件变化相对频繁或要控制的标签结构比较大,那么选择 v-show 指令比较合适。因为 v-if 指令在从隐藏变为显示时,需要重新创建 DOM 结构,效率相对低一些,但 v-show 指令在隐藏时,标签结构没有被删除,比 v-if 指令占用的内存更大一些。这也就是编程世界中经常说到的 "以空间换时间的技术",即用更大的占用空间,换取后面更少的执行时间。

还有一些需要注意的是,如果初始渲染条件的值为 false,则 v-if 指令是不会解析模板产生 HTML 标签的,有时也称它为懒加载,但 v-show 指令不是懒加载

补充:

v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行。请查看列表渲染指南获取更多细节。

警告

同时使用 v-ifv-for 是不推荐的,因为这样二者的优先级不明显。请查看列表渲染指南获得更多信息。

列表渲染指令

在项目开发的页面中,有很多列表效果需要动态显示。这在 Vue 中被称为列表渲染,可以通过 v-for 指令来实现。通过 v-for 指令可以遍历多种不同类型的数据,数组是较为常见的一种类型,当然类型还可以是对象或数值。

动态显示列表后,我们可以对列表进行添加、删除、修改、过滤、排序等操作。在通过 v-for 指令进行列表渲染时,有一个特别重要的属性 key,本节会对其背后的原理专门进行分析。

列表的动态渲染

  • 当我们在模板中通过 v-for 指令来遍历数组显示列表时,指令的完整写法如下。

v-for="(item, index) in array"

其中,item为被遍历的array数组的元素别名,index为数组元素的下标。如果不需要下标,则可以简化为 "v-for="item in array""。item 和 index 的名称不是固定的,也可以使用其他的合法名称。

  • 当我们使用 v-for 指令遍历一个对象时,遍历的是对象自身所有可遍历的属性,指令的完整写法如下。

v-for="(value, name) in obj"

其中,value为被遍历的obj对象的属性值,name为属性名。如果只需要属性值,则可以简化为 "v-for=value in object"。value 和 name 的名称不是固定的,也可以使用其他的合法名称。

  • 当 v-for 指令遍历的目标是一个正整数n时,其一般用于让当前模板在1~n的取值范围内重复产生n次,指令的完整写法如下。

v-for="value in n"

其中,value为1开始到n为止依次递增的数值。

请观察下方代码的运行效果。

<div id="app">
  <h2>测试遍历数组<人员列表>(用得很多)</h2>
  <ul>
    <li v-for="(p,index) in persons">{{index}}--{{p.name}}--{{p.age}}</li>
  </ul>

  <h2>测试遍历对象<汽车信息>(用得少)</h2>
  <ul>
    <li v-for="(value,name) in car">{{name}}--{{value}}</li>
  </ul>

  <h2>测试遍历指定次数(用得少)</h2>
  <ul>
    <li v-for="value in num">{{value}}</li>
  </ul>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        persons: [
          { id: '001', name: '张三', age: 22 },
          { id: '002', name: '李四', age: 24 },
          { id: '003', name: '王五', age: 23 },
        ],
        car: {
          name: '奥迪A8',
          price: '70万',
          color: '黑色',
        },
        num: 5,
      }
    },
  }).mount('#app')
</script>

上方代码中,分别演示了遍历的数据类型为数组、对象、数值的3种情况。

与 v-if 指令类似,我们也可以在 template 标签上使用 v-for 指令,实现对多个标签的列表渲染。

<div id="app">
  <h2>在template上使用v-for</h2>
  <ul>
    <template v-for="p in persons">
      <li>{{p.name}}--{{p.age}}</li>
      <span>学员</span>
    </template>
  </ul>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.31/vue.global.min.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        persons: [
          { id: '001', name: '张三', age: 22 },
          { id: '002', name: '李四', age: 24 },
          { id: '003', name: '王五', age: 23 },
        ],
        car: {
          name: '奥迪A8',
          price: '70万',
          color: '黑色',
        },
        num: 5,
      }
    },
  }).mount('#app')
</script>

我们可以利用 v-for 指令对数据进行循环,但是在阅读 Vue 官方文档时可以发现,Vue官方建议在使用 v-for 指令进行列表渲染时,最好同时指定唯一的key属性。这个key属性可以提高列表更新渲染的性能,且key属性的值最好不要使用下标,因为某个数组元素的下标是不稳定的,这样会影响内部 DOM Diff 算法的效率,甚至有可能产生错误的效果。key属性的值应该是数组元素对应的稳定的数值或字符串值,比如数组元素的id属性。现在对前面编写的人员列表代码进行完善,完整的代码应该是下方这样的。

<h2>在使用 v-for 指令进行列表渲染时指定唯一的 key 属性</h2>
<ul>
  <li v-for="(p,index) in persons" :key="p.id">
    {{index}}--{{p.name}}--{{p.age}}
  </li>
</ul>

列表的增、删、改

在实际项目开发中,在动态显示数组列表后,一般不会使用其只作为展示存在,可能还需要对其进行增、删、改操作。

下图展示了一个简易的列表,初始显示张三、李四、王五共3个人员信息列表,请思考如何实现页面效果和需求。

用户需求如下:

① 点击 "向第一位添加" 按钮,在第一个人员信息列表前面添加一条随机产生的人员信息。

② 点击 "向最后一位添加" 按钮,在最后一个人员信息列表后面添加一条随机产生的人员信息。

③ 点击 "删除" 按钮,直接删除对应的人员信息。

④ 点击 "更新" 按钮,将对应的人员信息替换为一条随机产生的人员信息。

实现代码如下所示。

<div id="app">
  <h2>测试列表的增删改</h2>
  <ul>
    <li v-for="(p,index) in persons" :key="p.id" style="margin-top: 5px">
      {{p.name}}--{{p.age}} --
      <button @click="deleteItem(index)">删除</button> --
      <button
        @click="updateItem(index, {id: Date.now(), name: '小兰', age: 12})"
      >
        更新
      </button>
    </li>
  </ul>
  <button @click="addFirst({id: Date.now(), name: '小明', age: 10})">
    向第一位添加
  </button>
  --
  <button @click="addLast({id: Date.now(), name: '小红', age: 11})">
    向最后一位添加
  </button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        persons: [
          { id: '001', name: '张三', age: 19 },
          { id: '002', name: '李四', age: 18 },
          { id: '003', name: '王五', age: 20 },
        ],
      }
    },

    methods: {
      addFirst(newP) {
        // 向第一位添加
        this.persons.unshift(newP)
      },
      addLast(newP) {
        // 向最后一位添加
        this.persons.push(newP)
      },
      deleteItem(index) {
        // 删除指定下标的
        this.persons.splice(index, 1)
      },
      updateItem(index, newP) {
        // 将指定下标的替换为新的人
        this.persons.splice(index, 1, newP)
      },
    },
  }).mount('#app')
</script>

在上方代码中为每个功能按钮都封装了相应的方法来实现对应的功能,从而实现对数组列表的增、删、改操作。其实每个功能方法内部,都是通过调用数组变更内部元素的方法(包括 unshift、push和splice方法)来实现的。

这里补充下数组的splice的语法:

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

参数

是否必需

描述

start

修改开始的位置索引(从0开始)

deleteCount

要删除的元素个数(如果为0或不传,则不删除)

item1, item2, ...

要添加到数组的新元素

那么如果不调用这些数组的内置方法,直接通过下标来添加或者替换数组列表,能触发页面自动更新吗?答案是:Vue2 中不可以,但在 Vue3 中是完全没有问题的。而通过调用函数变更内部元素的方法来操作元素,不管是 Vue2 还是 Vue3,都是可以触发页面自动更新的。这是因为 Vue 内部对 data 中的数组的一系列变更方法进行了重写包装,被包装的更新方法一共有7个,分别是 push、pop、shift、unshift、splice、sort和reverse方法,这7个方法内部会先对数组列表进行相应操作,再更新页面

在 Vue3 中,如果想在最后一个人员信息列表后面添加一条人员信息,就可以使用下方代码。

this.persons[this.persons.length] = newP

如果将指定下标的人员信息替换为新添加的人员信息,就可以使用下方代码。

this.persons[index] = newP

官网补充:

可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似,另外也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:

<li v-for="{ message } in items">
  {{ message }}
</li>

<div v-for="item of items"></div>

data() {
  return {
    parentMessage: 'Parent',
    items: [{ message: 'Foo' }, { message: 'Bar' }]
  }
}

这两个语法只是补充,感觉最好还是用括号和in。

列表的过滤

对列表进行过滤显示,是项目开发中常见的功能之一。列表过滤分为两种,分别是后台过滤和前台过滤。后台过滤是发送请求并提交条件参数给后台,由后台计算出过滤后的数组返回前台显示;而前台过滤是后台返回所有数据的数组,由前台进行过滤处理后显示。本节要实现的就是前台的列表过滤效果。

现在的需求为:当输入框没有任何输入时,显示所有人员的列表;当输入姓名时,下方会实时显示所有匹配的人员列表。初始页面效果和过滤页面效果下图所示。

我们先对上方需求进行分析,然后再编码实现。

页面由标题、输入框和列表组成。对于输入框输入的姓名,可以使用 v-modle 指令将输入的数据收集到 data 对象的keyword属性中。下方显示的列表无疑是通过遍历数据得到的,但是这个数据不能直接是所有人员的列表(初始列表),它需要是一个根据keyword属性进行过滤后的列表。对于这部分的处理,我们可以先在data对象中定义包含所有人员的数组persons,再定义一个根据keyword属性进行过滤的计算属性filterPersons,其过滤的条件就是人员的name中包含keyword属性。

这里要想到使用计算属性,因为这就是展示的数据要依赖已有的数据计算,使用计算属性再合适不过了。

下面给出代码。

<div id="app">
  <h2>人员列表</h2>
  <input type="text" placeholder="请输入姓名" v-model="keyword" />
  <ul>
    <li v-for="(p,index) of filterPerons" :key="index">
      {{p.name}}-{{p.age}}-{{p.sex}}
    </li>
  </ul>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        persons: [
          // 所有人员的列表
          { id: '001', name: '廖景山', age: 21, sex: '男' },
          { id: '002', name: '熊小巧', age: 19, sex: '女' },
          { id: '003', name: '吴天山', age: 20, sex: '男' },
          { id: '004', name: '谢思萌', age: 22, sex: '女' },
        ],
        keyword: '', // 输入的用户名
      }
    },

    computed: {
      filterPerons() {
        // 过滤后用于显示人员列表的计算属性
        return this.persons.filter((p) => {
          return p.name.indexOf(this.keyword) !== -1
        })
      },
    },
  }).mount('#app')
</script>

列表的排序

前面我们实现的对列表数据的过滤,对于大部分网站和用户来讲,这个功能并不够完善。现在的需求为:在对人员列表进行过滤的同时,提供排序的功能。需要增加3个按钮,分别是 "年龄升序" "年龄降序" "原顺序",点击 "年龄升序" 或者 "年龄降序" 按钮,根据年龄对人员列表进行升序或者降序处理,点击 "原顺序" 按钮,人员列表变为原本顺序。

根据需求,我们需要实现如下图所示的页面效果。

下面对这个需求进行分析。过滤后的人员列表有3种状态:升序、降序和原顺序。这里可以设计一个data数据sortType来标识这3种状态。因为有3种状态,所以不能将其设计为布尔类型,number类型和字符串类型都可以满足,这里将其设计为number类型。用0代表原顺序,1代表降序,2代表升序,值指定为0。点击按钮时,sortType更新相应的数值。在 filterPersons计算属性中,先根据keyword产生一个过滤后的数组,再根据sortType选择是否进行排序,从而决定是进行升序排列还是降序排列。

<div id="app">
  <h2>人员列表</h2>
  <input type="text" placeholder="请输入姓名" v-model="keyword" />
  <ul>
    <li v-for="(p,index) of filterPerons" :key="p.id">
      {{p.name}}-{{p.age}}-{{p.sex}}
    </li>
  </ul>
  <button @click="sortType = 2">年龄升序</button> --
  <button @click="sortType = 1">年龄降序</button> --
  <button @click="sortType = 0">原顺序</button>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        persons: [
          // 所有人员的列表
          { id: '001', name: '廖景山', age: 21, sex: '男' },
          { id: '002', name: '熊小巧', age: 19, sex: '女' },
          { id: '003', name: '吴天山', age: 20, sex: '男' },
          { id: '004', name: '谢思萌', age: 22, sex: '女' }
        ],
        keyword: '', // 输入的用户名
        sortType: 0, //0原顺序 1降序 2升序
      }
    },

    computed: {
      // 过滤并排序后的人员列表
      filterPerons() {
        // 取出依赖数据
        const { persons, keyword, sortType } = this

        // 对总数组进行过滤产生过滤后的数组
        const arr = persons.filter((p) => {
          return p.name.indexOf(keyword) !== -1
        })
        // 只有当sortType不为0时, 才进行排序
        if (sortType != 0) {
          arr.sort((p1, p2) => {
            if (sortType === 1) {
              // 降序
              return p2.age - p1.age
            } else {
              // 升序
              return p1.age - p2.age
            }
          })
        }
        return arr
      },
    },
  }).mount('#app')
</script>

这个代码写法还是很优雅的,解结与判断都要学习下。

事件处理

前面已经多次演示通过在button上绑定点击事件监听来处理点击响应的操作了,本节就来详细介绍Vue中的事件处理。在学习之前需要说明一点,Vue既支持原生DOM事件处理,也支持Vue自定义事件处理,但本节只关注原生DOM事件处理,至于Vue自定义事件处理,在后面再进行学习。

绑定事件监听

我们可以在HTML标签上使用 v-on 指令来绑定原生DOM事件监听,基本语法格式如下。

v-on:事件名="handler"

当然,在通常情况下,我们一般使用简化形式 "@事件名="handler""。handler为事件处理器,Vue支持多种handler的写法。下面会对handler的写法分别进行讲解,先来看一下初始代码。

<div id="app">
  <h2>num: {{num}}</h2>
  <button>测试event1(点击提示按钮的文本)</button><br /><br />
  <button>测试event2(点击提示指定的特定数据)</button><br /><br />
  <button>测试event4(点击num增加3)</button><br /><br />
  <button>测试event3(点击提示按钮的文本和指定的特定数据)</button>
  <br /><br />
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        num: 2,
      }
    },
  }).mount('#app')
</script>

handler有3种写法,下面进行具体讲解。

(1)handler是一个方法名,对应methods配置中的一个同名方法。

当事件触发时,对应的方法会被自动调用。这是用得最多的一种写法,我们可以使用它来实现第1个按钮的点击功能。大致的用法如下。

<button @click="handlerClick1">测试event1(点击提示按钮的文本)</button>

methods: {
  handlerClick1(event) {
    alert(event.target.innerHTML)
  }
}

对于上方代码来说,当点击按钮时触发handleClick1方法。需要特别说明的是,handleClick1方法中定义的形式参数(形参)event,接收的就是事件对象,这是原生DOM事件监听回调原本的特性。

(2)hanlder是一条语句,只是这条语句有两种含义:可以是调用methods配置中某个方法的语句,也可以是䞣更新data数据的语句。下面分别进行说明。

① 比如现在要实现点击第2个按钮,提示指定的数据。我们就可以让handler是调用methods配置中某个方法的语句,并传入自定义数据。在这种情况下,一般会省略语句最后的分号,来看下方代码。

<button @click="handlerClick2('特定任意类型数据')">测试event2(指定特定数据)</button>

methods: {
  handlerClick2(msg) {
    alert(msg)
  }
}

需要特别说明的是,方法中调用的语句在事件发生前不会被执行,只有在事件发生后,才会被自动调用执行。

② 比如现在要实现点击第3个按钮,让num的数量加3。我们就可以让handler是直接更新data数据num的语句。同样省略分号,来看下方代码。

<button @click="num += 3">测试event3(点击num增加3)</button>

(3)handler是一个箭头函数,它就是事件监听回调函数,可以接收一个event对象。

我们可以在箭头函数中调用方法或直接更新data数据,来看下方代码。

<button @click="event => handlerClick2('特定任意类型数据')">
  测试event2(指定特定数据)
</button>
<button @click="event => handlerClick4('特定任意类型数据', event)">
  测试event4(点击提示按钮的文本和自定义数据)
</button>

上方代码使用简单函数的方式重写了之前代码的实现。

当然如果不需要event,就可以在定义时省略。在这种情况下,明显第2种写法比第3种写法简单,但如果在调用方法时既需要event,也需要自定义数据,就需要用第3种写法来实现了。比如现在要实现点击第4个按钮,提示按钮的文本和指定的数据。我们可以用handler和第3种写法来实现,代码如下。

<button @click="event => handlerClick4('特定任意类型数据', event)">
  测试event4(点击提示按钮的文本和自定义数据)
</button>


methods: {
  handlerClick4(msg, event) {
    alert(msg + '--' + event.target.innerHTML)
  }
}

但这并不是最简化的实现方式,当handler是方法调用语句时,Vue提供了一个隐含的变量$event来代表event对象。这样我们可以用更简化的代码实现上面的需求。

 <button @click="handlerClick4('特定任意类型数据', $event)">
  测试event4(点击提示按钮的文本和自定义数据)
</button>

其实在实际开发中,箭头函数写法的使用频率很小。这是因为我们完全可以选择最后提供的方式来代替。

事件修饰符

在原生DOM事件处理中,我们可以在事件监听回调函数中通过event.preventDefault来阻止事件默认行为,还可以通过event.stopPropagation来停止事件冒泡。Vue设计了简洁的事件修饰符语法来实现对事件的这两种操作及其他操作。

事件修饰符是用点表示的事件指令后缀,比如 "@事件名.事件修饰符名"="handler"。常用的事件修饰符有下面几个。

  • .stop:停止事件冒泡

  • .prevent:阻止事件默认行为

  • .once:事件只处理一次

下面通过案例来演示常用事件修饰符的使用方法。

<div id="app">
  <a href="http://www.baidu.com" @click.prevent="test1">去百度</a>
  <br />
  <br />
  <div
    style="width: 200px; height: 200px; background: yellow"
    @click="test2"
  >
    outer
    <div
      style="width: 100px; height: 100px; background: green"
      @click.stop="test3"
    >
      inner
    </div>
  </div>
  <br />
  <button @click.once="test4">只响应一次点击</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        num: 2,
      }
    },

    methods: {
      test1() {
        alert('响应点击链接')
      },
      test2() {
        alert('点击outer')
      },
      test3() {
        alert('点击inner')
      },
      test4() {
        alert('响应点击')
      },
    },
  }).mount('#app')
</script>
  1. 在通常情况下,点击链接默认会跳转页面,而上方代码通过 ".prevent" 事件修饰符阻止事件默认行为,最终就不会跳转页面了。

  2. 我们在外部div和内部div上都绑定了事件。当点击内部div的绿色区域时,默认内部div会响应,但是由于事件冒泡,因此外部div也会响应。而上方代码通过 ".stop" 事件修饰符停止事件冒泡,外部div就不会响应了。

  3. 因为上方代码为按钮指定了 ".once" 事件修饰符,所以点击按钮时,其只会响应一次。

按钮修饰符

每个按键都有一个对应的key值,用户可以通过event.key得到各个按键的key值。请思考下方代码的运行效果。

<div id="app">
  <input type="text" @keyup="hint" />
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue

  createApp({
    methods: {
      hint(event) {
        alert(event.key)
      },
    },
  }).mount('#app')
</script>

在输入框获取焦点后,用户按任意键盘按键其就可以提示对应的key值。比如,Enter键提示Enter;按A键,提示a;按上方向键,提示ArrowUp。如果我们想要在按某个键盘按键时进行警告提示处理,又该如何实现呢?

最直接的处理方式就是在方法中判断key值。Vue提供了键盘按钮修饰符来简化此操作,基本语法为 "@事件名.key="handler"",事件名可以是任意ODM支持的按键事件,比如keydown或keyup,key为按键对应key值的kekab-case形式,比如Enter键为enter,上方向键为arrow-up,具体如下。

<!--  点击enter键抬起时触发 -->
<input type="text" @keyup.enter="hint" />
<!--  点击向上方向键抬起时触发 -->
<input type="text" @keyup.arrow-up="hint" />
<!--  点击A键抬起时触发 -->
<input type="text" @keyup.a="hint" />

但事实上一些按键的key值并不是特别友好的,为此Vue提供了更加友好的按键别名来进一步简化代码,具体如下。

  • .space:空格键。

  • .up:上方向键。

  • .down:下方向键。

  • .left:左方向键。

  • .right:右方向键。

这里以 .up 为例进行演示,具体如下:

<!--  点击向上方向键抬起时触发 -->
<input type="text" @keyup.up="hint">

除了键盘按键修饰符,Vue还提供了鼠标修饰符和系统按键修饰符,如下表。

值得一提的是,系统按钮修饰符一般会与键盘按键修饰符或鼠标按键修饰符配合使用,下面就是 Alt 键与 Enter键的组合。

<!-- alt键与enter键的组合 -->
<input @keyup.alt.enter="hint" />

更多补充参考官网:https://cn.vuejs.org/guide/essentials/event-handling.html

收集表单数据

在项目开发中,表单是常见的页面效果。我们常常需要让表单输入框根据JavaScript中的变量数据来实现动态显示,同时需要将用户输入的数据实时同步到JavaScript变量中。在Vue中,比较原始的做法是给输入框绑定动态value属性和input监听来实现,语法格式如下。

<div id="app">
  <h1>{{msg}}</h1>
  <input :value="msg" @input="msg=$event.target.value">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
<script>
  const { createApp } = Vue
  createApp({
    data() {
      return {
        msg: 'hello Vue3'
      }
    },
  }).mount("#app")
</script>

其实可以通过 v-model 指令来简化代码,具体如下。

<input v-model="msg">

使用这种方式,输入框可以根据msg初始化动态显示和更新动态显示。同时用户在输入时,输入的数据会自动保存到msg上。这就是前面所说的双向数据绑定的效果。

下面具体讲解使用 v-model 指令收集表单数据的相关内容。

使用v-model指令

v-model 指令可应用在各种不同类型的表单输入元素上,包括 input、textarea、select。

它会根据所应用的元素的不同,自动扩展到不同的DOM属性和事件监听上。下面列举了3种情况。

  • text类型和password类型的input和textarea:value属性和input事件监听。

  • radio类型和checkbox类型的input:checked属性和change事件监听。

  • select:value属性和change事件监听。

下面通过一个案例如演示使用 v-model 指令收集用户注册表单的各种数据的方法,如下图所示。

该表单页面中共有8个表单输入元素,其中账号、密码和年龄为输入框,性别为单选按钮,爱好、阅读并接受《用户协议》为复选框,所属校区为下拉列表,其他信息为文本框。

下面给出具体代码的实现。

<div id="app">
  <form @submit.prevent="confirm">
    账号:<input type="text" v-model="userInfo.account" /> <br /><br />
    密码:<input type="password" v-model="userInfo.password" /> <br /><br />
    年龄:<input type="number" v-model="userInfo.age" /> <br /><br />
    性别: 男<input
      type="radio"
      name="sex"
      v-model="userInfo.sex"
      value="male"
    />
    女<input
      type="radio"
      name="sex"
      v-model="userInfo.sex"
      value="female"
    />
    <br /><br />
    爱好: 学习<input
      type="checkbox"
      v-model="userInfo.hobby"
      value="study"
    />
    打游戏<input type="checkbox" v-model="userInfo.hobby" value="game" />
    吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat" />
    <br /><br />
    所属校区
    <select v-model="userInfo.city">
      <option value="">请选择校区</option>
      <option value="beijing">北京</option>
      <option value="shanghai">上海</option>
      <option value="shenzhen">深圳</option>
      <option value="wuhan">武汉</option>
    </select>
    <br /><br />
    其他信息:
    <textarea v-model="userInfo.other"></textarea> <br /><br />
    <input type="checkbox" v-model="userInfo.agree" />阅读并接受<a
      href="https://www.prajnalab.com"
      >《用户协议》</a
    ><br /><br />
    <button>提交</button>
  </form>
  <div>预览:{{userInfo}}</div>
</div>
<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        userInfo: {
          account: '',
          password: '',
          age: 18,
          sex: 'female',
          hobby: ['study'],
          city: 'beijing',
          other: '',
          agree: false,
        },
      }
    },

    methods: {
      confirm() {
        alert(`提交注册请求 用户信息: ${JSON.stringify(this.userInfo)}`)
      },
    },
  }).mount('#app')
</script>

在这个案例中,我们利用v-model指令收集 input、textarea和select表单元素的数据。这里有几点需要注意。

(1)如果我们想将多个表单元素输入数据收集到一个单独的对象userInfo中,那么 v-model 指令指定的表达式必须是 userInfo.xxx,而xxx则需要是对象userInfo中的某个已有的属性名,用来存储当前表单元素输入数据。

(2)具有多个radio的单选按钮,必须指定用于收集的不同value属性,否则效果完全不正确。值得一提的是,如果 v-model 指令指定的是同一个表达式 userInfo.sex,则收集的是被选中的某个 radio 的value属性。如果初始想选中某个radio,则需要让sex的初始值为被选中radio的value属性。

(3)具有多个checkbox复选框,需要指定用于收集的不同value属性。与单选按钮类似,v-model指令指定的也是同一个表达式userInfo.hobby,收集到的是所有被选中checkbox的value属性组成的数组。如果初始想选中某个checkbox,则hobby数组的初始值需要包含其他value属性。

(4)具有单个checkbox的复选框,不需要指定value属性,v-model指令收集的是布尔值,具体地说,当被选中时为true,反之为false。

(5)select上的v-model指令用于收集被选中的option和value属性。

(6)表单提交监听需要绑定在form标签上,同时需要通过 .prevent 事件修饰符来阻止表单自动提交。

(7)案例中的点击按钮 @submit.prevent="confirm",在 confirm 回调中发送注册的AJAX请求。

相关指令修饰符

Vue为v-model指令提供了相关的指令修饰符,用于满足特定场景的需求,基本语法为 "v-model.指令修饰符名"。下面就来学习 v-model 指令相关的3个指令修饰符。

1).lazy

在默认情况下,绑定 v-model 指令的输入框在用户输入过程中,会自动将输入数据实时同步到 data 的对应属性上。而加上 .lazy修饰符后,在用户输入过程中输入数据不会被实时同步到data的对应属性上,只有当输入框失去焦点时输入数据才会被同步。

事实上,在不添加 .lazy 修饰符时,内部绑定的是input事件,而添加.lazy修饰符后,内部绑定的是change事件。input事件在输入过程中触发,而change事件在输入数据被修改且输入框失去焦点后触发。

2).trim

默认情况下,输入文本两端的空格会被同步到data的对应属性上,如果手动去除这些空格,对于开发者来说会比较麻烦,此时可以直接通过.trim修饰符将文本两端的空格自动去除后,再同步到data的对应属性上。

3).number

在默认情况下,一个非number类型的输入框收集的都是文本字符串,即便输入的内容是数值,也会被转换为数值字符串。通过 .number 修饰符可以在输入的是纯数值文本,或者前面部分是数值文本的情况下,将数据自动转换为数值后再同步到data的对应属性上。如果前面部分不是数值文本,则会直接将文本字符串同步到data的对象属性上。

下面通过一个综合案例来演示上面3种指令修饰符的使用方法,如下代码所示。

<div id="app">
  <form @submit.prevent="confirm">
    账号1:<input type="text" v-model.lazy="userInfo.account1" />
    <br /><br />
    账号2:<input type="text" v-model.trim="userInfo.account2" />
    <br /><br />
    账号3:<input type="text" v-model.number="userInfo.account3" />
    <br /><br />
    <button>提交</button>
  </form>
  <div>预览:{{userInfo}}</div>
</div>

<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        userInfo: {
          account1: '',
          account2: '',
          account3: '',
        },
      }
    },

    methods: {
      confirm() {
        alert(`提交注册请求 用户信息: ${JSON.stringify(this.userInfo)}`)
      },
    },
  }).mount('#app')
</script>

这个代码唯一需要注意的是 .number,可以适配置 1abc 这种,会转成数字1。

注意

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用data 选项来声明该初始值。

Vue实例的生命周期

任何事物都有生命发展过程,它们都需要经历诞生、成长和消亡的过程。Vue实例也是一样的,其在应用过程中也会经历开始、挂载、更新及销毁的过程,我们将这个过程称为Vue实例的生命周期。在生命周期的不同阶段会触发不同的事件,这就像人在婴儿时需要喝奶汲取营养,在幼儿时期需要练习语言行走,在少儿时期需要学习知识与成长,在青年、中年时期需要工作与发展,在迟暮之年渐渐步入死亡。对应在开发中,Vue实例在不同阶段会主动通过调用特定的回调函数来通知应用程序,我们将其称为 "生命周期钩子函数"。

生命周期流程图

Vue实例的生命周期主要包括初始、挂载、更新、销毁几个不同的阶段,它们是按照顺序依次执行的。值得一提的是,所有阶段的执行次数和目标不都是相同的。其中,初始、挂载和销毁这3个阶段就像人的诞生与死亡一样,都只执行一次;更新阶段就像人的成长一样,是在不断发展和变化的,可以被不断地触发。下面通过图来演示Vue实例的生命周期。

如果要结合Vue3请参考官网图:https://cn.vuejs.org/guide/essentials/lifecycle.html

Vue实例的生命周期分析

Vue实例的生命周期钩子函数的代码编写位置与data、methods、computed、watch等配置属于同级并列关系。每个阶段生命周期钩子函数发生的情况都不同,下面我们结合代码来分析每个生命周期钩子函数的差异。

<!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">
    <title>生命周期</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
</head>

<body>
    <div id="app">
        <p ref="pRef">{{message}}</p>
        <button @click="message = 'Prajnalab拥有优质的互联网技术培训'">更新内容</button>
    </div>
    <script>
        const {
            createApp
        } = Vue

        const app = createApp({
            data() {
                return {
                    message: '欢迎来到Prajnalab',
                }
            },
            beforeCreate() {
                console.log('beforeCreate', this.message, this.$refs.pRef)
            },
            created() {
                console.log('created', this.message, this.$refs.pRef)
            },
            beforeMount() {
                console.log('beforeMount', this.message, this.$refs.pRef)
            },
            mounted() {
                console.log('mounted', this.message, this.$refs.pRef)
            },
            beforeUpdate() {
                console.log('beforeUpdate')
                debugger;
            },
            updated() {
                console.log('updated')
            },
            beforeUnmount() {
                console.log('beforeUnmount', this.$refs.pRef)
                // debugger;
            },
            unmounted() {
                console.log('unmounted', this.$refs.pRef)
            }
        })

        app.mount('#app');

        setTimeout(() => {
            app.unmount();
        }, 5000);
    </script>
      </script>
</body>
</html>

上面的代码演示了8个生命周期钩子函数,下面分别进行分析。

1)beforeCreate

beforeCreate是Vue实例的第1个生命周期钩子函数。在该生命周期钩子函数中出现了this对象,这个this指向的是当前Vue的实例对象。如果实例对象上有数据或者方法等内容,则后续可以通过this.xxx方式调用。值得一提的是,这个生命周期钩子函数是在Vue实例完成后立即触的,但此时props(属性接收)、data(数据初始化)、methods(方法调用)、computed(属性计算)及 watch(监听)等内容都还没有进行初始化,更不用说 $el 的真实DOM了。因此在该生命同期钩子函数中,this.message数据内容this.\$refs.pRef获取的真实DOM都为undefined(未定义)

简单理解:此时只是构造了Vue组件实例对象本身,但这个对象里面的属性还没有初始化!

2)created

created生命周期钩子函数是在处理完所有与状态相关的选项后被调用的,这就意味着在调用该生命周期钩子函数之前,props、data、methods、computed、watch等内容已经设置完成。因此,this.message数据内容会打印出 "欢迎来到Prajnalab" 字符串内容,但是因为挂载阶段还没有开始,所以 this.$refs.pRef 属性仍旧为undefined。

简单来说就是真实DOM还没有挂载但是组件实例上的选项都已经初始化过了。

3)beforeMount

beforeMount是在挂载真实DOM之前被触发的生命周期钩子函数。如果有 template 模板内容,则会将其编译成render函数调用。因为这一阶段还没有获取真实DOM,所以相关的内容都停留在虚拟DOM阶段。因此在这一阶段,this.message数据内容会打印出 "欢迎来到Prajnalab" 字符串内容,但 this.$refs.pRef 属性仍旧为undefined。

beforeMount 确实是 Vue 生命周期中一个存在感较低、很少被主动使用的钩子。大多数情况下,在 created 里完成操作就足够了。

4)在这一阶段的网页中,el 元素对象最终被实例的 $el 元素对象内容代替,成功地将虚拟DOM内容渲染到了真实DOM上。用户现在可以查看到网页元素最终渲染的效果,因此当前 this.$refs.pRef 打印出来的结果是一个真实DOM。

简单来说,就是从这个阶段开始你才能去拿真实的DOM,一个经典的场景就是Echart挂载。

到这里,前面出现的生命周期钩子函数都只会按照讲解顺序执行一次。我们可以将其称为 "初始挂载阶段",生命周期顺序以及相关内容输出打印的结果如下图所示。

5)beforeUpdate

组件在因为一个响应式状态变更即将更新真实DOM前,会调用 beforeUpdate 生命周期钩子函数。比如在示例代码中可以通过按钮修改 message 信息,这就产生了一个响应式数据的变更。示例代码在生命周期钩子函数中设置了一个debugger断点进行效果的查看,最终会发现页面中显示的仍旧是数据修改之前的真实DOM。

6)updated

updated生命周期钩子函数与beforeUpdate生命周期钩子函数类似,区别是前者在更新真实DOM前被调用后,后者在更新真实DOM后被调用。将之前的断点调调试继续运行,则会发现页面中的message内部已经被替换成 "Prajnalab拥有优质的互联网技术培训"。

7)beforeUnmount

当调用当前的 beforeUnmount 生命周期钩子函数时,组件实例依然具有全部的功能。同样在该生命周期钩子函数中进行 debugger 断点调试,并且利用定时器进行组件实例的unmount销毁操作后,就可以看到beforeUnmount生命周期钩子函数。此时页面中仍将保留真实DOM显示的状态,控制台中的 this.$refs.pRef 仍旧显示真实DOM。

8)unmounted

unmounted生命周期钩子函数在一个组件实例被销毁后被调用,此时对应组件实例的真实DOM也不复存在。继续当前的debugger调试,程序会进入unmounted生命周期,显然 this.$refs.pRef对象内容已经被清空,网页中不再显示任何的真实DOM。

常用的生命周期钩子函数

Vue拥有这么多的生命周期钩子函数,哪些才是项目开发中常用的呢?下面结合实际情况从6个方面对各个生命周期钩子函数进行分析。

(1)对于 beforeCreate 来说,通过 this 不能操作数据、不能调用方法,也不能操作真实DOM,能够处理的内容少之又少,只能设置一些无关数据操作的定时器与网络连接

(2)虽然 created 中已经拥有 data、methods、computed、watch等内容,这就意味着可以尝试进行数据请求并修改的操作,但此时并没有真实DOM,谁也无法确保在Vue实例化过程中是否会出现异常,从而导致真实DOM无法渲染,最终造成数据请求和数据修改的操作没有意义。

(3)beforeMount主要处理的是虚拟DOM生成,如果想要操作虚拟DOM,则可以在这一阶段实现。但在开发过程中开发人员直接操作虚拟DOM的情况并不常见,因此这一生命周期钩子函数也不常用。

(4)其实最常用的生命周期钩子函数是 mounted,因为此时已经确保所有 data、method、computed、wacth等内容的存在,可以操作所有响应式数据。在该生命周期钩子函数中,可以对请求数据、后续接口处理操作定义的方法、计算与监听等。而且在该生命周期钩子函数中真实DOM已经存在,说明实例渲染无误,那么在这里再操作真实DOM也变得简单清楚。

(5)beforeUpdate与updated是在响应式数据更新时被触发的生命周期钩子函数。在这两个生命周期钩子函数中操作需要十分小心,因为修改任何一个数据都会触发这两个生命周期钩子函数,这就意味着它们的触发频率是非常高的。

这里需要思考的是,在更新阶段是否要进行类似数据请求的操作,以及数据请求操作频率的问题,这将牵扯到与性能相关的问题,因此这两个生命周期钩子函数并不常用,如果一定要使用,则需要注意利用特定的条件对触发的频率进行限制。

(6)因为在 unmounted阶段已经完全销毁了组件实现,所以在这个生命周期钩子函数中操作的内容相对较少。如果需要进行清除定时器、取消监听、断开网络连接等操作,建议在beforeUnmount组件实例未完全销毁阶段进行,这样也能够确保在 unmounted 阶段销毁的组件实现是比较干净纯粹的

综上所述,mounted、beforeUnmounted是项目开发中常用的生命周期钩子函数。

其它的生命周期介绍见官网:https://cn.vuejs.org/api/options-lifecycle.html#beforecreate

过滤与动画

动画的类型主要划分为CSS样式动画和JavaScript脚本动画,其中,CSS样式动画可以划分为transition过渡动画和animation逐帧动画。在Vue中只能使用内置组件transition实现单一元素的动画,对于多元素分组动画,则可以使用transition-group组件处理。

基于CSS的过渡动画效果

基于CSS的样式实现动画效果,开发人员需要编写自定义动画样式。样式的名称不能随意命名,需要遵循一定的规则标准。我们将动画划分为两类,分别是开始动画和结束动画。

开始动画的类名主要有3个,包括 v-enter-from(进入动画起始)、v-enter-active(进入动画生效)、v-enter-to(进入动画结束);结束动画的类名与开始动画的类名是对应的,主要有3个,v-leave-form(离开动画起始)、v-leave-active(离开动画生效),v-leave-to(离开动画结束),下面通过图例演示这6个类名发生的时刻。值得一提的是,动画类名中的v指代的是用户自定义的动画样式类名,而不是字典意义上的v字母

也就是v可以替换成自定义的单词

除了上面这种形式,我们还可以为过渡效果命名。比如 fade-enter-active、fade-leave-active等自定义动画样式类名,就是利用fade单词替换了动画类名中的v字母。这就是自定义动画样式的类名规则,即只能使用自定义动画样式名称替换v字母,后面的命名规则不可改变。

现在只需要利用内置组件 transition 包裹动画元素,并给 transition 组件设置属性 name,name的值对应自定义动画样式名称fade。需要注意的是,transition组件的操控范围只限一个子元素,无法对多个包裹的子元素进行动画处理。

请看下面的代码,观察其运行效果。

<!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" />
    <title>基于 CSS 的过渡效果</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <style>
      /* 开始与离开动画都是透明度渐变动画 */
      .fade-enter-active,
      .fade-leave-active {
        transition: opacity 0.5s ease;
      }

      /* 开始初始透明为0,结束最终透明度也为0 */
      .fade-enter-from,
      .fade-leave-to {
        opacity: 0;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <button @click="show = !show">切换动画</button>
      <transition name="fade">
        <div v-if="show">Hello Vue3!</div>
      </transition>
    </div>

    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            show: false,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

点击 "切换动画" 按钮后可以看到动画元素的显现是一个渐显的过程,而动画元素的隐藏则是一个渐隐的过程,完全实现了基于CSS的过渡动画效果。

基于CSS的逐帧动画效果

CSS的过渡动画只能实现开始与结束的动画效果,而animation逐帧动画却可以实现更丰富、更细腻的动画效果。不过对于Vue来说,不管是过渡动画还是逐帧动画,用户都是利用6个样式类来自定义样式类名,从而实现动画效果的。

比如定义一个slide滑动逐帧动画效果,只需要在slide-enter-active和slide-leave-active中,指定slide-in与slide-out的逐帧动画名称,并在逐帧动画中利用百分比或者from、to来实现X轴位移操作。在自定义动画样式后,只需将Vue的transition动画组件的name属性值修改成slide,即可实现指定元素滑入位移和滑出位移的动画效果。

下方代码就是在前面过渡动画代码基础上修改的,可对比体会。

<!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" />
    <title>基于 CSS 的逐帧效果</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <style>
      /* 开始与离开动画都是指定逐帧动画名称 */
      .slide-enter-active {
        animation: slide-in 0.5s ease;
      }

      .slide-leave-active {
        animation: slide-out 0.5s ease;
      }

      /* 逐帧动画可以使用百分比,如果只是开始与结束可以使用from、to */
      @keyframes slide-in {
        0% {
          transform: translateX(100px);
        }

        100% {
          transform: translateX(0);
        }
      }

      @keyframes slide-out {
        from {
          transform: translateX(0);
        }

        to {
          transform: translateX(100px);
        }
      }
    </style>
  </head>
  <body>
    <div id="app">
      <button @click="show = !show">切换动画</button>
      <transition name="slide">
        <div v-if="show">Hello Vue3!</div>
      </transition>
    </div>

    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            show: false,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

TODO 关于CSS动画这一块我还不是很理解,有时间再学习下,现在只要知道Vue中如何绑定类名。

基于第三方动画类库的CSS动画效果

对于Vue的CSS动画,是不是只有自定义动画样式的处理方案呢?其实Vue的CSS动画还可以和第三方动画类库完美结合,从而可以更快、更便捷地实现炫酷的CSS动画效果。

animate.css是一个拥有众多绚丽动画方式的CSS动画类库(这里只做简单演示,相关内容可自行查看bootstrap官方网站)。Vue为了更好地与第三方动画类库结合,为transition组件设置了enter-from-class、enter-active-class、enter-active-class、enter-to-class、leave-active-class、leave-to-class 6个样式绑定的属性,这6个属性的名称与之前的CSS动画类名大致相同,意义也相差无几,这里不多做讲解。

我们可以通过CDN的方式引入animate.css CSS动画类库,在遵循其标准的前提下,直接利用 transition 组件提供的6个属性设置动画效果。任何动画样式都需要先设置 animate_animated 样式,以此表明它是 animate.css 提供的动画样式,然后设置特定的动画效果样式名称,比如在 enter-active-class 中设置 animate_bounceInRight,在 leave-active-class 中设置 animate_bounceOutRight 后,即可在不写任何自定义动画样式的情况下,实现绚丽的动画效果。

将上面描述的过程通过代码实现,具体如下。

<!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" />
    <title>基于第三方动画类库的CSS动画效果</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <link
      href="https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.min.css"
      rel="stylesheet"
    />
  </head>

  <body>
    <div id="app">
      <button @click="show = !show">切换动画</button>
      <transition
        enter-active-class="animate__animated animate__bounceInRight"
        leave-active-class="animate__animated animate__bounceOutRight"
      >
        <div v-if="show">Hello Vue3!</div>
      </transition>
    </div>

    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            show: false,
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

基于JavaScript的动画效果

在Vue中,除了利用CSS实现动画效果,还支持JavaScript的操作实现。transition组件提供了8个有关JavaScript动画的钩子函数,主要包括 before-enter(动画进入前)、enter(动画进入)、after-enter(动画进入后)、

enter-cancelled(进入取消)、before-leave(动画离开前)、leave(动画离开)、after-leave(动画离开后)、leave-cancelled(离开取消)。从名称上来看,这8个transition组件提供的钩子函数与Vue生命周期钩子函数相似,但要知道它们是完全不同的。这8个transition组件的钩子函数的位置是在method方法当中,而不是与method方法并列。

Vue动画的钩子函数主要包括两个参数,即el和done,其中,el是要操作的元素目标,done是一个函数,代表过渡是否结束。值得一提的是,当我们想要在@enter和@leave中确认动画是否结束时,可以通过调用done函数来实现,否则钩子函数将被同步调用,过渡将会立即完成,这就会产生与预期动画不一致的结束。

下面利用before-enter、enter、before-leave、leave这4个钩子函数实现一个进度条效果,请思考下面的代码。

<!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" />
    <title>基于JS的动画效果</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
  </head>

  <body>
    <div id="app">
      <button @click="show = !show">切换动画</button>
      <transition
        @before-enter="beforeEnter"
        @enter="enter"
        @before-leave="beforeLeave"
        @leave="leave"
      >
        <div
          style="width: 100px; height: 100px; background-color: green"
          v-if="show"
        ></div>
      </transition>
    </div>

    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            show: false,
            elementWidth: 100,
          }
        },
        methods: {
          beforeEnter: function (el) {
            // el是动画元素目标
            this.elementWidth = 100
            // 初始元素宽度
            el.style.width = this.elementWidth + 'px'
          },
          enter: function (el, done) {
            // 通过定时器,每隔10毫秒,将元素宽度增加10px
            let round = 1
            const interval = setInterval(() => {
              el.style.width = this.elementWidth + round * 10 + 'px'
              round++
              // 当达到指定条件时,停止定时器,并终止动画
              if (round > 20) {
                clearInterval(interval)
                done() // 动画完成
              }
            }, 20)
          },
          beforeLeave: function (el) {
            this.elementWidth = 300
            el.style.width = this.elementWidth + 'px'
          },
          leave: function (el, done) {
            // 通过定时器,每隔10毫秒,将元素宽度减少10px
            let round = 1
            const interval = setInterval(() => {
              el.style.width = this.elementWidth - round * 10 + 'px'
              round++
              // 当达到指定条件时,停止定时器,并终止动画
              if (round > 20) {
                clearInterval(interval)
                done() // 动画完成
              }
            }, 20)
          },
        },
      }).mount('#app')
    </script>
  </body>
</html>

在上面的代码中,初始进度条div不显示,当点击 "切换动画" 按钮后,在动画进入前(beforeEnter方法中),为要实现的进度条设置了初始宽度;当动画进入时(enter方法中),通过定时器实现了进度条逐渐增加的效果,并在实现效果后终止动画;在动画离开前(beforeLeave方法中),再次设置进度条初始宽度;在动画离开后(leave方法中),做的事情与动画进入时类似,即可通过定时器实现进度条逐渐减少的效果,并在实现效果后终止动画。

多元素分组动画效果

transition动画组件只能对单一元素进行动画控制,如果想实现列表等多元素的分组动画操作,则需要利用 transition-group组件实现。transition组件与transition-group组件的属性基本一致,唯一不同的是,transition-group组件的tag标签属性指定一个元素作为容器元素来渲染,由于transition-group组件控制的是多元素的分组动画,因此必须给它包裹的每个子元素设置唯一的key属性来进行区分,否则无法实现动画效果。

请看下面代码的运行效果。

<!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" />
    <title>分组动画效果</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <link
      href="https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.min.css"
      rel="stylesheet"
    />
  </head>

  <body>
    <div id="app">
      <button class="btn btn-pirmary" @click="addItem">Add Item</button>
      <!-- 指定ul作为容器元素来渲染 -->
      <!-- transition-group中拥有与transition相同的属性 -->
      <transition-group
        tag="ul"
        enter-active-class="animate__animated animate__bounceInRight"
        leave-active-class="animate__animated animate__bounceOutRight"
      >
        <!-- 每个子元素都必须有唯一值key属性 -->
        <li
          v-for="(number,index) in numbers"
          @click="removeItem(index)"
          :key="number"
        >
          {{number}}
        </li>
      </transition-group>
    </div>

    <script>
      const { createApp } = Vue

      createApp({
        data() {
          return {
            numbers: [1, 2, 3, 4, 5],
          }
        },
        methods: {
          addItem() {
            // 列表随机位置添加一个元素
            const pos = Math.floor(Math.random() * this.numbers.length)
            this.numbers.splice(pos, 0, this.numbers.length + 1)
          },
          removeItem(index) {
            // 移除指定位置的元素
            this.numbers.splice(index, 1)
          },
        },
      }).mount('#app')
    </script>
  </body>
</html>

TODO 动画这一块感觉CSS都没有学明白....有时间补下

内置指令

指令调用是Vue模板编写中应用率非常高的方式之一,Vue为开发者提供了众多的内置指令,主要包括 v-text、v-html、v-show、v-if、v-else、v-else-if、v-for、v-on、v-bind、v-model、v-slot、v-pre、v-once、v-memo、v-cloak,其中,v-show、v-if、v-else、v-else-if、v-for、v-on、v-bind、v-model 指令之前已经涉及到,这些指令的相关内容比较简单,这里不多做讲解,v-slot指令在后面专门进行讲解。

本节主要讲解 v-text、v-html、v-pre、v-once、v-memo、v-cloak 指令的相关知识。

v-text和v-html指令

v-text指令会渲染指令中的value属性,不管渲染的是什么内容,该指令都会将其渲染为纯文本后输出。如果value属性中带有HTML标签内容,那么页面中仍旧会显示带有标签的文本结果。

v-html指令和v-text指令类似,如果想对渲染内容进行HTML解析,就可以通过v-html指令实现。值得一提的是,该指令内部会用innerHTML的方式进入内容插入和显示,而v-text指令内部会用innerText的方式进行内容插入和显示。

下面在一个文件中分别使用 v-html 和 v-text 指令实现带链接的跳转功能,实现代码如下。

<span v-text="'<a href=http://www.prajnalab.com>点击链接跳转<a>'"></span>
<span v-html="'<a href=http://www.prajnalab.com>点击链接跳转<a>'"></span>

v-pre指令

v-pre指令会跳过当前所在元素及其子元素的编译,假如定义了一个message响应式数据,我们想将插值表达式中的message字符串内容渲染在页面上,就可以利用v-pre指令实现,页面不会将message的结果值进行渲染、显示,而仅仅将message插值表达式的内容进行输出。

<p v-pre>{{ message }}</p>

v-once指令

v-once顾名思义就是只渲染一次。具体地说,由 v-once 指令包含的解析内容只会解析渲染一次。如果想要在首次渲染后修改 v-once 指令中的响应式数据内容,则会发现其不会出现任何改变。简单地说,动态显示时,使用该指令后不会再更新对应的data数据。

<div id="app">
  <p v-once>{{ message }}</p>
  <!-- 修改message以后p标签的message内容不会发生改变 -->
  <button @click="message = 'hi Vue3!'">修改数据</button>
  </div>
<script>
  const { createApp } = Vue
  
  createApp({
    data() {
      return {
        message: 'Hello Vue3!',
      }
    },
  }).mount('#app')
</script>

v-memo指令

v-memo指令可以实现高效缓存,在指定依赖条件下进行模板编译。在非依赖条件下,Vue则对v-memo指令包含的内容进行跳过编译处理。

假如当前有两个响应式数据count和update,但v-memo指令限制的数据是count,那么只有在 "count++" 按钮被点击的情况下,v-memo指令包含的内容都会以实时更新渲染,而在 "update++" 按钮被点击的情况下,v-memo指令包含的内容并不会发生响应式变化。

需要注意的是,如果v-memo指令设置的是[](空数组),v-memo 指令就相当于v-once指令的作用,即只进行一次渲染处理。

上面描述过程的实现代码如下。

<!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" />
  <title>内置指令</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <div v-memo="[count]">
      <p>count: {{ count }}</p>
      <p>update: {{ update }}</p>
    </div>
    <button @click="count++">count++</button>
    <button @click="update++">update++</button>
  </div>
  <script>
    const { createApp } = Vue
    createApp({
      data() {
        return {
          count: 18,
          update: 22,
        }
      },
    }).mount('#app')
  </script>
</body>

只让部分响应式数据(的变化)触发响应式更新

v-cloak指令

v-cloak指令用于隐藏尚未完成编译的DOM模板。有时会出现网络延迟或异步操作的情况,导致模板中的插值表达式内容并不一定能够及时被编译解析,网页中则会出现一部分未被解析的插值表达式,我们可以理解为花屏状态。此时就可以利用v-cloak指令,将指令中包含的内容先进行隐藏,当全部内容被解析编译完以后再显示,这样可以提升用户体验。需要注意的是,v-cloak指令不能单独使用,需要配合样式来实现。

<!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" />
    <title>内置指令v-cloak</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
    <style>
      [v-cloak] {
        display: none;
      }
    </style>
  </head>
  <body>
    <!-- v-cloak指令包含的内容会先行隐藏,直到页面编译通过才能显示 -->
    <div id="app"><div v-cloak>{{message}}</div></div>
    <script>
      const { createApp } = Vue
      createApp({
        data() {
          return {
            message: 'Hello Vue3!',
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

当出现网络延迟或异步操作时,页面会将内容先隐藏,直到编译通过才显示 "Hello Vue3!"。