Skip to content

4.2 基础概念

前面我们基本认识了 Vue3,并搭建了本地开发环境。现在我们从基础概念和 API 入手,完整的学习 Vue3 框架。

4.2.1 状态与方法

Vue 基于数据驱动视图的思想实现了框架运行的基本逻辑。能够驱动视图变化的数据就被称为状态。

在 Vue 中,状态被定义在 data() 方法返回的对象中。常见的用法是在单文件组件内定义状态,此时的状态只对该组件有效,因此也被称为局部状态。局部状态在组件初始化时被创建,并将状态挂在到组件实例上,因此状态可以在组件实例中通过 this 访问。

js
export default {
  data() {
    return {
      count: 1,
      tag: "数量",
    };
  },
  created() {
    console.log(this.tag, this.count); // 数量 1
  },
};

代码中的 created() 方法会在组件初始化后执行(后面会介绍)。我们看到在这个方法内通过 this 访问到了状态的值。

状态可以直接被绑定到模版中,在模版中不需要使用 this,直接绑定状态名即可:

js
<templete>
  <h2>{{ tag }}</h2>
  <h3>{{ count }}</h3>
</templete>

当修改状态时,通常是通过 methods 属性定义方法,该属性下包含组件的所有方法:

js
export default {
  data() {
    return {
      count: 1,
      tag: "数量",
    };
  },
  methods: {
    changeCount() {
      this.count = 2;
    },
    changeTag() {
      this.tag = "长度";
    },
  },
};

与状态一样,方法也通过 this 访问。方法是修改状态的媒介,定义好方法之后,可以在实例中调用,也可在模版中调用:

js
export default {
  methods: {
    changeCount() {
      this.count = 2;
    },
    change() {
      this.changeCount();
    },
  },
  created() {
    this.changeCount();
  },
};
html
<template>
  <button @click="changeCount">修改数量</button>
</template>

但是要注意:Vue 将 methods 下所有方法内的 this 都指向了组件实例,以此确保 this 在任意情况下指向正确。因此在 methods 中定义方法不可以使用箭头函数,因为箭头函数的 this 会指向父元素,这可能会导致指向错误。

js
// 错误示例
export default {
  methods: {
    change: () => {
      this.count = 2;
    },
  },
};

4.2.2 条件与列表

在模版的动态渲染中,常常用到条件渲染和列表渲染,这里会涉及到 Vue 的另一个知识点 ——— 指令。

在 Vue 模版中,以 “v-” 开头的属性被称为指令,可以说指令是一种特殊属性,被用于控制如何渲染元素。Vue 提供了许多内置指令来实现不同的功能。

条件渲染

内置指令 v-if 用于按照条件渲染一块内容。条件可以是一个状态,也可以是一个表达式,当值为 true 时内容被渲染,否则不渲染。

vue
<span v-if="bool">A</span>
<span v-if="tag == 'ok'">B</span>

data() { return { bool: true, tag: 'ok' } }

与 v-if 指令搭配使用的还有 v-else、v-else-if。其实这些指令都对应着 JavaScript 中的 if/else 表达式,举例如下:

vue
<span v-if="status == 1">已成功</span>
<span v-else-if="status == 0">未成功</span>
<span v-else>出现异常</span>

将这些指令的逻辑用 JavaScript 表达如下:

js
var = status = 'xxx'
if(status==1) {
  return '已成功'
} else if(status==0) {
  return '未成功'
} else {
  return '出现异常'
}

与 v-if 非常相似的一个指令是 v-show。v-show 的作用也是控制元素是否显示,当表达式的值为 true 时,元素会显示。如下:

js
<span v-show="is_show">我已显示</span>

那么 v-if 与 v-show 有什么区别呢?

v-if 的关键字是“渲染”,而 v-show 的关键字是“显示”。当 v-if 的表达式为 true 时,元素会被渲染到浏览器上;反之,元素不会被渲染。

而 v-show 不管表达式的值是什么,元素都会被渲染,浏览器中一定会存在这个元素。但 v-show 通过切换 CSS 样式中的 display 属性来控制元素的显示。

那什么时候使用 v-if,什么时候使用 v-show 呢?这里引用官网的介绍:

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染

内置指令 v-for 用于将一个数组渲染成元素列表。v-for 指令与 JavaScript 中的 for 循环逻辑基本一致,举例如下:

js
data() {
  return {
    lists: [
      { id: 1, name: '西瓜' },
      { id: 2, name: '香蕉' }
    ]
  }
}
js
<ul>
  <li v-for="(item,index) in lists">
    {{index}}:{{ item.name }}
  </li>
</ul>

v-for 指令的表达式格式为 “(item,index) in lists”。其中 lists 表示源数组,item 和 index 分别表示数据项和对应的索引。上述代码渲染后的结果是:

js
<ul>
  <li>0:西瓜</li>
  <li>1:香蕉</li>
</ul>

值得庆祝的是,v-for 还支持直接遍历对象。v-for 会自动调用 Object.keys() 转换对象,得到属性名和属性值。

js
var json = {
  name: 'sofa',
  seats: 5,
  color: 'orange',
}

<li v-for="(value,key) in json">
  {{key}}:{{ value }}
</li>

因为虚拟 DOM 的缘故,使用 v-for 时必须指定一个唯一的 key 属性来保证最高效率的更新。key 建议使用每个数组项中的唯一值,不建议使用索引。

js
<li v-for="(item,index) in lists" :key="item.id">
  {{index}}:{{ item.name }}
</li>

4.2.3 模版语法

前面介绍了在模版中绑定数据,以及使用简单的指令。模版中还有很多语法我们一一介绍。

绑定 HTML

在模版中使用双花括号()绑定数据,这种方式只能渲染文本,不能渲染 HTML。若想插入 HTML,需要使用 v-html 指令:

js
return {
  content: '<span>文本内容</ssan>'
}
<div>{{content}}</div>       // 渲染为字符串
<div v-html="content"></div> // 渲染为DOM元素

绑定属性

模版中除了数据是动态的,我们希望属性也可以动态指定,比如 class。常见需求是动态切换 class 类名来实现 UI 变化。

使用 v-bind 指令来实现属性的动态绑定,一般使用三目运算符,比如:

js
<div v-bind:class="active?'menu active':'menu'">菜单</div>

因为 v-bind 非常常用,因此 Vue 提供了特定的简写语法:

js
// 将 “v-bind:” 简写为 “:”
<div :class="active?'menu active':'menu'">菜单</div>

特殊情况:当 v-bind 绑定的值为布尔类型时,此时会控制属性是否显示。如果值为假值(false、null、undefined),属性会被移除。

js
return {
  disabled: false
}
<button :disabled="disabled">菜单</button>

如果元素上要绑定的属性非常多,Vue 还提供了更便捷的方法 ——— 绑定对象。将所有属性写在一个对象内,v-bind 会自动解析。

js
return {
  attrs: {
    id: 'main',
    class: 'content'
  }
}
<button v-bind="attrs">菜单</button>

表达式

无论是双花括号语法还是指令,Vue 都支持绑定状态或表达式。表达式发挥了 JavaScript 的能力,使绑定更加灵活。表达式有以下常见的几种形式:

  • 三目运算符
  • 数据处理函数
  • 自定义函数

这几种形式必须返回一个值,否则表达式不成立。假设数据如下:

js
data() {
  return {
    msg: 'you are brave'
  }
}

methods: {
  showMsg(str) {
    if(!str) {
      return 'no msg'
    } else {
      return 'msg:'+str
    }
  }
}

三种运算符举例如下:

html
<div class="contanor">
  <p>{{msg.length>3?'标准':'偏少'}}</p>
  <p>{{msg.split(' ').join('-')}}</p>
  <p>{{showMsg(msg)}}</p>
</div>

4.2.4 计算属性与监听器

Vue 中有一个非常强大的功能 ——— 计算属性。计算属性的本质是依据某个状态返回一个新值,当模版中需要使用较为复杂的表达式时,将它提取为一个计算属性非常合适。

假设现在有一个商品列表数据,我们需要展示商品列表的总价,你可能会这样:

js
export default {
  data() {
    return {
      lists: [
        { name: '鸡蛋', price: 3.5 },
        { name: '西红柿', price: 4.5 },
        { name: '黄瓜', price: 6 },
      ]},
      totalPrice: 0
    }
  },
  methods: {
    getPrice() {
      this.totalPrice = this.lists.reduce((a,b)=> a+b.price, 0)
    }
  }
}

上面代码中,我们用状态 totalPrice 表示总价,然后在一个方法内计算总价。这样做的弊端是:当商品数据变化时,需要手动调用该函数重新计算总价。

因为总价是基于商品数据计算得来的,所以用计算属性非常合适。计算属性定义在 computed 对象下,在模版中使用计算属性和使用普通状态一致。

js
<div>{{totalPrice}}</div>

computed: {
  totalPrice() {
    return this.lists.reduce((a,b)=> a+b.price, 0)
  }
}

计算属性必须有返回值,否则无效。当商品数据变化时,计算属性会自动更新。

使用计算属性时要注意以下两个问题:

  • 计算属性不应有副作用(如请求 API)
  • 不能直接修改计算属性的值

但如果确实要在状态变化之后执行一些“副作用”操作(请求 API),那该怎么做呢?这里就要用到另一个功能 ——— 监听器。

监听器与计算属性非常相似,以至于常常被混用,但他们的功能区分很明确。计算属性是一个纯函数,只返回一个新值;而监听器不需要有返回值,只监听数据变化并执行某些操作。

监听器定义在 watch 对象下,举例如下:

js
export default {
  data() {
    return {
      name: '朱元璋'
    }
  }
  watch: {
    name(val, oldval) {
      console.log(val, oldval)
      fetch('https://testapi/xxx', val)
    }
  }
}

上面代码中定义了状态 name 并监听。当 name 修改后,监听器就会被触发。监听器函数的两个参数表示 name 修改前后的值:前者是新值,后者是旧值。在监听器内可以发起一个请求,也可以执行其他任意操作。

监听器默认是“浅层”监听。也就是说,只有状态被重新赋值时才会被监听到。如果监听一个数组,数组项被修改,监听器默认是监听不到的。

那怎么办呢?监听器提供了深层监听的方式,代码如下:

js
export default {
  data() {
    return {
      lists: [
        { name: '朱元璋' },
        { name: '朱标' },
      ]
    }
  }
  watch: {
    lists: {
      handler(val) {
        console.log(val) // list 数组
      },
      deep: true,
    }
  }
}

深层监听的开销很大,因此优先使用浅层监听,只有监听复杂数据才使用深层监听。

4.2.5 事件处理

前面我们在模版中使用 @click 语法来触发一个点击事件,@ 并不是新语法,它只是指令 v-on 的简写。指令 v-on 用于监听 DOM 事件,以下两种写法是一样的:

vue
<button @click="test">点击</button>
<button v-on:click="test">点击</button>

指令 v-on 的事件名对标 HTML 的原生事件名。规则如下:

  • onclick(HTML)等于 @click(Vue)
  • onchange(HTML)等于 @change(Vue)
  • oninput(HTML)等于 @input(Vue)

指令 v-on 的值可以是一个简单的 JavaScript 语句,也可以是一个方法。方法可以是方法名,也可以调用方法并传参。

js
<button @click="fun">方法名</button>
<button @click="fun('custom')">调用方法</button>
<button @click="count++">语句</button>

methods: {
  fun(e) {
    if(typeof e == 'string') {
      console.log(e)
    } else {
      console.log(e.target.tagName)
    }
  }
}

当 v-on 的值是方法名时,方法会默认接收一个 event 参数。该参数是原生 DOM 事件对象,可以获取到许多有用信息。比如用 event.target 获取触发该事件的元素。

事件处理中还有一些特殊操作,比如阻止事件冒泡、阻止默认事件,分别需要通过 event.stopPropagation() 方法和 event.preventDefault() 方法来实现。为了简化这些操作,Vue 提供了事件修饰符。

事件修饰符在指令 v-on 之后,用 . 表示修饰规则。代码如下:

html
<!-- 阻止冒泡 -->
<a @click.stop="doThis"></a>

<!-- 阻止默认事件 -->
<form @submit.prevent="onSubmit"></form>

4.2.6 表单双向绑定

表单是前端中操作最频繁,也是处理最复杂的部分。即便 Vue 提供了数据绑定,但普通的表单处理还需要事件监听配合,操作起来略显繁琐。如下:

js
<input :value="text" @input="e => text = e.target.value" />

上述代码中,input 元素绑定了状态 text。当用户编辑输入框内容时,为了保证状态与视图同步,还需要监听用户输入事件,将状态 text 的值手动更新为编辑后的内容。

为了简化这一操作,Vue 提供了 v-model 指令。使用 v-model 改造上述代码:

html
<input v-model="text" />

很显然,v-model 使用更简单,并且状态会自动更新。那么 v-model 是如何实现这一功能呢?

事实上,v-model 只是一个语法糖,它依然是通过事件监听来修改状态。只不过它将数据绑定和事件监听封装在了指令内部,对外只需要绑定状态,通过这种超简单的做法实现了双向数据绑定。

指令 v-model 不仅对 input 生效,对其他表单元素如 <textarea><select> 同样有效。只不过不同的表单元素对应的事件可能不同,v-model 在指令内部做了兼容处理。

不同的表单元素 v-model 的处理规则如下:

html
<!-- 输入框 -->
<input :value="value" @input="e=> value=e.target.value" />
<input v-model="value" />

<!-- 多行输入框 -->
<textarea :value="value" @input="e=> value=e.target.value" />
<textarea v-model="value" />

<!-- 下拉框 -->
<select :value="value" @change="e=> value=e.target.value" />
<select v-model="value" />

<!-- 多选框 -->
<input type="checkbox" :checked="value" @change="e=> value=e.target.checked" />
<input type="checkbox" v-model="value" />

<!-- 单选框 -->
<input type="radio" :checked="value" @change="e=> value=e.target.checked" />
<input type="radio" v-model="value" />

4.2.7 DOM 操作

Vue 的理念是数据驱动视图,因此大多数情况下要避免直接操作 DOM。但是在某些情况下,我们仍然需要访问和操作 DOM 元素。

基于此情况,Vue 提供了 ref 属性来标记对某个元素的引用。当组件挂载后,所有标记了 ref 属性的元素都可以通过 this.$refs 找到。

js
<input ref="dom"/>

mounted() {
  this.$refs.dom.focus()
}

上面代码中,我们为元素标记了名为 dom 的引用。当组件挂载后,即可通过 this.$refs.dom 访问到这个元素。

使用 ref 属性要注意以下两个问题,否则会引起错误:

  1. 只可以在组件挂载后访问元素,否则引用不存在。
  2. ref 属性值必须唯一,否则会导致引用覆盖。

请记住一个原则:只有某些少数的特殊场景才需要操作 DOM,绝大多数的逻辑请使用数据驱动!