Vue3官方文档通读

2022/1/15 JavaScriptVue

Vue3,从核心的知识点,到代码使用。

# Vue 核心概念

# 基本概念

Vue是一个渐进式的MVVM框架,由数据驱动用户界面。

# 响应式原理

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

当Vue创建实例时,会遍历data中的属性,给属性增加getter、setter,并在内部追踪相关依赖,在属性变化时通知到依赖。每个组件都有对应的watcher,会在组件渲染的时候把属性记录为依赖,在依赖项的setter被调用时,会通知组件的watcher去更新组件。

其中给属性增加getter、setter的方法在Vue2、3中不一样,其中:

# Object.defineProperty 与 Proxy 对比

Object.defineProperty给一个对象增加一个属性的方法,因为有get set回调参数,所以实现了Vue的数据监听功能。

Object.defineProperty(obj, key, {
 configurable: false, // 以下配置是否可更改
 enumerable: false, // 是否在Object.keys()中出现
 value: undefind, // 默认值
 writable: false, // value是否可编辑
 get(){}, // 取值前执行
 set(){}, // 赋值前执行
})
1
2
3
4
5
6
7
8

Proxy给对象创建一个代理,用来拦截和自定义操作

new Proxy(obj, {
 get(){}, // 取值前执行
 set(){}, // 赋值前执行
 ... // 等等还有很多操作可以捕捉,暂时我还么用到
})
1
2
3
4
5

Proxy优势

  • 可以监听整个对象
  • 可以监听数组变化
  • 有很多操作可以拦截,甚至监听了Object.defineProperty()
  • Proxy返回了新对象,可以直接操作新对象,不用遍历属性进行修改
  • 新的标准,新浏览器支持的更好

Object.defineProperty优势

  • 兼容性稍好 支持到IE9

Object.defineProperty劣势

  • 操作麻烦
  • 不能监听其他的操作
  • Array Map Set都不支持(Vue通过封装修改数组的方法来进行监听)

# 生命周期

通过Vue的原理可以知道,每个实例从创建到销毁,会发生一些事件,而这些事件的回调就是生命周期的钩子。我们在钩子中执行函数,就可以在Vue的生命周期中执行对应的操作。

使用生命周期的时候不要使用箭头函数,这样会丢失this

流程翻译:

  1. Vue.createApp: 创建一个Vue对象
  2. app.mount(dom): 在dom上挂载Vue对象
  3. Init event & lifeycle: 初始化生命周期事件
  4. Init injections & reactivity: 初始化数据侦听、计算方法、属性等除了挂载dom的操作
  5. Complie template into render function: 将模版转换成渲染函数
  6. Complie el's innerHTML as template: 把el的innerHTML作为渲染模板
  7. Create app.$el and append it to el: 创建虚拟dom并把dom挂载到el上
  8. Virtual DOM re-rendered and dispatch: 虚拟dom重新渲染并派发到真实dom上
  9. app.unmount(): 卸载对象

生命周期图示

生命周期钩子函数简述:

  • beforeCreate: 在实例初始化之后,创建数据侦听、事件侦听器之前调用
  • created: 创建实例完成后(已处理数据侦听、方法事件的回调、计算属性),此时dom未挂载且Vue.$el不可使用
  • beforeMount: 挂载dom之前调用,render函数首次调用
  • mounted: 实例挂载完成后调用,此时Vue.$el可以使用;子元素不一定渲染完成,可以使用this.$nextTick(callback);, 在所有子元素渲染完成后触发回调。
  • brforeUpdate: 数据更新但dom暂未更新的时候调用,适合在现有dom更新之前访问它
  • updated: 数据更新完且dom更新完之后调用,应该避免在此更新状态
  • beforeUnmount: 卸载、销毁实例前调用,此时组件仍是正常的
  • unmounted: 彻底卸载后调用,此时 指令、事件、子组件都已移除

其他钩子函数:

  • activated: 被 keep-alive 缓存的组件激活时调用。
  • deactivated: keep-alive 组件失活时调用。
  • errorCaptured: 捕获一个来自后代组件错误时调用,返回fasle可以阻止错误继续上报
  • renderTracked: 虚拟DOM重新渲染的时候调用
  • renderTriggered: 虚拟DOM重新渲染触发时使用

服务器渲染时不会调用的钩子: beforeMount、mounted

生命周期钩子函数 - 官方文档 v3 (opens new window)

# 模版语法

<template>
 <div>
  // 基本操作
  <span>{{mes}}</span> 文本差值({{}})
  <span v-if="isShow"></span> 指令(v-) 
  // 指令
  <span v-once>{{mes}}</span> 文本差值,仅渲染不更新
  <span v-html="htmlVal"></span> 输出为HTML
  <span v-bind:id="productId"></span> 绑定属性
  <span v-show="isShow"></span> 是否展示
  <span v-if="isShow"></span> 是否渲染
  <span v-for="item in datas" :key="item.id"></span> 循环渲染
  <span v-bind:[attr]="value"></span> 动态参数
  <span v-on:[eventName]="handleFunc"></span> 动态事件
  <span v-on:click.prevent="handleClick"></span> 修饰符prevent 指调用event.preventDefault()
  // 缩写
  <span :href="url"></span> v-bind 缩写
  <span :[attr]="value"></span> v-bind 动态参数 缩写
  <span @click="handleClick"></span> v-on 缩写
  <span @[eventName]="handleFunc"></span> v-on 动态事件 缩写
 </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

不要在模版表达式中,使用全局变量。 因为模版表达式在沙盒中执行,只能使用部分表达式。 Infinity, undefined, NaN, isFinite, isNaN, parseFloat, parseInt, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, Math, Number, Date, Array, Object, Boolean, String, RegExp, Map, Set, JSON, Intl, BigInt

# 数据绑定

由于class和style可能需要较长数组拼接实现,有拼错的可能,所以vue专门给这两个属性增加了对象、数组的支持

子组件的最外层盒子会使用调用组件时属性中的class

// 父
<template>
 <div
  class="box"
  :class="{ active: isActive }"  // => class="active"
  :class="[ isActive ? 'active' : '', errorClass, { redColor: isRed }]" // => class="active error red"
  :style="styleObj" // styleObj:{color: '#000'}
  :style="[styleObj, colorStyle]" // styleObj:{color: '#000'} // 可以数组叠加
  :style="{display: ['-webkit-box', '-ms-flexbox', 'flex']}" // 自动匹配支持的值
 >
  <child class="childBox"></child>
 </div>
</template>
// 子
<template>
 <span 
  class="pink"  // => class="childBox pink"
 ></span>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# v-if与v-show

v-if 与 v-else-if 、 v-else 匹配,语法一致。

v-show 与 v-if 的却别在于:会渲染dom,但display = none,适用于频繁切换显示隐藏的。需注意:在一个dom上不能同时使用 v-show、v-else且不支持在template上使用v-show。

v-if 与 v-for不推荐一起使用,v-if优先级高,在使用循环里的值时,v-if无法取到 会报错

# v-for

v-for支持 Array、Object、Number

<template>
 <ul id="object-rendering">
  <li v-for="(item, key) in objects">
   {{ key }} - {{ item }}
  </li>
 </ul>
 <ul id="array-rendering">
  <li v-for="(item, index) in arrays" :key="item.id">
   {{ item.message }}
  </li>
 </ul>
 <ul id="number-rendering">
  <li v-for="item in 5">
   {{ item }}
  </li>
 </ul>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用v-for,建议要绑定key,这样会提高性能。因为数组数据顺序改变后,页面的更新逻辑是“就地更新”:不是移动dom,而是直接更新。

替换数组时,vue的处理逻辑不是丢弃整个列表,而是用了智能的启发式方法,是一个高效的操作。

v-for子组件时,需要手动绑定v-for中的参数:item / index等。理由:这样会降低框架中处理组件的耦合性。

# Data与Methods语法

<template>
 <div>
  <span>{{}}</span>
 </div>
</template>
<script>
 const app = Vue.createApp({
  data(){
   return {
    num: 1,
    num1: 100
   }
  },
  computed:{ // 计算属性的getter
   total(){
    return this.num + this.num1
   }
  },
  watch:{ // 侦听器
   total(){
    return this.num + this.num1
   }
  },
  methods{
   add(){
    this.num++
   }
  },
 })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  1. data需要返回一个函数。理由:在组件复用时,非函数的情况下,会使组件间使用相同的data。
  2. methdos中的方法需要避免使用箭头函数。理由:使用箭头函数会导致丢失this,不利于开发。

# 计算属性

当模版中需要使用较复杂操作时,使用计算属性来代替直接运算取值。(当页面刷新的时候调用,重新渲染的时候不会再次执行。)

看起来和methods很像,但区别在于computed会缓存数据,所以未修改时不会重复计算,可以解决很多性能问题。

默认的computed只有getter,也可以自定义setter。

const app = Vue.createApp({
 computed:{ // 计算属性的getter
  total:{
   get(){
    return this.num + this.num1
   },
   set(val){
    console.log('新的结果是:'+val)
   } 
  }
 },
})
1
2
3
4
5
6
7
8
9
10
11
12

# 侦听器

当数据变化是执行异步操作或较大开销操作的时候调用。

只在数据变化时调用、支持异步操作

const app = Vue.createApp({
 watch:{ // 侦听器
  qty(oldVal, newVal){
   console.log(oldVal, newVal)
  }
 },
})
1
2
3
4
5
6
7

对比:

  • 语法:如果一个computed里需要使用两个参数,那么用watch实现就需要对两个参数分别监听,虽然两个功能一样,但表现不一致,相对的优势也就又些区别
  • 异步:watch支持 computed不支持
  • 缓存:computed有缓存

# 事件处理

v-on(缩写为 @)时绑定事件的写法。

支持的参数包括:js代码、函数名、函数、多个函数。

支持传递特殊参数:$event【原始事件值】,执行函数名时,默认传递$event。

<template>
 <div>
  <span v-on:click="handleClick1">1</span>
  <span @click="num++">2</span>
  <span @click="handleClick3">3</span>  // 传递参数 event
  <span @click="handleClick4(111)">4</span>
  <span @click="handleClick4($event, 111)">4</span> // 特殊变量:原始的事件
  <span @click="handleClick3(111), handleClick4(222)">5</span> // 执行多个函数
 </div>
</template>
1
2
3
4
5
6
7
8
9
10

# 修饰符

修饰符会自动执行部分代码,相当于代码简写。

# @event的修饰符

事件修饰符 功能 等效代码
.stop 阻止单击事件继续冒泡 event.stopPropagation()
.prevent 阻止元素默认行为 event.preventDefault()
.capture 添加事件的监听器 在默认事件前执行
.self 只在触发元素为当前元素时执行
.once 事件只触发一次
.passive 立即触发默认行为
  • 修饰符可以连续使用,需要注意顺序,例如:
    • @click.prevent.self 会阻止本身以及子元素的默认点击事件
    • @click.self.prevent 阻止元素自身的默认点击事件
  • .passive 立即触发事件,防止其中包含preventDefault()的情况

# @keyup的修饰符

按键修饰符 功能
.enter 回车
.tab Tab键
.delete 删除键
.esc 退出键
.space 空格键
.up
.down
.left
.right

其他还有很多,用到的时候再去官方文档上查···

# 双向绑定

使用v-model可以在input、textarea、select元素上创建双向绑定。忽略表单元素的value、selected、checked属性的初始值,使用v-model绑定的值。相对的,绑定了v-model的表单元素,会触发text、textarea的input事件、其他的会触发change事件。

相当于vue使用v-model来快捷实现了可输入元素的事件回调来触发绑定数据的更新。

同时,vue还可以对双向绑定的值进行二次绑定

// 相当于:vm.toggle === 'yes' || 'no'
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// 相当于:vm.pick == vm.a
<input type="radio" v-model="pick" v-bind:value="a" />
// 相当于:vm.selected.number === 123
<select v-model="selected">
  <option :value="{ number: 123 }">123</option>
</select>
1
2
3
4
5
6
7
8

与事件类似,v-model也有修饰符:

事件修饰符 功能
.lazy 把默认修改数据的事件从input变成change
.number 把返回的值使用parseInt格式化为Number
.trim 返回的值自动过滤首尾空格

修饰符还可以自定义: 查看详细

# 组件思想与使用

组件思想 => 把页面拆成功能,页面上可以重复使用某个功能且互不影响。

Vue中组件分为两种模式:全局注册(使用量大)、局部注册(使用量小);以此减小内存的压力。

组件之间需要考虑到的问题包含:组件的参数(父子组件间怎么传值、组件组件间怎么传值)、组件的事件(传递出去被监听到)、组件的双向数据绑定。

# 组件的参数

创建组件的语法:app.component,类似Vue.createApp。

组件名(component的第一个参数)是大小写不敏感的,即驼峰和短线分割等效;组件的option(component的第二个参数)除了基本的 data、template、methods等,多了props参数(用来接收父组件传递进来的值)。

子组件也可以支持v-model,需要手动在子组件中手动加入props: modelValue emits: update:modelValue即可。

const app = Vue.createApp({
 data(){
  return {
   list:[{name: 'zhang3', val: 3},{name: 'li4', val: 4}]
  }
 },
 template: `
  <div>
   <blog-post v-for="(item, index) in list" :key="item.name" 
    :name="item.name" :val="item.val"
    @blog-change="blogChange($event, index)"
    v-model="item.val"
   >
    另一种传递值的途径  value={{item.val}}
   </blog-post>
  </div>
 `,
 methods:{
  blogChange(val,index){
   // 自定义事件的响应,如果不需要父组件中的参数,即这里不需要index的情况,@blog-change="blogChange"即可
   this.list[index].val = val
  }
 }
})
// 全局组件
app.component('blog-post', {
 props: ['name','val','modelValue'],
 emits: ['blogChange', 'update:modelValue'], 
 template: `
  <div>
   <h4>{{ name }}</h4>
   <span>{{ val }}</span>
   <button @click="handleClick">add</button>
   <input @input="handleChange" />
   <slot></slot>
  </div>
 `,
 methods:{
  handleClick(){
   // 为什么不用 this.val++? 因为props是只读的,只能让父组件来修改
   // 触发自定义事件
   this.$emit('blogChange', this.val + 1, this.name) // 父组件使用$event 接收$emit传递的第二个值,所以这里的this.name父组件是收不到的
  },
  handleChange(event){
   // 双向绑定
   this.$emit('update:modelValue', parseInt(event.target.value)) // update:modelValue 为固定写法,props中的modelValue也=同样。parseInt与双向绑定无关
   // 另一种实现方法是@input换成computed,modelValue、update:modelValue不变
  }
 }
})
const vm = app.mount("#app")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 跨层级传递参数

除了经常听到的Vuex和Pinia可以快捷实现跨层级的传递参数,vue本身也提供了一个方法父组件 provide + 子组件 inject,但是这个还是需要父子组件有关联,而不是像Vuex等状态管理插件可以全局操作使用参数。

// 假设组件层级关系是 TodoList{ TodoItem {} TodoListFooter {ClearTodosButton TodoListStatistics} }
app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() { 
    return {
   // todosLength: this.todos.length, // 非响应式的
   todosLength: Vue.computed(()=>{
    return this.todos.length // 响应式
   })
  }
  },
  template: `
    <div>
      {{ todos.length }}
    </div>
  `
})
app.component('todo-list-statistics', {
  inject: ['todosLength'],
  created() {
    console.log(`Injected property: ${this.todosLength}`) // > 注入的 property: 2
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

需要注意的是:

  1. provide如果不是函数,调用data取数据时会报错
  2. provide返回的数据如果需要用响应式,需要手动处理Vue.computed
  3. provide/inject必须是父子组件,中间可以隔辈,但是不能没有从属关系

# 动态组件

如果使用组件时 需要根据情况来切换当前使用的组件,可以使用:is来实现,例如Tab功能。

 <!-- currentTabComponent返回注册了的组件名或者一个组件选项对象 -->
 <component :is="currentTabComponent"></component>
1
2

# 特殊情况

如果在table中不写tr 而是直接使用组件,产生的解析异常是由于浏览器强制某些元素与子元素的关系,例如ul-li、table-tr-td、select-option等。

<table>
 <!-- blog-post-row是已经注册过的组件, is="vue:" 是固定写法-->
  <tr is="vue:blog-post-row"></tr>
</table>
1
2
3
4

# Vue 进阶部分

# 组件进阶

在介绍组件思想与用法时,部分参数还有多种写法以适应多种使用环境,另外还有部分需要注意的地方可能会影响我们使用。

# 组件名规范

根据W3C的规范,html标签都是小写,所以自定义组件在使用时,都需要使用短线分割;如果component定义时名称为驼峰命名法,vue会自动识别,且支持短线分割使用

# 注册模式

全局模式和局部模式在上面的说命中没有体现,这里详细说下。

app.component()绑定的组件为全局组件。

Vue.createApp({components: {'component-a': ComponentA,}})绑定的是局部组件,局部组件不会继承,所以子组件不能使用父组件绑定的其他组件

在使用了webpack等模块系统时,局部组件的写法推荐为:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  }
}
1
2
3
4
5
6
7
8
9

# 受控的props

props可以等于简单的字符串数组,也可以限制其类型等

props: {
  item: {
  type: String, // 还可以为 其他数据类型,也可以传递数组[String, Boolean]表示支持多种类型
  required: true, // 空判断
  default(){ // default可以是值或者有return函数
   return 100
  },
  validator(val){
   return val > 0
  }
 },
}
1
2
3
4
5
6
7
8
9
10
11
12

# 隐式贯穿

针对 非props的attribute,且组件根节点只有一个 的情况

如果属性没有在props里定义的(常见的比如class、style等),可以使用$attr来调用,这类属性,vue会自动把属性加到组件的最外层节点上;

如果事件没有$emit调用的,也可以使用$attr来获取。

如果不想使用这个特性(vue会自动把属性加到组件的最外层节点上),给组件设置inheritAttrs: false来关闭,关闭之后还可以使用v-bind="$attr"来吧功能绑定到其他dom上

如果组件根节点不是一个,上面的特性会直接失效。

# 自定义事件

this.$emit(event, data)需要注意一下几点:

  • event如果和浏览器的原生事件重复的时候,会替代原生事件
  • enevt最好在emits:{}中定义,便于管理
  • emits:{}支持对象(函数返回校验结果)和数组
  • app.component('custom-form', {
      // emits: ['inFocus', 'submit'],
      emits: {
      inFocus: null,
      submit: (data)=>{
       return data > 0 // 返回boolen值,确认是否抛出事件
      }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
app.component('custom-form', {
  // emits: ['inFocus', 'submit'],
  emits: {
  inFocus: null,
  submit: (data)=>{
   return data > 0 // 返回boolen值,确认是否抛出事件
  }
})
1
2
3
4
5
6
7
8

v-model 需要使用 modelValue、update:modelValue 来配合,如果需要自定义,可以通过修改modelValue参数来实现:

<template>
 <my-component v-model:title="value" v-model:writer="name"/>
</template>
<script>
 app.component('my-component', {
  props: {
   title: String,
   writer: String
  },
  emits: ['update:title', 'update:writer'],
  template: `
   <input type="text" :value="title" @input="$emit('update:title', $event.target.value)">
   <input type="text" :value="write" @input="$emit('update:write', $event.target.value)">
  `
 })
</script> 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 自定义修饰符

自定义修饰符的名称,在组件内可以通过this.modelModifiers来获取;

如果v-model绑定了其他参数,modelModifiers则改为参数+Modifiers,例如v-model:title.capitalize取值时参数名为this.titleModifeiers.capitalize

<template>
 <my-component v-model.capitalize="myText"></my-component>
</template>
<script>
 app.component('my-component', {
  props: {
   modelValue: String,
   modelModifiers: {
    default: () => ({})
   }
  },
  emits: ['update:modelValue'],
  template: `
   <input type="text" :value="modelValue" @input="emitValue">
  `,
  created() {
   console.log(this.modelModifiers) // create之后就可以看到绑定的修饰符 { capitalize: true }
  }
  methods: {
   emitValue(e) {
    let value = e.target.value
    // 如果绑定多个,可以依次处理
    if (this.modelModifiers.capitalize) {
     value = value.charAt(0).toUpperCase() + value.slice(1) // capitalize修饰符实现逻辑
    }
    this.$emit('update:modelValue', value)
   }
  },
 })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

返回双向绑定

# 插槽 slot

下面的内容里,插槽代表的是 父组件中传递参数 的部分,<slot>代表 子组件中接受参数的部分

插槽中可以插入文本、HTML、参数等任何代码传递给子组件;<slot></slot>中可以写代码,作为插槽的默认值

需要注意的是,插槽中的参数只能取到代码所在组件的参数,不能取到<slot>所在组件的参数

如何在插槽中使用子组件中的参数呢? 使用props,<slot :item="item">在插槽里给v-slot赋值,就可以取到prop对象了

另外给插槽增加了name属性后,插槽就不唯一了,语法(v-slot 只能用在template上):

<!-- 父组件 -->
<base-layout>
  <template v-slot:header>
    <h1>v-slot:header也可以缩写为#header</h1>
  </template>
 <template v-slot:default>
    <p>如果v-slot的值是default,子组件中可以省略name属性</p>
  </template>
  <template v-slot:footer="props"> 
    <p>props不是固定值,可以自定义。{{props.value}}</p>
  </template>
</base-layout>
<!-- 子组件 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer" :value="'测试文案'"></slot>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

和动态事件名类似,插槽也可以使用[slotName]动态命名,插槽props也可以使用解构赋值来取值。

# 动态组件is

在使用:is指令做动态切换组件时,组件切换后会重新渲染,如果需要组件常驻(缓存失活的组件),需要使用<keep-alive>将元素包裹起来。

<keep-alive>
 <component :is="currentTabComponent"></component>
</keep-alive>
1
2
3

# 异步组件

在组件很多,项目很大的情况下,一次加载所有组件会造成内存占用大,白屏时间长等情况,所以需要把组件留在服务器等待异步加载片段

vue的解决办法是defineAsyncComponent函数,它接收一个返回promise的函数,等从服务器取到组件代码时,通过resolve传递

// defineAsyncComponent
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
app.component('async-example', defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
   resolve({
    template: '<div>I am async!</div>'
   })
  })
 }
))
1
2
3
4
5
6
7
8
9
10
11

# 操作DOM

尽管Vue解决了大部分问题,但是还有一些插件或者问题的处理办法需要直接操作dom

那么就可以在组件上设置ref=''属性,在渲染dom完成后,可以在this.$ref对象中取到DOM对象

# 边界处理

在实际生产过程中,由于种种原因,可能会出现需要强制手动更新的情况,Vue也对这种情况做了兜底功能【手动强制更新渲染】:this.forceUpdate()

同理,在某种特殊情况下,会需要页面更新做监听回调,Vue提供了this.nextTick(callback)

有的组件可能是纯静态组件(仅渲染一次不会再变动),为了节省性能可以使用v-once来控制只渲染一次

# 动画组件

css动画的是靠切换class来控制,所以vue给常用的动画节点(状态)自动添加class,另外还在动画期间添加了钩子函数。

# 进入/离开

vue提供了一个<transition>组件来给元素切换进入/离开效果,控制切换的途径是:

  • 使用v-if
  • 使用v-show
  • 动态组件
  • 组件根节点

当切换元素显示隐藏包含在<transition>中,vue会做一下处理:

  1. 检测到动画,自动切换class
  2. 添加动画的钩子函数
  3. 如果没有检测到动画,直接操作dom
# 自动切换的class
class name 触发节点 自定义class的属性
v-enter-from 插入动画的开始样式 enter-from-class
v-enter-active 动画的过程,transition/animate enter-active-class
v-enter-to 插入动画的结束样式 enter-to-class
v-leave-from 删除动画的开始样式 leave-from-class
v-leave-active 删除的过程,transition/animate leave-active-class
v-leave-to 删除动画的结束样式 leave-to-class

需要注意:<transition>组件接受name属性,响应的,自动切换的class会自动由v-XXX变成name-XXX
class的属性除了默认的v-和name-还可以自定义,组件接受自定义class的属性,会替换默认class,这样可以兼容第三方css动画库

如果过度/动画控制的时间不能满足使用要求,组件还支持自定义切换时间:duration="100"或把进入离开时间分别定义:duration="{ enter: 500, leave: 800 }"

<!-- animate demo -->
<div id="demo">
  <button @click="show = !show">Toggle show</button>
  <transition name="bounce">
    <p v-if="show">测试文案</p>
  </transition>
</div>
<script>
 Vue.createApp({
  data() {
   return {
    show: true
   }
  }
 }).mount('#demo')
</script>
<style>
 .bounce-enter-active {animation: bounce-in 0.5s;}
 .bounce-leave-active {animation: bounce-in 0.5s reverse;}
 @keyframes bounce-in {
  0% {transform: scale(0);}
  50% {transform: scale(1.25);}
  100% {transform: scale(1);}
 }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 动画的钩子函数

适用于第三方的js动画库,例如gsap

使用js动画时为了防止css影响,可以设置:css="false" 来禁用transition对css相关的识别,还减小了性能消耗

属性 触发时间点 参数
@before-enter 进入前 el
@enter 进入过程 el,done[func]
@after-enter 进入后 el
@enter-canceled 进入取消 el
@before-leave 离开前 el
@leave 离开过程 el,done[func]
@after-leave 离开后 el
@leave-canceled 离开取消 el
<!-- 第三方动画库demo -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
<div id="demo">
  <button @click="show = !show">Toggle</button>
  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    :css="false"
  >
    <p v-if="show">测试文案</p>
  </transition>
</div>
<script>
 Vue.createApp({
  data() {
   return {
    show: false
   }
  },
  methods: {
   beforeEnter(el) {
    gsap.set(el, {
     scaleX: 0.8,
     scaleY: 1.2
    })
   },
   enter(el, done) {
    gsap.to(el, {
     duration: 1,
     scaleX: 1.5,
     scaleY: 0.7,
     opacity: 1,
     x: 150,
     ease: 'elastic.inOut(2.5, 1)',
     onComplete: done // 必须使用done来进行回调,否则将立即执行,即没有动画
    })
   },
  }
 }).mount('#demo')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 其他参数
属性 可选值 功能
apper - 用来控制第一次渲染时是否开启过渡
mode in-out(先进后出)
out-in(先出后进)
过度模式:组件内有多个元素时,进入和离开的元素执行顺序:

# 列表过渡

如果transition组件中有很多元素,那么在操作元素离开或插入时,当前dom的插入或者消失对其他元素的影响是瞬间的(当前元素动画完善,但是其他元素没有对应的位移动画)

<transition-group>通过新的泪.v-move{transition: transform 0.8s ease}来实现了FLIP解决了问题,这个类主要决定过度时长和缓动曲线,transform则是缓动中变化的属性

FLIP:

  • F(First): 元素的初始状态
  • L(Last): 元素的最终状态
  • I(Invert): 假设元素从(0,0)移动到(100,100),让元素先移动到(100,100),根据DOM元素属性的改变,会被浏览器集中起来,在下一帧渲染时一并处理可以确定一个时间点:DOM改变了,但还没移动过去(渲染),此时我们可以拿到最终状态的位置还有位移方向、距离,由此可得下一帧的前一个位置是translate(-100px, -100px),即动画从(0,0)=>(100,100)修改为(-100,-100)(0,0)
  • P(Play): 从倒置的位置开始运动到起始位置

FLIP例子 (opens new window)

<template>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
 <div id="list-complete-demo" class="demo">
  <button @click="shuffle">Shuffle</button>
  <button @click="add">Add</button>
  <button @click="remove">Remove</button>
  <transition-group name="list-complete" tag="p">
   <span v-for="item in items" :key="item" class="list-complete-item">
    {{ item }}
   </span>
  </transition-group>
 </div>
</template>
<script>
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
      nextNum: 10
    }
  },
  methods: {
    randomIndex() {
      return Math.floor(Math.random() * this.items.length)
    },
    add() {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove() {
      this.items.splice(this.randomIndex(), 1)
    },
    shuffle() {
      this.items = _.shuffle(this.items)
    }
  }
}
Vue.createApp(Demo).mount('#list-complete-demo')
</script>
<style>
.list-complete-item {transition: all 0.8s ease;display: inline-block;margin-right: 10px;} /* transition与v-move中使用的功能一致,只是这样写比较方便 */
.list-complete-enter-from,
.list-complete-leave-to {opacity: 0;transform: translateY(30px);} /* 离开结束,避免的元素相叠 */
.list-complete-leave-active {position: absolute;} /* 离开过程,防止瞬间被覆盖 */
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 状态过渡

这个是更复杂的动画实现,主要依赖于js动画插件gsap,具体实现,需要学习gsap的文档或者tweenmax的文档 (opens new window)(tweenmax是gsap的核心集)

# 可复用/组合

# Mixin

Mixin对象也包含了组件本身的属性项,在app中使用的时候,优先使用本身的数据,其次使用mixin进来的数据,methods同data取值逻辑

const myMixin = {
 data() {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
  created() {
    console.log('mixin 对象的钩子被调用')
  }
}
const app = Vue.createApp({
  mixins: [myMixin],
 data() {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created() {
    console.log('组件钩子被调用')
  console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

但是这种逻辑容易出现冲突,或者需要非常明确各个组件的参数、方法等,防止其不会出现重名情况,所以有了新的实现方法 组合式API

这个是2.X时代就有的功能,但是种种原因用的项目很少···

# 组合式API

对组合式API的理解:

一般我会写一些工具函数在项目中,但是这些工具函数没有业务逻辑的参与也不支持响应式,而这个组合式API就相当于按照功能点把业务划分开,然后在组件内部可以复用。

所以,当一个Vue组件内部包含了很多功能部分,且这些部分并不是通过自组件的形式引用,那么可以使用组合式API的方法,在组件注册最开始,引入一个功能组(composables),这个功能组实际上返回了一个函数,这个函数接收参数props,返回双向绑定的数据(ref)或者方法,这些数据、方法在组件内可以使用,且支持响应式。

那么就有这样的文件依赖结构 api / util(js) => composables(js) => component(js、css、html) => page => project

# 自定义指令

除了自带的指令外,vue还支持自定义指令,自定义指令支持的钩子函数包含

  • created 在绑定元素的 attribute 或事件监听器被应用之前调用
  • beforeMount 当指令第一次绑定到元素并且在挂载父组件之前调用
  • mounted 在绑定元素的父组件被挂载前调用
  • beforeUpdate 在更新之前调用
  • updated 在组件及自组件更新时调用
  • beforeUnmount 在卸载绑定元素的父组件之前调用
  • unmount 在解除绑定切父组件已经卸载时调用
  • // 局部
    app.component({
     directives: {
      focus: { // 指令的定义
       mounted(el, binding) {
        console.log(binding) // 指令绑定的值 如果 v-focus:[direction]="value",binding => {arg, value}
        el.focus()
       }
      }
     }
    })
    // 全局
    app.directive('focus', {// 当被绑定的元素挂载到 DOM 中时……
      mounted(el) {
        el.focus()// 聚焦元素
      }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
// 局部
app.component({
 directives: {
  focus: { // 指令的定义
   mounted(el, binding) {
    console.log(binding) // 指令绑定的值 如果 v-focus:[direction]="value",binding => {arg, value}
    el.focus()
   }
  }
 }
})
// 全局
app.directive('focus', {// 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    el.focus()// 聚焦元素
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# teleport组件

功能:把当前的dom插入某个dom后

<teleport to="body"> // 还支持 to="#id"
 <p>123</p>
</teleport>
<!-- 结果 -->
<body>
 <div id="app"></div>
 <p>123</p>
</body>
1
2
3
4
5
6
7
8

# 渲染函数

当template返回的值不能满足实际使用时,可以使用 h() 函数来返回VNode,实际使用时,常常与JSX一起

# 高阶指南

这部分主要讲了自定义组件和Vue的原理、机制

# Vue + Web Components

Web Components是原生的自定义组件功能 MDN文档链接 (opens new window)

目的是为了解决原生环境下一个功能部分(由HTML、CSS、JavaScript)在页面中往往是不在一起,很难复用的问题。

具体表现为:定义一个类然后使用customElements.define('popup-info', PopUpInfo);来创建一个自定义元素popup-info官方案例源代码 (opens new window)

# 在 Vue 中使用 Web Components

Vue会默认把自定义组件解析为vue组件,如果组件没注册等,会抛出异常。在编译选项里设置compilerOptions.isCustomElement就可以按照规则,忽略满足条件的自定义组件,按照原生方式处理。

# Vue创建自定义元素

除了支持原生自定义解析外,vue提供了defineCustomElement方法(api与vue组件完全一致)来创建自定义元素(代替自定义的类)

具体的使用,涉及到属性、事件等,需要根据实际情况查看官方文档 (opens new window)

# 对比

功能基本一致,但vue但自定义组件更完善

建议在需要使用第三方插件且插件使用了原生自定义组件方案实现的情况下使用

# 响应性

# 概念性原理

概念:数据发生改变时,使用了数据的其他部分也需按照数据的改变重新计算。

但是JavaScript不是这样工作的,所以需要手动处理:

  1. 当数值读取时追踪
    • Proxy的set
    • 创建读取逻辑的函数(createEffect 副作用),创建栈存储函数,在函数被调用之前存入,调用完取出
  2. 当被读取的数值改变时进行检测
    • Proxy的set
  3. 重新运行读取逻辑
    • set中执行trigger函数
# 声明函数

Vue中的data函数返回的对象,就是通过reactive函数来处理的,如果传入的数据不是对象(只传递一个字符串或是布尔值),可以使用ref函数处理,功能与reactive函数一致

import { reactive } from 'vue'
// 响应式状态
const state = reactive({
  count: 0
})
1
2
3
4
5

这部分的理解可以参考Pinia的文档。


当我们需要一个属性依赖于另一个计算属性时,可以使用computed函数

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})  // 新的属性,依赖count
plusOne.value = 1
console.log(count.value) // 0
1
2
3
4
5
6
7
8
9

当我们需要监听属性时,可以使用watchEffect函数

const count = ref(0)
const stop = watchEffect((onInvalidate) => {
 console.log(count.value) // -> logs 0
 onInvalidate(()=>{
  // 清理失效时的回调
 })
}) // 返回停止函数
setTimeout(() => {
  count.value++  // -> logs 1
}, 100)
stop() // 显式调用 停止监听函数
1
2
3
4
5
6
7
8
9
10
11

# 渲染机制

生成虚拟DOM(JavaScript对象),虚拟DOM包含数据、元素、子集,在更新数据时先更新虚拟DOM,然后虚拟DOM和实际DOM之间Diff,实现高效的DOM更新。

# Vue 生态

# 状态管理

Vue中可以使用内置的reactive方法来监听数据,并且触发更新使用数据的组件。

另外还有 Vuex / Pinia 等第三方工具,都可以实现状态管理,其中Pinia相对更简单易懂。

# reactive

reactive方法接受对象,并返回这个对象,并监听对象更新去触发dom的更新。

const { createApp, reactive } = Vue
const sourceOfTruth = reactive({
  message: 'Hello'
})
const appA = createApp({
  data() {
    return sourceOfTruth
  }
}).mount('#app-a')
1
2
3
4
5
6
7
8
9

但是由于这种情况下参数在任意地方都可以修改,任意组件可以操作就会导致很难管理,所以可以自定义一个类,然后定义action去处理。

const store = {
  debug: true,
  state: reactive({
    message: 'Hello!'
  }),
  setMessageAction(newValue) {
    if (this.debug) {
      console.log('setMessageAction triggered with', newValue)
    }
    this.state.message = newValue
  },
  clearMessageAction() {
    if (this.debug) {
      console.log('clearMessageAction triggered')
    }
    this.state.message = ''
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第三方框架也是按照这个思路慢慢发展成型的。

# 路由管理

对于复杂的单页面应用,路由的跳转就需要一个功能齐全的插件来处理了,官方提供了Vue Router来实现,官方文档 (opens new window)

当然也存在很多第三方路由,比如:Page.js / Director等

# The End

Vue的核心就是数据响应式更新DOM,围绕着这一思想,慢慢发展成一个完整的,覆盖大部分实际开发需求的功能性框架。