vue2学习文档

Vue2学习文档

英文官网:

  • Vue2: https://vuejs.org/
  • Vue3:https://vuejs.org/

中文官网

  • Vue2: https://v2.cn.vuejs.org/
  • Vue3: https://cn.vuejs.org/

入门

前言

用来构建用户界面的渐进式的js库

与其它前端JS框架的关联

  • 借鉴Angular的模板和数据绑定技术
  • 借鉴React的组件化和虚拟DOM技术
特点
  • 声明式

    无需亲自操作DOM,利用模板描述界面

    只要更新data数据,Vue就会自动更新DOM

  • 组件化

    将功能界面拆分成多组件组合使用,提高代码复用率,更好维护

  • 虚拟DOM+DIff算法

    实现最小化DOM更新

MVVM

MVVM(Model-View-ViewModel)是一种软件架构设计模式

Vue是MVVM模式的一个实现库

MVVM支持双向绑定,当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改,反之修改V层则会通知M层数据进行修改
请添加图片描述

MVVM的组成
  • M: Model 模型

    • 数据模型,主要负责业务数据相关
    • 包含n个可变数据的data对象
    • 作用:给View提供响应式数据
  • V: View 视图

    • 模板页面,负责视图相关
    • 作用:读取data数据进行动态显示
  • VM: ViewModel 视图模型

    • V与M沟通的桥梁,负责监听M或者V的修改,是实现MVVM双向绑定的要点

    • ViewModel是Vue.js的核心,是Vue的实例对象,简称vm对象

    • 作用:model到view和view到model的双向数据绑定

Diff算法❗
概述❗
  • 什么是diff算法

    diff算法就是比较新旧虚拟DOM树,寻找差异的算法。在源码中是通过patch函数来完成的,所以也称为patch算法

  • diff算法比较思路:深度优先,同级比较

    • 深度优先即针对一个元素一路深挖下去,广度优先的反面

    • 同级比较只进行同级比较,不进行父子级等比较

在这里插入图片描述

执行过程❗
1.基本执行过程
  1. 当组件内部的响应式数据发生更新的时候,就会执行vue内部的updateComponent函数,在函数内部先执行_render方法生成新的虚拟DOM,把虚拟DOM传递给_update方法,并调用_update方法
  2. _update函数中,首先定义一个变量保存旧的虚拟DOM(在vm._vnode_属性上),然后再把_update接受的新的虚拟DOM vNode放在vm的_vnode_属性上,此时在_update函数中就有了新旧虚拟DOM,最后使用patch方法对新旧虚拟DOM进行比较
function Vue(){
    const updateComponent = ()=>{
        this._update(this._render())
    }
}
function _update(vNode){
    //将旧虚拟DOM存下来,以便新虚拟DOM赋值vm._vnode
    const oldVnode = vm._vnode;
    vm._vnode = vNode;
    //使用patch函数进行新旧虚拟DOM比较
    patch(oldVnode, vNode)
}
2.patch比较过程
  1. patch函数首先使用sameVnode方法专门比较两个节点是否相同(相同:两个虚拟DOM节点的标签类型相同[input还要比较type类型],并且key的值也要相同,如果没有写key,则key的值是undefined)

  2. 如果sameVnode比较两个节点相同,则直接进入更新流程

    更新流程:

    1. 把旧节点的真实DOM拿到新节点位置复用
    2. 对比新旧节点的属性是否相同,如果不同则更新
    3. 开始比较子节点
  3. 如果sameVnode比较两个不相同,则直接遍历新的节点创建新的元素,并直接删除旧的元素

3.比较子元素

在这里插入图片描述

  1. vue使用四个指针分别指向 新旧子节点列表 的 首尾节点
  2. 首先比较新旧树的头指针,判断是否相同,如果相同则进入更新流程…(即比较两个的子节点,深度优先原则)
  3. 继续比较两个树的头指针,如果不相同,则比较新旧树的尾指针,如果相同则进入更新流程…
  4. 如果头指针比较和尾指针比较都不相同,则比较头尾指针是否相同,如果相同则进入更新流程…
  5. 如果上边的规则都不相等,则会以新树的头指针为基础,循环旧的虚拟DOM节点,如果存在相等则直接拿过进入更新流程,如果找不到则直接拿当前的新的虚拟DOM节点创建真实DOM
  6. 当新树的头指针超过尾指针的时候,比较结束,如果旧树中存在剩余节点,则删除这个剩余节点
key的作用❗
  • 在新旧虚拟DOM对比更新的时候,默认的diff算法是"就地复用"原则
  • “就地复用”:多个子节点比较的时候,如果没有添加key属性,则key属性都是undefined,所以每一个新旧DOM的key都是相同的,所以就会简单的按照节点的顺序依次比较(如果新旧节点的顺序发生变化,vue仍然都是创建新节点删除旧节点)
  • 我们可以给每一个节点添加一个key属性,方便Vue跟踪每一个元素的身份,从而在diff算法计算的时候可以按照key确定比较节点
  • key的作用:高效的更新渲染虚拟DOM
  • 不要使用遍历出来的index作为key(index不是稳定性),key的要求是 唯一性!!! 稳定性!!!
简短版Diff算法流程
  1. 当组件创建和更新的时候,vue会执行内部的update函数,该函数使用render函数生成的虚拟DOM树,将新旧DOM树调用patch方法进行比较找到差异,并更新真实DOM
  2. 对比的过程被称作diff,vue内部使用patch函数完成对比过程
  3. 在对比的时候,vue采用深度优先,同级比较的方式
  4. 在比较的时候,通过虚拟DOM的key和tag来判断是否相同
    1. 第一:先对根节点的新旧虚拟DOM进行对比,如果相同则将旧节点复用,然后更新数据,递归比较子节点,如果不同则直接根据新的虚拟DOM递归创建新的节点,并且删除旧的DOM
    2. 第二:比较子节点,首先vue给新旧虚拟DOM都使用了两个指针,分别指向头尾,然后不断向中间靠拢进行对比(头头,尾尾,头尾,尾头,乱序),提高对比性能,在比较的过程中如果发现相同则进入更新流程,否则新增和移除dom
    3. 按照上边的两点一直递归,直到整个DOM对比完成
Vue扩展插件
  • vue-cli: vue脚手架
  • vue-router: 路由
  • vuex: 状态管理
  • vue-lazyload: 图片懒加载
  • vee-validate: 表单校验
  • vant-ui: 基于vue的UI组件库(移动端)
  • element-ui: 基于vue的UI组件库(PC端)
插件配置
谷歌插件
  • Vue.js devtools.crx
vsCode插件
  • Vetur

    vue-helper

基本使用

使用
<head>
  <!-- 1. 引入vue库 -->
  <script src="../js/vue.js"></script>
</head>
<body>
  <!-- 2. 定义一个容器元素 -->
  <div id="app">{{msg}}</div>

  <script>
    /* 3. 创建vue实例对象 */
    new Vue({
      // 页面根元素
      el: '#app',
      // 定义响应式数据
      data: {
        msg: 'Hello Vue!'
      },
      // 要解析显示的模板页面, 模板页面中可以读取data数据
      // template会将页面挂载的根元素替换掉
      template: `
        <div>
          <p>{{ msg }}</p>  
        </div>
      `
    })
  </script>
</body>
插值语法

{{ js表达式 }}

  • 作用:动态显示表达式的值
  • 内部查找的是vue实例对象的属性或方法
  • 支持的数据类型:
    • 都支持(包括基本类型和对象类型
    • 在显示时,会自动将数据转换为字符串值显示
    • true / false => 显示对应名称字符串
    • undefined / null => 不显示
  • ⚠vue的插值语法只在vue实例上找数据

React中的JSX表达式

标签体文本和标签属性值上使用

<p name={表达式}>{表达式}</p>

作用: 动态显示表达式的值

Vue配置项

el-挂载

el: 用来指定模板的根元素

  • 选择器字符串
  • 元素对象
  • 本质调用$mount
挂载vue对象
  1. 自动挂载——el: 'selector'
  2. 手动挂载——vue.$mount('selector')

挂载后,vue内部才会进行模板的解析/编译生成虚拟DOM,进而生成真实DOM显示

// 创建vue对象
const vm = new Vue({
  // 1.自动挂载  => 内部会调用$mount进行挂载
  // el: '#root',
  data: {
    content: 'abc'
  }
})
// 2.手动挂载
vm.$mount('#root')

vm的函数必须在挂载之前定义

const vm = new Vue({
data: {
content: 'abc'
}
})
vm.fn = ()=> 'fn函数'
vm.$mount('#root');//挂载

data-数据

data: 用来指定可变的数据,模板可以直接读取,对应react的state

  • 包含n个数据的对象
  • 返回数据对象的函数
响应式更新data数据

⚠vue3中所有数据类型直接修改都有响应式,无需注意以下

基本类型

直接赋值

vue能监听到数据发生的变化,并作出修改

对象类型❗

Vue内部会对data中所有层次属性都通过defineProperty添加getter/setter实现对data数据的深度劫持(监视)

但defineProperty不能监视添加和删除属性,只能监视读取和修改属性

  • 修改对象已有属性 => 响应式 => 界面自动更新

  • 添加新属性/删除属性 => 不是响应式 => 界面不会自动更新

修改已有属性

直接修改,为响应式

this.obj.yyy = value

添加新属性
  1. Vue.set( target, propertyName, value )
  2. vm.$set( target, propertyName, value )
删除属性
  1. Vue.delete( target, propertyName )
  2. vm.$delete( target, propertyName )
数组类型❗

Vue实现数组数据的响应式,不是通过defineProperty给元素添加getter/setter

而是对数组更新元素的7个方法进行包装:

  • push/pop
  • unshift/shift
  • splice
  • sort
  • reverse

先调用原生方法对数组进行更新,再去更新DOM

对于其他未实现响应式的数组方法,可以用新数组替换旧数组

template-模板

template: 用来生成真实DOM的模板字符串

  • 由: html + vue语法(插值语法 + 指令语法)组成的
  • 模板最终会替换掉页面中的空容器元素
  • 缺点: 编码没有提示, 代码没有高亮, 不利于编写和阅读
  • 解决: 可以不写此配置, 将模板直接写在html标签中

methods-方法

methods:包含n个方法的对象

  • methods的所有方法都会被自动添加到vm上
  • 方法的this都被绑定指向vm

⚠methods中的方法不要用箭头函数

实现methods
function Vue(options){
	this._data = options.data;
    this._initData();
    this._initMethods(options.methods)
}
Vue.prototype = {
    _initData(){},
    _initMethods(methods){
        Object.keys(methods).forEach(key => {
            this[key] = methods[key].bind(this)
        })
    }
}

computed-计算属性

计算属性监视内部使用的属性的变化,一旦发生变化,计算属性就要重新计算

  • 计算属性调用情况
    1. 初始显示
    2. 依赖数据发生变化

⚠computed属性在使用时同data属性,无需带上小括号调用,{{ fullName }}

什么时候使用计算属性

  • 如果一个数据仅需直接展示,直接展示
  • 如果一个数据要进行处理之后才能展示,可以考虑使用计算属性
  • 如果模板页面中表达式较长,可以使用计算属性完成
计算属性两种写法
只读-value
computed: {
  fullName () {
  	// setTimeout(() => 计算的结果)
  	// 进行特定的计算
  	return 同步计算的结果  => 专门用于显示
  }
}
可读可写-get & set
computed: {
  fullName () {
	get () {
		return 计算后的结果
	},
	// 监视计算属改变, 当fullName被指定了新的值时调用, 
	set (value) {
		// 可以在其中更新其它数据/发请求/保存数据
	}
  }
}
computed VS. methods
computedmethods
调用情况1.初始显示
2.依赖数据发生变化
1.data中的数据发生变化
(只要data中的数据发生变化,vue需要重新解析模板,就会重新调用methods)
多次读取一次
计算属性有缓存,多次读取只计算一次(只在相关响应式依赖发生改变时才会重新求值)
methods多次读取需要执行多次
watch vs. computed
watchcomputed
是否可异步可以执行异步操作
监视数据变化后,做一些特定工作, 比如:更新其它数据/保存数据/发请求提交数据
只能同步返回结果,不能异步返回计算结果

分别适用场景

  • method: 事件的回调或者我们封装一些功能函数(更新数据/提示界面/发请求, 一般不用来返回一个要显示的数据)
  • computed: 当要显示的数据是由已有n个数据来共同确定(需要有计算过程)的
  • watch: 用于当某个数据发生改变时就需要做一些相应工作(比如: 更新其它数据/发请求/保存数据)

watch-侦听器

被监听的属性可以是属性(data)也可以是计算属性(computed)

使用

⚠监听属性首先需要保证有该属性(data)或计算属性(computed)

  1. watch配置写法

    watch: {
      firstName: {
        handler (newVal, oldVal) {
          console.log('watch2 firstName', newVal, oldVal)
          // 更新fullName4
          setTimeout(() => {
            this.fullName4 = newVal + '-' + this.lastName
          }, 1000);
        }
      }
    }
    
  2. $watch方法

    vm.$watch('lastName', function (value) {
      this.fullName4 = this.firstName + '-' + value
    })
    
  3. watch配置简写

    watch: {
      firstName(newVal, oldVal){
        console.log(oldVal)
      }
    }
    
监听配置
说明
立即监听 immediate: true初始化时就执行一次监听
深度监听 deep: truewatch默认不监测对象内部值的改变(监视一层)
配置deep:true可以监测对象内部值改变(监视多层)

要使用监听配置,必须用一般写法1

{
	handler (value, oldValue) {} // 监视的回调
	deep: true, // 深度监视
	immediate: true, // 初始化执行一次,  后面数据时会再次执行
}

⚠**immediate & mounted**

如果某个功能既需要watch监听执行, 又需要初始化(mounted)时执行, 直接给watch设置immediate立即监听

监听对象属性

有两种写法:

  1. ["xx.xx"](){}
  2. "xx.xx"(){}
watch:{"person.name"(){}}
data(){
    return{
        person:{
            name:"asa",
            age:22
        }
    }
}
监听多个属性

watch本身不能同时监听多个属性,只能利用computed实现

computed可以同时监听多个属性的变化,并重新计算值

  1. 让computed返回一个对象,对象包含所有监听的属性,一旦监听的属性发生改变,则computed就会重新计算得到新的属性值
  2. watch深度监听第1步的computed,只要computed内部有任何变化,watch都可以监听到
computed:{
    nameAge(){
        return{
            name:this.name,
            age:this.age
        }
    }
}
watch:{nameAge(){}}

Vue指令

vue指令:vue中自定义的一些标签属性,都以v-开头

  • 作用:对所在标签进行特定操作

    • 绑定动态属性值
    • 绑定事件监听
    • 显示/隐藏
    • 复制产生多个

v-bind 强制绑定:

基本使用

v-bind可以给给任意标签属性,指定动态的属性值

  • 内置属性: value / src / class / style
  • 自定义属性: xxx
语法
v-bind:属性名="表达式"

//可简写为
:属性名="表达式"
实例
<body>
  <div id="test">
    <img :src="imgSrc" yyy="xxx" />
  </div>

  <script src="../js/vue.js"></script>
  <script>
    const vm = new Vue({
      el: "#test",
      data: {
        imgSrc: "https://v2.cn.vuejs.org/images/logo.svg",
        xxx: "123",
      },
    });
    setTimeout(() => {
      vm.imgSrc = "https://sponsors.vuejs.org/images/aircode.png";
    }, 1000);
  </script>
</body>
style和class动态绑定
style动态绑定
<p :style="{fontSize: '20px', 'backgroun-color': 'pink' color: val}">str</p>
  • v-bind:style 是一个包含样式属性的对象
  • 样式命名方式
    1. 驼峰式 (camelCase)
    2. 破折号方式 (kebab-case)
  • 样式值的单位不能省略

React的style写法

<p style={{fontSize: 20, 'background-color': 'pink'}}>style动态绑定</p>
class动态绑定

:class="xxx" ,xxx可以是字符串、对象、数组

hasA、hasB: boolean,
myClass: 'classA',
myClass2: ['classA', 'classB']
  1. 类名确定,且只有一个,但不确定有没有 => 三元表达式写法

    <p :class="hasA ? 'classA' : ''">情况1</p>
    

    类名确定,且有多个,但不确定有没有 => 对象写法

    <p :class="{classA: hasA, classB: hasB}">情况2</p>
    
    **类名只有一个,类名不确定时 => 字符串写法**```js
    <p :class="myClass">情况3</p>
    
    **类名有多个,类名不确定时 => 数组写法**
    ​```js
    <p :class="myClass2">情况4</p>
    
    **有固定/静态类名,也有动态类名  => 可分开写,会自动合并它们**```js
    <p class="classC" :class="myClass">aaaa</p>
    

React的class写法

  • 在react中,静态类名和动态类名不能分开,需写在一起
<p class={hasA ? 'classA' : ''}>情况1</p>
<p class={hasA ? 'classC classA' : 'classC'}>情况1</p>
批量绑定多个

v-bind="包含多个属性的对象"

<div id="test">
  <p v-bind="obj">aaaaaaa</p>
</div>

<script src="../js/vue.js"></script>
<script>
  const vm = new Vue({
    el: '#test',
    data: {
      obj: {
        a: 1,
        b: 2
      }
    }
  })
</script>

v-model 数据双向绑定

  • 数据单向绑定(v-bind):data => 页面,数据只能从data流向页面
  • 数据双向绑定:data <=> 页面
    • 页面能读取data数据进行动态显示,一旦更新数据页面会自动更新
    • 当用户改变页面输入时,输入的数据会自动保存到data中
基本使用
<div id="test">
  <input type="text" v-model:value="msg" />
  <p>{{msg}}</p>
</div>
<script src="../js/vue.js"></script>
<script>
  const vm = new Vue({ el: "#test", data: { msg: 123 } });
</script>
v-model本质

监听用户的输入事件,从而更新数据

  • 动态value data => view的绑定
  • input事件监听 view => data的绑定

也就是双向数据绑定:data数据 和 模板页面

  • 页面能读取data数据进行动态显示,一旦更新数据页面会自动更新 data => view 绑定
  • 当用户改变页面输入时,输入的数据会自动保存到data中 view => data 绑定
收集表单数据

对于checkbox、radio、option,需要设置value值,选中后会自动将绑定数据更新为选中项的value值

radio
<span>性别: </span>
<input type="radio" id="female" value="女" v-model="gender">
<label for="female"></label>
<input type="radio" id="male" value="男" v-model="gender">
<label for="male"></label><br>
checkbox
<span>爱好: </span>
<input type="checkbox" id="basket" value="basket" v-model="hobbies">
<label for="basket">篮球</label>
<input type="checkbox" id="foot" value="foot" v-model="hobbies">
<label for="foot">足球</label>
select & option
<select v-model="city">
  <option value="">未选择</option>
  <option value="芬兰">芬兰</option>
</select><br>
重置表单

将value值设置为初始值即可

<input type="button" value="重置" @click="reset">
reset () {
  this.user = {
    username: '',
    gender: '女',
    hobbies: ['foot'],
    city: '',
  }
修饰符

v-model.xxx="事件函数"

说明
.lazy转为change事件进行同步,失焦时触发同步
(v-model默认为input事件,输入即触发同步)
.number将输入值转为数值类型
.trim过滤输入值的首尾空白字符
v-model父子组件绑定双向通信

v-on 事件绑定@

基本使用

一般写法:v-on:事件类型="事件函数"

简写:@:事件类型="事件函数"

  • 事件回调函数需要写在methods中
  • 事件函数中的this指向的是vue的实例对象vm
事件对象及传参
  1. 事件函数默认接收一个event对象

    <button @click="fn">默认传递event</button>
    
    fn(e) { ...code }
    
  2. 传递自定义参数

    <button @click="fn2('abc')">传递自定义参数</button>
    
    fn2(msg) {}
    
  3. 传递自定义参数和event对象

    <button @click="fn3('abc', $event)">传递自定义参数+event</button>
    
    fn3(msg, e) {}
    

原理

vue在解析模板会自动在事件函数外层包裹一个函数

($event) => { 事件函数 }
修饰符
事件修饰符

在@事件名后, 加.xxx来对事件监听进行进一步处理

@click.stop="fn"

说明
.stop停止事件传播
.self事件只有绑定事件的元素能触发,点击子元素不能触发
.prevent阻止事件默认行为
.once只执行一次
.capture默认绑定的事件都是在冒泡阶段执行,添加capture修饰符可以设置捕获阶段执行
按键修饰符

@按钮事件后, 加.xxx来事件监听进行进一步处理

@keyup.enter="fn"

说明
.enter监视enter键
.keyCode监视keyCode对应的按钮,不推荐使用(enter键: 13

条件渲染

v-if

条件性地渲染内容

这块内容只会在表达式为true时才渲染

v-if="表达式1"
v-else-if="表达式2"
v-else
  1. v-else-if 和 v-else 注意点

    • v-else-if必须用在紧跟在带有v-if的元素后面的元素上,中间不能有其他元素

    • v-else必须用在紧跟在带有v-if或v-else-if的元素后面的元素上,中间不能有其他元素

  2. 可在<template>上使用 v-if 条件渲染,最终渲染结果不包含 <template>

    <template v-if="表达式"></template>
    
v-show
v-show="表达式"

通过切换display样式来控制渲染内容的显示和隐藏

带有 v-show 的元素始终会被渲染并保留在 DOM 中

区分v-if & v-show
v-ifv-show
false隐藏删除标签修改display为none
true重新显示v-if重新创建修改diplay的样式(不需要创建标签)
更新显示效率相较慢(需要创建标签)更快
占用内存相较小相较大
利用空间换时间:隐藏时占用更大的空间,来换取重新显示时更快
适用场景1.界面切换频繁
2.DOM较大
在模板中初始读取空对象或空数组内部对象中的数据

{a:{}} -> a.b.c
(空对象或空数组内部对象为undefined,再向下读取即为读取undefined的数据,会报错)

v-for 列表渲染

v-for 和 v-if/v-show不能同时用在同一个元素上

问题

  • v-for的优先级大于v-if,在编译阶段先v-for生成多个虚拟DOM,任何在v-if去掉多个虚拟DOM,造成性能浪费
  • v-for和v-if一起使用,可能结果预期不明确

解决方案

  • 遍历时如果部分DOM条件渲染,使用计算属性解决

    定义一个计算属性,返回过滤后的列表,再v-for遍历

  • 遍历时如果全部DOM同时一个条件渲染,把v-if写在外层容器上

    避免列表渲染和完全隐藏同步执行导致预期不明确

遍历数组

item:数组项

index:下标

v-for="item in 数组"
v-for="(item, index) in 数组"
遍历对象

item:键值

key:键名

v-for="item in 对象"
v-for="(item, key) in 对象"
遍历字符串
v-for="item in 字符串"
v-for="(item, index) in 字符串"
遍历数值

数值为多少,就遍历多少次

如10,即从item为从1到10,index为从0到9

v-for="item in 数值"
v-for="(item, index) in 数值"

其他指令

v-text & v-html
说明
v-text=“content”将content设置为标签的innerText
v-html=“content”将content设置为标签的innerHTML

插值语法相对于v-text更灵活:可以是标签体部分文本

v-once & v-pre
说明
v-once初始显示后,不再更新,用于不变的数据显示 => 优化内部解析的性能
(v-once显示后不会再更新,即使引用了动态值,动态值更新,也不会变)
v-pre不解析指令/插值语法,直接按结构HTML显示
v-cloak

v-cloak存在于元素上直到关联实例结束编译

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

<div v-cloak>...</div>

<style>
  div [v-cloak]{
    display: 
  }
</style>
  • 在简单项目中,使用v-cloak来解决屏幕闪动;
  • 但在大型、工程化项目(webpack、vue-router)中不用:只有一个空的 div 元素,元素中的内容是通过路由挂载来实现的,这时我们就不需要用到 v-cloak 指令。

Vue响应式原理解析

核心方法

Object.create

ECMAScript5新增的一个静态方法,用来定义一个实例对象

该方法可以指定对象的原型和对象特性,使用现有对象来提供新创建对象的__proto__

语法
Object.create({ 对象原型 }, { 对象配置项... })

//对象配置项详写
{键名: {
    value:键值/(get&set),
    configurable: true, 
    //键值是否可以被配置,默认是false,不可被配置(删除操作);
    enumerable: true, 
    //是否可枚举,是否可以被循环,默认值是false
}, 对象配置项2}
  1. 对象原型-必选——指定原型对象,可以为null

  2. 对象配置项-可选参数

    包含一个或多个属性描述符的 JavaScript对象:

    • value:指定属性值
    • writable:默认为 false,设置属性值是否可写
    • enumerable:默认为 false,设置属性是否可枚举( for/in)
    • configurable:默认为false,设置是否可修改属性特性和删除属性
    • get:作为该属性的 getter 函数,如果没有 getter 则为undefined。函数返回值将被用作属性的值。
    • set:作为属性的 setter 函数,如果没有 setter 则为undefined。函数将仅接受参数赋值给该属性的新值。
具体使用
  1. 创建一个对象的原型

    var obj = Object.create({ myname: "张三" });
    console.log(obj);
    
    //两种创建方法结果相同
    var obj = {};
    obj.__proto__ = {myname:"张三"};
    console.log(obj);
    
  2. 创建一个可控对象

    相当于{myname: '张三', age: 20}

    var obj = Object.create(
      {},
      {
        myname: {
          // 配置项
          value: "张三", // 键值 ;
          configurable: true, 
          //键值是否可以被配置,默认是false,不可被配置(删除操作);
          enumerable: true, 
          //是否可枚举,是否可以被循环,默认值是false
        },
        age: { value: 20 },
      }
    );
    console.log(obj);
    delete obj.myname
    for(var key in obj){
        console.log(key);
    }
    
  3. value值属性可以被拆分成get和set

    value 与 get&set两者不可同时存在,一个对象里只能有其中一个

    get:当访问、获取属性时触发

    set:当设置属性时触发

    get&set均为异步执行函数

    var str = "张三";
    var obj = Object.create(
      {},
      {
        myname: {
          // value:"张三", // 不要和get set 一起写 ;
          get: function () {
            // 当获取myname属性的时候触发;
            // 访问 myname属性的时候就是get操作
            console.log("get函数被触发了");
            return str;
          },
          set: function (newvalue) {
            // 当设置myname的时候会触发set函数;
            console.log("设置了属性:", newvalue);
            str = newvalue;
          },
        },
      }
    );
    console.log(obj);
    console.log(obj.myname);//get操作,获取操作;
    obj.myname = "修改了"; 
    //重新赋值就是一个set 操作,会触发set函数;
    console.log(obj);
    
Object.defineProperty

给对象添加属性,或者修改现有属性,并返回此对象

  • 如果指定的属性名在对象中不存在,则执行添加操作

  • 如果在对象中存在同名属性,则执行修改操作

  • Object.defineProperty()是vue 2.0 实现响应式的核心函数

语法
Object.defineProperty(obj, prop, { 对象配置项... })
  • obj:要定义属性的对象
  • prop:要定义或修改的属性键名
  • 对象配置项:要定义或修改的属性的描述符,具体与Object.create的相同
具体使用
  1. 新创建一个可控对象

    var obj = Object.defineProperty({},"myname",{
        get:function(){
            console.log("获取了get触发");
            return "张三";
        },
        set:function(newvalue){
            console.log("新值是",newvalue)
        },
        configurable:true,
        enumerable:true
    })
    
  2. 修改一个单属性对象为可控对象

    var obj = {
      myname: "张三",
    };
    //修改对象为可控对象
    var value = obj.myname;
    Object.defineProperty(obj, "myname", {
      get: function () {
        return value;
        console.log("get");
      },
      set: function (newvalue) {
        value = newvalue;
      },
      configurable: true,
      enumerable: true,
    });
    
  3. 修改一个多属性多层级对象为可控对象

    • 由于get&set是异步执行,在for循环修改对象属性时,key会指向最后一个

    • 若对象属性也为对象,需递归修改属性

    var obj = {
      myname: "张三",
      age: 20,
      obj1: {
        hobby: "篮球",
      },
    };
    
    function observe(obj){
      var keys = Object.keys(obj);//将对象的所有key值提取放在一个数组里
      keys.forEach(function (key) {
        var value = obj[key];
        if (typeof value === "object") {
          observe(obj[key]);
          //深层次递归修改对象属性
        }
        Object.defineProperty(obj, key, {
          configurable: true,
          enumerable: true,
          get: function () {
            return value;
          },
          set: function (newvalue) {
            value = newvalue;
          },
        });
      });
    }
    

ES6对象新语法

  1. Object.keys(obj)

    将对象的所有key值提取放在一个数组里,返回数组

  2. Object.values(obj)

    将对象的所有键值提取放在一个数组里,返回数组

  1. 可控对象实现修改数据时同步渲染页面

    function observe(obj) {
      var keys = Object.keys(obj);
      keys.forEach(function (key) {
        var value = obj[key];
        if (typeof value === "object") {
          observe(obj[key]);
        }
        Object.defineProperty(obj, key, {
          configurable: true,
          enumerable: true,
          get: function () {
            return value;
          },
          set: function (newvalue) {
            if (value !== newvalue) value = newvalue;
            var appEle = document.querySelector("#app");
            appEle.innerHTML = newvalue;
            //修改触发set后,再次进行渲染
          },
        });
      });
    }
    var obj = {
      myname: "张三",
      age: 20,
      obj1: {
        hobby: "篮球",
      },
    };
    // 初次渲染
    var appEle = document.querySelector("#app");
    appEle.innerHTML = obj["myname"];
    

数据代理

  1. 模板template中读取数据的来源——vm对象自身或原型链上的属性或方法
  2. Vue中的数据代理:通过vm对象来代理对_data中属性的操作
    1. 读取vm的属性,读取的是data对象中的属性
    2. 向vm保存属性,数据保存在data对象中
  3. Vue中数据代理的好处:模板中可以更加方便的操作_data中的数据
实现原理

遍历data对象中所有的属性,通过Object.defineProperty给vm添加与data对象中对应的同名属性,由vm来代理内部data对象的属性操作(读/写)

  • get方法:读取data对象中的同名属性返回
  • set方法:将设置的最新值保存到data对象的同名属性上
实现数据代理
  1. 遍历data对象中的所有属性, 给vm添加相应的同名属性
  2. 利用defineProperty方法给vm添加属性
  3. get:读取data对象中对应的属性值返回
  4. set:将最新的属性值value保存到data对象的同名属性上
for (let key of Object.keys(vm._data)) {
  Object.defineProperty(vm, key, {
    get() {
      return vm._data[key];
    },
    set(value) {
      vm._data[key] = value;
    },
  });
}

数据劫持

为了捕获到数据的改变,进而重新解析模板

  • _data身上的每一个属性不直接给值,而是都有对应的:reactiveSetter、reactiveGetter;
  • 当修改_data上的属性时,该属性对应的reactiveSetter就会调用; 且在reactiveSetter中Vue会:更新数据、重新解析模板;
  • 当读取_data上的属性时,该属性对应的reactiveGetter就会调用,返回对应的值;
实现原理

Object.defineProperty

  1. 遍历_data中的数据,得到属性和值
  2. 重写_data上的属性,并且书写为存取器数据
  3. 当取的时候直接返回当前属性的值,并收集当前的模板信息
  4. 当设置的时候修改当前属性的值,并通知所有的模板进行重新获取最新的值
实现数据劫持
const data = {
  name: "laowang",
  age: 18,
};
//变成响应式数据
observer(data);

function observer(target) {
  if (typeof target !== "object" || target === null) {
    return target;
  }

  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(target, key, value) {
  //深层次监听
  observer(value);
  Object.defineProperty(target, key, {
    get() {
      return value;
    },
    set(newValue) {
      //可能新改的值是一个对象
      observer(value);
      if (newValue !== value) {
        value = newValue;
        console.log("更新视图");
      }
    },
  });
}

Vue数据响应式原理

响应性是 Vue的一个核心特性,用于监听视图中绑定的数据,当数据发生改变时视图自动更新。

只要状态发生改变,系统依赖部分发生自动更新就可以称为响应性。

Vue构造函数的创建
  1. 创建Vue构造函数,并实现数据代理

    function Vue(options) {
      //将options中的内容 添加在vm实例的_data属性上
      this._data = options.data;
      //开始进行数据代理
      for (let key of Object.keys(this._data)) {
        Object.defineProperty(this, key, {
          get() {
            return this._data[key];
          },
          set(value) {
            this._data[key] = value;
          },
        });
      }
    }
    
  2. 实例化Vue得到vm,并传入数据

    onst vm = new Vue({
      data: {
        count: 123,
        user: {
          name: "xiaowang",
          age: 18,
        },
        course: ["html", "js", "css"],
      },
    });
    
数据劫持

在Vue构造函数中,把vm身上的_data属性变成响应式的,我们可以封装一个oberve函数去操作

//1.创建一个Vue构造函数
function Vue(options) {
  ....
  //处理_data数据 把它变成响应式的
  observe(this._data);
}
1.observe函数

observe函数首先对_data中的数据进行响应式

  • observe函数判断数据是否为对象
  • 如果不是对象则直接返回
  • 反之进行响应式处理

observe函数主要是先判断,再响应式处理,因此把整个响应式处理提炼为一个Observe类更加方便

function observe(value) {
  //如果dataObj不是一个对象,则直接返回
  if (typeof value !== "object" || value === null) {
    return value;
  }

  //把响应式处理逻辑 定义为一个类,然后对每一个需要响应式处理的数据操作
  new Observer(value);
}
2.Observer类
  • 由于进入Observer的对象类型可能是数组也可能是对象,需分类处理
  • 如果是对象,则直接开始响应式操作
  • 如果是数组,则先遍历数组的值,然后再次调用observe进行响应式处理
  • 数据响应式的核心逻辑——使用defineReactive函数进行封装
class Observer {
  constructor(obj) {
    this.value = obj;
    //判断当前的数据是数组还是对象
    if (Array.isArray(obj)) {
      //如果是数组,则调用observeArray方法,重新对数组操作
      this.observeArray(obj);
    } else {
      //如果是对象,则直接进行响应式操作
      this.walk();
    }
  }
    
  //数组的响应式操作
  observeArray() {
    for (var i = 0; i < this.value.length; i++) {
      observe(this.value[i]);
    }
  }

  //对象的响应式操作
  walk() {
    //对当前内部的数据开始响应式
    Object.keys(this.value).forEach((key) => {
      //数据响应式的核心逻辑
      defineReactive(this.value, key);
    });
  }
}
3.defineReactive核心逻辑
//函数中拿到当前需要响应式的属性名,及它所在的对象
function defineReactive(obj, key) {
  //根据属性名和对象,得到当前的属性值
  let value = obj[key];

  //对属性值再次进行observe操作(观察)
  observe(value);
  console.log(obj, key, "defineReactive");
    
  Object.defineProperty(obj, key, {
    get() {
      console.log("正在访问", key);
      return value;
    },
    set(newValue) {
      console.log("正在设置key", key, newValue);
      value = newValue;
      //对新的值继续进行observe
      observe(value);
    },
  });
}
依赖收集(订阅发布模型)

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的

需要再次执行依赖收集,在解析模板代码时,遇到 {{name}} 就会进行依赖收集

实现一个 Dep 类,用于依赖收集和派发更新操作

依赖收集类
class Dep {
  constructor() {
    //定义一个数组用来保存所有的订阅者(Watcher)
    this.subs = [];
  }

  //收集订阅者方法
  depend(watcher) {
    this.subs.push(watcher);
  }

  //通知订阅者方法
  notify() {  
  }
}
收集依赖
  • 首先在defineReactive中实例化一个依赖收集类
  • 在getter函数中,一旦有watcher(观察者)读取数据,则把这个watcher收集到依赖里
  • 在setter函数中,一旦设置,则调用notify,通知所有观察者
function defineReactive(obj, key) {
  ......

  //实例化一个订阅发布模型
  let dep = new Dep();

  .....
  Object.defineProperty(obj, key, {
    get() {
      .....
      //有一个观察者在访问这个数据,所以应该将这个观察者存入订阅发布中心
      dep.depend(xxxxx);
      .....
    },
    set(newValue) {
      ......
      //找到订阅发布器 通知观察者
      dep.notify();
    },
  });
}
观察者逻辑

只要在任意位置使用模板语法,就会实例化一个观察者,比如:

new Watcher(vm, "count");
观察者类
class Watcher {
  constructor(data, key) {
    this.value = data;
    this.key = key;
    //给Dep设置一个静态变量,其实就是为了全局共享这个Watcher实例,然后我们在defineReactive的getter函数中可以收集到这个watcher
    Dep.target = this;
    this.get();
    //一旦收集完成,就把这个target设置为null
    Dep.target = null;
  }

  //get方法主要是为了获取值,获取值的时候就会defineReactive的getter函数
  get() {
    return this.value[this.key];
  }

  //update方法,主要是当defineReactive的setter函数调用的时候,得到最新的值,然后更新视图
  update() {
    console.log("监听" + this.key + "的wathcer被触发了,新的值是" + this.get());
  }
}
完成依赖收集

在defineReactive的getter中完成依赖收集

get() {
    if (Dep.target) {
        dep.depend(Dep.target);
    }
    return value;
},
通知订阅者更新

在Dep的notify通知订阅者方法中,开始通知订阅者执行update更新

notify() {
    this.subs.forEach((watcher) => {
        watcher.update();
    });
}

$nextTick

在vue中,数据更新是同步的,用户界面Dom更新是异步的

  • 数据改变:更新用户界面(数据类型改变也算改变,如“1”变为数字1
  • 数据不变:不会触发更新用户界面

数据更新,Dom会出现更新渲染,而渲染Dom需要时间

若此时操作Dom,会操作无效,需要等Dom渲染完后,再进行操作

$nextTick(()=>{})

$nextTick会等待用户界面渲染mounted完成后,再触发参数中的回调函数

实例
操作Dom

等待用户界面渲染mounted完成后,在操作Dom

const inputRef = ref();
const toEdit = (row: attrValueItemType) => {
  row.isEdit = true;
  nextTick(() => {
    inputRef.value.focus();
  });
};
父子组件

父组件在mounted中请求接收数据后传给子组件;子组件在mounted中进行渲染

而由于父组件的mounted在子组件的mounted之后运行,故子组件在渲染时数据为undefined

解决办法:使用watch观察需渲染数据的变化,并在$nextTick中再创建轮播图渲染

watch: {
// 等 carouselList 数据发生变化时触发
carouselList() {
 $nextTick(()=>{
     new Swiper(".swiper", { });
 })
},
},

Vue生命周期

在这里插入图片描述

render配置

Vue内部解析tempalte模块自动生成render函数

该函数执行返回虚拟dom,后期调用该函数来更新(初始化执行一次 + 每次更新都执行)

render(createElement: () => VNode)    返回VNode
  • 接收的参数

    内部定义好的用来创建虚拟DOM/Node的函数

  • 返回值:虚拟DOM/Node

    用来生成真实DOM显示界面 / 与旧的虚拟DOM进行diff比较

    实现最小化DOM更新

  • 调用时机

    1. 初始化:调用render, 得到虚拟DOM => 生成真实DOM,替换el元素显示
    2. 更新数据:调用render, 得到新的虚拟DOM => 与旧的虚拟DOM进行diff比较, 实现最小化DOM更新

1.init 初始化

  1. 做一些关于事件和生命周期的初始化准备工作 (不重要)

  2. 执行 beforeCreate()

    • 不能访问data中的属性
    • 不能调用methods中的方法
  3. 初始化 data/methods/computed/watch /props配置

    • 实现data的数据代理
    • 实现data的数据劫持
    • 将methods的方法挂载到vm上,并指定this为vm
  4. 执行 created()

    • 能访问data中的属性
    • 能调用methods中的方法

    (⚠最早可以访问data、methods的钩子)

2.mount 挂载

  1. [编译模板]( 如果有render函数, 不需要此过程

    将template字符串编译生成render函数,此时render还不执行

  2. 执行 beforeMount()

  3. 调用render函数生成虚拟DOM => 生成真实DOM,并替换页面中的el元素显示 (使用patch函数)

  4. 执行 mounted()

    最早能获取DOM元素的钩子

3.update 更新

update阶段多次执行

  1. 当data发生更改后,执行 beforeUpdate()

    • 此时读取界面,是旧界面
    • 读取数据,是新数据
  2. 调用 render函数产生新虚拟DOM => 执行patch函数(进行新旧虚拟DOM Diff,进行最小化DOM更新)

  3. 执行 updated()

    此时读取界面,是新界面

4.destroy 销毁

  1. 执行 beforeDestroy()
  2. 将vm内部的数据代理/数据劫持/重新render/DOM Diff都干掉
  3. 执行 destroyed()
    1. 此时vm已销毁,不再具有管理页面的能力

生命周期触发顺序

父组件需等待子组件挂载好后才能挂载好

  1. 父组件beforeCreate
  2. 父组件created
  3. 父组件beforeMount
  4. 子组件beforeCreate
  5. 子组件created
  6. 子组件beforeMount
  7. 子组件mounted
  8. 父组件mounted

常用钩子

  • created/mounted——【初始化异步操作】
    1. 发送ajax请求
    2. 启动定时器
    3. 绑定自定义事件
    4. 订阅消息等
  • beforeDestroy——【收尾工作】
    1. 清除定时器
    2. 解绑自定义事件
    3. 取消订阅消息等

常见问法

  • 哪个钩子中不能访问data中的数据、methods中的方法? —— beforeCreate
  • 想给vm上追加一些属性,最早可以在哪个钩子中操作? —— beforeCreate
  • data中的数据、methods中的方法,最早可以在哪个钩子中获取? —— created
  • 哪个钩子中数据和页面其实是不同步的?—— beforeUpdate、created、beforeMount
  • 最早可以获取DOM元素的钩子——mounted

Vue-Cli脚手架

在线文档: https://cli.vuejs.org/zh/

使用vue脚手架创建项目

下载vue-cli
npm install -g @vue/cli
创建vue项目
vue create vue-demo
运行项目
npm run serve

不同版本的Vue

  1. vue.esm.js——带编译器的版本
    • 完整版的Vue,包含:核心功能 + 模板解析器
    • 可进行运行时的模板编译,即main.js文件中可以写template
    • 缺点:项目的打包文件变大, 一般不用
  2. vue.runtime.esm.js——不带编译器的版本
    • 不能编译main.js中的template,只能通过提供render来渲染App组件
    • 优点:打包文件较小, 一般都用此版本

vue单文件组件中的模板是在运行前,由webpack的vue-loader提供的模板编译器来解析的template

scoped(作用域)样式

  • 不加scoped的style中的样式是全局样式,可以应用到当前组件外的组件
  • 加scoped之后,只能影响当前组件的样式

关闭语法检查

  1. 忽略后边一行的检查

    /* eslint-disable-next-line */ 
    
  2. 忽略之后的检查

    /* eslint-disable */ 
    
  3. 关闭某个规则的检查

    package.json 或者.eslintrc.js: 全局规则配置文件修改eslint配置'rules': {'no-new': 'off'}

  4. 关闭所有检查

    vue.confog.js中配置 关闭所有eslint检查 lintOnSave: false

Vue组件化编程

前言&理解

Vue与VueComponent
VueComponent

组件的本质是一个构造函数,名为VueComponent

  • VueComponent不是程序员定义的,是Vue.extend()生成的
  • Vue.extend()每次生成的都是一个全新的VueComponent构造函数
  • 每次使用组件时<Student></Student>或<Student/> Vue就会帮我们执行: new VueComponent(options)
  • 关于this
    • 组件配置中
      • data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
    • new Vue(options)配置中
      • data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】
  • VueComponent的实例对象,可称之为:组件实例对象
  • 组件实例对象就是一个小型的vm(组件实例对象也有:数据劫持、数据代理、生命周期…)
Vue与VueComponent的关系

在这里插入图片描述

VueComponent.prototype.proto === Vue.prototype

  • 关系:组件对象的原型对象的原型对象是Vue的原型对象

  • 原理:VueComponent.prototype = Object.create(Vue.prototype);

  • 目的:组件实例对象(vc)可以访问到 Vue原型上的属性、方法

vue模板

输入vue2vue3可快捷获取vue模板

  1. 打开设置->用户代码片段

在这里插入图片描述

  1. 搜索打开vue.json并配置

    {
      "Printtovue": {
        "prefix": "vue2",
        "body": [
          "<template>",
          "  <div></div>",
          "</template>",
          "",
          "<script>",
          "export default {",
          "  name: '',",
          "}",
          "</script>",
          "",
          "<style scoped>",
          "",
          "</style>",
          ""
        ],
        "description": "快速创建vue单文件组件"
      },
    
      "Printtovue3": {
        "prefix": "vue3",
        "body": [
          "<template>",
          "  <div></div>",
          "</template>",
          "",
          "<script lang=\"ts\">",
          "export default {",
          "  name: '',",
          "}",
          "</script>",
          "",
          "<script lang=\"ts\" setup>",
    
          "</script>",
          "",
          "<style scoped>",
          "",
          "</style>",
          ""
        ],
        "description": "快速创建vue单文件组件"
      }
    }
    

基本使用

Vue组件分类

  • 非单文件组件:一个文件中可以定义n个组件,文件后缀可以是:.html
  • 单文件组件:一个文件中只定义一个组件,文件后缀是:.vue
创建组件
基本使用
data❗

组件的data配置项必须是一个返回对象的函数

  • 如果data配置是对象 => 这个组件多个实例共享同一个data对象,一旦一个组件对象的数据更新,多个界面都会更新
  • 如果data配置是函数 => 这个组件的多个实例都有自己data对象,各自更新各自的

vue的官方回答:

  • 当一个组件被定义,data必须声明为返回一个初始数据对象的函数
  • 因为组件可能被用来创建多个实例,如果data仍是一个纯粹的对象,则所有的实例将共享引用同一个数据对象
  • 通过提供data函数,每次创建一个新实例后,能够调用data函数,从而返回初始数据的一个全新副本数据对象
组件名
  1. 一个单词组成——School
    1. 命名(首字母大写):School
    2. 使用:<School> 或 <school>
  2. 多个单词组成——MySchool
    1. 命名(CamelCase命名):MySchool
    2. 使用:<MySchool> 或 <my-school>
注册组件
  1. 局部注册

    只能在对应的模板中进行使用

    components: {组件标签名: 组件}
    
  2. 全局注册

    任意模板中都能使用

    Vue.component('组件标签名', 组件)
    
1.非单文件创建组件
定义组件
  1. Vue.extends
const School = Vue.extend({
  template:`
        <div>
        <h2>学校名称:{{name}}</h2> 
        </div>`,
  data(){
    return {
      name:'尚硅谷',
    }
  }
})
  1. Vue.extends简写
const App = {
  name:'App',
  components:{School},
  template:`
        <div> 
            <School/>
        </div>
    `,
}
2.单文件创建组件(工程化方式)

组件单文件组成

  1. template
  2. script
  3. style
main.js-入口文件
/* 
    1.该文件是脚手架的入口文件
    2.一般我们在该文件中:去创建vm,并接管容器
*/

//引入Vue
import Vue from 'vue'
import App from './App'

new Vue({
  el:'#demo',//指定当前的vm接管哪个容器
  components:{App}, //注册App组件
  template:'<App/>'
})
组件.vue
<template>
  <!-- 组件的结构 -->
  <div class="school">
    <h2>学校名称:{{name}}</h2>
  </div>
</template>

<script>
  //此处编写:组件的配置对象并暴露出去
  export default {
    name:'School',
    data() {
      return {
        name:'尚硅谷',
      }
    },
  }
</script>

<style scoped>
  /* 组件的样式 */
</style>
App.vue-根组件
<template>
  <div class="app">
    <School/>
  </div>
</template>

<script>
  //引入School组件
  import School from './components/School'
  export default {
    name:'App',
    components:{School},
  }
</script>

<style>
  /* 组件的样式 */
</style>
ref属性

给元素或子组件注册标识,便于获取组件中的标签对象或子组件对象(id的替代者)

  • 应用在html标签上获取的是真实DOM元素
  • 应用在组件标签上是组件实例对象
使用
  1. 给html标签打标识

    <h1 ref="xxx">.....</h1>
    
  2. 给标签组件打标识

    <School ref="xxx"></School>
    
获取

获取标签对象或组件对象

this.$refs.xxx

操作dom的方式

  • ref(获取模板

  • 自定义指令(操作模板

    ui组件库不太建议用,不好获取dom

⚠操作DOM必须等待页面渲染mounted完之后再进行

组件通信❗

组件间通信

重要通信方案
props父 -> 子
自定义事件子 -> 父
v-model父 <-> 子
v-bind:xxx.sync父 <-> 子
插槽 v-slot父 <-> 子 (通信的是标签数据)
vuex / pinia兄弟或祖孙
次要通信方案
全局事件总线兄弟或祖孙
provide / inject祖孙
$attrs / $listeners父 <-> 子
$children / $refs / $parent父 <-> 子
$root共享数据,将数据放置在vm上
通过$root获取
props
  • 父向子: 非函数属性
  • 子向父: 函数属性
  • 祖孙之间: 需要进行多次逐层传递
使用

传递的分别为字符串、数值、data数据

<MyComponent name='tom' :setName='setName' :set-age="1"></MyComponent>
声明接收

接收的所有属性都会成为组件对象的属性

prop名格式为kebab-case,接收时可以使用小驼峰接收 set-age => setAge

  1. 数组 [属性名]

    props: ['name', 'setName']
    
  2. 对象: {属性名: 属性值类型}

    props: { 
      name: String,
      setName: Function
    }
    
  3. 对象: {属性名: {type: 属性值类型, required: true}}

    props: {
      name: {
        type: String, 
        required: true, 
        default:xxx
      }
    }
    

⚠props对象写法注意点:

  • 无required,最好有default值

  • 使用default

    • 基本数据类型直接写值 default: xxx

    • 引用数据类型需使用函数返回值 default: () => []

      原理同组件data使用函数返回对象一样

props只读⚠
  • 基本数据类型的props一旦修改,直接就会报错!

  • 对于对象类型的props:

    • 若修改的是整个对象(地址值变化),会报错;
    • 若修改的是对象中的属性,则不会报错(有时候程序员会利用这个小bug)
自定义事件emit

实现子向父通信

基本使用
  1. 父组件——绑定自定义事件

    给子组件标签绑定自定义事件, 指定事件回调函数, 用于接收数据并处理

    1. 常用方式

      <Child @事件名="回调函数fn">
      
    2. 其他方式

      this.$refs.footer.$on('事件名', 回调函数fn)
      $on前面为指定要绑定的组件
      

    ⚠**$event的理解**

    • 简单理解:代表event对象

    • 真正理解:代表事件回调函数的第一个参数

      • 原生DOM事件:事件回调函数参数event,所以$event就是event
      • Vue自定义事件:事件回调函数参数要看触发事件所传入的参数,$event就是传入的第一个参数
      //触发名为setCurrentImgIndex的事件时,会执行右侧的表达式
      //$event表示事件对象,这里将事件对象的值赋给currentImgIndex,从而更新currentImgIndex的值
      @setCurrentImgIndex="currentImgIndex = $event"
      //上下两种写法一样
      @setCurrentImgIndex="handleSetCurrentImgIndex"
      handleSetCurrentImgIndex(newIndex) {
        this.currentImgIndex = newIndex;
      }
      

    ⚠**.native的使用**

    <child-comp @click.native="fn"/>

    • 原生标签可直接@绑定DOM原生事件
    • 而组件绑定的事件都被认为是自定义事件(即使名字和DOM原生事件一样),需要自己触发
    • 要被组件绑定DOM原生事件,需要使用.native修饰符
  2. 子组件——分发触发自定义事件(事件名, 数据)

    this.$emit(事件名, 数据)
    
事件处理方法
  1. 绑定事件

    $on(事件名, 回调函数)
    
  2. 分发触发事件

    $emit(事件名, 数据)
    
  3. 解绑事件

    $off() / $off(事件名) / $off(事件名, 回调函数)
    
  4. 绑定事件,触发一次后解绑

    $once(事件名, 回调函数)
    
v-model

表单项使用,每个组件只能用一次

v-model 原理:双向数据绑定

  1. 普通input、textarea元素:绑定value属性和input事件
  2. radio、checkbox元素:绑定checked属性和change事件
  3. select元素:绑定的value属性和change事件
  4. 给组件绑定,绑定value属性和input事件
    • value属性的值就是表达式的值
    • input事件用来修改值的,触发事件传入的第一个参数就是要改的值

本质:动态value属性与自定义input监听(接收子组件分发的数据更新父组件数据)

  1. 父组件

    <CustomInput v-model="name"/>
    <!-- 等价于 -->
    <CustomInput :value="name" @input="name=$event"/>
    
  2. 子组件

    <input type="text" :value="value" @input="$emit('input', $event.target.value)">
    props: ['value']
    
.sync

非表单项使用

在原本父向子的基础上增加子向父

.sync本质:通过事件监听来接收子组件分发过来的数据并更新父组件的数据

使用
  1. 父组件使用v-bind绑定数据向子组件传递prop

    v-bind:xxx.sync = "xxx2"
    
  2. 子组件接收prop数据

    props:["xxx"]
    
  3. 子组件绑定自定义事件

    触发自定义事件,将父组件的数据更新为自定义事件传入的第一个参数

    @click="$emit('update:xxx', arg)"
    
实例
//父组件
<ImageList
  :skuImageList="goodsDetail.skuImageList"
  :currentImgIndex.sync="currentImgIndex"
/>
//子组件
<img
  :class="{
    active: currentImgIndex === index,
  }"
  :src="img.imgUrl"
  @mouseenter="$emit('update:currentImgIndex', index)"
/>

:currentImgIndex.sync="currentImgIndex"

  • 绑定props数据:currentImgIndex

  • 绑定自定义事件:update:currentImgIndex

    当触发自定义事件传入的第一个参数,就会将currentImgIndex数据更新

插槽slot

当需要传入子组件的内容不是单纯的数据,而是标签结构 => 使用插槽

slot可以实现组件的动态内容

  • 插槽的默认内容:当没有向插槽传入内容时, 显示默认内容
  • 插槽的分类
    • 默认插槽: 默认name为default
    • 具名插槽: 指定自定义name
  • v-slot:xxx可以简写为#xxx
默认插槽

slot无name属性,默认名称为default

  1. 设置插槽

    <child-com>组件

    <template>
      <div>
        <slot></slot>
      </div>
    </template>
    
  2. 使用插槽

    只有一个slot时,可以不指定slot名字

    <template>
      <div>
        <child-com>写入slot的内容,可传入html</child-com>
      </div>
    </template>
    
具名插槽
  1. 设置插槽

    <child-com>组件

    <template>
      <div>
        <slot></slot>
        <slot name="content">
           <h4>插槽的默认内容</h4>
        </slot>
      </div>
    </template>
    
  2. 使用插槽

    • 有多个slot时,使用必须指定slot名字,不指定则视为使用默认插槽
    • 具名插槽不能省略template(v-slot 只能添加在 <template>
    <template>
      <div>
        <child-com>
          <h3>默认插槽的内容</h3>
          <hr/>
          <!-- template v-slot:content -->
          <template #content>
            <h3>content插槽的内容</h3>
          </template>
        </child-com>
      </div>
    </template>
    
作用域插槽

当父组件传入的插槽内容需要使用子组件的数据时使用

  1. 子组件传递插槽属性

    将要传递的数据使用v-bind绑定在<slot>上,这些attribute 被称为插槽 prop

    <slot :row="todo" :$index="index"></slot>
    
  2. 父组件接收

    父组件通过v-slot接收传过来的数据,可以解构

    <template v-slot:content="scope">{{scope.row}}</template>
    //解构数据使用
    <template v-slot:content="{row, $index}">{{row}}</template>
    
vuex

放置共享数据

全局事件总线
  • 类似于PubSub
  • 可以实现任意组件间通信
1.自定义事件实现

能用内部的尽量用内部自定义实现,避免引入第三方库

  1. main.js——确定事件总线

    new Vue({
      // 最早能访问vm对象的勾子
      beforeCreate () {
        // 将vm对象指定为事件总线对象, 并挂载到Vue原型对象上 => 让所有组件都直接可见
        Vue.prototype.$bus = this
      },
      render: h => h(App),
    }).$mount('#app')
    
  2. 监听绑定事件组件

    1. 绑定事件监听——mounted

      在mounted中绑定

      this.$bus.$on('事件名', 事件函数fn)
      fn (data){}
      
    2. 解绑事件——beforeCreate

      在beforeCreate中解绑

      this.$bus.$off('事件名')
      
  3. 分发触发事件组件

    this.$bus.$emit('事件名', 数据)
    
2.事件处理库 mitt

vue3推荐使用,vue3废弃了$on语法

https://www.npmjs.com/package/mitt

  1. 下载

    npm i mitt
    
  2. 引入

    import mitt from 'mitt'
    
  3. 在main.js主入口文件创建事件发射器并绑定到Vue原型对象

    Vue.prototype.$emitter = mitt()
    
  4. 事件操作

    1. 分发事件

      this.$emitter.emit(事件名, 数据)
      
    2. 绑定事件

      this.$emitter.on(事件名, fn) 
      fn (data){}
      
    3. 解绑事件

      this.$emitter.off(事件名)
      
3.消息订阅发布库 pubsub-js

https://www.npmjs.com/package/pubsub-js

  1. 下载

    npm i pubsub-js
    
  2. 引入

    import PubSub from 'pubsub-js'
    
  3. 在main.js主入口文件绑定到Vue原型对象

    Vue.prototype.$PubSub = PubSub
    
  4. 消息操作

    1. 发布消息

      this.$PubSub.publish(消息名, 数据)
      
    2. 订阅消息

      this.$PubSub.subscribe(消息名, (msgName, data) => {})
      
    3. 取消订阅

      this.$PubSub.unsubscribe(消息名)
      
$attrs / $listeners

祖孙通信

多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时

$attrs
  • 包含父作用域里除 class 和 style 除外的 props 属性集合

  • 实现当前组件的父组件向当前组件的子组件通信(祖孙间通信)

  • 通过 this.$attrs 获取父作用域中所有符合条件的属性集合

  • 通过 v-bind=“$attrs” 将父组件传入的n个属性数据传递给当前组件的子组件

$listeners
  • 包含父作用域里 .native 除外的监听事件集合

  • 实现当前组件的子组件向当前组件的父组件通信 (孙向祖通信)

  • 通过v-on=“$listeners” 将父组件绑定给当前组件的事件监听绑定给当前组件的子组件

$children / $refs / $parent

不推荐使用

$refs
  • 实现父组件向指定子组件通信
  • $refs是包含所有有ref属性的标签对象或组件对象的容器对象
  • 使用: 通过 this.$refs.child 得到子组件对象, 从而可以直接更新其数据或调用其方法更新数据
//父组件
<button @click="updateChild1">累加Child1的count</button>
<Child1 ref="child1"></Child1>

updateChild1() {
    console.log(this);
    this.$refs.child1.count += 1;
},
$children
  • 实现父组件向多个子组件通信
  • $children是所有直接子组件对象的数组(不包含孙子组件)
  • 使用: 通过this.$children 遍历子组件对象, 从而可以更新多个子组件的数据
$parent
  • 实现子组件向父组件通信
  • $parent是当前组件的父组件对象
  • 使用: 通过this.$parent 得到父组件对象, 从而可以更新父组件的数据
provide / inject

实现祖孙组件间直接通信

  1. 在祖组件中通过provide配置向后代组件提供数据

    provide() {
      return {
        count: this.count,
      };
    },
    
  2. 在后代组件中通过inject配置来声明接收数据

    inject: ["count"]
    

注意:

  • 不太建议在应用开发中使用, 一般用来封装vue插件
  • provide提供的数据本身不是响应式的 ==> 父组件更新了数据, 后代组件不会变化
  • provide提供的数据对象内部是响应式的 ==> 父组件更新了数据, 后代组件也会变化
provide / inject的响应式写法
  1. 方法1:传递的参数用一个方法返回

    provide: function() {
      return {
        newName: () => this.name
      }
    }
    
  2. 方法2:把需要传递的参数定义成一个对象

    provide: function() {
      return {
      // 传递一个对象
        obj: this.obj
      }
    }
    
$root
  • vue所有组件实例身上都有一个$root属性,它们指向整个项目的根组件,一般也就是App组件
  • 可以借助$root做事件总线类似的传值方式
//发送数据组件
this.$root.$emit("hello", "HelloWorld")
//接受数据组件
this.$root.$on("hello",name => {
    console.log("hello",name);
})

scoped(作用域)样式❗

scoped样式

scoped让样式只能影响当前组件的标签及子组件的根标签

**原理:**scoped的2个变化

  • 标签:给当前组件的所有标签和子组件的根标签添加一个data自定义属性

在这里插入图片描述

  • 样式:选择器的右侧添加一个data自定义属性的属性选择器

    • => 目标标签必须要有这个自定义属性 =>

    • 只有当前组件和子组件根标签才有这个对应的属性

在这里插入图片描述

样式穿透-深度作用域选择器❗

用于设置影响子组件(UI组件)的子标签的样式

>>> 选择器

原理
  • 将data自定义属性移动到 >>> 的位置
  • 将最右侧的属性选择器移到左侧, 对目标元素就没有自定义属性的要求 => 就可 以匹配子组件的子标签了
.title {
color:aqua;
}
.box >>> .title {
color:aqua;
}
>>> .title {
color:aqua;
}

在这里插入图片描述

Vue ajax

项目开发中一般使用axios

使用位置

  • 初始化请求: mounted 或 created
  • 用户操作界面元素(如: 点击按钮): 在事件回调中
  • 数据改变后请求: 监视的回调中

跨域代理配置

vue.config.js

devServer: {
  proxy: {  // 代理服务器的作用: 转发请求
    // 前缀路径, 请求的路径中必须包含此路径
    '/dev-api': {
      target: 'https://api.github.com', // 目标接口地址
      changeOrigin: true, // 让接口服务器端读取的请求源基础地址是接口服务器的地址
      pathRewrite: { // 去掉/dev-api
        '^/dev-api': ''
      }
    },
  }
}

mixin混入

用来复用多个组件间相同的js代码

  1. 将重复的代码(带配置)抽取到单独的模块中, 并暴露

    export const myMixin = {
      data () {
        return {
          m: 3
        }
      },
      methods: {
        fn () {
          console.log('fn')
        }
      }
    }
    
  2. 在组件中引入mixin对象, 并通过mixins来配置

    import {myMixin} from '@/mixins'
    mixins: [myMixin]
    //mixins的配置项为数组
    
  3. 使用this.代码名即可调用

    this.fn()
    
  4. 如果组件的代码与mixin的重名了,会优先使用自己的

自定义功能

自定义过滤器

对要显示的数据进行特定格式后再显示

  1. 过滤器也可以接收额外参数、多个过滤器也可以串联;
  2. 不修改原本的数据, 而是产生新的对应的数据;
  3. 过滤器中的this永远都是undefined
基本使用
注册过滤器
  1. 全局过滤器

    Vue.filter('过滤器名称', (value, 任意多个参数) => {
    	//value即需要过滤格式化的值
        //根据value和其它参数进行计算产一个新的值
    	return 新值
    })
    
  2. 局部过滤器

    { 
      name:'School',
      filters:{
      	fn(){ 局部过滤器处理函数 }
      }
    }
    
使用过滤器
{{表达式 | 过滤器1(任意多个数据) | 过滤器2(任意多个数据)}}
//表达式即指value,要过滤格式化的值
步骤
  1. 注册过滤器

  2. 引入过滤器——main.js

    import './filters'
    
  3. 使用过滤器

自定义指令

对普通 DOM 元素进行底层操作

基本使用
  1. 自定义指令

    1. 全局自定义指令 Vue.directive()

      // 注册一个全局自定义指令 `指令名`
      Vue.directive('指令名', (el, [binding]) => {
          根据binding中value去操作el元素
      })
      
    2. 局部自定义指令

      通过配置项的 directives属性配置自定义指令函数

    • 参数
      • el:当前指令作用的元素
      • binding:binding.value属性代表当前指令的值
  2. 使用组件

    <div v-指令名[="表达式"]>
    
    • 处理指令的函数默认调用的时机:初始化 & 更新
  • ⚠注意点
    • 指令定义时不加v-,但使用时必须要加v-;
    • 指令名如果是多个单词,要使用kebab-case命名方式;
    • 指令的回调函数中this是undefined;
自定义指令的钩子函数

自定义指令的第二个参数可配置所有钩子函数

Vue.directive('指令名',{
    bind(){},
    inserted(){},
    update(){},
    componentUpdate(){},
    unbind(){}
})
钩子函数
  • bind:初始化指令的时候执行
  • inserted:被绑定元素插入父节点时调用
    • 要删除元素,必须有父元素
    • 固在bind钩子函数中无法删除,再早只能在inserted中删除
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
  • componentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

自定义指令的简写:自定义指令的第二个参数不书写配置对象,而是直接写为一个函数,则这个函数默认是钩子函数中的bind函数(即基本使用的写法

插件

插件通常用来为 Vue 添加全局功能

在Vue语法的基本上,提供一些新的语法

插件的功能范围没有严格的限制——一般有下面几种:实例方法/静态方法/组件/指令/过滤器

  1. 添加全局方法或者 property。
  2. 添加全局资源:指令/过滤器/过渡等。
  3. 通过全局混入来添加一些组件选项。
  4. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。
自定义插件
对象插件

对象必须要有一个intall方法

const myPlugin = {
	//Vue.use会自动调用install
	install (Vue) {
		通过Vue来扩展语法
	}
}
函数插件

函数本身就是install方法

function myPlugin (Vue) {
	通过Vue来扩展语法
}
使用插件
  1. 引用并安装

    use会调用插件对象的install或插件函数

    Vue.use(引入的插件)
    
  2. 使用插件扩展的语法

实例
  1. main.js——在new Vue之前引入并安装插件

    // 引入对象插件
    import myPlugin from './myPlugin'
    // 安装插件
    Vue.use(myPlugin) 
    
  2. myPlugin/index.js

    import Msg from './Msg.vue'
    import dayjs from 'dayjs'
    
    const myPlugin = {
      // 当安装插件时自动调用, 在此方法扩展vue的语法
      install (Vue) {
        console.log('install')
        // 添加静态方法
        Vue.staticFn = function () {
          console.log('staticFn')
        }
        // 添加实例方法
        Vue.prototype.$fn = function () {
          console.log('$fn')
        }
    
        // 注册全局指令
        Vue.directive('upper-text', (el, binding) => {
          console.log('upper-text', binding)
          // 给元素指定文本内容(需要转成大写)
          el.innerText = binding.value.toUpperCase()
        })
        // 注册全局过滤器
        Vue.filter('DateFormat', function DateFormat(value, formatStr='YY-MM-DD HH:mm:ss') {
          console.log('DateFormat', value, formatStr)
          // return 'abc'
          // return moment(value).format('YYYY-MM-DD HH:mm:ss')
          return dayjs(value).format(formatStr)
        })
        
        // 注册全局的组件
        Vue.component('Msg', Msg)
      }
    }
    
    export default myPlugin
    
  3. App.vue

    <template>
      <div>
        <p>{{startTime|DateFormat}}</p>
        <p v-upper-text="msg"></p>
        <Msg/>
      </div>
    </template>
    
    <script>
    import Vue from 'vue'
    export default {
      name: 'App',
      data () {
        return {
          startTime: Date.now()-1000,
          msg: 'What You See!'
        }
      },
      mounted () {
        Vue.staticFn()
        this.$fn()
      },
    }
    </script>
    
    

Vuex

Vuex官网

  • github站点: https://github.com/vuejs/vuex
  • 在线文档: https://vuex.vuejs.org/zh-cn/

Vuex五大核心

  • state
  • getter
  • mutation
  • action
  • module 模块化

Vuex概述

作用:管理多个组件共享状态 / 实现任意组件间通信

在这里插入图片描述

在这里插入图片描述

组成部分

  • state:集中式管理的数据(多个组件共享的数据)
  • getters:只读计算属性数据(一定会依赖state)
  • mutations:如果更新数据需要进行异步操作,定义使用action
  • actions:如果更新数据直接更新即可,直接定义mutation
  • modules:模块
state

存储store的状态数据(多个组件共享的数据

定义
state: {
	xxx: value值,
}
使用
$store.state.xxx
getters

根据state数据计算产生数据的方法的对象,类似计算属性computed

定义
getters: {
	yyy (state) {
		return 根据state计算产生一个新值
	}
}
使用
$store.getters.yyy
mutations

直接更新state的方法(回调函数)

mutation调用完后,vuex devtool会直接同步更新state数据

定义
  • 参数1:state对象
  • 参数2(可选):接收的参数,形参(由action或组件提供)
mutations: {
	aaa (state, data) {  // data由action或组件提供 	}
}
使用
$store.commit("mutation名称", [传入的参数(可选)])

⚠vuex中的mutations可以写异步操作,但不建议,因为工具中的数据没有响应式变化,不会响应式更改

actions

间接同步/异步更新状态数据的方法(定时器, ajax)

定义
  • 参数1:context对象,一般直接解构出{commit, state}使用(commit用于触发执行mutation)

    • context就是一个小型的store对象:commit、dispatch、state、getters等内容(一般只需要用commit和state)
  • 参数2(可选):接收的参数,形参(由组件提供)

actions: {
	bbb ({commit, state}, data) { //data由组件提供
		// 异步操作/逻辑操作
		commit('mutation名称', data)
	}
}

mutation的命名

action调用的mutaion,名称和action一样,但大写

  • action:login

  • mutation:LOGIN

使用
$store.dispatch("action名称", [传入的参数(可选,只能传入一个参数)])

⚠dispatch的返回值为promise对象,promise的状态由所调用的action决定

基本使用

  1. 下载vuex

    npm i vuex@3
    
  2. 创建store

    //1.引入
    import Vue from 'vue'
    import Vuex from 'vuex'
    //2.安装vuex
    Vue.use(Vuex)
    //3.创建store
    const store = new Vuex.Store({
      state: {},
      getters: {},
      mutations: {},
      actions: {},
    })
    //4.暴露store
    export default store
    
  3. 主入口文件配置/挂载store

    挂载后,在组件中可通过this.$store得到store对象

    import store from './store'
    new Vue({
      render: h => h(App),
      store, // 注册store => 将store对象添加到Vue原型对象上的$store => 让所有组件通过$store得到store对象
    }).$mount('#app')
    
  4. 组件中读取或更新状态

    //读取
    this.$store.state.xxx   读state数据
    this.$store.getters.yyy  读取getters计算属性数据
    //更新
    this.$store.dispatch('action名称', 数据)   触发action调用
    this.$store.commit('mutaion名称', 数据)   触发mutation调用
    

Vuex模块化

按照功能将数据分为多个模块进行管理

  1. 定义vuex模块

    1. 将不同功能模块相关的state/getters/mutations/actions的配置单独定义一个vuex的子模块文件 store/modules下

    2. 开启vuex模块的namespaced(命名空间)

      开启命名空间,隔离不同模块的actions和mutations

      1. state、getters变为了多模块的结构(不开启时不是)

        $store.getters( '模块名/getter名称')
        
        $store.getters.模块名('getter名称')
        $store.state.模块名('state名称')
        
        $store.getters.模块名.getter名称
        $store.state.模块名.state名称
        
      2. 在组件中dispatch或commit时, 要指定模块的名称:

        $store.commit( '模块名/mutaion名称')
        $store.dispatch( '模块名/action名称')
        
      3. 在vuex子模块中

        • mutaion中的state不是总状态,是当前模块的state
        • actioin中执行commit时,不需要指定模块名
    export default {
      namespaced: true, // 开启命名空间 => 触发mutation或action调用时必须先指定模块名称
      state: {},
      getters: {},
      mutations: {},
      actions: {},
    }
    
  2. 整合注册vuex模块

    new Vuex.Store({
        ...,
        modules: {moduleA, moduleB}
    })
    

映射函数简化编码

频繁使用vuex中的某项,使用map映射更简洁

  1. 使用前需先引入
import {mapState} from 'vuex'

2. **mapState、mapGetters在computed中映射**

3. **mapMutations、mapActions在methods中映射**
mapState

使用:$store.state.模块名.count

  1. 对象写法

    ...mapState({  
    	count2: 'count'
    })
    // 返回值对象: {count2 () {return this.$store.state['count']}}
    
  2. 数组写法

    ...mapState(['count'])
    // 返回值对象: {'count' () {return this.$store.state['count']}}
    
  3. 模块化写法

    ...mapState('模块名', ['count'])
    
mapGetters
  1. 对象写法

    ...mapGetters({ 
      evenOrOdd2: 'evenOrOdd'
    })
    // 返回值对象: {evenOrOdd2 () {return this.$store.getters.evenOrOdd}}
    
  2. 数组写法

    ...mapGetters(['evenOrOdd'])
    // 返回值对象: {evenOrOdd(){returnthis.$store.getters.evenOrOdd}}
    
  3. 模块化写法

    ...mapGetters('模块名', ['evenOrOdd'])
    
mapMutations
  1. 对象写法

    ...mapMutations({
      handleIncrement2: 'increment',
      handleDecrement2: 'decrement',
    })
    
  2. 数组写法

    ...mapMutations(['increment', 'decrement'])
    
  3. 模块化写法

    ...mapMutations('模块名', ['increment', 'decrement'])
    
mapActions
  1. 对象写法

    ...mapActions({
      handleIncrementIfOdd2: 'incrementIfOdd',
      handleIncrementAsync2: 'incrementAsync'
    })
    
  2. 数组写法

    ...mapActions(['incrementIfOdd', 'incrementAsync'])
    
  3. 模块化写法

    ...mapActions('模块名', ['incrementIfOdd', 'incrementAsync'])
    

Vuex数据持久化

  • vuex的状态数据是保存内存中的,浏览器刷新或关闭后就释放了

  • 故保存在vuex实例store里的数据是会丢失的

  • 解决:将vuex的状态数据保存到 local中

  • 可使用vuex-persistedstate插件实现状态数据持久化

    1. 下载插件

      npm install vuex-persistedstate --save
      
    2. 使用

      const store = new Vuex.Store({
        ...,
        //1.保存所有模块的状态
        plugins: [createPersistedState()]
        //2.通过path指定只存储counter模块的state
        plugins: [
          createPersistedState({ 
            paths: ['counter']
          })
        ]
      })
      

Vue-Router

http://router.vuejs.org/zh-cn/

前言

vue-router
  • vue的一个插件库,
  • 用于路由页面管理和实现SPA
  • 基于vue的项目基本都会用到此库
路由
  • 一个路由就是一个映射(对应)关系(key:value)
  • key为路由路径path, value可能是function/component
  • 路由又分为后台路由和前台路由2种
后台路由

node服务器端路由, value是function

用来处理客户端提交的请求并返回一个响应数据

  • 注册路由:app.get(path, function(req, res))router.get(path, function(req, res))
  • 当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
前台路由

浏览器端路由,value是component

当请求的是路由path时, 浏览器端没有发送http请求, 但界面会更新显示对应的组件

当浏览器的path变为/about时, 当前路由组件就会变为About组件

路由器&路由
  • 路由: 一组key-value的对应关系route
  • 路由器: 管理多个路由router
    在这里插入图片描述

⚠注意区分vue中的router和route

  • router 包含路由导航的方法
  • route 包含参数数据

路由原生实现

读懂,会讲即可

代码不用记

hash路由原生实现

利用location对象的hash语法来实现的

  • 使用location.hash进行hash地址的变更
  • 使用onhashchange事件监听hash的变更,在回调中根据最新hash路径找到对应的路由组件显示
<script>
  // 路由表
  const routes = [
      { path: "/home", component: "<h2>欢迎来到首页</h2>" },
      { path: "/center", component: "<h2>我真的不是大冤种</h2>" },
      { path: "/hot", component: "<h2>武汉尚硅谷XXX拿到30k offer</h2>" },
  ];

  //给所有的button绑定点击事件
  for (var i = 0; i < oBtns.length; i++) {
      oBtns[i].onclick = function () {
          // 更新location对象的hash值
          location.hash = this.dataset.path;
      };
  }

  //js提供了一个hashchange事件,可以监听hash地址的改变
  //并且历史记录改变的时候,也是hash的改变,也会触发hashchange事件
  window.onhashchange = function (e) {
      //拿到当前的hash   location.hash #/home => /home
      const hashPath = location.hash.slice(1)
      //去路由表中找到对应的hash地址,加载内容
      routes.forEach((route) => {
          if (route.path === hashPath) {
              oView.innerHTML = route.component;
          }
      });
  };
</script>
history路由原生实现

利用history对象的pushState/replaceState语法和onpopstate事件 来实现的

  • 在点击切换路由时:
    • 调用history的pushState来添加一个新的路由路径
    • 根据这个路径查找对应的路由组件更新显示
  • 绑定popState事件, 根据当前路径查找对应的路由组件更新显示
  • 注意: popState事件只能监视浏览的前进和后退
<script>
  // 路由表
  const routes = [
    { path: '/home', component: '欢迎来到首页' },
    { path: '/center', component: '我真的不是大冤种' },
    { path: '/hot', component: '武汉尚硅谷XXX拿到30k offer' },
  ];

  //给所有的button绑定点击事件
  for (var i = 0; i < oBtns.length; i++) {
    oBtns[i].onclick = function () {
      // 调用history对象的pushState方法来添加一个state (路由路径),可以无刷新的改变地址
      history.pushState(null, null, this.dataset.path);

      // 得到手动遍历查找对应的路由组件去更新显示
      routes.forEach((route) => {
        if (route.path === this.dataset.path) {
          oView.innerHTML = route.component;
        }
      });
    };
  }

  // 监视浏览器的前进和回退的, 不能监视pushState导致的变化
  // 监听历史记录改变的事件 popstate
  window.onpopstate = function () {
    //当历史记录改变后,获取最新的地址 location.pathname
    console.log(location.pathname);
    //点击完按钮之后,直接遍历routes 改变content内容
    routes.forEach((route) => {
      if (route.path === location.pathname) {
        oView.innerHTML = route.component;
      }
    });
  };
</script>
hash模式&history模式区别❗
  • hash模式

    • hash模式把前端路由的路径用#拼接在真实URL后面
    • 当用户切换路由的时候使用location.hash的方式切换hash值
    • #后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发hashchange事件
    • hash模式的浏览器兼容性较好,但看起来不够优雅
  • history模式

    • history模式用到了HTML5中的history API,可直接更新浏览器URL地址而不用发请求,不带#
    • 用到了history API:replaceState、pushState、back、forward和go 5个方法。
    • 使用window的popState事件可以监听历史记录的改动,并加载一个组件
    • history兼容性不如hash模式,而且浏览器在刷新的时候会按照路径发送真实的资源请求,因此在线上部署基于historyAPI的单页面应用的时候,一定要后端配合支持才行
      • 后端:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面

基本使用

  1. 安装 vue-router

    npm i vue-router@3
    
  2. 定义路由组件

    和以前创建组件一样创建

  3. 创建路由器, 注册路由

    //引入Vue
    import Vue from "vue";
    //引入VueRouter
    import VueRouter from "vue-router";
    //引入组件
    import Login from "../components/Login";
    import Home from "../components/Home";
    //应用路由器
    Vue.use(VueRouter);
    
    //创建router实例对象,去管理一组一组的路由规则
    const router = new VueRouter({
      routes: [
        {
          path: "/login", //路径
          component: Login, //组件
        },
        {
          path: "/home",
          component: Home,
        },
      ],
    });
    
    //暴露router
    export default router;
    
  4. 创建 vm 时注册路由器配置

    import Vue from "vue";
    import App from "./App";
    //引入路由器
    import router from "./router";
    Vue.config.productionTip = false;
    
    new Vue({
      el: "#root",
      render: (h) => h(App),
      router, // 此处传入注册路由器
    });
    
  5. 通过<router-view>指定路由展示位置

    <template>
      <div>
        <!-- 指定在此显示当前路由组件 -->
        <router-view></router-view>
      </div>
    </template>
    
  6. 通过手动修改地址来测试切换路由

注意点

  1. 路由组件通常存放在views或pages文件夹,一般组件或公共组件通常存放在components文件夹

  2. 通过切换“隐藏” 了的路由组件,默认是被销毁掉的,需要的时候再去创建

  3. 组件实例的$route属性:存储着当前路由信息(当前路径、路由参数)

  4. 组件实例的$router属性:整个应用的路由器,包含跳转路由, 动态注册路由等方法

  5. 不可直接修改$route.query、params的值

    修改后相当于请求参数不变,导航到同一个地方,会出问题

多级嵌套路由

通过路由实现组件的嵌套展示,叫做嵌套路由

  1. 创建子路由组件

    1. views/Home/Course/index.vue
    2. views/Home/Game/index.vue
    3. views/Home/News/index.vue
  2. 配置路由规则,使用 children 配置项注册子路由

    routes: [
      // 路由 path component
      {path: '/login', component: Login},
      {
        path: '/home', 
        component: Home,
        // 注册二级子路由
        children: [
          {path: '/home/news', component: News},
          {path: 'game', component: Game},
          {path: 'course', component: Course},
        ]
      },
      // 当请求项目根路径时, 自动跳转/login路由路径
      {
        path: '/',
        redirect: '/home'
      }
    ]
    
  3. 在父路由组件中指定<router-view>来显示子路由组件

    <template>
      <div>
        <h1>Home</h1>
        <router-view></router-view>
      </div>
    </template>
    

路由重定向

将指定路径重新定向到已有路由

通过路由规则的redirect属性指定一个新的路由地址,设置路由重定向

  1. 在自身路由中定义redirect

    {
      path: "/home",
      component: Home,
      // 写法一: 当访问/home时, 自动跳转到/home/course
      redirect:"/home/course",
      children: [
        ...
      ]
    }
    
  2. 在子路由中定义redirect

    children: [
      // 写法二: 当访问/home时, 自动跳转到/home/course
      // ''空串相对路径即指当前的父路由路径
      {path: '', redirect: '/home/course'},
    ],
    

声明式&编程式路由导航

声明式导航

利用**<router-link>**实现路由跳转

功能
to指定要跳转的目标位置location1. string类型:path字符串,可以用相对路径值,且匹配忽略大小写
2.object类型:{name, path}
replace指定是否为replace模式默认为false,即push模式
active-class指定当前路由链接的类名默认为router-link-active
tag指定生成的html标签默认为a,已弃用
to
  1. string类型

    <router-link to="/home/course">course</router-link>
    <router-link to="game">game</router-link>
    
  2. object类型

    <router-link :to="{ path: '/home/course' }">course</router-link>
    //设置为replace模式
    <router-link :to="{ path: 'game' }" replace>game</router-link>
    //通过路由的name指定跳转的路由,每个路由的name为唯一标识,相当于id
    <router-link :to="{ name: 'News' }">news</router-link>
    
active-class
1.active-class

只能修改单个路由链接的类名

<router-link to="/home/news" active-class="active">News</router-link>
//只有路由指向为/home/news的导航链接,激活后添加类名为active
2.linkActiveClass

全局修改当前路由链接的类名

const router = new VueRouter({
  // 路由表
  routes: [ ... ],
  // 全局修改当前路由链接的类名
  linkActiveClass: 'active2'
})
tag

tag指定生成的html标签,目前已弃用

  • <router-link>slot</router-link>中自带插槽
  • a为默认插槽值,如需使用其他html标签,只需在其中写明即可
<router-link>
  <button>导航到首页</button>
</router-link>
编程式导航

利用router对象的push或replace方法实现路由跳转

获取方法

组件中通过 this.$router得到router对象

  • $router.push(location)
  • $router.replace(location)
location的值
  1. string类型

    path字符串, 可以用相对路径值, 且是忽略大小写匹配的

    <button @click="$router.push('/home/course')">course</button>
    <button @click="$router.push('game')">game</button>
    
  2. object类型

    {name, path}

    <button @click="$router.replace({ path: '/home/course' })">course</button>
    <button @click="$router.replace({ path: 'game' })">game</button>
    //通过name指定跳转路由
    <button @click="$router.replace({ name: 'News' })">news</button>
    
router其他跳转方法
  • back(): 回退到上一个路由
  • forward(): 前进到下一个路由
  • go(n): 前进或后退指定个数的路由
编程式导航重复跳转路由报错❗

适用于面试题

  • 开发时遇到过什么印象深刻的问题?

场景: 手里有2个项目(一个老的, 一个新的), 老的没这个问题, 但新的就有这个问题

解决: 百度/查看文档 => 最后在vue-router官方仓库的Issue(提交的仓库问题)

编程式导航跳转同一个路由,且携带参数不变时,会报错

而声明式不会报错

方式一
原因

vue-router版本问题

  • 3.1.0之前,push方法没有返回值

    • router.push(location) void
    • router.push(location, onResolved, onRejected) void
  • 3.1.0之后,push方法如果没有指定回调函数, 返回值为promise对象

    • router.push(location).then(onResolved, onRejected) => 此语法重复跳转到当前路由就会抛出失败的promise错误

注意:旧的语法(传了回调)没有问题,只有新的语法(没有传入回调)才有问题

解决

在路由表中配置

通过重写VueRouter原型对象上的push/replace方法, 来解决重复跳转报错的问题

const originPush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, onResolved, onRejected) {
  console.log('push', onResolved, onRejected)
  debugger
  // 如果传入了回调, 用的旧语法, 不会有错误的, 调用原函数进行处理
  if (onResolved || onRejected) {
    originPush.call(this, location, onResolved, onRejected)
  } else { // 没有传回调, 用的新语法, 可能会抛出失败的promise错误
    return originPush.call(this, location).catch(() => {
      console.log('error')
    })
    // originPush.call(this, () => {})
  }
}

为什么声明式导航不报错?

默认传入一个成功的回调, 用的是旧的语法

方式二
原因

vue对window.history做了二次封装,判断新路径和旧路径是否相等,相等就报错

解决

使用原生history来push,就不会报错了

$router.history.push()

路由组件传参

1.params
  1. 路由表配置params路径参数 :xxx

    {
      name: 'Course', 
      path: 'course', 
      component: Course,
      children: [
        {
          name: 'CourseItem', 
          path: 'item/:id/:name',   // 必须要有冒号占位
          component: CourseItem
        }
      ]
    },
    
  2. 路由跳转,携带params参数

    ⚠传递params参数时,to的对象写法只能用name,不能用path

    <!-- to字符串写法 -->
    <router-link :to="`/home/course/item/${c.id}/${c.name}`">{{c.name}}</router-link>
      	
    <!-- to对象写法 -->
    <router-link 
      :to="{name: 'CourseItem', params: {id: c.id, name: c.name}}">
      {{c.name}}
    </router-link>
    
  3. 目标路由组件中读取params参数 $route.params.xxx

    <li>课程ID: {{ $route.params.id }}</li>
    <li>课程名称: {{ $route.params.name }}</li>
    
2.query
  1. 路由跳转,携带params参数

    <!-- to的字符串写法 -->
    <router-link 
      :to="`/home/course/item?id=${c.id}&name=${c.name}`">
      {{c.name}}
    </router-link>
    
    <!-- to的对象写法 -->
    <router-link 
      :to="{ name: 'CourseItem', query: {id: c.id, name: c.name} }">
      {{c.name}}
    </router-link>
    //传递query参数,可以用path
    <router-link 
      :to="{ path: '/home/course/item', query: {id: c.id, name: c.name} }">
      {{c.name}}
    </router-link>
    
  2. 目标路由组件中读取query参数 $route.query.xxx

    <li>课程ID: {{ $route.query.id }}</li>
    <li>课程名称: {{ $route.query.name }}</li>
    
3.props❗

面试问得多

1.设置props参数

在路由的props配置项中配置参数

  1. 对象:专门指定自定义数据

    //对象: props: {a: 1, b: 2} => 专门指定自定义数据的
    {
      name: 'CourseItem',  // 一定指定name
      path: 'item/:id/:title',  // params参数占位
      component: CourseItem,
      props: {a: 1, b: 2}
    }
    
  2. 布尔值(true):专门将params参数映射成props

    //布尔值(true): props: true => 专门将params参数映射成props
    //对象: props: {a: 1, b: 2} => 专门指定自定义数据的
    {
      name: 'CourseItem',  // 一定指定name
      path: 'item/:id/:title',  // params参数占位
      component: CourseItem,
      props: true
    }
    
  3. 函数:传递自定义,params参数和query参数

    //函数: props: route => ({自定义/params/query}) => 传递自定义, params参数和query参数
    //对象: props: {a: 1, b: 2} => 专门指定自定义数据的
    {
      name: 'CourseItem',  // 一定指定name
      path: 'item/:id/:title',  // params参数占位
      component: CourseItem,
      props: route => ({c: 2, id2: route.params.id, title2: route.query.title2})
    }
    
2.获取props参数

目标路由组件通过props配置项接收props参数,读取同读取props一样

props: ['a', 'b', 'id', 'title', 'c', 'id2', 'title2']
4.meta元信息

路由配置项meta,可以用来给特定路由组件指定需要的数据

1.注册路由时配置meta
{
  ...
  meta: {
    isHideFooter: true, // 标识隐藏footer
  }
},
2.读取meta参数
$route.meta.xxx
实例

设置除login外,其他路由均显示footer部分

  1. 注册路由时,配置meta

    {
      name: 'Login', 
      path: '/login', 
      component: Login,
      meta: {
        isHideFooter: true, // 标识隐藏footer
      }
    },
    
  2. 在App组件中根据$route.meta.isHideFooter来判断是否显示Footer界面

    <template>
      <div>
        <!-- 指定在此显示当前路由组件 -->
        <router-view></router-view>
        <div class="footer" v-if="!$route.meta.isHideFooter">这是页面的Footer</div>
      </div>
    </template>
    
监视路由参数变化

使用watch监听$route,只要路由变化就调用执行

路由变化,路由中的参数也会变化

watch: {
  $route: {
    handler (value) {
      console.log(`根据${value.params.id}发请求获取详情信息`)
    },
    immediate: true, // 初始化就执行一次
  }
}

动态组件

vue内容,非vue-router内容

vue的动态组件
  • Vue提供了一个<component>组件,用来做动态组件
  • <component>是一个占位符,接受一个is属性,is属性的值是一个 字符串类型 的已经被引入和注册的组件的名称
  • 通过改变is强制绑定的值,来切换替换<component>的组件
<template>
  <button @click="comp = News">新闻</button>
  <button @click="comp = Play">娱乐</button>
  
  <component :is="comp"></component>
</template>
缓存组件keep-alive

vue提供的,不是vue-router提供的

  • 路由切换时,隐藏组件实质是销毁组件,显示组件实质是挂载组件
  • 缓存路由组件让不展示的路由组件保持挂载,不被销毁(DOM 结构会移除,但支撑带结构的组件不被销毁)
keep-alive基本使用

keep-alive 缓存路由组件

<keep-alive><router-view>包裹,隐藏组件时不会将组件销毁,组件依然挂载着

功能
include指定缓存路由组件名(组件配置中的name属性)
exclude指定不缓存路由组件名(组件配置中的name属性)
max指定缓存路由的最大缓存数⚠1.当前激活路由也算在缓存数中
2.按照最新使用的排序来缓存
<!-- 缓存所有对应的路由组件对象 -->
<keep-alive>
  <router-view></router-view>
</keep-alive>

<!-- 缓存指定的一个 -->
<keep-alive include="Login">
  <router-view></router-view>
</keep-alive>
  
<!-- 缓存指定的多个 -->
<keep-alive :include="['Login','Home']">
  <router-view></router-view>
</keep-alive>
keep-alive钩子函数

使用keep-alive组件缓存后,多了两个钩子函数

名称调用时间
activated激活时1.mounted之后调用第一次
2.导航来到该路由时调用
deactivated失活时离开路由调用

路由懒加载

路由懒加载的原理即异步组件

异步组件

将应用分割成小一些的代码块,并且只在需要时才从服务器加载一个模块

组件导入分类

组件的导入分为两种

  1. 静态导入模块 import utils from './utils'

    • 静态导入将所有模块打包在一起
    • 主要为了提高首页的访问检验(更快), 访问首页时, 需要加载的打包文件更小了
  2. 动态导入 import('./utils')

    • 单独打包 code split

    • 在注册组件的时候,可以进行异步导入,即异步组件

      components: {
        Hello: () => import("./components/Hello.vue"),
      },
      
vue异步组件❗

使用import模块化静态引入vue组件,webpack在打包构建时,会把所有的js都打包到一起,里面包含许多暂时没有使用的模块,这就会导致包的体积过大,造成进入首页时需要加载的内容过多,出现长时间白屏现象

  1. vue允许以一个函数的方式定义组件,函数内部使用import方法动态引入某个模块,并将结果返回
  2. webpack在打包时,遇到import动态引入,会将import动态引入的资源进行单独打包
  3. vue只有在这个组件需要被渲染的时候才会触发该函数,且把结果缓存起来以便未来重渲染
  4. 函数内import方法返回promise实例,当import引入成功,promise变为成功状态,就可以渲染当前组件了
路由懒加载

更高效

在路由表中,使用异步导入组件即可

  • 外层函数开始不执行,请求对应的路径时才会执行
  • 执行函数进才会请求加载对应的打包文件
const Home = () => import('@/views/Home')
const Login = () => import('@/views/Login')
const Course = () => import('@/views/Home/components/Course')
  • 懒加载的缺点:访问其它路由更慢了 => 需要发请求加载对应的打包文件
  • 解决:预加载 => 提前加载后面需要其它的打包文件

路由导航守卫

官网

  • https://v3.router.vuejs.org/zh/guide/advanced/navigation-guards.html

注意点

  • 刷新页面也是路由跳转,相当于从根路由跳转到当前路由

    (如当前处于路由/home处,刷新页面,from为/,to为/home

  • 参数或查询的改变并不会触发进入/离开的导航守卫

    • 可以通过观察 $route 对象来应对这些变化
    • 或使用 beforeRouteUpdate 的组件内守卫

路由导航守卫:当路由跳转时生效

基本使用

router.beforeEach 全局前置守卫为例

const router = new VueRouter({ ... })
// 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
  // ...
})

每个守卫方法接收三个参数:

  1. to: Route:即将要进入的目标路由对象

  2. from: Route:当前导航正要离开的路由

  3. next: Function:一定要调用该方法来 resolve 这个钩子

    执行效果依赖 next 方法的调用参数:

    • next():跳转到to指定的目标路由

    • next('/') 或者 next({ path: '/' }):跳转到一个不同的地址。

      当前的导航被中断,然后进行一个新的导航

      可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto prop 或 router.push 中的选项。

确保 next 函数在任何给定的导航守卫中都被严格调用一次

它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错

全局导航守卫

(最常用):对所有路由生效

  • beforeEach 全局路由前置守卫(路由跳转之前触发)
  • beforeResolve 全局路由解析守卫
  • afterEach 全局路由后置守卫(路由跳转之后触发)

在路由配置中配置全局导航守卫:router/index.ts

import routes from "./routes";

const router = createRouter({
  routes,
});

router.beforeEach((to, from, next) => {
  console.log("全局前置守卫");
  next();
});

router.beforeResolve((to, from, next) => {
  console.log("全局解析守卫");
  next();
});

router.afterEach((to, from) => {
  console.log("全局后置钩子");
});

export default router;
路由独享守卫

只对某个路由生效

  • beforeEnter

在路由表中配置路由独享守卫:router/routes.ts

export default [
  {
    path: "/home",
    name: "Home",
    component: () => import("../views/Home/index.vue"),
    beforeEnter(to: any, from: any, next: any) {
      console.log("item路由独享守卫");
      next();
    },
  },
];
组件内守卫

只对某个组件生效

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave

在组件中配置组件内守卫:

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "Item",
  beforeRouteEnter(to, from, next) {
    console.log("item组件被进入了");
    next();
  },
  beforeRouteUpdate(to, from, next) {
    console.log("item组件更新时守卫");
    next();
  },
  beforeRouteLeave(to, from, next) {
    console.log("Item组件被离开");
    next();
  },
});
</script>
路由守卫导航解析流程❗

只要导航跳转,全局守卫就会调用(即使导航到同一个组件,全局守卫也会调用

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

在这里插入图片描述

切换路由表模式

一共有3种模式:

  • hash
  • history
  • abstract:渲染后台的路由时使用
const router = new VueRouter({
  //配置路由表模式
  mode: 'history', // hash abstract 
  // 路由表
  routes: [ ... ],
})

Scroll Behavior

https://v3.router.vuejs.org/zh/guide/advanced/scroll-behavior.html

  • vue-router单页应用,只是切换了路由组件,滚动条还在原来的位置

  • 针对这种情况,vue使用Scroll Behavior来指定跳转路由后滚动条的位置

  • 每次路由跳转时,scrollBehavior函数的返回值决定滚动条的位置

基本使用

在路由表中进行配置即可

const router = new VueRouter({
  mode: "history", // 路由模式
  routes, // 路由配置
  // 滚动行为:每次路由跳转时,函数的返回值决定滚动条的位置
  scrollBehavior() {
    // return 期望滚动到哪个的位置,此处为返回最顶部
    return {
      x: 0,
      y: 0,
    };
  },
});

Vue动画/过渡

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果

transition

在这里插入图片描述

在这里插入图片描述

  1. transition在进入/离开的过渡中,会有 6 个 class 切换:

    • 当子组件刚进入时,同时添加v-enter-fromv-enter-active,然后删除v-enter-from

    • 当子组件进入动画结束后添加v-enter-to

  2. transition可指定name属性,指定后6个class的v替换为name属性值

    • 如name为xxx => xxx-enter-from
  3. 中只能有一个直接子元素

过渡动画
  1. 在目标元素外包裹

  2. 要实现动画的元素必须使用v-if或v-show来控制

    使用v-if或v-show才能实现消失和显示

  3. 给xxx-enter-active指定进入的过渡

  4. 给xxx-leave-active指定离开的过渡

  5. 给xxx-enter 和 xxx-leave-to 指定隐藏时的样式

<template>
  <div>
    <button @click="isShow=!isShow">切换</button>
    <transition>
      <div v-show="isShow" class="box"></div>
    </transition>
  </div>
</template>

<style scoped>
  .box {
    width: 100px;
    height: 100px;
    background: pink;
  }
  /* 显示和隐藏的过渡样式 */
  .v-enter-active, .v-leave-active {
    transition: opacity 2s;
  }
  /* 隐藏时的样式 */
  .v-enter, .v-leave-to {
    opacity: 0;
  }
</style>

<script>
export default {
  name: 'App',
  data () {
    return {
      isShow: true
    }
  }
}
</script>
关键帧动画
  1. 在目标元素外包裹
  2. 编写样式
    • 进入时样式:xxxx-enter-active
    • 离开时样式:xxxx-leave-active
<template>
  <div>
    <button @click="show = !show">Toggle show</button>
    <br>
    <transition name="bounce">
      <p v-if="show">Lorem ipsum</p>
    </transition>
  </div>
</template>

<style scoped>
p {
  display: inline-block;
}

/* 显示动画 */
.bounce-enter-active {
  animation: bounce-in .5s;
}
/* 隐藏动画 */
.bounce-leave-active {
  animation: bounce-in reverse .5s;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }

  50% {
    transform: scale(1.5);
  }

  100% {
    transform: scale(1);
  }
}
</style>

<script>
export default {
  name: 'App',
  data () {
    return {
      show: true
    }
  }
}
</script>
多元素过渡

多元素之间的切换很生硬

vue为此提供了过渡模式即mode属性:

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入
<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>

动画库Animate.css

官网:https://animate.style

  1. 安装

    $ npm install animate.css --save
    
  2. main.js中引入

    import 'animate.css';
    
  3. 添加类名来使用动画样式

    1. animate__animated为固定添加类名
    2. 另一个为指定的动画样式
    <h1 class="animate__animated animate__bounce">An animated element</h1>
    

半场动画

重排重绘

重绘(Repaint)和重排(Reflow)是浏览器渲染页面时的两个关键步骤。

  • 重绘是指当元素的样式发生改变,但不影响其布局的情况下,浏览器会重新绘制(repaint)该元素,使其呈现新的样式。

  • 重排是指当元素的布局发生改变,浏览器需要重新计算并重新布局(reflow)整个页面或部分页面的情况。重排会导致其他元素的位置和尺寸发生变化。

触发重绘重排
  1. 修改元素的样式属性:例如修改元素的颜色、背景、字体大小等。

  2. 修改元素的尺寸属性:例如修改元素的宽度、高度、边距等。

  3. 修改元素的位置属性:例如修改元素的定位方式、左右上下偏移等。

  4. 添加或删除元素:例如在页面中添加或删除元素。

  5. 修改页面的布局结构:例如修改页面的结构或布局方式。

  6. 获取布局信息数据也可触发

    在这里插入图片描述

优化策略

触发重绘和重排的操作会导致浏览器重新计算和渲染页面,这些操作可能会影响页面的性能。

为了提高页面的渲染性能,可以采取以下几点优化策略:

  1. 减少重绘和重排的次数:尽量避免频繁地修改元素的样式和布局属性,可以通过批量修改、使用 CSS 动画等方式减少重绘和重排的次数。
  2. 使用 CSS3 动画:CSS3 动画使用 GPU 加速,可以减少重绘和重排的开销。
  3. 使用 transformopacity 属性:transformopacity 属性可以在不影响布局的情况下改变元素的样式,从而避免重排。
  4. 使用文档片段(Document Fragment):将多个 DOM 操作封装在文档片段中,然后一次性插入到页面中,可以减少重排的次数。
半场动画实例

利用动画的生命周期勾子来实现半场动画

在这里插入图片描述

<template>
  <div>
      <div class="box">
        <button @click="isShow=!isShow">+</button>
        <transition
          v-on:before-enter="beforeEnter"
          v-on:enter="enter"
          v-on:after-enter="afterEnter">
          <div class="ball" v-show="isShow"></div>
        </transition>
        

        <img class="cart" src="https://img1.baidu.com/it/u=3761831681,4005524739&fm=253&fmt=auto&app=138&f=PNG?w=500&h=500" alt="">
      </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      isShow: false,
    }
  },
  methods: {
    // 进入动画开始前调用
    beforeEnter: function (el) {
      console.log('beforeEnter')
      // 让小球回到初始位置
       el.style.transform = 'translate(0, 0)'
    },
    
    /* 
    进入动画开始时调用
    在此给el添加过渡相关的样式
    */
    enter: function (el, done) {
      console.log('enter')
      el.offsetLeft // 只要执行触发重排代码, 过渡才会有效果 读取一属性也会触发重排
      // 指定过渡样式
      el.style.transition = 'all .5s'
      el.style.transform = 'translate(450px, 430px)' 
      
      done()  // 标识动画完成, 触发afterEnter调用
    },
    // 进入动画结束时调用
    afterEnter: function (el) {
      console.log('afterEnter')
      // 隐藏小球
      this.isShow = false
    },
  }
}
</script>

<style scoped>
  .box {
    width: 500px;
    height: 500px;
    border: 1px solid #ccc;
    position: relative;
  }
  .cart {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 50px;
    height: 50px;
  }
  .ball {
    width: 20px;
    height: 20px;
    background: red;
    border-radius: 50%;
  }
</style>

列表动画

在v-for这种场景中,使用 组件

<transition-group>
  • 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>(可以通过 tag 属性 更换为其他元素)
  • 过渡模式不可用,因为我们不再相互切换特有的元素
  • 内部元素总是需要提供唯一的 key attribute 值
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身
实例
<template>
  <div id="list-demo" class="demo">
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list" tag="p">
      <span v-for="item in items" v-bind:key="item" class="list-item">
        {{ item }}
      </span>
    </transition-group>
  </div>
</template>
<style>
  .list-item {
    display: inline-block;
    margin-right: 10px;
  }
  .list-enter-active, .list-leave-active {
    transition: all 1s;
  }
  .list-enter, .list-leave-to
  /* .list-leave-active for below version 2.1.8 */ {
    opacity: 0;
    transform: translateY(30px);
  }
</style>

路由跳转动画

https://v3.router.vuejs.org/zh/guide/advanced/transitions.html

可以用 <transition> 组件给<router-view> 添加过渡效果

<transition> 的所有功能在此处都适用

<template>
  <div>
    <div>
      <router-link to="/login">Login</router-link> &nbsp;
      <router-link to="/home">Home</router-link>
    </div>
    <hr>
    <transition name="fade" mode="out-in">
      <router-view></router-view>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'App',
}
</script>

<style scoped>
/* 显示/隐藏的过渡样式 */
.fade-enter-active, .fade-leave-active {
  transition: all .5s;
}
/* 隐藏时的样式 */
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

拓展

手写代码

call & bind
区别
  1. 同:都是用来改变函数中的this的
    1. call立即调用
    2. bind只有当调用bind返回的函数时才会调用
  2. 如何选择:主要看函数是否是需要立即调用, 如果不是,只能用bind
实现bind
Function.prototype.bind = function (thisObj, ...args) {
  console.log('bind')
  // bind中的this是原函数
  return (...args2) => {
    return this.call(thisObj, ...args, ...args2)
  }
}
实现call
Function.prototype.call = function (obj,...args){
    if(obj===undefined || obj===null){
        obj = window;
    }
    obj = Object(obj);
    // 将原函数添加为obj的方法
    obj.fn = this;
    // 通过obj来调用方法
    const result = obj.fn(...args);
    // 删除这个方法
    delete obj.fn;
    return result;
}
throttle & debounce
  • 在项目中直接使用lodash来防抖节流

    • Lodash——js库

      https://www.lodashjs.com/

  • 防抖、节流应用:

    • 轮播图

    • 触发频繁发送请求

两者关系
  1. 共同点

    解决同一类问题:事件高频触发处理的问题(请求或更新界面的次数太多 )

  2. 区别——在一段较长的时间内事件高频触发

    1. 节流:每隔一定时间执行一次 => 执行少量几次
    2. 防抖:只执行最后一次
节流throttle

判断时间差(当前时间与上一次处理的时间)大于指定时间才调用处理函数

function throttle(callback, time) {
  let pre = 0;
  return function (e) {
    const now = Date.now();
    if (now - pre > time) {
      callback.call(this, e);
      pre = now;
    }
  };
}
document.querySelector("#btn3").onclick = throttle(fn, 1000);
防抖debounce

事件发生后, 启动延迟定时器来执行处理函数, 但在启动前先清除未执行的定时器

function debounce(callback, time) {
  let timer = null;
  return function (e) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      callback.call(this, e);
      timer = null;
    }, time);
  };
}
document.querySelector("#btn2").onclick = debounce(fn, 1000);
new
  1. 创建空obj

  2. 将obj的__proto__指向构造函数的prototype

  3. call调用构造函数创建实例对象

  4. 判断构造函数的返回值,若为复杂数据类型返回,简单数据类型返回实例对象

function Person(name, age) {
  this.name = name;
  this.age = age;
  // return 1;
  // return [1];
  // return {m:1}
  // return ()=>{}
}
function newFn(type, ...args) {
  const obj = {};
  obj.__proto__ = type.prototype;
  const result = type.call(obj, ...args);
  return result instanceof Object ? result : obj;
}
const person1 = new Person("asa", 12);
const person2 = newFn(Person, "ooo", 13);
console.log(person1);
console.log(person2);
深拷贝
区别深拷贝与浅拷贝

被拷贝的对象/数组的结构:内部要包含了对象/数组

  • 浅拷贝不会拷贝包含对象/数组,只会拷贝它的地址值 => 只拷贝一层
  • 深拷贝会拷贝包含的所有层级的对象/数组 => 拷贝多层
浅拷贝
对象{...obj}
数组[...arr]

⚠**obj1 = objobj1 = {..obj}不同**

深拷贝

项目中一般使用lodash的cloneDeep

json深拷贝

json无法保存函数,函数会丢失,若深拷贝目标中有函数不要使用json深拷贝

json深拷贝适用于请求获取的数据

function cloneDeep1(target) {
  return JSON.parse(JSON.stringify(target));
}
递归深拷贝

Object的toString会返回一个字符串"[object 数据类型]"

Object.prototype.toString.call([1]) =>'[object Array]'

//设置判断数据类型的函数
function targetType(target) {
  return Object.prototype.toString.call(target).slice(8, -1);
}
//递归深拷贝函数
function cloneDeep2(target) {
  const type = targetType(target);
  if(type==="Object"||type==="Array"){
    let cloneTarget;
    if(type==="Object"){
      cloneTarget={}
      Object.keys(target).forEach(key=>{
        cloneTarget[key] = cloneDeep2(target[key])
      })
    }else if(type==="Array"){
      cloneTarget=[];
      target.forEach(item=>{
        cloneTarget.push(cloneDeep2(item))
      })
    }
    return cloneTarget;
  }
  return target
}

生成唯一Id

Date.now() => 不能连续调用 / 并发访问时都有可能产生重复的值

uuid

uuid: 36位的唯一字符串

https://www.npmjs.com/package/uuid

下载
npm i uuid
使用
//1.ES6
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

//2.commonjs
const { v4: uuidv4 } = require('uuid');
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
nanoid

nanoid: 21位的唯一字符串

https://www.npmjs.com/package/nanoid

一般用这个,相对短些

下载
npm i nanoid
使用
import { nanoid } from 'nanoid'
model.id = nanoid() //=> "V1StGXR8_Z5jdHi6B-myT"

日期格式化库

一般和过滤器结合使用

moment

https://momentjs.com/

import moment from 'moment';
moment(value).format('YYYY-MM-DD HH:mm:ss')

在这里插入图片描述
在这里插入图片描述

dayjs

https://dayjs.gitee.io/docs/zh-CN/installation/installation

dayjs与moment的语法基本一样, 但体积更小

import dayjs from 'dayjs'
dayjs(value).format('YYYY-MM-DD HH:mm:ss')

Element库

常见的Vue UI组件库

(最流行的Vue UI组件库为Element)

  • 移动端UI组件库

    1. Vant https://youzan.github.io/vant/#/zh-CN
    2. Cube UI https://didi.github.io/cube-ui/#/zh-CN
  • PC端UI组件库

    1. Element UI https://element.eleme.cn/#/zh-CN
    2. IView UI https://www.iviewui.com

推荐vscode插件:vue-helper

一般使用
1. 下载
npm i element-ui
2.引入
完整引入
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)
按需引入
  1. 按需引入还需下载功能包

    npm i babel-plugin-component -D
    
  2. 添加babel相关配置(babel.config.js)

    plugins: [
      [
        "component",  // 为babel-plugin-components配置
        {
          "libraryName": "element-ui",
          "styleLibraryName": "theme-chalk"
        }
      ]
    ]
    
  3. 引入并只注册需要使用的组件

    import Vue from 'vue'
    
    import {
      Button,
      MessageBox,
      Message
    } from 'element-ui'
    
    // 注册Button组件, 本质: Vue.component(Button.name, Button)
    Vue.use(Button)
    // 将MessageBox的显示确认框的方法confirm搭载到Vue原型对象上  => 所有组件都可以调用显示确认框
    Vue.prototype.$confirm = MessageBox.confirm
    Vue.prototype.$message = Message
    
3.使用UI组件

根据官网实例使用即可

快捷使用

https://github.com/ElementUI/vue-cli-plugin-element

Element为vue-cli专门提供了Element插件

可以快速地搭建一个基于 Element 的项目,无需自己配置

  1. 下载

    1. 指令只要输一遍

      vue add element
      
    2. 指定完整引入还是按需引入

在这里插入图片描述

  1. 指定语言

在这里插入图片描述

  1. 还原App组件的内容

    ⚠Element插件会同时配置App.vue的内容,需要改回原来写的代码

  2. 在plugins/element.js执行引入操作

  3. 使用UI组件

  4. 重启服务器

    由于改了配置,需要重启服务器,避免ui组件样式刷新不出来

Mock/模拟数据接口

mock数据:后端接口还未开发完成,所以需要mock数据

注意

  1. 数据结构要找后端要(mock数据结构要和之后后端提供的数据结构一致
  2. 后端接口将来会开发好,记得将requestMock改回request,修改请求地址等
mockjs使用

官网

  • http://mockjs.com/
  • https://github.com/nuysoft/Mock
  1. 安装mockjs

    npm i mockjs
    
  2. 定义mock

    import Mock from "mockjs";
    Mock.mock("url", "请求方法" ,{ 响应数据即response.data })
    

    url:当请求该url时,mock拦截请求,返回第三个参数指定的响应数据

    import Mock from "mockjs";
    Mock.mock("/mock/getHomeBannerList", "get", {
      code: 200,
      message: "",
      ok: true,
      data: [
        {
          id: "1",
          imgUrl: "/images/banner1.jpg",
        },
        {
          id: "2",
          imgUrl: "/images/banner2.jpg",
        },
        {
          id: "3",
          imgUrl: "/images/banner3.jpg",
        },
        {
          id: "4",
          imgUrl: "/images/banner4.jpg",
        },
      ],
    });
    
  3. 主入口main.js中引入,让mock生效

    import "./mock/index";
    
  4. 准备一个新的请求文件 - requestMock

    目的:防止和正常请求request冲突

    // utils/requestMock.js
    const request = axios.create({
      baseURL: "/mock", // 只要修改请求前缀,不和/api一致即可
      timeout: 20000,
    });
    
  5. 定义请求函数

    import requestMock from "@/utils/requestMock";
    //获取首页轮播图列表
    export const reqGetHomeBannerList = () => {
      return requestMock({
        method: "GET",
        url: "/getHomeBannerList",
      });
    };
    
  6. 组件发送请求,获取数据

Swiper轮播图

  • 官网:https://swiperjs.com/
  • api文档:https://swiperjs.com/swiper-api
vue2使用

Vue2只能使用这种方法

https://swiperjs.com/get-started

  1. 安装swiper

    $ npm i swiper
    
  2. 主入口main.js引入

    1. 全部引入(加载慢

      // import Swiper JS
      import Swiper from 'swiper';
      // import Swiper styles
      import 'swiper/css';
      
      //在目标组件中创建swiper实例对象
      const swiper = new Swiper(...);
      
    2. 按需引入

      // core version + navigation, pagination modules:
      import Swiper from 'swiper';
      import { Navigation, Pagination, Autoplay } from 'swiper/modules';
      // import Swiper and modules styles
      import 'swiper/css';
      import 'swiper/css/navigation';
      import 'swiper/css/pagination';
      
      //在目标组件中创建swiper实例对象
      //按需引入时,需要指明引入的模块
      const swiper = new Swiper('.swiper', {
        // configure Swiper to use modules
        modules: [Navigation, Pagination],
        ...
      });
      
  3. 目标组件中

    1. 设置swiper布局

      <div class="swiper">
        <!-- Additional required wrapper -->
        <div class="swiper-wrapper">
          <!-- 轮播内容 -->
          <div class="swiper-slide">轮播内容 1</div>
          <div class="swiper-slide">轮播内容 2</div>
          ...
        </div>
        <!-- 下方页面小圆点 -->
        <div class="swiper-pagination"></div>
      
        <!-- 前后翻页 -->
        <div class="swiper-button-prev"></div>
        <div class="swiper-button-next"></div>
      
        <!-- 滚动条 -->
        <div class="swiper-scrollbar"></div>
      </div>
      
    2. 创建swiper实例,并设置swiper配置

      new Swiper(".swiper", {
        // 按需引入时需指明引入的模块
        modules: [Navigation, Pagination, Autoplay],
        // 无限轮播
        loop: true,
        // 自动轮播:https://swiperjs.com/swiper-api#autoplay
        autoplay: {
          // 操作完后禁止自动轮播
          disableOnInteraction: false,
          // 鼠标移入停止轮播
          pauseOnMouseEnter: true,
        },
        // 小圆点: https://swiperjs.com/swiper-api#pagination
        pagination: {
          el: ".swiper-pagination",
          clickable: true,
        },
        // 左右翻页按钮
        navigation: {
          nextEl: ".swiper-button-next",
          prevEl: ".swiper-button-prev",
        },
      });
      
vue3使用

Vue3可以使用Swiper组件,更简单快捷

https://swiperjs.com/element

使用常见错误
  1. 使用本地照片404

    • 原因

    编译打包时没有打包所需的本地图片,所以后面访问时访问不了

    • 解决方法

    将图片数据放入public目录中即可

    public中的文件,脚手架不会处理,会原封不动地打包

  2. 轮播内容无内容(undefined)时,new Swiper会导致功能错乱

    • 原因

    父组件在mounted中请求接收轮播图的数据后传给子组件(轮播图所在位置);子组件在mounted中进行渲染

    而由于父组件的mounted在子组件的mounted之后运行,故子组件在渲染时数据为undefined

    • 解决方法
    watch: {
      // 等 carouselList 数据发生变化时触发
      carouselList() {
       new Swiper(".swiper", { });
      },
    },
    

    使用watch观察需渲染数据的变化,并在$nextTick中再创建轮播图渲染

  3. 同时选中多个swiper组件运行,会导致功能错乱

    • 原因
    new Swiper('.swiper', {})
    

    每个轮播图只能new swiper一次

    复用swiper组件,如果使用.swiper选择器,那创建的轮播图类名都是.swiper,就会使同时操作多个轮播图运行,导致轮播功能错误

    • 解决方法
    new Swiper(this.$refs.swiperRef, {})
    

    为避免这种情况,将通过选择器获取swiper容器,改为通过refs获取容器(能使用选择器的地方就能用ref),ref可以获取所在的真实dom元素,故只会操作一个轮播图,不会导致错乱

放大镜

计算比例:剩余距离即比例

<template>
  <div class="spec-preview">
    <img src="../images/s1.png" />
    <div class="event" @mousemove="handleMouseMove"></div>
    <div class="big">
      <img
        src="../images/s1.png"
        :style="{
          top: -scale * y + 'px',
          left: -scale * x + 'px',
        }"
      />
    </div>
    <div
      class="mask"
      :style="{
        top: y + 'px',
        left: x + 'px',
      }"
    ></div>
  </div>
</template>

<script>
/*
  小绿:200 * 200
  左图:400 * 400
  右图(大图):800 * 800
*/
const MASK_SIZE = 200; // 小绿大小
const SMALL_IMG_SIZE = 400; // 左图大小
const BIG_IMG_SIZE = 800; // 右图大小

export default {
  name: "ZoomComp",
  data() {
    return {
      x: 0,
      y: 0,
      scale: BIG_IMG_SIZE / SMALL_IMG_SIZE,
    };
  },
  methods: {
    handleMouseMove(e) {
      // 计算小绿的位置:x y
      let x = e.offsetX - MASK_SIZE / 2;
      let y = e.offsetY - MASK_SIZE / 2;

      if (x < 0) x = 0;
      if (x > SMALL_IMG_SIZE - MASK_SIZE) x = SMALL_IMG_SIZE - MASK_SIZE;
      if (y < 0) y = 0;
      if (y > SMALL_IMG_SIZE - MASK_SIZE) y = SMALL_IMG_SIZE - MASK_SIZE;

      this.x = x;
      this.y = y;
    },
  },
};
</script>

vee-validate

用于表单验证

官网:https://www.npmjs.com/package/vee-validate

vue2和vue3的使用版本不同,以下为vue2使用

  1. 安装

    npm i vee-validate@3
    
  2. 表单校验目标组件引入&注册组件

    import { ValidationObserver, ValidationProvider, extend } from "vee-validate";
    
    components: {
      ValidationObserver,
      ValidationProvider,
    },
    
  3. 定义表单校验规则

    import { required } from "vee-validate/dist/rules";
    //校验手机号
    extend("phoneRequired", {
      ...required,
      message: "手机号是必填项",
    });
    const phoneReg = /^1[3-9][0-9]{9}$/;
    extend("phone", {
      // val就是校验表单项的数据
      validate(val) {
        // 返回true通过,返回false就失败
        return phoneReg.test(val);
      },
      message: "请填写正确的手机号",
    });
    //校验确认密码
    extend("rePasswordRequired", {
      ...required,
      message: "确认密码是必填项",
    });
    extend("rePassword", {
      // val就是校验表单项的数据
      validate(val, { password }) {
        // 返回true通过,返回false就失败
        return val === password;
      },
      message: "两次密码输入不一致",
      params: ["password"],
    });
    
  4. 配置表单

    1. 使用 ValidateObserver 组件包裹整个表单,并定义提交表单事件和回调

      <ValidationObserver v-slot="{ handleSubmit }">
        <form @submit.prevent="handleSubmit(register)"></form>
      </ValidationObserver>
      
      methods: {
        register() {
          console.log("表单校验通过了");
        },
      },
      
    2. 使用 ValidateProvider 包裹单个表单项,用来定义表单校验规则等

      //手机
      <ValidationProvider
        //phoneRequired|phone中的|表示同时校验2个rule
        rules="phoneRequired|phone"
        v-slot="{ errors }"
        tag="div"
        mode="lazy"
      >
        <label>手机号:</label>
        <input type="text" placeholder="请输入你的手机号" v-model="user.phone" />
        <span class="error-msg">{{ errors[0] }}</span>
      </ValidationProvider>
      //确认密码
      <ValidationProvider
        :rules="`rePasswordRequired|rePassword:${user.password}`"
        v-slot="{ errors }"
        tag="div"
        mode="lazy"
      >
        <label>确认密码:</label>
        <input type="text" placeholder="请输入确认密码" v-model="user.rePassword" />
        <span class="error-msg">{{ errors[0] }}</span>
      </ValidationProvider>
      //提交按钮
      <div class="btn">
        <button>完成注册</button>
      </div>
      

二维码qrcode

  • 在线生成二维码:草料二维码
  1. 安装

    npm install --save qrcode
    
  2. 使用

    返回的结果url即可用在img的src,会显示二维码图片

    import QRCode from "qrcode";
    const url = await QRCode.toDataURL(codeUrl);
    

图片懒加载

vue-lazyload

只适用于vue1和vue2

官网:https://www.npmjs.com/package/vue-lazyload

  1. 安装

    npm i vue-lazyload@1 -S
    
  2. main.js中配置

    import VueLazyload from "vue-lazyload";
    // 图片需要准备好
    const loadImage = require("./assets/imgs/loading.gif");
    const errorImage = require("./assets/imgs/error.jpg");
    
    Vue.use(VueLazyload, {
      preLoad: 1.3, //提前多少预加载
      error: errorImage,
      loading: loadImage,
      attempt: 1, //请求几次(此处为第1次请求失败后不再请求
    });
    
  3. 使用

    xxx为图片的url,即一般src中写的内容

    <img v-lazy="xxx" />
    
封装v-lazy

https://blog.csdn.net/weixin_43288600/article/details/132239842

  • 响应式监听目标元素的可见性

    https://www.vueusejs.com/core/useIntersectionObserver/

Web Socket

https://www.yuque.com/xpromise/fontend/sy7xax007n3s7pdg

概念

WebSocket 是 HTML5 提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。

它基于 TCP 传输协议,并复用 HTTP 的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。

特点

服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。

API
构造函数

new WebSocket(url[, protocols])

  • url:连接WebSocket服务器地址
  • protocols:: 一个协议字符串或者一个包含协议字符串的数组
实例方法
  • close():关闭WebSocket连接
  • send(data):发送消息给WebSocket服务器
事件
  • close:监听WebSocket关闭的事件
  • error:监听WebSocket错误的事件
  • message:监听客户端接收消息的事件
  • open:监听WebSocket连接好的事件
基本使用
客户端代码
<template>
  <div>
    <h1>聊天室</h1>
    <ul>
      <li v-for="(msg, index) in list" :key="index">{{ msg }}</li>
    </ul>
    <input type="text" v-model.trim="message" />
    <button @click="sendMessage">发送消息</button>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      message: "",
      list: [],
    };
  },
  mounted() {
    // 文档:https://www.yuque.com/xpromise/fontend/sy7xax007n3s7pdg
    // 1. 链接上webSocket服务器
    this.ws = new WebSocket("ws://192.168.14.81:5000");
    // 3. 接受消息
    this.ws.onmessage = (e) => {
      this.list.push(e.data);
    };
  },
  methods: {
    sendMessage() {
      if (!this.message) return;
      // 2. 点击按钮,向服务器发送消息
      this.ws.send(this.message);

      this.list.push(this.message);

      this.message = "";
    },
  },
};
</script>
服务端代码
  1. 下载

    npm i ws
    
  2. 使用

    // 如果想在nodejs平台使用ES6模块化语法,只需在package.json写上"type": "module",
    import { WebSocketServer } from "ws";
    
    // 创建ws服务器
    // ws://localhost:5000
    const wss = new WebSocketServer({ port: 5000 });
    
    // connection 事件:监听客户端的链接事件
    wss.on("connection", function connection(ws) {
      // ws 链接上的客户端对象
    
      // 监听客户端向服务器发送消息的事件
      ws.on("message", function message(data) {
        // data 就是消息的内容
        console.log("客户端向服务器发送的消息", data.toString());
        // 将消息转发其他所有人
        // wss.clients.forEach(ws => ws.send(data.toString()))
        wss.clients.forEach((client) => {
          if (client === ws) return;
          client.send(data.toString());
        });
      });
    });
    
  • 35
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js是一款流行的JavaScript框架,为了方便开发者学习和使用,Vue官方提供了详细的中文文档。针对Vue2版本,我们可以通过以下方式实现离线查看中文文档。 1. 下载中文文档:在Vue官方GitHub仓库中,可以找到对应Vue2版本的中文文档。我们可以通过`git clone`命令将文档仓库克隆到本地,或者直接下载该仓库的zip文件。这样我们就得到了Vue2中文文档的离线版本。 2. 使用本地服务器:将克隆或下载的文档部署到本地服务器,例如使用Node.js的`http-server`模块。首先,安装Node.js,然后在命令行中运行`npm install -g http-server`命令安装全局http-server模块。接下来,通过命令行进入文档所在的文件夹,运行`http-server`命令启动服务器。这样我们就可以通过浏览器访问`localhost`来查看离线版的Vue2中文文档。 3. 使用浏览器插件:Vue中文文档官网提供了一个浏览器插件Vue Devtools,其中包含了Vue2中文文档的离线版本。我们可以在浏览器的插件商店(如Chrome Web Store)中,搜索并下载Vue Devtools插件。安装完成后,打开浏览器插件界面,选择“文档”,即可离线查看Vue2中文文档。 这些方法都可以实现Vue2中文文档的离线查看,方便我们随时学习和使用Vue.js框架。无论是开发者新手还是有经验的开发者,都可以通过离线文档快速学习和查询Vue.js的相关知识。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值