Skip to content

Vue3-01-基础

[TOC]

API

  • createApp():创建一个应用实例

前言

Vue3新特点

1、源码

  • 源码通过monorepo的形式来管理

    将许多项目的代码存储在同一个repository中,这样做的目的是多个包本身相互独立,可以有自己的功能逻辑、单元测试等,同时又在同一个仓库下方便管理

  • 源码使用TypeScript来进行重写

    • Vue2:Vue使用Flow来进行类型检测

    • Vue3:Vue的源码全部使用TypeScript来进行重构,并且Vue本身对TypeScript支持也更好了

2、性能

  • 使用Proxy进行数据劫持

    • Vue2:使用Object.defineProperty来劫持数据的getter和setter方法。缺陷: 无法劫持和监听对象的添加和删除属性

    • Vue3:使用Proxy来实现数据的劫持

  • 删除了一些不必要的API

    • 移除了实例上的$on, $off 和 $once

    • 移除了一些特性:如filter、内联模板等

  • 编译方面的优化

    生成Block Tree、Slot编译优化、diff算法优化

3、新的API

  • 由Options API 到 Composition API

    • Vue2:通过Options API来描述组件对象,包括data、props、methods、computed、生命周期等选项

      缺陷: 多个逻辑可能是在不同的地方,代码的内聚性非常差

    • Vue3:使用的是Composition API

      **优点:**可以将相关联的代码放到同一处进行处理,而不需要在多个Options之间寻找

  • Hooks函数增加代码的复用性

    • **Vue2:**通过mixins在多个组件之间共享逻辑

      缺陷: mixins也是由一大堆的Options组成的,并且多个mixins会存在命名冲突的问题

    • **Vue3:**通过Hook函数,来将一部分独立的逻辑抽取出去,并且它们还可以做到是响应式的

Vue的安装

1、CDN引入

  • CDN : 内容分发网络。Content Delivery Network 或 Content Distribution Network

    CDN指通过相互连接的网络系统,利用最靠近每个用户的服务器。更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户。来提供高性能、可扩展性及低成本的网络内容传递给用户

  • 目前开源的CDN服务器:unpkgJSDelivrcdnjs

  • 语法:

    html
    <script src="https://unpkg.com/vue@next"></script>
  • 案例:

    image-20221005160543855

2、下载和引入

  • 语法:

    html
    <script src="../js/vue.js"></script>
  • 案例:

    image-20221005160710224

3、NPM包管理

  • 语法:

    sh
    # Vue2 安装
    npm i vue@2
    
    # Vue3 安装
    npm i vue
    # 或
    npm i vue@next

4、VueCli脚手架

  • 语法:

    sh
    # 1. VueCli 安装
    npm i vue-cli
    
    # 2. 项目 安装
    vue create 项目名

5、Vite脚手架

基于 vite 工具,使用 create-vue 创建项目

shell
npm init vue@latest

问题: vite@3.2.4 要求node的版本是:"engines": "node": "^14.18.0 || >=16.0.0",否则报错

声明式和命令式

  • 命令式编程:每完成一个操作,都需要通过JavaScript编写一条代码,来给浏览器一个指令
  • 声明式编程:在createApp传入的对象中声明需要的内容,模板template、数据data、方法methods。(如Vue、React、Angular)

MVVM模型

  • MVVM:Model View ViewModel

    Vue虽然并没有完全遵守MVVM的模型,但是整个设计是受到它的启发的

    image-20221005172740845

  • MVC:Model View Controller

    是在前期被使用非常框架的架构模式,比如iOS、前端

createApp()

创建一个应用实例

1、语法

html
function createApp(rootComponent: Component, rootProps?: object): App

2、参数

  • rootComponent:根组件对象,如App。根组件选项:
    • template
    • data
    • methos
    • computed
    • watch
    • props
  • rootProps:要传递给根组件的 props

3、返回值

  • App: app实例对象

组件选项

Component组件的选项

template

  • 使用

    image-20221005215818178

  • script标签

    image-20221005172230782

  • template标签

    image-20221005172256597

data

1、语法

image-20230210104559245

2、面试题:组件中 data 为什么是一个函数?

1、因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响。

2、如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响。

3、而new Vue的实例,是不会被复用的,因此不存在引用对象的问题。

3、Vue3中 data后如果是一个对象会报如下错误:

image-20230210105011841

4、data中返回的对象会被Vue的响应式系统劫持

data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或访问都是在劫持中被处理

  • 访问:可以在template中通过 访问 counter 属性
  • 修改:在JS中修改了 counter 的值时,counter 属性和template中的 的值也会随之改变

具体的响应式原理,后续再说

案例:计数器

  • 原生实现

    image-20221005171525639

  • Vue实现

    image-20221005170906885

    image-20221005170808837

知识补充

methods中的this

不能使用箭头函数

我们在methods中要使用data返回对象中的数据,那么这个this是必须有值的,并且应该可以通过this获取到data返回对象中的数据。

那么我们这个this能不能是window呢?

  • 不可以是window,因为window中我们无法获取到data返回对象中的数据;
  • 但是如果我们使用箭头函数,那么这个this就会是window了;

我们来看下面的代码:

  • 我将increment换成了箭头函数,那么它其中的this进行打印时就是window;
js
const App = {
  template: "#my-app",
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    increment: () => {
      // this.counter++;
      console.log(this);
    },
    decrement() {
      this.counter--;
    }
  }
}

为什么是window呢?

  • 这里涉及到箭头函数使用this的查找规则,它会在自己的上层作用域中来查找this;
  • 最终刚好找到的是script作用域中的this,所以就是window;

this到底是如何查找和绑定的呢?

this到底指向什么

事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过bind绑定了this:

image-20230209141450508

vue3源码的this绑定过程

VSCode增加代码片段

我们在前面练习Vue的过程中,有些代码片段是需要经常写的,我们在VSCode中我们可以生成一个代码片段,方便我们快速生成。

VSCode中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。

具体的步骤如下:

image-20230209141505536

生成代码片段

  • 第三步,在VSCode中生成代码片段;

打开生成代码片段的配置

image-20230209141522089

选择HTML

image-20230209141545327

生成代码片段

image-20230209141555560

模板语法

React的开发模式:

  • React使用的jsx,所以对应的代码都是编写的类似于js的一种语法;
  • 之后通过Babel将jsx编译成 React.createElement 函数调用;

Vue也支持jsx的开发模式(后续有时间也会讲到):

  • 但是大多数情况下,使用基于HTML的模板语法;
  • 在模板中,允许开发者以声明式的方式将DOM绑定到底层组件实例的数据;
  • 在底层的实现中,Vue将模板编译成虚拟DOM渲染函数,这个我会在后续给大家讲到;

所以,对于学习Vue来说,学习模板语法是非常重要的。

插值语法

mustache

如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值:

html
<div>{{message}}</div>

并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中,当data中的数据发生改变时,对应的内容也会发生更新。

当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式:

html
<body>
  <div id="app"></div>

  <template id="my-app">
    <div>
      <!-- mustache基本使用 -->
      <h2>{{message}}</h2>
      <!-- JavaScript表达式 -->
      <h2>{{ counter * 2}}</h2>
      <h2>{{message.split(" ").reverse().join(" ")}}</h2>
      <!-- 调用一个methods中的函数 -->
      <h2>{{reverse(message)}}</h2>
    </div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    // 创建App组件
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          counter: 10,
        }
      },
      methods: {
        reverse(msg) {
          return msg.split(" ").reverse().join(" ")
        }
      }
    }

    // 创建应用程序, 并且挂载
    Vue.createApp(App).mount('#app');
  </script>
</body>

但是下面的代码是错误的:

html
<!-- 错误的写法 -->
<!-- 这是一个赋值语句, 不是表达式 -->
<h2>{{var name = "Hello"}}</h2>
<!-- 控制流的if语句也是不支持的, 可以使用三元运算符 -->
<h2>{{ if (true) { return message } }}</h2>

三元运算符是可以的:

html
<!-- 三元运算符 -->
<h2>{{ true ? message: counter }}</h2>

内置指令-文本渲染

v-text

用于更新元素的 textContent:

html
<span v-text="msg"></span>
<!-- 等价于 -->
<span>{{msg}}</span>

v-html

默认情况下,如果我们展示的内容本身是 html 的,那么vue并不会对其进行特殊的解析。

如果我们希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示:

html
<body>
  <div id="app"></div>

  <template id="my-app">
    <div v-html='info'></div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          info: `<span style='color: red; font-size: 30px'>哈哈哈</span>`
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

v-pre

v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签:

  • 跳过不需要编译的节点,加快编译的速度;
html
<div v-pre>{{message}}</div>

显示效果

image-20230209141614013

指令-绑定

v-bind

前端讲的一系列指令,主要是和内容相关的,元素除了内容之外还会有各种各样的属性。

绑定属性我们使用v-bind:

  • 缩写:

  • 预期any (with argument) | Object (without argument)

  • 参数attrOrProp (optional)

  • 修饰符

    • .camel - 将 kebab-case attribute 名转换为 camelCase。
  • 用法:动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。

绑定基本属性

前面我们讲的Mustache等语法主要是将内容插入到 innerHTML 中。

很多时候,元素的属性也是动态的:

  • 比如a元素的href属性、img元素的src属性;
html
  <div id="app"></div>

  <template id="my-app">
    <div>{{message}}</div>
    <img v-bind:src="src" alt="">
    <!-- 语法糖写法 -->
    <img :src="src" alt="">
    <!-- 注意这两种写法的不同 -->
    <img src="src" alt="">

    <!-- 绑定a元素 -->
    <a :href="href"></a>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          src: "https://avatars.githubusercontent.com/u/10335230?v=4",
          href: "http://www.baidu.com"
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
绑定class属性

对象语法

  • 我们可以传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class。
html
  <div id="app"></div>

  <template id="my-app">
    <!-- 1.普通的绑定方式 -->
    <div :class="className">{{message}}</div>
    <!-- 2.对象绑定 -->
    <!-- 动态切换class是否加入: {类(变量): boolean(true/false)} -->
    <div class="why" :class="{nba: true, 'james': true}"></div>
    <!-- 案例练习 -->
    <div :class="{'active': isActive}">哈哈哈</div>
    <button @click="toggle">切换</button>
    <!-- 绑定对象 -->
    <div :class="classObj">哈哈哈</div>
    <!-- 从methods中获取 -->
    <div :class="getClassObj()">呵呵呵</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          className: "why",
          nba: 'kobe',
          isActive: false,
          classObj: {
            why: true,
            kobe: true,
            james: false
          }
        }
      },
      methods: {
        toggle() {
          this.isActive = !this.isActive;
        },
        getClassObj() {
          return {
            why: true,
            kobe: true,
            james: false
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

数组语法:

  • 我们可以把一个数组传给 :class,以应用一个 class 列表;
html
  <div id="app"></div>

  <template id="my-app">
    <div :class="['why', nba]">哈哈哈</div>
    <div :class="['why', nba, isActive? 'active': '']">呵呵呵</div>
    <div :class="['why', nba, {'actvie': isActive}]">嘻嘻嘻</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          nba: 'kobe',
          isActive: true
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
绑定style属性

对象语法:

  • :style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。
  • CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名
html
  <div id="app"></div>

  <template id="my-app">
    <div :style="{color: 'red', fontSize: '30px', 'background-color': 'blue'}">{{message}}</div>
    <div :style="{color: 'red', fontSize:  size+'px', 'background-color': 'blue'}">{{message}}</div>
    <div :style="styleObj">{{message}}</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          size: 50,
          styleObj: {
            color: 'red', 
            fontSize: '50px', 
            'background-color': 'blue'
          } 
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

数组语法:

  • :style 的数组语法可以将多个样式对象应用到同一个元素上;
html
  <div id="app"></div>

  <template id="my-app">
    <div :style="[styleObj1, styleObj2]">{{message}}</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          size: 50,
          styleObj1: {
            color: 'red', 
            fontSize: '50px', 
            'background-color': 'blue'
          },
          styleObj2: {
            textDecoration: 'underline',
            fontWeight: 700
          } 
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
动态绑定属性

属性的名称和值都是动态的

html
  <template id="my-app">
    <!-- 属性的名称是动态的 -->
    <div :[name]="value">{{message}}</div>
  </template>
绑定一个对象
  • info对象会被拆解成div的各个属性;
html
  <template id="my-app">
    <div v-bind="info">{{message}}</div>
  </template>

v-on

前面我们绑定了元素的内容和属性,在前端开发中另外一个非常重要的特性就是交互。

在前端开发中,我们需要经常和用户进行各种各样的交互:

  • 这个时候,我们就必须监听用户发生的时间,比如点击、拖拽、键盘事件等等
  • 在Vue中如何监听事件呢?使用v-on指令

v-on的使用:

  • 缩写@

  • 预期Function | Inline Statement | Object

  • 参数event

  • 修饰符

    • .stop - 调用 event.stopPropagation()
    • .prevent - 调用 event.preventDefault()
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyAlias} - 仅当事件是从特定键触发时才触发回调。
    • .once - 只触发一次回调。
    • .left - 只当点击鼠标左键时触发。
    • .right - 只当点击鼠标右键时触发。
    • .middle - 只当点击鼠标中键时触发。
    • .passive - { passive: true } 模式添加侦听器
  • 用法:绑定事件监听

案例演练:

html
  <div id="app"></div>

  <template id="my-app">
    <div>{{message}}</div>
    <!-- 基本使用 -->
    <!-- 1.绑定函数 -->
    <button v-on:click="btnClick">按钮1</button>
    <button @click="btnClick">按钮2</button>
    <div v-on:mousemove="mouseMove">div的区域</div>

    <!-- 2.绑定对象 -->
    <button v-on="{click: btnClick, mousemove: mouseMove}">特殊按钮3</button>

    <!-- 3.内联语句 -->
    <!-- 默认会把event对象传入 -->
    <button @click="btn4Click">按钮4</button>
    <!-- 内联语句传入其他属性 -->
    <button @click="btn4Click($event, 'why')">按钮5</button>

    <!-- 4.修饰符 -->
    <div @click="divClick">
      <button @click.stop="btnClick">按钮6</button>
    </div>
    <input type="text" @keyup.enter="onEnter">

  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World"
        }
      },
      methods: {
        btnClick() {
          console.log("按钮被点击了");
        },
        btn4Click(event) {
          console.log(event);
        },
        btn4Click(event, message) {
          console.log(event, message);
        },
        mouseMove() {
          console.log("鼠标移动");
        },
        divClick() {
          console.log("divClick");
        },
        onEnter(event) {
          console.log(event.target.value);
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

v-model

指令-条件渲染

在某些情况下,我们需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了。

Vue提供了下面的指令来进行条件判断:

  • v-if
  • v-else
  • v-else-if
  • v-show

下面我们来对它们进行学习。

v-show

v-show和v-if的用法看起来是一致的:

html
  <template id="my-app">
    <h2 v-show="isShow">哈哈哈哈</h2>
  </template>

v-if、v-else、v-else-if

v-if、v-else、v-else-if用于根据条件来渲染某一块的内容:

  • 这些内容只有在条件为true时,才会被渲染出来;
  • 这三个指令与JavaScript的条件语句if、else、else if类似;
html
<!-- vue3中, template不再要求必须只有一个根元素 -->
<template id="my-app">
    <input type="text" v-model.number="score">
    <h2 v-if="score > 90">优秀</h2>
    <h2 v-else-if="score > 80">良好</h2>
    <h2 v-else-if="score > 60">普通</h2>
    <h2 v-else>不及格</h2>
</template>

v-if的渲染原理:

  • v-if是惰性的;
  • 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉;
  • 当条件为true时,才会真正渲染条件块中的内容;

v-show和v-if区别

首先,在用法上的区别:

  • v-show是不支持template;
  • v-show不可以和v-else一起使用;

其次,本质的区别:

  • v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行切换;
  • v-if当条件为false时,其对应的原生压根不会被渲染到DOM中;

开发中如何进行选择呢?

  • 如果我们的原生需要在显示和隐藏之间频繁的切换,那么使用v-show;
  • 如果不会频繁的发生切换,那么使用v-if;

template元素

因为v-if是一个指令,所以必须将其添加到一个元素上:

  • 但是如果我们希望切换的是多个元素呢?
  • 此时我们渲染div,但是我们并不希望div这种元素被渲染;
  • 这个时候,我们可以选择使用template;

template元素可以当做不可见的包裹元素,并且在v-if上使用,但是最终template不会被渲染出来:

  • 有点类似于小程序中的block
html
  <template id="my-app">
    <template v-if="showHa">
      <h2>哈哈哈哈</h2>
      <h2>哈哈哈哈</h2>
      <h2>哈哈哈哈</h2>
    </template>
    <template v-else>
      <h2>呵呵呵呵</h2>
      <h2>呵呵呵呵</h2>
      <h2>呵呵呵呵</h2>
    </template>
    <button @click="toggle">切换</button>
  </template>

指令-列表渲染

在真实开发中,我们往往会从服务器拿到一组数据,并且需要对其进行渲染。

  • 这个时候我们可以使用v-for来完成;
  • v-for类似于JavaScript的for循环,可以用于遍历一组数据;

v-for支持的遍历类型:

  • 遍历数组v-for="(item, index?) in array"
  • 遍历对象v-for="(value, key?, index?) in object"
  • 遍历数字v-for="num in number"

v-for

基本使用

v-for的基本格式是遍历数组 "item in 数组"

  • 数组通常是来自data或者prop,也可以是其他方式;
  • item是我们给每项元素起的一个别名,这个别名可以自定来定义;
html
  <template id="my-app">
    <h2>电影列表</h2>
    <ul>
+      <li v-for="item in movies">{{item}}</li>
    </ul>
  </template>

我们知道,在遍历一个数组的时候会经常需要拿到数组的索引:

  • 如果我们需要索引,可以使用格式:"(item, index) in 数组"
  • 注意上面的顺序:数组元素项item是在前面的,索引项index是在后面的;
html
  <template id="my-app">
    <h2>电影列表</h2>
    <ul>
      <li v-for="(item, index) in movies">{{index}}-{{item}}</li>
    </ul>
  </template>
支持类型

v-for也支持遍历对象,并且支持有一二三个参数:

  • 一个参数:"value in object";
  • 二个参数:"(value, key) in object";
  • 三个参数:"(value, key, index) in object";
html
  <template id="my-app">
    <h2>遍历对象</h2>
    <ul>
      <li v-for="(value, key, index) in info">
        {{index}} - {{key}} - {{value}}
      </li>
    </ul>
  </template>

v-for同时也支持数字的遍历:

template元素

类似于v-if,你可以使用 template 元素来循环渲染一段包含多个元素的内容:

  • 我们使用template来对多个元素进行包裹,而不是使用div来完成;
html
  <div id="app"></div>

  <template id="my-app">
    <ul>
      <template v-for="(value, key) in info">
        <li>{{key}}</li>
        <li>{{value}}</li>
        <hr>
      </template>
    </ul>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          info: {
            name: 'why',
            age: 18,
            height: 1.88
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

数组更新检测

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

image-20230210115813247

js
  arr.value.push('英语') // 末尾追加元素
  arr.value.pop() // 删除末尾元素
  arr.value.shift() // 删除第一个元素
  arr.value.unshift('第一个') // 在第一个元素之前追加元素

  arr.value.reverse() // 颠倒数组中元素
  arr.value.sort((a, b) => a - b) // 排序:小->大

  arr.value.splice(1, 2) // 从第1个元素开始删除2个元素
  arr.value.splice(1, 0, '历史') // 从第1个元素开始 添加 '历史'
  arr.value.splice(1, 1, '历史') // 从第1个元素开始 替换 11 为 '历史'

替换数组的方法

上面的方法会直接修改原来的数组,但是某些方法不会替换原来的数组,而是会生成新的数组,比如 filter()map()concat()slice()。它们是不能被Vue响应式侦听的

js
    const nums = [10, 21, 34, 6];
    const newNums = nums.filter(num => num % 2 === 0);
+    nums = newNums // 需要将返回的 newNums 重新赋值给原数组 nums

指令-其他指令

v-once

用于指定元素或者组件只渲染一次:

  • 当数据发生变化时,元素或者组件以及其所有的自己诶单将视为静态内容并且跳过;
  • 该指令可以用于性能优化;
html
<h2 v-once>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>

如果是子节点,也是只会渲染一次:

html
<div v-once>
  <h2>当前计数: {{counter}}</h2>
  <button @click="increment">+1</button>
</div>

v-cloak

这个指令保持在元素上直到关联组件实例结束编译。

和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。

html
[v-cloak] {
  display: none;
}
<div v-cloak>
  {{ message }}
</div>

<div> 不会显示,直到编译结束。

key和diff算法

认识VNode和VDOM

在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性。

这个key属性有什么作用呢?我们先来看一下官方的解释:

  • key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;
  • 如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;
  • 而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;

官方的解释对于初学者来说并不好理解,比如下面的问题:

  • 什么是新旧nodes,什么是VNode?
  • 没有key的时候,如何尝试修改和复用的?
  • 有key的时候,如何基于key重新排列的?

我们先来解释一下VNode的概念:

  • 因为目前我们还没有比较完整的学习组件的概念,所以目前我们先理解HTML元素创建出来的VNode;
  • VNode的全称是Virtual Node,也就是虚拟节点;
  • 事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;
  • VNode的本质是一个JavaScript的对象;
html
<div class="title" style="font-size: 30px; color: red;">哈哈哈</div>

在我们的Vue中会被转化创建出一个VNode对象:

js
const vnode = {
  type: 'div',
  props: { 
    'class': 'title',
    style: {
      'font-size': '30px',
      color: 'red'
    }
  },
  children: '哈哈哈'
}

Vue内部在拿到vnode对象后,会对vnode进行处理,渲染成真实的DOM。

  • 这一部分我会在后面专门和大家阅读createApp函数中讲到;

DOM渲染过程

image-20230209141721832

如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree:

html
<div>
  <p>
    <i>哈哈哈哈</i>
    <i>哈哈哈哈</i>
  </p>
  <span>嘻嘻嘻嘻</span>
  <strong>呵呵呵呵</strong>
</div>

image-20230209141744023

这个和我们的key有什么关系呢?

  • 接下来我们就进行具体的分析;

key作用和diff算法

我们先来看一个例子:

  • 这个例子是当我点击按钮时会在中间插入一个f;
html
  <div id="app"></div>

  <template id="my-app">
    <ul>
      <li v-for="item in letters">{{item}}</li>
    </ul>
    <button @click="insertF">insert f</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          letters: ['a', 'b', 'c', 'd']
        }
      },
      methods: {
        insertF() {
          this.letters.splice(2, 0, 'f');
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

我们可以确定的是,这次更新对于ul和button是不需要进行更新,需要更新的是我们li的列表:

  • 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表;
  • 因为对于列表中 a、b、c、d它们都是没有变化的;
  • 在操作真实DOM的时候,我们只需要在中间插入一个f的li即可;

那么Vue中对于列表的更新究竟是如何操作的呢?

  • Vue事实上会对于有key和没有key会调用两个不同的方法;
  • 有key,那么就使用 patchKeyedChildren方法;
  • 没有key,那么就使用 patchUnkeyedChildren方法;

有key和没有key的操作

image-20230209141756642

没有key执行操作

没有key对应的源代码如下:

patchUnkeyedChildren

image-20230209141817663

它的过程画图就是如下的操作:

image-20230209141834876

我们会发现上面的diff算法效率并不高:

  • c和d来说它们事实上并不需要有任何的改动;
  • 但是因为我们的c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;

有key执行操作

如果有key,那么会执行什么样的操作呢?

image-20230209141847283

第一步的操作是从头开始进行遍历、比较:

  • a和b是一致的会继续进行比较;
  • c和f因为key不一致,所以就会break跳出循环;

image-20230209141900326

第二步的操作是从尾部开始进行遍历、比较:

image-20230209141914993

第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:

image-20230209141936581

第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:

image-20230209141950116

第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

image-20230209142004009

所以我们可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:

  • 在没有key的时候我们的效率是非常低效的;
  • 在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;

Options API

computed

认识计算属性computed

我们知道,在模板中可以直接通过插值语法显示一些data中的数据。

但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;

  • 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
  • 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
  • 在模板中放入太多的逻辑会让模板过重和难以维护;
  • 并且如果多个地方都使用到,那么会有大量重复的代码;

我们有没有什么方法可以将逻辑抽离出去呢?

  • 可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;
  • 但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;
  • 另外一种方式就是使用计算属性computed;

什么是计算属性呢?

  • 官方并没有给出直接的概念解释;
  • 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性
  • 计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例;

那接下来我们通过案例来理解一下这个计算属性。

计算属性的基本使用

计算属性的用法:

  • **选项:**computed

  • 类型:{ [key: string]: Function | { get: Function, set: Function } }

示例:

js
    data() {
      return {
        firstName: 'Tom',
        lastName: 'Chang'
      }
    },
        
    // 1. 写法一:完整写法的语法糖
    computed: {
+      fullName() {
+        return this.firstName + this.lastName
+      }
    },
        
    // 2. 写法二:完整的写法,有 setter 和 getter
    computed: {
+      fullName: {
+        get() {
+          return this.firstName + ' ' + this.lastName
+        },
+        set(value) {
+          this.firstName = value.split(' ')[0]
+          this.lastName = value.split(' ')[1]
+        }
      }
    
    methods: {
      setFullName() {
          // 3. 为 computed 属性设置值
+        this.fullName = 'Jerry Chang'
      }
    }
    },

我们来看三个案例:

  • 我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;

  • 我们有一个分数:score

    • 当score大于60的时候,在界面上显示及格;
    • 当score小于60的时候,在界面上显示不及格;
  • 我们有一个变量message,记录一段文字:比如Hello World

    • 某些情况下我们是直接显示这段文字;
    • 某些情况下我们需要对这段文字进行反转;

我们可以有三种实现思路:

  • 思路一:在模板语法中直接使用表达式;
  • 思路二:使用method对逻辑进行抽取;
  • 思路三:使用计算属性computed;

思路一的实现:模板语法

  • 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
  • 缺点二:当有多次一样的逻辑时,存在重复的代码;
  • 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
html
  <template id="my-app">
    <!-- 1.实现思路一: -->
    <h2>{{ firstName + lastName }}</h2>
    <h2>{{ score >= 60 ? "及格": "不及格" }}</h2>
    <h2>{{ message.split("").reverse().join(" ") }}</h2>
  </template>

思路二的实现:method实现

  • 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
  • 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
html
  <!-- 2.实现思路二: -->
  <template id="my-app">
    <h2>{{ getFullName()}}</h2>
    <h2>{{ getResult() }}</h2>
    <h2>{{ getReverseMessage() }}</h2>
  </template>
  

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          firstName: "Kobe",
          lastName: "Bryant",
          score: 80,
          message: "Hello World"
        }
      },
      methods: {
        getFullName() {
          return this.firstName + " " + this.lastName;
        },
        getResult() {
          return this.score >= 60 ? "及格": "不及格";
        },
        getReverseMessage() {
          return this.message.split(" ").reverse().join(" ");
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

思路三的实现:computed实现

  • 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;
  • 我们会发现无论是直观上,还是效果上计算属性都是更好的选择;
  • 并且计算属性是有缓存的;
html
  <!-- 3.实现思路三: -->
  <template id="my-app">
+    <h2>{{ fullName }}</h2>
+    <h2>{{ result }}</h2>
+    <h2>{{ reverseMessage }}</h2>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          firstName: "Kobe",
          lastName: "Bryant",
          score: 80,
          message: "Hello World"
        }
      },
+    computed: {
+      fullName() {
+        return this.firstName + this.lastName;
+      },
+      result() {
+        return this.score >= 60 ? "及格": "不及格";
+      },
+      reverseMessage() {
+        return this.message.split(" ").reverse().join(" ");
+      }
+    }
    }

    Vue.createApp(App).mount('#app');
  </script>

计算属性 vs methods

在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属性有缓存。

接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:

html
<div id="app"></div>

<template id="my-app">
  <!-- 1.使用methods -->
+  <h2>{{getResult()}}</h2>
+  <h2>{{getResult()}}</h2>
+  <h2>{{getResult()}}</h2>

  <!-- 2.使用computed -->
+  <h2>{{result}}</h2>
+  <h2>{{result}}</h2>
+  <h2>{{result}}</h2>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        score: 90
      }
    },
+    computed: {
+      result() {
+        console.log("调用了计算属性result的getter");
+        return this.score >= 60 ? "及格": "不及格";
+      }
+    },
    
+    methods: {
+      getResult() {
+        console.log("调用了getResult方法");
+        return this.score >= 60 ? "及格": "不及格";
+      }
+    }
  }

  Vue.createApp(App).mount('#app');
</script>

打印结果如下:

  • 我们会发现methods在多次使用时,会调用多次;
  • 而计算属性虽然使用了三次,但是计算的过程只调用了一次;

image-20230209142024380

这是什么原因呢?

  • 这是因为计算属性会基于它们的依赖关系进行缓存;
  • 在数据不发生变化时,计算属性是不需要重新计算的;
  • 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;
html
  <template id="my-app">
    <input type="text" v-model="score">

    <!-- 省略代码 -->
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          score: 90
        }
      },
      // 省略代码
    }

    Vue.createApp(App).mount('#app');
  </script>

我们来看一下当分数变化时,方法和计算属性的反应:

image-20230209142042398

score发生变化

计算属性的setter和getter

计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。

但是,如果我们确实想设置计算属性的值呢?

  • 这个时候我们也可以给计算属性设置一个setter的方法;
html
  <template id="my-app">
    <h2>{{fullName}}</h2>
    <button @click="setNewName">设置新名字</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          firstName: "Kobe",
          lastName: "Bryant"
        }
      },
+      computed: {
        // 1. 定义计算属性fullName的get和set写法
+        fullName: {
+          get() {
            return this.firstName + " " + this.lastName;
          },
+          set(value) {
            const names = value.split(" ");
            this.firstName = names[0];
            this.lastName = names[1];
          }
        }
      },
      methods: {
        setNewName() {
            // 2. 修改fullName
+          this.fullName = "coder why";
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

你可能觉得很奇怪,Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?

  • 事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已;

Vue源码内部的逻辑判断

image-20230209142104134

侦听器watch

侦听器watch的基本使用

侦听器的用法如下:

  • **选项:**watch
  • 类型:{ [key: string]: string | Function | Object | Array}

示例

什么是侦听器呢?

  • 开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;
  • 当数据变化时,template会自动进行更新来显示最新的数据;
  • 但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;

举个栗子(例子):

  • 比如现在我们希望用户在input中输入一个问题;
  • 每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;
  • 那么,我们就需要实时的去获取最新的数据变化;
html
  <div id="app"></div>

  <template id="my-app">
    <label for="question">
      请输入问题:
      <input type="text" id="question" v-model="question">
    </label>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          question: "",
          info: {
            name: "why"
          }
        }
      },
      watch: {
        question(newValue, oldValue) {
          this.getAnwser(newValue);
        }
      },
      methods: {
        getAnwser(question) {
          console.log(`${question}的问题答案是哈哈哈哈`);
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

侦听器watch的配置选项

我们先来看一个例子:

  • 当我们点击按钮的时候会修改info.name的值;
  • 这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。
html
  <div id="app"></div>

  <template id="my-app">
    <button @click="btnClick">修改info</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          info: {
            name: "why"
          }
        }
      },
      watch: {
        info(newValue, oldValue) {
          console.log(newValue, oldValue);
        }
      },
      methods: {
        btnClick() {
          this.info.name = "kobe";
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>

这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出相应的:

  • 这个时候我们可以使用一个选项deep进行更深层的侦听;
  • 注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;
js
  watch: {
    info: {
      handler(newValue, oldValue) {
        console.log(newValue, oldValue);
      },
      deep: true
    }
  }

还有另外一个属性,是希望一开始的就会立即执行一次:

  • 这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;
js
  watch: {
    info: {
      handler(newValue, oldValue) {
        console.log(newValue, oldValue);
      },
      deep: true,
      immediate: true
    }
  }

侦听器watch的其他方式

我们直接来看官方文档的案例:

js
  const app = Vue.createApp({
    data() {
      return { a: 1, b: 2, c: { d: 4 }, e: "test", f: 5 };
    },
    watch: {
      a(val, oldVal) {
        console.log(`new: ${val}, old: ${oldVal}`);
      }, 
      // 字符串方法名
      b: "someMethod", 
      // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
      c: {
        handler(val, oldVal) {
          console.log("c changed");
        },
        deep: true,
      }, 
      // 该回调将会在侦听开始之后被立即调用
      e: {
        handler(val, oldVal) {
          console.log("e changed");
        },
        immediate: true,
      }, 
      // 你可以传入回调数组,它们会被逐一调用
      f: [
        "handle1",
        function handle2(val, oldVal) {
          console.log("handle2 triggered");
        },
        {
          handler: function handle3(val, oldVal) {
            console.log("handle3 triggered");
          },
        },
      ],
    },
    methods: {
      someMethod() {
        console.log("b changed");
      },
      handle1() {
        console.log("handle 1 triggered");
      },
    },
  });

另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性:

js
  'info.name': function(newValue, oldValue) {
    console.log(newValue, oldValue);
  }

还有另外一种方式就是使用 $watch 的API:

  • 我们可以在created的生命周期(后续会讲到)中,使用 this.$watchs 来侦听;

    • 第一个参数是要侦听的源;
    • 第二个参数是侦听的回调函数callback;
    • 第三个参数是额外的其他选项,比如deep、immediate;
js
  created() {
    this.$watch('message', (newValue, oldValue) => {
      console.log(newValue, oldValue);
    }, {deep: true, immediate: true})
  }

阶段案例

案例介绍

现在我们来做一个相对综合一点的练习:书籍购物车

案例效果如下:

image-20230209142122183

案例说明:

  • 1.在界面上以表格的形式,显示一些书籍的数据;
  • 2.在底部显示书籍的总价格;
  • 3.点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);
  • 4.点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~);

项目搭建

模板引擎的搭建

我们先来搭建一下模板引擎:

  • 它需要一个table来包裹需要展示的书籍内容;
  • 需要一个h2元素用于计算商品的总价格;

在这里我们有一些数据和方法需要在组件对象(createApp传入的对象)中定义:

  • 比如book的数据;
  • formatPrice、decrement、increment、handleRemove的方法等;
html
<template id="my-app">
  <table>
    <thead>
      <tr>
        <th></th>
        <th>书籍名称</th>
        <th>出版日期</th>
        <th>价格</th>
        <th>购买数量</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in books" :key="item.id">
        <td>{{index + 1}}</td>
        <td>{{item.name}}</td>
        <td>{{item.date}}</td>
        <td>{{formatPrice(item.price)}}</td>
        <td>
          <button @click="decrement(index)">-</button> {{item.count}}
          <button @click="increment(index)">+</button>
        </td>
        <td><button @click="handleRemove(index)">移除</button></td>
      </tr>
    </tbody>
  </table>
  <h2>总价: {{formatPrice(totalPrice)}}</h2>
</template>

css样式的实现

为了让表格好看一点,我们来实现一些css的样式:

css
  table {
    border: 1px solid #e9e9e9;
    border-collapse: collapse;
    border-spacing: 0;
  }
  th,
  td {
    padding: 8px 16px;
    border: 1px solid #e9e9e9;
    text-align: left;
  }
  th {
    background-color: #f7f7f7;
    color: #5c6b77;
    font-weight: 600;
  }

代码逻辑的实现

接下来我们来看一下代码逻辑的实现:

js
<script>
  const App = {
    template: "#my-app",
    data() {
      return {
        books: [
          {
            id: 1,
            name: "《算法导论》",
            date: "2006-9",
            price: 85.0,
            count: 1,
          },
          {
            id: 2,
            name: "《UNIX编程艺术》",
            date: "2006-2",
            price: 59.0,
            count: 1,
          },
          {
            id: 3,
            name: "《编程珠玑》",
            date: "2008-10",
            price: 39.0,
            count: 1,
          },
          {
            id: 4,
            name: "《代码大全》",
            date: "2006-3",
            price: 128.0,
            count: 1,
          },
        ],
      };
    },
    computed: {
      filterBooks() {
        return this.books.map((item) => {
          item.price = "¥" + item.price;
          return item;
        });
      },
      totalPrice() {
        return this.books.reduce((preValue, item) => {
          return preValue + item.price * item.count;
        }, 0);
      },
    },
    methods: {
      formatPrice(price) {
        return "¥" + price;
      },
      decrement(index) {
        this.books[index].count--;
      },
      increment(index) {
        this.books[index].count++;
      },
      handleRemove(index) {
        this.books.splice(index, 1);
      },
    },
  };
  Vue.createApp(App).mount("#app");
</script>

小的细节补充

这里我们补充一个小的细节,当我们的数量减到1的时候就不能再减了:

image-20230209142136965

这里我们可以对button进行逻辑判断:

  • 当item.count的数量 <= 1的时候,我们可以将disabled设置为true;
html
<button @click="decrement(index)" :disabled="item.count <= 1">-</button>

表单输入

v-model基本用法

表单提交是开发中非常常见的功能,也是和用户交互的重要手段:

  • 比如用户在登录、注册时需要提交账号密码;
  • 比如用户在检索、创建、更新信息时,需要提交一些数据;

这些都要求我们可以在代码逻辑中获取到用户提交的数据,我们通常会使用v-model指令来完成:

  • v-model指令可以在表单 inputtextarea以及select元素上创建双向数据绑定;
  • 它会根据控件类型自动选取正确的方法来更新元素;
  • 尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理;
html
<div id="app"></div>
<template id="my-app">
  <input type="text" v-model="message" />
  <h2>{{message}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
  const App = {
    template: "#my-app",
    data() {
      return { message: "Hello World" };
    },
  };
  Vue.createApp(App).mount("#app");
</script>

实现效果如下

image-20230209142151236

v-model的原理

官方有说到,v-model的原理其实是背后有两个操作:

  • v-bind绑定value属性的值;
  • v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中;

v-model的原理

image-20230209142204567

事实上v-model的原理会比上面的更加复杂:

v-model的编译过程

image-20230209142219975

v-model绑定其他

我们再来绑定一下其他的表单类型:textarea、checkbox、radio、select

html
<div id="app"></div>
<template id="my-app">
  <!-- 1.绑定textarea -->
  <div>
    <textarea v-model="article" cols="30" rows="10"></textarea>
    <h2>article当前的值是: {{article}}</h2>
  </div>
  <!-- 2.绑定checkbox -->
  <!-- 2.1.单选框 -->
  <div>
    <label for="agreement">
      <input id="agreement" type="checkbox" v-model="isAgree" />同意协议
    </label>
    <h2>isAgree当前的值是: {{isAgree}}</h2>
  </div>
  <!-- 2.2.多选框 -->
  <div>
    <label for="basketball">
      <input
        id="basketball"
        type="checkbox"
        value="basketball"
        v-model="hobbies"
      />篮球
    </label>
    <label for="football">
      <input
        id="football"
        type="checkbox"
        value="football"
        v-model="hobbies"
      />足球
    </label>
    <label for="tennis">
      <input id="tennis" type="checkbox" value="tennis" v-model="hobbies" />网球
    </label>
    <h2>hobbies当前的值是: {{hobbies}}</h2>
  </div>
  <!-- 3.绑定radio -->
  <div>
    <label for="male">
      <input type="radio" id="male" v-model="gender" value="male" />男
    </label>
    <label for="female">
      <input type="radio" id="female" v-model="gender" value="female" />女
    </label>
    <h2>gender当前的值是: {{gender}}</h2>
  </div>
  <!-- 4.绑定select -->
  <div>
    <select v-model="fruit">
      <option disabled value="">请选择喜欢的水果</option>
      <option value="apple">苹果</option>
      <option value="orange">橘子</option>
      <option value="banana">香蕉</option>
    </select>
    <h2>fruit当前的值是: {{fruit}}</h2>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const App = {
    template: "#my-app",
    data() {
      return {
        article: "Hello World",
        isAgree: false,
        gender: "male",
        fruit: "",
      };
    },
  };
  Vue.createApp(App).mount("#app");
</script>

v-model的值绑定

目前我们在前面的案例中大部分的值都是在template中固定好的:

  • 比如gender的两个输入框值male、female;
  • 比如hobbies的三个输入框值basketball、football、tennis;

在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到data返回的对象中,再通过v-bind来进行值的绑定,这个过程就是值绑定

这里不再给出具体的做法,因为还是v-bind的使用过程。

v-model的修饰符

v-model在使用的时候可以在后面跟一些修饰符,来完成一些特殊的操作。

lazy修饰符

lazy修饰符是什么作用呢?

  • 默认情况下,v-model在进行双向绑定时,绑定的是input事件,那么会在每次内容输入后就将最新的值和绑定的属性进行同步;
  • 如果我们在v-model后跟上lazy修饰符,那么会将绑定的事件切换为 change 事件,只有在提交时(比如回车)才会触发;
html
<template id="my-app">
  <input type="text" v-model.lazy="message" />
  <h2>{{message}}</h2>
</template>

number修饰符

我们先来看一下v-model绑定后的值是什么类型的:

  • message总是string类型,即使在我们设置type为number也是string类型;
html
<template id="my-app">
  <!-- 类型 -->
  <input type="text" v-model="message" />
  <input type="number" v-model="message" />
  <h2>{{ message }}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
  const App = {
    template: "#my-app",
    data() {
      return { message: "" };
    },
    watch: {
      message(newValue) {
        console.log(newValue, typeof newValue);
      },
    },
  };
  Vue.createApp(App).mount("#app");
</script>

如果我们希望转换为数字类型,那么可以使用 .number 修饰符:

html
<template id="my-app">
  <!-- 类型 -->
  <input type="text" v-model.number="score" />
</template>

另外,在我们进行逻辑判断时,如果是一个string类型,在可以转化的情况下会进行隐式转换的:

  • 下面的score在进行判断的过程中会进行隐式转化的;
js
const score = "100";
if (score > 90) {
  console.log("优秀");
}

trim修饰符

如果要自动过滤用户输入的首尾空白字符,可以给v-model添加 trim 修饰符:

html
<template id="my-app">
  <input type="text" v-model.trim="message" />
</template>

v-model组件使用

v-model也可以使用在组件上,Vue2版本和Vue3版本有一些区别,具体的使用方法,后面讲组件化开发再具体学习。