Skip to content

Vue.js 基础

第一章:初识 Vue

一、什么是 Vue

Vue.js 是一款用于构建用户界面的渐进式 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

重要名词解释:

  • 渐进式怎么理解?理解一:用什么引入什么;理解二:用什么学什么(循序渐进的学习)。

  • 框架与库的区别

二、特点

  • 声明式

    MVVM 模型:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。

    M:模型 (Model) ==> data 中的数据

    V:视图 (View) ==> 模板代码

    VM:视图模型 (ViewModel) ==> Vue 实例

  • 响应式:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

  • 组件化

  • 虚拟 DOM + 优秀的 diff 算法

三、快速入门

第一步:使用 <script src="./lib/vue.js"></script> 引入 Vue。之后会在 window 对象中注入 Vue 构造函数。

第二步:想让 Vue 工作,就必须创建一个 Vue 实例,且要传入一个配置对象。

第三步:HTML 部分片段交给 Vue 解析渲染。

例子

html
<!-- vue模版 -->
<div id="root">
  <!-- 插值语法 -->
  <!-- {{xxx}} 中的 xxx 要写 js 表达式,且 xxx 可以自动读取到 data 中的所有属性 -->
  <!-- vm 身上所有的属性及 Vue 原型上所有属性,在 Vue 模板中都可以直接使用 -->
  <h1>hello, {{ name }}</h1>
</div>

root 容器里的代码依然符合 html 规范,只不过混入了一些特殊的 Vue 语法。root 容器里的代码被称为【Vue 模板】。

javascript
// 设置为 false 以阻止 vue 在启动时生成生产提示
Vue.config.productionTip = false; // 修改为不提示

new Vue({
  // 配置对象
  // el是element缩写,指定当前vue实例为哪一个容器服务,值通常为css选择器格式,也可以用使用原生js代码
  el: '#root', // document.getElementById('root')
  data: {
    // data用来存储数据,以后会用函数来表示
    name:'张三',
    age:21
  }
});

观察发现:

  • 一旦 data 中的数据发生改变,那么页面中用到该数据的地方也会自动更新。
  • Vue 实例和容器是一一对应的。真实开发中只有一个 Vue 实例,并且会配合着组件一起使用。
  • data 中所有的属性,最后都出现在了 vm 身上。

Vue 3.x 版本之后,创建 vm 方式发生了变化。

javascript
const app = Vue.createApp({
  data() {
    return {
      name:'张三',
      age:21
    }
  }
})
app.mount("#root")

第二章:基础语法

一、模板语法

html 中包含了一些 JS 语法代码,语法分为两种,分别为:

  • 插值语法 / Mustache(双大括号表达式)
  • 指令语法(以 v- 开头)

Vue 模板语法有 2 大类:

  • 插值语法

    功能:用于解析标签体内容

    写法:{{xxx}}。xxx 是 js 表达式,且可以直接读取到 data 中的所有属性。

  • 指令语法

    功能:用于解析标签(包括标签属性、标签体内容、绑定事件 ......)

    举例:v-bind:href="xxx" 或简写为 :href="xxx",xxx 同样要写 js 表达式。且可以直接读取到 data 中的所有属性。

    备注:Vue 中有很多的指令,且形式都是 v-???,此处是拿 v-bind 举个例子。

注意:js 表达式和 js 代码(语句)

表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方。

    (1). a

    (2). a + b

    (3). demo(1)

    (4). x === y ? 'a' : 'b'

    (5). [1,2,3,4].map(item => item**2)

js 代码(语句)

    (1). if(){}

    (2). for(){}

二、数据绑定

1. 基本

Vue 中有 2 种数据绑定的方式。

1)单向绑定 (v-bind)

数据只能从 data 流向页面。

特殊用法

vue
<!-- 动态绑定属性 -->
<template id="my-app">
  <!-- 屬性的名称是动态的 -->
  <div :[name]="value">{{ message }}</div>
</template>

<!-- v-bind 直接绑定一个对象: info 对象会被拆解成 div 的各个属性 -->
<template id="my-app">
  <div v-bind="info">{{ message }}</div>
</template>

.camel 修饰符。用于将 kebab-case 属性名转换为 camelCase。

vue
<svg :view-box.camel="viewBox"></svg>

<!-- 这会被解析为: -->
<svg :viewBox="viewBox"></svg>
2)双向绑定 (v-model)

数据不仅能从 data 流向页面,还可以从页面流向 data。

v-model 的三个修饰符:

  • lazy:失去焦点再收集数据。

  • number:输入字符串转为有效的数字。

  • trim:输入内容首尾空格过滤。

html
<!-- 准备好一个容器 -->
<div id="root">
  <!-- 普通写法 -->
  <!-- 单向数据绑定:<input type="text" v-bind:value="name"><br/>
       双向数据绑定:<input type="text" v-model:value="name"><br/> -->

  <!-- 简写 -->
  单向数据绑定:<input type="text" :value="name"><br/>
  <!-- v-model:value 可以简写为 v-model,因为 v-model 默认收集的就是 value 值 -->
  双向数据绑定:<input type="text" v-model="name"><br/>

  <!-- 如下代码是错误的,因为v-model只能应用在表单类元素(输入类元素)上 -->
  <!-- <h2 v-model:x="name">你好啊</h2> -->
</div>

双向绑定一般都应用在表单元素上。如:input、select 等。对于不同表单标签,默认绑定的属性名不一样:

  • 文本类型的 <input><textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"><input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件。

2. v-model 深入

1)表单元素使用 v-model
[1] 基本使用

v-model 指令是一个语法糖,它相当于绑定了 value 属性和 input 事件。

v-model 在内部同时使用 v-bind 指令(用于绑定 value 属性)和 v-on 指令(用于监听 input 事件)来实现的。所以,v-model="something" 实际上是 v-bind:value="something" v-on:input="something = $event.target.value" 的简写。

vue
<template>
  <div>
    <h3>需求:不用v-model实现双向绑定</h3>
    <input type="text" :value="uname" @input="changeValue" />
    <p>结论:v-model 可以拆分成 :value + @input</p>
  </div>
</template>

<script>
export default {
  data () {
    return {
      uname: 'zhangsan'
    }
  },
  methods: {
    changeValue (e) {
      // 把输入框的值,赋值给uname
      this.uname = e.target.value
    }
  }
}
</script>

<style lang="less" scoped></style>
[2] 父子使用 - inpute

子组件有 inpute 输入文本框,在父组件引入子组件。把输入框数据定义在父组件,子组件的输入框内容改变,父组件 data 中的数据就改变。

父组件:

vue
<template>
  <div>
    <h3>需求:实现输入框组件的v-model</h3>
    <!-- <MyInput :value="age" @input="changeAge"></MyInput> -->
    <MyInput v-model="age"></MyInput>
    <p>结论:组件的v-model需要子组件的配合。</p>
    <p>结论:子组件需要接收value属性,并触发input事件</p>
    <hr />

    <p>总结:v-model ==== :value + @input</p>
    <p>总结:父传子,为了把数据传给子组件,设置表单元素的默认值</p>
    <p>总结:子传父,为了把表单元素的值,传给父组件,修改data数据</p>
  </div>
</template>

<script>
import MyInput from './components/MyInput.vue'
export default {
  data () {
    return {
      age: 20
    }
  },
  components: {
    MyInput
  },
  methods: {
    changeAge (val) {
      this.age = val
    }
  }
}
</script>

<style lang="less" scoped></style>

子组件:

vue
<template>
  <input
    type="text"
    :value="value"
    @input="$emit('input', $event.target.value)"
  />
</template>

<script>
export default {
  name: 'MyInput',
  props: ['value']
}
</script>

<style lang="less" scoped></style>
[3] 父子使用 - select

父组件:

vue
<template>
  <h3>需求:实现下拉框组件的v-model</h3>
  <!-- <MySelect :value="address" @input="changeSelect"></MySelect> -->
  <MySelect v-model="address"></MySelect>
  <p>下拉框的v-model相当于 :value + @input</p>
</template>

<script>
import MySelect from './components/MySelect.vue'
export default {
  data () {
    return {
      address: '上海',
    }
  },
  components: {
    MySelect
  },
  methods: {
    changeSelect (val) {
      this.address = val
    }
  }
}
</script>

子组件:

vue
<template>
  <select :value="value" @change="$emit('input', $event.target.value)">
    <option value="北京">北京市</option>
    <option value="上海">上海市</option>
    <option value="广州">广州市</option>
    <option value="深圳">深圳市</option>
  </select>
</template>

<script>
export default {
  name: 'MySelect',
  props: ['value']
}
</script>

<style lang="less" scoped></style>
[4] sync 修饰符的原理

子组件是模态框,父组件有个按钮控制模态框显示与隐藏,使用 sync 修饰符实现。

结论:sync 可以看成下放权限,让父组件传给子组件的 prpos 属性得以修改。

vue
<template>
  <div>
    <button @click="visible = true">删除</button>
    <MyDialog :visible.sync="visible"></MyDialog>
    <!-- <MyDialog :visible="visible" @update:visible="xxx"></MyDialog> -->
    <!-- 
      :属性.sync="变量"
      等同于 :属性="变量"  +  @update:属性="xxx"
    -->
  </div>
</template>

<script>
import MyDialog from './components/MyDialog.vue'
export default {
  data () {
    return {
      visible: false // 控制弹出框是否显示
    }
  },
  components: {
    MyDialog
  }
}
</script>

<style></style>

子组件:

vue
<template>
  <div class="dialog" v-show="visible">
    <div class="dialog-header">
      <h3>友情提示</h3>
      <span class="close" @click="close">✖️</span>
    </div>
    <div class="dialog-content">我是文本内容</div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>

<script>
export default {
  props: ['visible'],
  methods: {
    close () {
      this.$emit('update:visible', false)
    }
  }
}
</script>

<style scoped>
// 省略 css
</style>
2)组件中的 v-model
[1] 基本使用

使用 v-model 指令在组件上进行双向绑定时,它实际上是绑定 modelValue 属性和监听 update:modelValue 事件的语法糖。

vue
<my-input v-model="message" />

等价于:

vue
<!-- my-input 组件中通过 $emit 传递值 -->
<my-input :model-value="message" @update:model-value="message = $event" />

这意味着 v-model 会自动处理 modelValue 属性的传递和 update:modelValue 事件的监听,从而实现数据的双向绑定。

vue
<template>
  <input :value="modelValue" @input="onInput" />
</template>

<script>
export default {
  name: 'MyInput',
  props: {
    modelValue: {
      type: String,
      required: true
    }
  },
  methods: {
    onInput(event) {
      this.$emit('update:modelValue', event.target.value);
    }
  }
}
</script>
[2] 组件上使用多个 v-model

子组件

vue
<template>
  <div>
    <input :value="modelValue1" @input="$emit('update:modelValue1', $event.target.value)">
    <input :value="modelValue2" @input="$emit('update:modelValue2', $event.target.value)">
  </div>
</template>

<script>
export default {
  props: ['modelValue1', 'modelValue2'],
  emits: ['update:modelValue1', 'update:modelValue2'],
}
</script>

父组件

vue
<template>
  <custom-component v-model:modelValue1="value1" v-model:modelValue2="value2" />
</template>

<script>
export default {
  data() {
    return {
      value1: '',
      value2: ''
    }
  }
}
</script>

三、el 与 data 的两种写法

el 有 2 种写法

  • new Vue 时候配置 el 属性。
  • 先创建 Vue 实例,随后再通过 vm.$mount('#root') 指定 el 的值。

data 有 2 种写法

  • 对象式
  • 函数式(以后学习到组件时,data 必须使用函数式,否则会报错)

一个重要的原则:由 Vue 管理的函数,一定不要写箭头函数,一旦写了箭头函数,this 就不再是 Vue 实例了。

javascript
// el的两种写法
const v = new Vue({
  // el:'#root', // 第一种写法
  data:{
    name:'张三'
  }
})
v.$mount('#root') // 第二种写法

// data的两种写法
new Vue({
  el:'#root',
  // data的第一种写法:对象式
  /*
    data:{
      name:'张三'
    }
  */

  // data的第二种写法:函数式
  data(){
    console.log('@@@',this) // 此处的this是Vue实例对象
    return {
      name:'张三'
    }
  }
})

四、数据代理

1. Object.defineproperty

javascript
let number = 18
let person = {
  name: '张三',
  sex: '男',
}

Object.defineProperty(person, 'age', {
  // value:18,
  // writable:true, // 控制属性是否可以被修改,默认值是false
  // enumerable:true, // 控制属性是否可以枚举,默认值是false
  // configurable:true // 控制属性是否可以被删除,默认值是false

  // 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
  get() {
    console.log('有人读取age属性了')
    return number
  },

  // 当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
  set(value) {
    console.log('有人修改了age属性,且值是', value)
    number = value
  }
})

// console.log(Object.keys(person))

console.log(person)

2. Vue 中的数据代理

数据代理

通过一个对象代理对另一个对象中属性的操作 (读/写) 。

javascript
let obj = {x:100}
let obj2 = {y:200}

Object.defineProperty(obj2,'x',{
  get(){
    return obj.x
  },
  set(value){
    obj.x = value
  }
})

Vue 中的数据代理

1)是什么?通过 vm 对象来代理 data 对象中属性的操作 (读/写) 。

2)Vue 中数据代理的好处:更加方便的操作 data 中的数据。

3)基本原理

      通过 Object.defineProperty() 把 data 对象中所有属性添加到 vm 上。

      为每一个添加到 vm 上的属性,都指定一个 getter/setter。

      在 getter/setter 内部去操作 (读/写) data 中对应的属性。

五、事件处理

1. 事件的基本使用

1)使用 v-on:xxx@xxx 绑定事件,其中 xxx 是事件名。

2)事件的回调需要配置在 methods 对象中,最终会在 vm 上。

3)methods 中配置的函数,都是被 Vue 所管理的函数,this 的指向默认是 vm 或组件实例对象。methods 中配置的函数,不要用箭头函数!!!否则 this 就不是 vm 了。

4)如果事件方法不需要额外参数,那么方法后的 () 可以不添加。比如:@click="demo"@click="demo($event)" 效果一致,但后者可以传参。

特殊写法

vue
<!-- 如果我们希望一个元素绑定多个事件,这个时候可以传入一个对象 -->
<button v-on="{ click: btnClick, mousemove: mouseMove }">特殊按钮</button>

2. 修饰符

prevent:阻止默认事件 (常用) 。

stop:阻止事件冒泡 (常用) 。

once:事件只触发一次 (常用) 。

capture:使用事件的捕获模式。

     ① 冒泡是从里往外冒,捕获是从外往里捕。

     ② 当捕获存在时,先从外到里的捕获,剩下的从里到外的冒泡输出。

self:只有 event.target 是当前操作的元素时才触发事件。

passive:事件的默认行为立即执行,无需等待时间回调执行完毕。

注意:事件修饰符可以连写,也就是说,一个事件可以绑定多个事件修饰符。

html
<!-- 准备好一个容器 -->
<div id="root">
  <h2>欢迎来到{{name}}学习</h2>
  <!-- 阻止默认事件(常用) -->
  <a href="http://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>

  <!-- 阻止事件冒泡(常用) -->
  <div class="demo1" @click="showInfo">
    <button @click.stop="showInfo">点我提示信息</button>
    <!-- 修饰符可以连续写 -->
    <!-- <a href="http://www.baidu.com" @click.prevent.stop="showInfo">点我提示信息</a> -->
  </div>

  <!-- 事件只触发一次(常用) -->
  <button @click.once="showInfo">点我提示信息</button>

  <!-- 使用事件的捕获模式 -->
  <div class="box1" @click.capture="showMsg(1)">
    div1
    <div class="box2" @click="showMsg(2)">
      div2
    </div>
  </div>

  <!-- 只有event.target是当前操作的元素时才触发事件 -->
  <div class="demo1" @click.self="showInfo">
    <button @click="showInfo">点我提示信息</button>
  </div>

  <!-- 事件的默认行为立即执行,无需等待事件回调执行完毕 -->
  <ul @wheel.passive="demo" class="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
</div>

3. 键盘事件

Vue 中常用的按键别名(修饰符)

    回车 ==> enter

    删除 ==> delete (捕获"删除"和"退格"键)

    退出 ==> esc

    空格 ==> space

    换行 ==> tab (特殊,必须配合 keydown 去使用)

    上 ==> up

    下 ==> down

    左 ==> left

    右 ==> right

Vue 未提供别名的按键,可以使用按键原始的 key 去绑定,但是注意要转为 kebab-case (短横线命名)。也可以使用 keyCode 去指定具体的按键 (不推荐) 。

系统修饰符 (用法特殊): ctrl、alt、shift、meta

  • 配合 keyup 使用:按下修饰按键的同时,再按下其他键,随后释放其他键,事件才触发。

    比如:@keyup.ctrl 表示 ctrl + 其他按键都可以触发事件。如果不想其他按键都可以触发事件,可以 @keyup.ctrl.y 表示只有 ctrl + y 可以触发事件。

  • 配合 keydown 使用:正常触发事件。

定制按键别名

Vue.config.keyCodes.自定义键名 = 键码

六、计算属性

1)定义:要用的属性不存在,要通过已有属性计算得来。

2)原理:底层借助了 Object.defineproperty 方法提供的 getter 和 setter。

3)get 函数什么时候执?

      初次读取时会执行一次。

      当依赖的数据发生改变时会被再次调用。

4)优势:与 methods 实现相比,内部有缓存机制(复用)。效率更高,调试方便。

计算属性最终会出现在 vm 上,直接读取使用即可。

如果计算属性要被修改,那必须写 set 函数去响应修改,且 set 中要引起计算时依赖的数据发生改变。

javascript
computed:{
  // 完整写法
  fullName:{
    // get有什么作用?当有人读取fullName时,get就会被调用,且返回值就作为fullName的值
    // get什么时候调用?1.初次读取fullName时。2.所依赖的数据发生变化时。
    get(){
      console.log('get被调用了')
      // console.log(this) //此处的this是vm
      return this.firstName + '-' + this.lastName
    },
    // set什么时候调用? 当fullName被修改时。
    set(value){
      console.log('set',value)
      const arr = value.split('-')
      this.firstName = arr[0]
      this.lastName = arr[1]
    }
  }
}

// 简写
// 注意:如果计算属性要被修改,那必须写set函数去响应修改,且必须使用完整写法
fullName(){
  console.log('get被调用了')
  return this.firstName + '-' + this.lastName
}

七、监视属性 / 侦听器

监视属性 watch。

1)当被监视的属性变化时,回调函数自动调用,进行相关操作。

2)监视的属性必须存在,才能进行监视。

3)监视的两种写法

      ① new Vue 时传入 watch 配置。watch 选项中的属性也可以简写为函数。

      ② 通过 vm.$watch 监视。

javascript
// 第一种写法
watch:{
  isHot:{
    immediate:true, // 初始化时调用一下handler
    // handler什么时候调用?当isHot发生改变时。
    handler(newValue,oldValue){
      console.log('isHot被修改了',newValue,oldValue)
    }
  }
}

// 第二种写法
vm.$watch('isHot',{
  immediate:true, // 初始化时调用一下handler
  // handler什么时候调用?当isHot发生改变时。
  handler(newValue,oldValue){
    console.log('isHot被修改了',newValue,oldValue)
  }
})

深度监视

1)Vue 中的 watch 默认不监测对象内部值的改变(只监测第一层)。

2)配置 deep:true 可以监测对象内部值改变(多层)。

注意:

  • Vue 自身可以监测对象内部值的改变,但 Vue 提供的 watch 默认不可以!
  • 使用 watch 时根据数据的具体结构,决定是否采用深度监视。
javascript
watch:{
  "number.a":{
    immediate:true, // 初始化时调用一下handler
    handler(newValue,oldValue){
	  console.log('a被修改了',newValue,oldValue)
    }
  },
  // 推荐这种写法
  number:{
    immediate:true, // 初始化时调用一下handler
    deep:true,// 深度监视
    handler(newValue,oldValue){
	  console.log('number被修改了',newValue,oldValue)
    }
  },
}

computed / watch 区别

1)computed 能完成的功能,watch 都可以完成。大多数情况下,computed 实现简单。

2)watch 能完成的功能,computed 不一定能完成,例如:watch 可以进行异步操作。

两个重要的小原则

1)被 Vue 管理的函数,最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象。

2)所有不被 Vue 所管理的函数(定时器的回调函数、ajax 的回调函数、Promise 的回调函数),最好写成箭头函数,这样的 this 的指向才是 vm 或组件实例对象。

其他写法

json
// 监听 b 属性。如果改变就执行 methods 中的 someMethod 方法
b: "someMethod",

// 传入回调函数组,它们会按顺序调用
f: [
  "handle1",
  function handle2(val, oldVal) {
    console.log("handle2 triggered");
  },
  {
    handler: function handle3(val, oldVal) {
      console.log("handle3 triggered");
    },
  },
],

// 在 created 生命周期中监听属性
/* 
  第一个参数是需要监听的属性
  第二个参数是侦听的回调函数callback
  第三个参数是额外的一些选项,比如deep, immediate;
*/
created() {
  this.$watch('message', (newValue, oldValue) => {
    console.log(newValue, oldValue);
  }, { deep: true, immediate: true })
},

八、绑定样式

class 样式

写法::class="xxx",其中 xxx 可以是字符串、对象、数组。

  • 字符串写法适用于:类名不确定,要动态获取。
  • 对象写法适用于:要绑定多个样式,个数不确定,名字也不确定。
  • 数组写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。数组中的每个元素可以是字符串,也可以是对象,变量。

class 与 :class 同时使用时会合并,不会覆盖 class。

style 样式

  • :style="{fontSize: xxx+"px"}",其中 xxx 是动态值。
  • :style="[styleObj1, styleObj2]",其中 styleObj1、styleObj2 是样式对象。
html
<!-- 准备好一个容器 -->
<div id="root">
  <!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
  <div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/><br/>

  <div class="basic" :class="isActive ? 'active' : ''" @click="btnClick">{{name}}</div> <br/><br/>
  <div class="basic" :class="{active: isActive}" @click="btnClick">{{name}}</div> <br/><br/>


  <!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
  <div class="basic" :class="classArr">{{name}}</div> <br/><br/>


  <!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
  <div class="basic" :class="classObj">{{name}}</div> <br/><br/>

  <!-- 绑定style样式--对象写法 -->
  <div class="basic" :style="styleObj1">{{name}}</div> <br/><br/>
  <!-- 绑定style样式--数组写法 -->
  <div class="basic" :style="styleArr">{{name}}</div>
</div>
<script type="text/javascript">
  Vue.config.productionTip = false

  const vm = new Vue({
    el: '#root',
    data: {
      name: '张三',
      isActive: false,
      mood: 'normal',
      // mood: 'normal abc cba',
      classArr: ['header1', this.isActive?'header2':mood, {header3: false, header4: false}],
      classObj: {
        header1: false,
        header2: false,
      },
      styleObj1: {
        fontSize: '40px',
        color: 'red',
      },
      styleObj2: {
        backgroundColor: 'orange' // 也可以这样写 'background-color': 'orange'
      },
      styleArr: [{
        fontSize: '40px',
        color: 'blue',
      }, {
        backgroundColor: 'gray'
      }]
    },
    methods: {
      changeMood() {
        const arr = ['happy', 'sad', 'normal']
        const index = Math.floor(Math.random() * 3)
        this.mood = arr[index]
      },
      btnClick: function() {
        this.isActive = !this.isActive;
      }
    },
  })
</script>

九、条件渲染

v-if

(1). v-if="表达式"

(2). v-else-if="表达式"

(3). v-else

适用于:切换频率较低的场景。

特点:不展示的 DOM 元素直接被移除。所以 v-if 是惰性的。

注意:v-if 可以和 v-else-if、v-else 一起使用,但要求结构不能被"打断"。

技巧:如果有一片 HTML 要控制是否显示,可以使用 template 标签来包裹,在 template 标签上使用条件渲染。在实际到浏览器后,template 标签会不在,不影响结构。

vue
<template>
  <div>
    <p v-if="condition === 'A'">Condition is A</p>
    <p v-else-if="condition === 'B'">Condition is B</p>
    <p v-else>Condition is neither A nor B</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      condition: 'A',
    };
  },
};
</script>

v-show

写法:v-show="表达式"

适用于:切换频率较高的场景

特点:不展示的 DOM 元素未被移除,仅仅是使用样式隐藏掉。给元素添加了 display:none 属性。

两者区别

用法上:v-show 是不支持 template。

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

十、列表渲染

1. 基本列表

v-for 指令

1)用于展示列表数据。

2)语法:v-for="(item, index) in xxx" :key="yyy" 或者 v-for="(value, key, index) in xxx" :key="yyy"

3)可遍历:数组、对象、字符串、指定次数。

注意:在 v-for 中 in 与 of 在使用中没有区别,都可以遍历数组和对象。在遍历数组时,key 与 index 的值相同,一般会略去 key 只写 (item,index)

2. key 的原理

面试题:react、vue 中的 key 有什么作用?(key 的内部原理)

1)虚拟 DOM 中 key 的作用?

      key 是虚拟 DOM 对象的标识,当数据发生变化时,vue 会根据【新数据】生成【新的虚拟DOM】,随后 Vue 进行【新虚拟DOM】与【旧虚拟DOM】的差异比较。

2)对比规则

      (1). 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key。

          ①. 若虚拟 DOM 中内容没变,直接使用之前的真实 DOM。

          ②. 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM。

      (2). 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key。

             创建新的真实 DOM,随后渲染到页面。

3)用 index 作为 key 可能引发的问题

      (1). 若对数据进行逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实 DOM 更新 ==> 界面效果没问题,但效率低。

      (2). 如果结构中还包含输入类的 DOM,会产生错误 DOM 更新 ==> 界面有问题。

4)开发中如何选择 key?

      (1). 最好使用每条数据的唯一标识作为 key,比如 id、手机号码、身份证号、学号等唯一值。

      (2). 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用 index 作为 key 是没有问题的。

3. 例子

1)列表过滤
javascript
// 用watch实现
new Vue({
  el:'#root',
  data:{
    keyWord:'',
    persons:[
      {id:'001',name:'马冬梅',age:19,sex:'女'},
      {id:'002',name:'周冬雨',age:20,sex:'女'},
      {id:'003',name:'周杰伦',age:21,sex:'男'},
      {id:'004',name:'温兆伦',age:22,sex:'男'}
    ],
    filPerons:[]
  },
  watch:{
    keyWord:{
      immediate:true,
      handler(val){
        this.filPerons = this.persons.filter((p)=>{
          return p.name.indexOf(val) !== -1
        })
      }
    }
  }
})

// 用computed实现
new Vue({
  el:'#root',
  data:{
    keyWord:'',
    persons:[
      {id:'001',name:'马冬梅',age:19,sex:'女'},
      {id:'002',name:'周冬雨',age:20,sex:'女'},
      {id:'003',name:'周杰伦',age:21,sex:'男'},
      {id:'004',name:'温兆伦',age:22,sex:'男'}
    ]
  },
  computed:{
    filPerons(){
      return this.persons.filter((p)=>{
        return p.name.indexOf(this.keyWord) !== -1
      })
    }
  }
})

当 watch 与 computed 都能实现功能的时候,优先使用 computed。

2)列表排序

HTML 代码:

html
<div id="root">
  <h2>人员列表</h2>
  <!-- v-model双向绑定 -->
  <input type="text" placeholder="请输入名字" v-model="keyword"/>
  <button @click="sortType = 2">年龄升序</button>
  <button @click="sortType = 1">年龄降序</button>
  <button @click="sortType = 0">原顺序</button>
  <ul v-for="p in filPersons" :key="p.id">
    <li>{{p.name}}-{{p.age}}-{{p.sex}}</li>
  </ul>
</div>

Vue 代码:

javascript
new Vue({
  el:'#root',
  data: {
    keyword : '',
	sortType: 0, // 0代表原顺序 1代表降序 2代表升序
	persons: [
	  {id: "001", name:'周冬雨', age:20, sex:'女'},
	  {id: "002", name:'马冬梅', age:19, sex:'女'},
	  {id: "003", name:'周杰伦', age:21, sex:'男'},
	  {id: "004", name:'温兆伦', age:22, sex: '男'},
	],
  },
  computed:{
    filPersons(){
      const arr = this.persons.filter(p => p.name.indexOf(this.keyword) !== -1);
	  // 判断是否需要排序
      if(!this.sortType) return arr;
	  // sort回调回收到前后两个数据项,a和b
      // sort会改变的原数组
      return arr.sort((p1, p2) => this.sortType === 1 ? p2.age-p1.age : p1.age - p2.age);
      }
    }
  }
});

十一、Vue 监视数据的原理

1)vue 会监视 data 中所有层次的数据,如果数据发生更改,会重新渲染页面。

2)如何监测对象中的数据发生改变(数据劫持)了?

      通过 setter 实现监视(其实和前面学习的数据代理一致,只不过 new vue() 传入的 data 对象会创建一个与原来数据一样的对象,在这个对象的每个属性上添加 getter 与 setter 方法,间接操作原 data,在 setter 方法中会有渲染页面的代码),且要在 new Vue 时就要传入要监测的数据。

      对象中后追加的属性,Vue 默认不做响应式处理。如需给后添加的属性做响应式,请使用如下 API。

      Vue.set(target, propertyName/index, value)vm.$set(target, propertyName/index, value)

3)如何监测数组中的数据?

参考地址:数组变化侦测

      修改 data 中的数组 arr[0]={},发现页面没有更新。发现没有给数组添加 setter 实现监视,那怎么办?

      通过包裹数组更新元素的方法实现。本质就是做了两件事:

          ① 调用原生对应的方法对数组进行更新。

          ② 重新解析模板,进而更新页面。

4)在 Vue 修改数组中的某个元素一定要用如下方法。

      ① 使用这些 API: push()、pop()、shift()、unshift()、splice()、sort()、reverse()

      ② Vue.set() 或 vm.$set()

      特别注意: Vue.set() 和 vm.$set() 不能给 vm 或 vm 的根数据对象添加属性。

十二、收集表单数据

<input type="text"/>,则 v-model 收集的是 value 的值,用户输入的就是 value 值。

<input type="radio"/>,则 v-model 收集的是 value 值,且要给标签配置 value 值。否则收集到的是 "on"。

<input type="checkbox"/>

  • 没有配置 input 的 value 属性,那么收集的就是 checked (勾选 or 未勾选,是布尔值)

    state 中属性配置的是布尔值。收集的就是布尔值。

    state 中属性配置的是数组。有一个选中,收集的就是["on"],有一个取消就是空数组。

  • 配置 input 的 value 属性:

    (1). v-model 的初始值是非数组,那么收集的就是 checked (勾选 or 未勾选,是布尔值) 。

    (2). v-model 的初始值是数组,那么收集的就是 value 组成的数组。

html
<!-- 准备好一个容器 -->
<div id="root">
  <form @submit.prevent="demo">
    账号:<input type="text" v-model.trim="userInfo.account"><br/><br/>
    密码:<input type="password" v-model="userInfo.password"><br/><br/>
    年龄:<input type="number" v-model.number="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.lazy="userInfo.other"></textarea><br/><br/>
    <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.baidu.com">《用户协议》</a>
    <button>提交</button>
  </form>
</div>

<script type="text/javascript">
  Vue.config.productionTip = false

  new Vue({
    el: '#root',
    data: {
      userInfo: {
        account: '',
        password: '',
        age: 18,
        sex: 'female',
        hobby: [],
        city: 'beijing',
        other: '',
        agree: ''
      }
    },
    methods: {
      demo() {
        console.log(JSON.stringify(this.userInfo))
      }
    }
  })
</script>

十三、过滤器

1)定义:对要显示的数据进行特定格式化后再显示 (适用于一些简单逻辑的处理) 。

2)语法

       注册过滤器: Vue.filter(name,callback)new Vue({filters:{}})

       使用过滤器: v-bind:属性= "xxx | 过滤器名" 或者 {{ xxx | 过滤器名 }}

3)备注

      过滤器也可以接收额外参数、多个过滤器也可以串联。

      并没有改变原本的数据,是产生新的对应的数据。

javascript
// 全局过滤器
Vue.filter('mySlice',function(value){
  return value.slice(0,4)
})

// 局部过滤器
filters:{
  timeFormater(value,str='YYYY年MM月DD日 HH:mm:ss'){
    return dayjs(value).format(str)
  }
}

十四、内置指令

以前学习过的指令。

v-bind : 单向绑定解析表达式,可简写为 `:xxx`
v-model : 双向数据绑定
v-on : 绑定事件监听,可简写为 `@xxx`
v-for : 遍历数组/对象/字符串
v-show : 条件渲染 (动态控制节点是否展示)
v-if : 条件渲染 (动态控制节点是否存在)
v-else-if : 条件渲染 (动态控制节点是否存在)
v-else : 条件渲染 (动态控制节点是否存在)

1. v-text

1)作用:向其所在的节点中渲染文本内容。

2)与插值语法的区别:v-text 会替换掉节点中的内容,而 {{xxx}} 则不会。

vue
<span v-text="msg"></span>
<!---- 等待于 --->
<span>{{msg}}</span>

2. v-html

1)作用:向指定节点中渲染包含 html 结构的内容。

2)与插值语法的区别

      ① v-html 会替换掉节点中所有的内容,而 {{xxx}} 则不会。

      ② v-html 可以识别 html 结构,而 v-text 则不会。

3)严重注意:v-html 有安全性问题!!!

      ① 在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击。

      ② 一定要在可信的内容上使用 v-html,永远不要用在用户提交的内容上!

3. v-cloak

v-cloak 指令 (不能指定值) 。

1)是什么:是一个特殊属性。本质上 Vue 实例创建完毕并接管容器后,会删掉 v-cloak 属性。

2)用途:使用 CSS 配合 v-cloak 可以解决网速慢时页面展示出 {{xxx}} 的问题。

3)例子

css
/* 1.给这个特殊属性添加自己想修饰的样式 */
[v-cloak] {
  display: none;
}
html
<!-- 2.使用此属性 -->
<div v-cloak>
  {{ message }}
</div>

当 message 还未被编译时,包含 v-cloak 指令的 div 元素会被隐藏。当 Vue 实例结束编译,v-cloak 指令被移除后,div 元素就会显示出来。

4. v-once

v-once 表示元素和所有的子元素在初始渲染后,将被视为静态内容。这意味着,如果数据发生变化,这些元素和子元素不会再被重新渲染。

使用 v-once 可以有助于提高 Vue.js 应用的性能,因为静态内容只需要被渲染一次,而不需要在每次数据变化时都重新渲染。

5. v-pre

1)用于跳过这个元素和它的子元素的编译过程。

2)可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。

6. v-memo

v-memo 是 Vue3 中引入的一个性能优化指令。v-memo 用于记忆(memoize)一个模板的子树。只有当其依赖的值发生变化时,这个子树才会重新渲染。

v-memo 接受一个依赖值数组作为参数。只有当数组中的值发生变化时,子树才会重新渲染。如果数组中的值没有变化,Vue 会跳过这个子树的更新。

vue
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.name]">
  <p>{{ item.name }}</p>
  <p>{{ item.description }}</p>
</div>

在这个例子中,只有当 item.id 或 item.name 改变时,对应的列表项才会重新渲染。

十五、自定义指令

1. 定义语法

① 局部指令

javascript
new Vue({
  el: '#app',
  data: {
    color: 'red'
  },
  directives: {
    'color': {
      inserted: function (el, binding) {
        el.style.color = binding.value;
      }
    }
  }
});
html
<div id="app" v-color="red">Hello, world!</div>

② 全局指令

Vue.directive(指令名,配置对象)Vue.directive(指令名,回调函数)

javascript
// 使用配置对象
Vue.directive('color', {
  inserted: function (el, binding) {
    el.style.color = binding.value;
  }
});

// 使用回调函数
// 回调函数何时执行?① 指令与元素成功绑定时(一上来)。② 指令所在的模板被重新解析时。 <==> bind & updata
Vue.directive('color', function (el, binding) {
  el.style.color = binding.value;
});

2. 配置对象中常用的 3 个回调

     ① bind:指令与元素成功绑定时调用。

     ② inserted:指令所在元素被插入页面时调用。

     ③ updata:指令所在模板结构被重新解析时调用。

javascript
Vue.directive('指令名称', {
  bind () {}, // 只执行一次;DOM渲染之前执行,里面可以进行样式操作
  inserted () {}, // 只执行一次;DOM渲染之后执行,里面可以进行行为操作
  update () {}, // 数据更新后执行
  componentUpdated () {}, // 父子组件都更新后执行
  unbind () {} // 指令解绑的时候执行
})

3. 备注

     ① 指令定义时不加 v-,但使用时要加 v-。

     ② 指令名如果是多个单词,要使用 kebab-case 命名方式,不要用 camelCase 命名。

html
<div id="root">
  <h2>{{name}}</h2>
  <h2>当前的n值是:<span v-text="n"></span> </h2>
  <!-- <h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2> -->
  <h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
  <button @click="n++">点我n+1</button>
  <hr/>
  <input type="text" v-fbind:value="n">
</div>
<script type="text/javascript">
  Vue.config.productionTip = false

  // 定义全局指令
  /* Vue.directive('fbind',{
    // 指令与元素成功绑定时(一上来)
    bind(element,binding){
      element.value = binding.value
    },
    // 指令所在元素被插入页面时
    inserted(element,binding){
      element.focus()
    },
    // 指令所在的模板被重新解析时
    update(element,binding){
      element.value = binding.value
    }
  }) */

  new Vue({
    el: '#root',
    data: {
      name: '张三',
      n: 1
    },
    directives: {
      // big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
      /* 'big-number'(element,binding){
        // console.log('big')
        element.innerText = binding.value * 10
      }, */
      big(element, binding) {
        console.log('big', this) // 注意此处的this是window
        element.innerText = binding.value * 10
      },
      fbind: {
        // 指令与元素成功绑定时(一上来)
        bind(element, binding) {
          element.value = binding.value
        },
        // 指令所在元素被插入页面时
        inserted(element, binding) {
          element.focus()
        },
        // 指令所在的模板被重新解析时
        update(element, binding) {
          element.value = binding.value
        }
      }
    }
  })
</script>

十六、生命周期

生命周期又名生命周期回调函数、生命周期函数、生命周期钩子。到底是什么?

① Vue 在关键时刻帮我们调用的一些特殊名称的函数。

② 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。

③ 生命周期函数中 this 指向的是 vm 或组件实例对象。

Vue 实例从创建到销毁的过程中,会经历一系列的生命周期阶段。在这些阶段中,Vue 提供了一些生命周期钩子函数。以下是 Vue 的生命周期钩子:

  • beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  • created:在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
  • beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例(Vue 根实例)挂载了一个文档内元素(<div id="app">),当 mounted 被调用时 vm.$el (Vue 生成的 DOM) 也在文档内。
  • beforeUpdate:数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器都会被移除,所有的子实例也都会被销毁。

常用的生命周期钩子:

1)mounted:发送 ajax 请求、启动定时器、绑定自定义事件、订阅消息等【初始化工作】。

2)beforeDestroy:清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。

关于销毁 Vue 实例:

1)销毁后借助 Vue 开发者工具看不到任何信息。

2)销毁后自定义事件会失效,但原生 DOM 事件依然有效。

3)一般不会再 beforeDestroy 操作数据,因为即便操作数据,也不会再触发更新流程了(你都要销毁了,别在这里更新了,做一些收尾操作得了)。

注意:

  • mounted 中做的事情,created 也基本可以,只要不操作 DOM 就行。强烈推荐网络请求在 created 中。

  • 除了现在学习的 8 个 4 对生命周期钩子外,其实还有 3 个隐藏的,需要学习组件后才可以了解。

第三章:组件基础

一、认识组件

以前开发时,HTML、CSS、JavaScript 关系混乱。

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。

在实际应用中,组件常常被组织成层层嵌套的树状结构。

二、使用组件方式

1. 非单文件组件

1)使用组件的三大步骤

① 定义组件(创建组件)。

② 注册组件。

③ 使用组件(写组件标签)。

2)如何定义一个非单文件组件?

使用 Vue.extend(options) 创建,其中 options 和 new Vue(options) 时传入的那个 options 几乎一样,但也有点区别。

① el 不要写,为什么?最终所有的组件都要经过一个 vm 管理,由 vm 中的 el 决定服务哪个容器。

② data 必须写成函数,为什么?避免组件被复用时,数据存在引用关系。

③ 使用 template 可以配置组件结构。

④ components 可以写在 options 里面,表示这个组件下有子组件(组件嵌套)。

const school = Vue.extend(options) 可简写为 const school = options
为什么可以这样?因为在 components 里,它会检查组件在定义的时候有没有写 Vue.extend(options),如果没有就会补上,如果有,就拿来直接用。

3)如何注册组件?

① 局部注册:靠 new Vue 的时候传入 components 选项。

② 全局注册:靠 Vue.component('组件名',组件)

组件名

一个单词组成

  • 第一种写法 (首字母小写) : school
  • 第二种写法 (首字母大写) : School

多个单词组成

  • 第一种写法 (kebab-case命名) : my-school
  • 第二种写法 (CamelCase命名) : MySchool (需要脚手架支持)

备注

  • 组件名要回避 HTML 中已有的元素名称。例如: h2、H2 都不行。
  • 可以使用 name 配置项指定组件在开发者工具中呈现的名字。
4)使用组件

第一种写法: <school></school>

第二种写法: <school />

备注:不使用脚手架时,<school /> 会导致后续组件不能渲染。

当使用 kebab-case (短横线命名法) 定义一个组件时,也必须在引用这个自定义的 kebab-case。例如 <my-component-name>

当使用 PascalCase (首字母大写命名法) 定义一个组件时,在引用自定义元素时需要使用两种命名法。也就是说 <my-component-name><MyComponentName> 都是可以接受的。

2. 单文件组件 SFC

[1] 使用步骤

Vue 的单文件组件(SFC)是一个 .vue 文件,它包含一个 Vue 组件的模板、脚本和样式。以下是一个基本的单文件组件的例子:

vue
<template>
  <div class="message">
    {{ message }}
  </div>
</template>

<script>
  export default {
    name: "APP",
    data() {
      return {
        message: 'Hello, Vue!'
      }
    }
  }
</script>

<style scoped>
  .message {
    color: blue;
  }
</style>

在这个例子中:

  • <template> 部分是组件的 HTML 模板。这里定义了组件的结构和内容。
  • <script> 部分是组件的 JavaScript 代码。这里定义了组件的数据和方法。
  • <style> 部分是组件的 CSS 样式。这里定义了组件的外观。scoped 属性表示这些样式只应用于当前组件。

要使用这个组件,需要在其他组件或应用中导入它:

javascript
import MyComponent from './MyComponent.vue'

然后,就可以在模板中使用这个组件:

vue
<template>
  <my-component></my-component>
</template>

<script>
import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}
</script>

注意:需要一个能够处理 .vue 文件的工具,如 Vite 或 webpack(需与 vue-loader 配合)。开发时大多数会使用 Vue CLI(其实使用的是 webpack)。

[2] scoped 样式

1)作用:让样式在局部生效,防止冲突。

2)写法:<style lang="css/less ……" scoped>

一般在 @/App.vue 里的 style 标签里写全局都使用的样式。这样在组件里都可以使用这里的样式了(前提是 App.vue 的 style 标签没有 scope 修饰)。

三、原理探究

1. Vue Component

1)当定义一个组件(Vue.extend)时,返回一个特殊的 VueComponent 构造函数。

javascript
Vue.extend = function (extendOptions) {
  // ......
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // ......
  return Sub
}

注意:每次调用 Vue.extend 时,返回的都是一个全新的 VueComponent!

2)当使用组件时(<school/><school></school>),Vue 解析时会帮我们创建 school 组件的实例对象,即 Vue 帮我们执行 new VueComponent(options)

3)关于 this 指向

  • 组件配置中

    data 函数、methods 中的函数、watch 中的函数、computed 中的函数,它们的 this 均是【VueComponent 实例对象】。

  • new Vue(options) 配置中

    data 函数、methods 中的函数、watch 中的函数、computed 中的函数,它们的 this 均是【Vue 实例对象】。

4)VueComponent 的实例对象,以后简称 vc (也可称之为:组件实例对象) 。

2. 内置关系

1)一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype

2)为什么要有这个关系:让组件实例对象 (vc) 可以访问到 Vue 原型上的属性、方法。

preview
图片加载中
预览

Released under the MIT License.