Vue学习笔记

一、Vue

Vue 是一套用于构建用户界面的渐进式 JavaScript 框架

渐进式指 Vue 是可以自底向上逐层递进的应用,即在简单应用中只需一个轻量小巧的核心库,若要构建复杂应用只需在这基础上引入各式各样 Vue 插件

Vue 特点

(1)采用组件化模式,提高代码复用率、且让代码更好维护(.vue 文件中包含 html、css、js)

(2)声明式编码,让编码人员无需直接操作 DOM 提高开发效率(如原生js中命令式编码需要 forEach 遍历、document.getElementById 获取 DOM 元素,而声明式编码中只需 <li v-for> 即可)

(3)使用虚拟 DOM + 优秀的 Diff 算法,尽量复用 DOM 节点

(4)遵循 MVVM 模式

(5)编码简化,体积小,运行效率高,适合移动/PC端开发

(6)它本身只关注 UI,也可以引入其他第三方库开发项目

创建 Vue 的基础

(1)想让 Vue 工作,就必须创建一个 Vue 实例,且要传入一个配置对象

(2)root 容器里的代码依然符合 html 规范,只不过混入了一些特殊的 Vue 语法,如 {{表达式或vue的data中的数据}}

(3)root 容器里的代码被称为【Vue 模板】

(4)Vue 实例和容器是一一对应的,一个 Vue 实例只能对应一个容器,一个容器只能对应一个 Vue 实例

(5)真实开发中只有一个 Vue 实例,并会配合组件一起使用

(6)一旦 data 中的数据发生改变,页面中用到该数据的地方也会自动更新

Hello World

引入 Vue

<script type="text/javascript" src="../js/vue.js"></script>

html 部分

<!--准备好一个容器-->
<div id="root">
    <h1>{{name}}</h1>
</div>
<script type="text/javascript">
    const x = new Vue({
        el:'#root', //el 用于指定当前 Vue 实例为哪个容器入伍,值通常为 css 选择器字符串,这里 '#root' 也可用 document.getElementById('root')
        data:{  //data 中用于存储数据,数据供 el 指定的容器去使用,值这里暂时写成一个对象
            name:'xxx'
        }
    })
</script>

二、Vue 核心

模板语法

模板语法即 html 中包含了一些 js 语法代码,Vue 模板语法分为两类

(1)插值语法 {{表达式}}

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

(2)指令(以 v- 开头)

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

如:v-bind:href=”表达式” 或 :href=”表达式”

数据绑定

单向数据绑定

单项绑定(v-bind):数据只能从 data 流向页面

当 data 中数据发生变化时,页面上使用该数据的地方也会发生相应变化

<input type="text" v-bind:value="xxx">

双向数据绑定

双向绑定(v-model):数据不仅能从 data 流向页面,还可以从页面流向 data

当 data 中数据发生变化时,页面上使用该数据的地方也会发生相应变化;同时,当页面上用户改变了该数据,则 data 中对应存储的该数据的值也会变

注意:v-model 只能应用在表单类元素(或输入元素)上,如 input、单选框、多选框、select、多行输入

<input type="text" v-model:value="xxx">

注意:v-model:value 可简写为 v-model,因为 v-model 默认收集的就是 value 值

el 和 data 的两种写法

el 的两种写法:

(1)new Vue 时配置 el 属性

(2)先创建 Vue 实例,再通过 vm.$mount(‘#root’) 指定 el 的值

const vm = new Vue({
    //el:'#root', //第一种写法
    data:{
        name:'xxx'
    }
})
vm.$mount('#root') //第二种写法

data 的两种写法:

(1)对象式

(2)函数式

在组件中 data 必须使用函数式,否则会报错

//对象式写法
const vm = new Vue({
    el:'#root',
    data:{
        name:'xxx'
    }
})
//函数式写法,必须返回一个对象,组件中必须使用函数式写法
const vm = new Vue({
    el:'#root',
    data:function(){ //注意这里可简写成 data(){...},但不能写成箭头函数
        console.log(this) //此处的 this 是 Vue 实例对象
        return{
            name:'xxx'
        }
    }
})

注意:由 Vue 管理的函数,一定不要写箭头函数,一旦写了箭头函数,this 就不再是 Vue 实例了,而会去外部找

MVVM

M:模型(Model),对应 data 中的数据

V:视图(View),模板

VM:视图模型(ViewModel),Vue 实例对象

data 中所有的属性,最后都出现在 vm 身上

vm 身上所有的属性及 Vue 原型上所有属性在 Vue 模板中都可以直接使用

MVVM

数据代理

数据代理的基础知识

Object.defineProperty(对象,’属性名’,{配置项}) 方法给一个对象添加/定义一个属性名(ES 6 的语法)

通过该方法添加的属性默认是不能被枚举的(即不能被遍历,如 Object.keys(对象) 不能获取到那个属性),且添加的属性不可被修改、删除

通过添加配置 enumerable:true 后可被枚举

通过添加配置 writable:true 后可被修改

通过添加配置 configurable:true 后可被删除

let person = {
    name:'xx',
    sex:'男'
}
Object.defineProperty(person,'age',{
    value:18,
    enumerable:true,  //控制属性是否可被枚举,默认是 false
    writable:true, //控制属性是否可被修改,默认是 false
    configurable:true //控制属性是否可被删除,默认是 false
})

通过 getter 使得属性和变量绑定,变量值改变则属性值也改变

通过 setter 改变属性的值

let number = 18
let person = {
    name:'xx',
    sex:'男'
}
Object.defineProperty(person,'age',{
    get:function(){  //当读取 person 的 age 属性时(person.age),get 函数(getter)就会被调用,且返回值就是 age 的值
        return number
    },
    set(value){  //当修改 person 的 age 属性时(如 person.age = 19),set 函数(setter)就会被调用,且会收到修改的具体值
        number = value  //因为 getter 中 age 属性和 number 关联所以这里要修改 number 的值才行
    }
})

数据代理

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

let obj = {x:100}
let obj2 = {y:100}
//通过 obj2 操作 obj 中的 x
Object.defineProperty(obj2,'x',{
    get:function(){
        return obj.x
    },
    set(value){
        obj.x = value
    }
})

Vue 中的数据代理

Vue 中的数据代理

Vue 中通过 vm 对象来代理 data 对象中属性的操作(读/写)

读取 vm 中属性时:ViewModel 中属性 —getter —> data 中的属性

修改 vm 中属性时:ViewModel 中属性 —setter —> data 中的属性

数据代理

上图中后边的橙色和紫色线就是 Vue 做的数据代理

Vue 把 data 中数据放到 vm 的 _data 属性中,即 vm._data = data(所以直接 vm.data 是 undefined),但这时要在模板(即视图 View )中显示数据需要 {{_data.name}},通过数据代理把数据放到 vm 上,这里还会做数据代理,就可以通过 {{name}} 来获取使用数据

Vue 中数据代理的好处

更加方便地操作 data 中数据

基本原理

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

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

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

事件处理

事件的基本使用

(1)使用 v-on:事件名@事件名 绑定事件

@click:鼠标点击事件
@scroll:滚动条滚动事件
@wheel:鼠标滚轮滚动事件
@keydown:键盘按下
@keyup:键盘松开

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

(3)methods 中配置的函数不要用箭头函数,否则 this 就不是 vm 了

(4)methods 中配置的函数都是被 Vue 所管理的函数,this 的指向是 vm 或组件实例对象

(5)@click=”demo” 和 @click=”demo($event)” 效果一致,但后者可以传参,$event 是 event 的占位符

(6)@事件名=”xxx” 其中 xxx 可以是简单的语句,但注意只能是对 vm 上有的东西进行操作,vm 上没有的不能进行操作,如 alert 语句就不行,因为 alert 在 window 上,vm 上没有 window,所以 vm 上没有 alert 方法,

<body>
    <div id="root">
        <button v-on:click="show">不传参</button>
        <button @click="show2($event,123)">传参</button>
    </div>
</body>
<script type="text/javascript">
    const vm = new Vue({
        el:'#root',
        data:{
            name:'xxx'
        },
        methods:{
            show(event){
                console.log(event.target.innerText)
                console.log(this)  //此处的 this 是 vm(Vue 实例对象)
            }
            show1:(event)=>{
                console.log(this)  //此处的 this 是 window
            }
            show2(event,number)=>{
                console.log(event,number)
            }
        }
    })
</script>

注意:受 Vue 管理的函数最好都写成普通函数,不要写箭头函数,因为 this 的问题

事件修饰符

prevent:阻止默认事件,如 <a href="http://www.xxx.com" @click.prevent="xxx"> 这样点击后不会跳转,相当于在事件回调函数中写 event.preventDefault()

stop:给子元素添加,阻止事件冒泡,相当于在事件回调函数中写 event.stopPropagation()

once:事件只触发一次

capture:给父元素添加,使用事件的捕获模式,事件触发时是先捕获后冒泡所以子元素事件先响应后响应父元素事件,而给父元素添加 .capture 使得事件在捕获阶段就开始处理,即先响应父元素事件后响应子元素事件

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

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

注意:修饰符可以连续写

键盘事件

Vue 中常用的按键别名,使用方式 @键盘事件.别名="回调函数",如 @keyup.enter="xxx" 表示按下回车时触发回调函数

回车:enter
删除:delete(捕获“删除”和“退格”键)
退出:esc
空格:space
换行:tab(必须配合 keydown 使用)
上:up
下:down
左:left
右:right

Vue 未提供别名的按键,可以使用按键原始的 key 值去绑定,但注意要转为 kebab-case(短横线命名,如 caps-lock)

系统修饰键(ctrl、alt、shift、meta(win键))用法特殊

(1)配合 keyup 使用:先按下修饰键的同时再按下其他键,随后释放其他键,事件才被触发
(2)配合 keydown 使用:正常触发事件

注意:系统修饰符可以连续写,如 @keyup.ctrl.="xxx" 表示按下 ctrl + y

不推荐使用 keyCode(event.keyCode)去指定具体的按键,如 @keyup.13="xxx"

定制按键别名:Vue.config.keyCodes.自定义键名 = 键码

计算属性 computed

当模板中要显示的数据需要进行一些处理时若使用 {{函数()}},则数据一改变,Vue 会重新解析模板,而插值中的函数会被重新调用,这导致效率低

Vue 认为 data 中的数据就是属性,计算属性就是对属性进行处理,计算属性存放在 computed:{}

计算属性:要显示的数据不存在,要通过计算已有属性(注意不是变量)计算得来

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

用法:(1)在 computed 对象中定义计算属性,计算属性必须写 get()(2)在页面中使用 {{计算属性}} 来显示计算的结果

计算属性的优势:与 methods 来实现相比,计算属性内部有缓存机制(复用),当数据没有改变时直接读缓存,效率更高,调式方便

注意:计算属性最终会出现在 vm 上,直接读取使用即可,不用显式调用 .get()

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

data:{
    firstName:'xx',
    lastName:'xxx'
},
computed:{
    fullName:{
        get(){ //读取 fullName 时,get 就会被调用,且返回值作为 fullName 的值
        //get 被调用的时机:(1)初次读取 fullName 时(2)所依赖的数据发生变化时
            console.log(this) //此处 this 是 vm
            return this.firstName + '-' + this.lastName
        }
        set(value){ //修改 fullName 时,set 就会被调用
            ...
            this.firstName = xxx
            this.lastName = xxx
        }
    }
}

当计算属性只读不改时可简写如下

data:{
    firstName:'xx',
    lastName:'xxx'
},
computed:{
    fullName(){  //这个函数可当计算属性的 getter 用
        return this.firstName + '-' + this.lastName
    }
}

监视/侦听属性 watch

监视属性可通过 vm 对象的 $watch() 或 watch 配置来监视指定的属性,当属性变化时,回调函数自动调用,在函数内部进行计算

监视属性必须存在才能进行监视

监视的两种写法

方式一:在 new Vue 时传入 watch 配置,即在 vm 对象中使用 watch

watch:{
    isHot:{
        immedeiate:true, //为 true 时使得初始化时让 handler 调用以下,默认时 false
        handler(newValue,oldValue){ //当 isHot 发生改变时 handler 函数被调用
            console.log(newValue,oldValue)
        }
    }
}

方式二:通过 vm.$watch 监视

vm.$watch('isHot',{
    immedeiate:true, //为 true 时使得初始化时让 handler 调用以下,默认时 false
    handler(newValue,oldValue){ //当 isHot 发生改变时 handler 函数被调用
        console.log(newValue,oldValue)
    }
})

深度监视

Vue 中 watch 默认不会监视对象内部值的改变(只监视一层)

配置 deep:true 可监视对象内部值的改变(监视多层)

注意:Vue 自身是可以监测对象内部值的改变,但 Vue 提供的 watch 默认不可以

考虑到性能效率问题,使用 watch 时根据数据的具体结构决定是否采用深度监视

data:{
    numbers:{
        a:1,
        b:1
    }
}
watch:{
    'number.a':{ //监视多级结构中某个属性的变化,需要加上引号
        handler(){
            console.log('a变了')
        }
    }
    numbers:{
        deep:true, //监视多级结构中所有属性的变化,若不开启这个,则监视的是 numbers,不会监视 numbers 中的属性,而 numbers 是个对象,其中属性变化但该对象地址值不变所以 numbers 也被认为没变
        handler(){
            console.log('numbers变了')
        }
    }
}

监视的简写

当 watch 配置项中只有 handler 时才能简写,使用简写就不能配置 immedeiate 和 deep 了

watch:{
    isHot(newValue,oldValue){
        console.log(newValue,oldValue,this) //这里 this 是 vm 对象,这里不能写箭头函数,若写箭头函数则 this 是 window
    }
}

vm.$watch('isHot',function(newValue,oldValue){
        console.log(newValue,oldValue,this) //这里 this 是 vm 对象,这里不能写箭头函数,若写箭头函数则 this 是 window
    }
})

对比 computed 和 watch

区别

(1)computed 能完成的功能 watch 都可以完成

(2)watch 能完成的功能 computed 不一定能完成,如 watch 可以进行异步操作(如定时器等)

原则

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

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

watch:{
    firstName(val){
        setTimeout(()=>{
            console.log(this) //写成箭头函数后就没有 this 了,会去外部找,这样 this 才是 vm 实例对象,若写普通函数,回调函数由 JS 引擎调用,this 就是 window
        },1000)
    }
}

绑定样式 class 和 style

当样式是动态变化时,class/style 绑定就是专门用来实现动态样式效果的技术

class 绑定

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

(1)字符串写法:class='data中定义的样式类字符串'适用于样式的类名不确定,需要动态指定的情况

(2)表达式是对象:{classA:isA,classB:isB},适用于要绑定的样式个数、名字确定,但要动态决定用不用

<div :class="classObj" @click="changeMood"></div>
//在 vm 中
data:{
    classObj:{
        classA:true,
        classB:false
    }
}
methods:{
    changeMood(){
        //修改 this.classArr
    }
}

(3)表达式是数组:['classA','classB'],适用于要绑定的样式个数、名字不确定

<div :class="classArr" @click="changeMood"></div>
//在 vm 中
data:{
    classArr:['classA','classB']
}
methods:{
    changeMood(){
        //修改 this.classArr
    }
}

style 绑定

(1):style="{color:activeColor,fontSize:fontSize+'px'}"

(2)对象写法::style="styleObj"

在 data 中定义 styleObj:{fontSize:'40px'}

(3)数组写法1::style="[styleObj1,styleObj2]"

在 data 中定义 styleObj1:{fontSize:'40px'},styleObj2:{color:'red'}

(4)数组写法2:style="styleArr"

在 data 中定义 styleArr:[{fontSize:'40px'},{color:'red'}]

条件渲染 v-if 与 v-show

条件渲染指令

(1)v-if 与 v-else、v-else-if,如<div v-if="表达式">x</div>,而 v-else 后无需写判断
v-if 当不渲染时节点在 html 中不存在
v-if 与 v-else、v-else-if 节点需要连在一起写
(2)v-show,如<div v-show="n===1">x</div>
v-show 实现时其实调整的是 display 属性,当不渲染时节点还在

比较 v-if 与 v-show

(1)v-if 不展示的 DOM 元素直接被移除,v-show 不展示的 DOM 元素未被移除,仅仅使用 display 样式隐藏掉,所以使用 v-if 的元素可能无法获取,而使用 v-show 一定可以获取到
(2)若要频繁切换 v-show 较好,因为 v-show 无论是否渲染,节点都在,只是通过 display 来控制是否显示,而若使用 v-if 就是不断向 DOM 中添加或删除节点,效率较低
(3)当条件不成立时,v-if 的所有子节点不会解析(在项目中使用)
(4)当 v-if 中成立时,v-else、v-else-if 中就不再判断直接掠过
(5)<template> 只能和 v-if 搭配,不能和 v-show 搭配(<template>在真正渲染时不会出现这一层)
<template v-if="n===1">
    <h2>a</h2>
    <h2>b</h2>
    <h2>c</h2>
</template>

列表渲染 v-for

可使用 v-for 指令用于展示列表数据

v-for 可遍历数组、对象、字符串、指定次数

v-for 的使用

遍历数组时 v-for="(元素,index) in 数组"

遍历对象时 v-for="(value,key) in 对象"

遍历字符串时 v-for="(char,index) in 字符串"

遍历对象时 v-for="(number,index) in 数字"

<ul>
    <li v-for="(p,index) in persons" :key="p.id">
        {{p.name}},{{p.age}}
    </li>
</ul>

注意:key 会在虚拟 DOM 中出现,在真实 DOM 中没有

key 的作用与原理

react、vue 中的 key 的作用?(key 的内部原理)

在虚拟 DOM 中 key 的作用:

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

【新虚拟 DOM】与【旧虚拟 DOM】的差异比较规则:

(1)旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:
    a)若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
    b)若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
(2)旧虚拟 DOM 未找到与新虚拟 DOM 相同的 key,则创建新的真实 DOM,随后渲染到页面

用 index 作为 key 可能会依法的问题:

(1)若对数据进行逆序添加、逆序删除等破坏顺序操作会产生没必要的真实 DOM 更新,这样界面效果没问题但效率低
(2)若结构中包含输入类的 DOM,则会产生错误 DOM 更新,导致界面有问题

当使用 index 作为 key 时

index作为key

当使用 id 作为 key 时

id作为key

开发中如何选择 key:

(1)最好使用每条数据的唯一标识作为 key,如 id、手机号、身份证号、学号等唯一值
(2)若不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,那么使用 index 作为 key 是没问题的

列表过滤

computed 和 watch 都能实现,列表过滤一般用 computed 更省事,因为 watch 还需在 data 中定义一个要显示的数据的数组

data:{
    keyword:'',
    persons:[{id:'001',name:'aaa'},{id:'002',name:'bbb'}],
    filper:[]  //为使用 watch 定义的数组
},
computed:{
    filtPersons(){
        return this.persons.filter((p)=>{
            return p.name.indexOf(this.keyword)!==-1
        })
    }
},
/*watch:{
    keyword(val){
        this.filper = this.persons.filter((p)=>{
            return p.name.indexOf(val)!==-1
        })
    }
}*/

列表排序

data:{
    keyword:'',
    sortType:0, //0 原顺序,1 降序,2 升序
    persons:[{id:'001',name:'aaa',age:17},{id:'002',name:'bbb',age:18}],
},
computed:{
    filtPersons(){
        const arr = this.persons.filter((p)=>{
            return p.name.indexOf(this.keyword)!==-1
        })
        if(this.sortType){
            arr.sort((p1,p2)=>{ //sort 函数改变原数组
                return this.sortType === 1 ? p2.age-p1.age : p1.age-p2.age
            })
        }
        return arr
    }
},

Vue 监测数据改变的底层原理

Vue 会监视 data 中所有层次的数据

对象更新检测

如何监测对象中的数据?

通过 setter 实现监视,且要在 new Vue 时就传入要监测的数据
(1)对象中后追加的属性,Vue 默认不做响应式处理
(2)如需给后添加的属性做响应式,要使用如下 API
    Vue.set(目标对象,要添加的属性名,属性值)
    this.$set(目标对象,要添加的属性名,属性值)

若不是响应式的数据,该数据的添加和修改都不会引起 Vue 的重新解析模板

Vue 监测对象改变的原理类似如下,这里简化成对象只有一层,即对象的属性不为对象,Vue 真正工作时会逐层递归一直找下去找到所有对象

let data = {
    name:'xx',
    address:'北京'
}
//创建一个监视的实例对象,用于监视 data 中属性的变化
const obs = new Observer(data)
//准备一个 vm 实例对象
let vm = {}
vm._data = data = obs
function Observer(obj){
    //汇总对象中所有的属性形成一个数组
    const keys = Object.keys(obj)
    keys.forEach((k)=>{
        get(){
            return obj[k]
        }
        set(val){
            console.log(`${k}被改了,这里会去解析模板,生成虚拟 DOM 等一些列工作...`)
            obj[k] = val
        }
    })
}

数组更新检测

如何监测对象中的数据?

通过包裹数组更新元素的方法来实现,本质就是做了两件事
(1)调用原生对应的方法(如 push、pop、splice 等)对数组进行更新
(2)重新解析模板,进而更新页面

在 Vue 修改数组中的元素一定要用如下方法

(1)使用 API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
(2)Vue.set(数组,索引,属性值) 或 this.$set(数组,索引,属性值)

对于 data 中的数组(如 arr),若在代码中使用 arr[0]=xxx 来修改 arr 数组中的元素不奏效,在页面中对应显示的数据不更新,需要通过 arr.splice(0,1,xxx)Vue.set(arr,0,xxx)this.$set(arr,0,xxx) 来更新第一个元素

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

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

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

Vue.set(目标对象,要添加的属性名,属性值)vm.$set(目标对象,要添加的属性名,属性值) 向响应式对象(即每个属性有 getter 和 setter)中添加一个属性(向 data 中的对象添加属性),并确保这个新的属性也是响应式的,且触发视图更新

Vue.set(数组,索引,属性值)vm.$set(数组,索引,属性值) 可修改数组中的元素

注意:参数中目标对象不能是 Vue 实例或 Vue 实例的根对象(即 data 本身)

data:{
    student:{
        name:'xx',
        age:18
    }
}
methods:{
    addSex(){
        this.$set(this.student,'sex','男')
        //或 Vue.set(this.student,'sex','男')
    }
}

数据劫持

数据劫持即给对象加上 getter 和 setter,若修改对象,对象上的 setter 劫持到数据变化后会更新数据然后重新解析 DOM 进行后续一系列操作

数据劫持的原理和数据代理一样也是依赖于 Object.defineProperty

数据劫持是响应式的根基

收集表单数据

单行输入 text / 密码 password:v-model 收集的是 value 值,用户输入的就是 value 值

<input type="text" v-model="account"/>
在 data 中需设置
account:''

单选 radio:v-model 收集的是 value 值,所以要给标签配置 value

男<input type="radio" name="sex" v-model="sex" value="male">
女<input type="radio" name="sex" v-model="sex" value="female">
这里要写 value,否则 v-model 默认监听 checked,这样导致一个选中其他也会选中
在 data 中需设置
sex:'female'  //初始默认选女

多选 checkbox:

当没有配置 input 的 value 属性时,收集的是 checked(是否勾选,布尔值),通过 v-model 的值可控制 checked

当配置了 input 的 value 属性时,收集的是 value 组成的数组

学习<input type="checkbox" v-model="hobby" value="study">
吃饭<input type="checkbox" v-model="hobby" value="eat">
在 data 中需设置
hobby:[]

若只需知道是否勾选
<input type="checkbox" v-model="agree">
在 data 中需设置
agree:''

下拉框 select

<select v-model="city">
    <option value="">请选择</option>
    <option value="beijing">北京</option>
    <option value="shanghai">上海</option>
</select>
在 data 中需设置
city:''

多行输入

<textarea v-model="other"></textarea>
在 data 中需设置
other:''

表单提交

<form @submit.prevent="handlerSubmit">...</form>

methods:{
    handlerSubmit(){ //在表单提交时触发
        console.log(JSON.stringfy(this.useInfo))
    }
}

输入的类型限制

<input type="number" v-model.number="age">
其中 type="number" 是原生的控制,使得用户在输入框中不能输入字母等,v-model.number 使得用户输入后 data 中的 age 为数字不为字符串,否则会默认用户输入的东西都存为字符串

v-model 的修饰符

v-model.number:输入字符串转为有效的数字

v-model.lazy:当输入框失去焦点时才更新 data 中的数据

v-model.trim:获取到的数据去除前后的空格

过滤器

过滤器可对要显示的数据进行特定格式化后再显示,适用于一些简单逻辑的处理,过滤器的本质是函数

语法:

注册过滤器:Vue.filter(过滤器名,回调函数) 或 new Vue{filters:{...}}
使用过滤器:{{数据名 | 过滤器名}} 或 v-bind:属性 = "数据名 | 过滤器名"

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

注意:过滤后并没有改变原本的数据,而是产生新的对应的数据

<h2>{{time|timeFormater}}</h2>
这里会把 time 作为参数传给 timeFormater,得到返回值后替换掉 {{}} 中的内容
	

{{time|timeFormater('YYYY_MM_DD')}}</h2> <h2>{{time|timeFormater('YYYY_MM_DD')|mySlice}}</h2> 这里会把 time 作为参数传给 timeFormater,得到返回值作为参数传给 mySlice,得到返回值后替换掉 {{}} 中的内容 局部过滤器:在 vm 实例对象中定义过滤器 filters:{ timeFormater(value,str='YYYY年MM月DD日 HH:mm:ss'){ return dayjs(value).format(str) }, mySlice(value){ return value.slice(0,4) } } 全局过滤器:在 new Vue 之前写定义过滤器 Vue.filter('mySlice1',function(value){ return value.slice(0,4) }) ## 内置指令 v-bind:单向绑定解析表达式可用冒号 : 来简写 v-model:双向数据绑定 v-for:遍历数字/对象/字符串 v-on:绑定事件监听,可简写为 @ v-if:条件渲染(动态控制节点是否存在) v-else:条件渲染(动态控制节点是否存在) v-show:条件渲染(动态控制节点是否显示) ### v-text 作用:向其所在的节点中渲染文本内容 v-text 不支持结构解析,即内容中的标签不能解析,只是当成字符串
相当于
{{变量}}</div>

与插值语法的区别:v-text 会替换掉节点中的内容,而 {{xx}} 则不会,一般还是用插值语法更灵活

v-html

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

v-html 支持结构解析,即内容中的标签可以解析

<div v-html="str"></div>
data:{
    str:'<h2>xxx</h2>'
}
页面上能识别 h2 标签

与插值语法的区别:

(1)v-html 会替换掉节点中所有的内容,{{xx}} 则不会
(2)v-html 可以识别 html 结构

注意:v-html 有安全性问题

(1)在网站上动态渲染任意 html 是非常危险的,容易导致 XSS(冒充用户值守,即盗走用户的 cookie 就可以冒充用户)攻击
(2)一定要在可信的内容上使用 v-html,永远不用用在用户提交的内容上

v-cloak

v-cloak 指令没有值,本质是一个特殊属性,Vue 实例创建完毕并接管容器后会删掉 v-loak 属性

使用 css 配置 v-cloak 可解决网速慢时页面展示出 {{xxx}} 的问题

<head>
    <style>
        [v-cloak]{
            display:none;
        }
    </style>
</head>
<body>
    <div id="root">
        <h2 v-cloak>{{name}}</h2>
    </div>
    <script type="text/javascript" src="xxxxx"></script> <!--假设网速慢该资源请求很慢-->
</body>
<script type="text/javascript">
    new Vue({...})
</script>

上述代码中若没有加 v-cloak,当请求资源的网速慢时,会先渲染没有经过 Vue 解析的 html,页面会显示 {{name}},当请求的资源返回后才会创建 Vue 实例然后解析模板重新渲染,而加了 v-cloak,当请求的资源还没返回时,根据 css 中的设置,页面不会先渲染 {{name}},这样用户体验更好

v-once

v-once 所在节点在初次动态渲染后就视为静态内容了,以后数据的改变不会引起 v-once 所在节点的更新,可以用于优化性能

<div id="root">
    <h2 v-once>初始化的 n 值是:{{n}}</h2>
    <h2>当前的 n 值是:{{n}}</h2>
    <button @click="n++">点击+1</button>
</div>

v-pre

v-pre 指令能跳过所在节点的编译过程(即 Vue 不会再去看该节点里有没有插值、指令等),可利用它跳过没有使用指令语法、插值语法的节点,会加快编译

<div id="root">
    <h2 v-pre>这是个字符串</h2>
    <h2>当前的 n 值是:{{n}}</h2>
    <button @click="n++">点击+1</button>
</div>

自定义指令

定义语法:

(1)局部指令
    new Vue({
        directives:{指令名:配置对象}
    })
    或
    new Vue({
        directives(){指令名:回调函数}
    })
(2)全局指令
    Vue.directive(指令名,配置对象)
    或
    Vue.directive(指令名,回调函数)

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

(1).bind:指令与元素成功绑定时调用
(2).inserted:指令所在元素被插入页面时调用
(3).update:指令所在模板结构被重新解析时调用

注意:指令定义时不加 v-,使用时要加 v-

指令名若是多个单词,使用时用 kebab-case 命名方式(如 v-big-number),不要用 camelCase 命名,定义时可直接拼一起加引号(如 ‘bignumber’)

例子1:定义一个 v-big 指令,和 v-text 功能类似,但会把绑定的数值放大 10 倍

<div id="root">
    <h2>当前的 n 值是:<span v-text="n"></span></h2>
    <h2>放大 10 倍后的 n 值是:<span v-big="n"></span></h2>
    <button @click="n++">点击+1</button>
</div>

new Vue({
    el:'#root',
    data:{
        n:1
    },
    directives:{
        //big 函数被调用的时机:(1)当指令与元素成功绑定时 big 函数会被调用(注意此时元素还没放入页面)(2)指令所在的模板被重新解析时
        big(element,binding){ //第1个参数为 DOM 元素,第2个参数为绑定对象,将元素和指令进行绑定
            console.log(this) //这里的 this 是 window
            element.innerText = binding.value * 10
        }
    }
})

例子2:定义一个 v-fbind 指令,和 v-bind 功能类似,但会把绑定的 input 元素默认获取焦点

<div id="root">
    <input type="text" v-fbind:value="n">
    <button @click="n++">点击+1</button>
</div>

new Vue({
    el:'#root',
    data:{
        n:1
    },
    directives:{
        fbind:{
            //指令与元素成功绑定时(一上来,还没放到页面)
            bind(element,binding){
                console.log(this) //这里的 this 是 window
                element.value = binding.value
            },
            //指令所在元素被插入页面时
            inserted(element,binding){
                console.log(this) //这里的 this 是 window
                element.focus()
            },
            //指令所在的模板被重新解析时
            update(element,binding){
                console.log(this) //这里的 this 是 window
                element.value = binding.value
            }
        }
    }
})

注意:上面两个例子定义的指令是局部指令,只能在当前 Vue 实例中使用

例子3:全局指令

Vue.directive('big',function(element,binding){
    element.innerText = binding.value * 10
})

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

生命周期

生命周期又称为生命周期回调函数、生命周期函数、生命周期钩子,是 Vue 在关键时刻调用的一些特殊名称的函数

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

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

生命周期1

生命周期2

beforeCreate():将要创建,是指数据监测、数据代理创建之前,即还没有 _data

created():创建完毕

beforeMount():将要挂载

mounted():挂载完毕,Vue 完成模板解析并把初始的真实 DOM 元素放入页面后(挂载完毕)调用 mounted,mounted 函数中的 this 为 Vue 实例对象(vm)或 组件实例对象,在 mounted 中完成发生 ajax 请求、启动定时器、绑定自定义事件、订阅消息等初始化操作

beforeUpdate():将要更新

Updated():更新完毕

beforeDestroy():将要销毁,在这完成如清除定时器、解绑自定义事件、取消订阅消息等收尾工作

destroyed():销毁完毕

vm.$destroy():用于完全销毁一个实例,清理它与其他实例的连接,解绑它的全部指令及自定义事件的监听器(注意不会解绑原生 DOM 事件,如 click 等),触发 beforeDestroy 和 Destroy 的钩子。一般不用 vm.$destroy 方法,最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期

关于销毁 Vue 实例:

(1)销毁后借助 Vue 开发者工具看不到任何信息
(2)销毁后自定义事件会失效,但原生 DOM 事件依然有效(如 click,继续点击依然会触发,但数据、页面不更新)
(3)一般不会在 beforeDestroy 操作数据,因为即便操作数据,也不会再触发更新流程了

三、Vue 组件化编程

模块与组件、模块化与组件化

模块

模块向外提供特定功能的 js 程序,一般就是一个 js 文件

作用:由于 js 文件很多很复杂所以通过模块化可复用 js,简化 js 的编写,提高 js 运行效率

组件

组件是用来实现局部特定功能效果的代码和资源的集合(如 html、css、js、image 等)

作用:由于一个界面的功能很复杂,通过组件可复用编码,简化项目编码,提高运行效率

模块化

当应用中的 js 都以模块来编写的,这个应用就是一个模块化的应用

组件化

当应用中的功能都是多组件的方式来编写的,这个应用就是一个组件化的应用

Vue 中使用组件

组件名

一个单词时的两种写法

(1)首字母小写
(2)首字母大写

多个单词时的两种写法

(1)kebab-case 命名,如 my-school
(2)CamelCase 命名,如 MySchool(需要 Vue 脚手架)

组件名尽可能回避 html 中已有的元素名称

可再定义组件时使用 name 配置项指定组件在开发者工具中呈现的名字

组件标签

两种写法

(1)<school></school>
(2)<school/>

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

三大步骤

(1)定义组件(创建)

(2)注册组件

(3)使用组件(写组件标签)

定义组件

使用 const xxx = Vue.extend(options) 或简写 const xxx = options 创建,其中 options 和 new Vue(options) 时传入的那个 options 几乎一样,但也有区别如下:

(1)el 不要写,因为最终所有的组件都要经过一个 vm 管理,由 vm 中的 el 决定服务哪个容器

(2)data 必须写成函数,因为避免组件被复用时,数据存在引用关系(即一个组件中数据变量另一个也变)

备注:使用 template 可以配置组件结构

注册组件

(1)局部注册:靠 new Vue 的时候传入 components 选项

(2)全局注册:靠 Vue.component(‘组件名’,组件)

编写组件标签

<school></school>

非单文件组件

非单文件组件即一个文件中包含有 n 个组件

<div id="root">
    <school></school>
</div>

//创建组件
const school = Vue.extend({
    //组件定义时,一定不要定义 el 配置项,因为最终所有的组件都要被一个 vm 管理,由 vm 决定服务于哪个容器
    template:`
        <div>{{schoolName}}</div>
    `,
    data(){  //data 一定要用函数式,避免使用对象式导致当前组件改变数据后复用的组件中数据也被改变
        return {
            schoolName:'xxx'
        }
    },
    methods:{
        showName(){
            console.log(xxx)
        }
    }
})
const student = Vue.extend({...})
//全局注册组件
Vue.component('student',student)
//创建 vm
new Vue({
    el:'#root',
    data:{
        msg:'hello'
    },
    //注册组件(局部注册),这种方式用的多
    components:{
        school:school
    }
})

单文件组件

单文件组件即一个文件中只包含有 1 个组件,单文件组件后缀都是 .vue

<template>
    <!--组件的结构-->
    <div class="xxx"></div>
</template>
<script>
    //组件交互相关的代码(data、methods 等)
    export default{
        name:'xx',
        data(){return{...}}
    }
</script>
<style>
    /*组件的样式*/
    .xxx{...}
</style>

在 App.vue 中

<template>
    <div>
        <xxx></xxx>
        <xxxx></xxxx>
    </div>
</template>
<script>
    import xxx from './xxx.vue'
    import xxxx from './xxxx.vue'
    export default{
        name:'xx',
        components:{xxx,xxxx}
    }
</script>

在 main.js 文件中

import App from './App.vue'

new Vue({
    el:'#root',
    template:`<App></App>`,
    components:{App}
})

在 html 文件中

<div id="root"></div>
<script type="text/javascript" src="./main.js"></script> 

组件嵌套

在组件定义中可使用 components 配置项添加子组件

const child = Vue.extend({...})
const father = Vue.extend({
    name:'father',
    template:`<child></child>`,
    data(){return {...}},
    components:{child}
})

在开发中一般会定义一个 app 组件用于管理所有组件,即作为所有组件的祖先,一人之下(vm 之下)万人之上(其他组件)

VueComponent

school 组件的本质是一个名为 VueComponent 的构造函数,且不是程序员定义的,是 Vue.extend 生成的

只需要写 <school></school><school/>,Vue 解析时会创建 school 组件的实例对象,即 Vue 会自动执行 new VueComponent(options)
注意每次调用 Vue.extend 返回的都是一个全新的 VueComponent

关于 this 指向

组件配置中

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

new Vue(options) 配置中

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

VueComponent 的实例对象即为组件实例对象,本文简称vc,Vue 的实例对象,本文简称 vm

Vue 实例与组件实例

组件是可复用的 Vue 实例,因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,如 data、computed、watch、methods 以及生命周期钩子等

仅有的例外是 el 是 Vue 实例特有的选项,且组件中 data 必须是一个函数,因此每个实例可维护一份被返回对象的独立的拷贝

一个重要的内置关系

一个重要的内置:

VueComponent.prototype.__proto__ === Vue.prototype

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

重要的内置关系

上图中 VueComponent.prototype.proto 本应指向 Object 的原型对象上,但 Vue 中把它指向了 Vue 的原型对象,这就是 VueComponent.prototype.proto === Vue.prototype

四、Vue 脚手架

Vue 脚手架(Vue CLI,Vue command line interface)是 Vue 官方提供的标准化开发工具(开发平台)

Vue 脚手架隐藏了所有 webpack 相关的配置,若项查看具体的 webpack 配置可执行 vue inspect > output.js

使用

全局安装 npm install -g @vue/cli

切换到要创建项目的目录后

创建 Vue 项目 vue create xxx

启动项目 npm run serve

执行 npm run serve 后先去执行项目的入口文件 main.js

main.js 中的 render 函数

vue.js 与 vue.runtime.xxx.js 的区别:

(1)vue.js 是完整版的 Vue,包含核心功能 + 模板解析器
(2)vue.runtime.xxx.js 是运行版的 Vue,只包含核心功能,没有模板解析器

因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去指定具体内容

因此 Vue 2 中的 main.js

new Vue({
    el:'#app',
    render: h => h(App)
})

在 main.js 中 import Vue form 'vue' 引入的 Vue 根据 node_modules/vue/package.json 中 module 配置,引入的是 node_modules/vue/dist/vue.runtime.esm.js,这是个残缺的 vue,完整的 vue 在 vue.js 中,该文件包含 vue 核心和模板解析器

其中 render: h => h(App)

render(createElement){
    return createElement('h1','hello')
}

修改脚手架默认配置

vue inspect > output.js

上述命令可输出 vue 脚手架的默认配置到 output.js 文件,但只是输出给程序查看,修改 output.js 文件无效

可通过 vue.config.js 文件自定义脚手架配置,该文件需要创建在和 package.json 同级的目录下,该文件会被 @vue/cli-service 自动加载,详情可见官方文档

module.exports = {
    pages:{
        index:{
            //入口文件
            entry:'src/main.js'
        }
    }
}

ref 属性

ref 属性被用来给元素或子组件注册引用信息(id 的替代者)

ref 属性应用在 html 标签上获取的是真实 DOM 元素,应用在组件标签上是组件实例对象(vc)

<h1 ref="xxx">xxx</h1>
<组件 ref="xxxx"></组件>

通过 this.$refs.xxx 来获取,相当于原生中 document.getElementById('xxx')

props 配置

props 让组件接收外部传来的数据

props 是只读的,Vue 底层会监测对 props 值的修改,若进行了修改就会发出警告

若确实需要修改,则需赋值 props 的内容到 data 中(Vue 底层实现时先接收到 props 数据后定义 data,若有重名也是 props 优先级高于 data,虽然这不允许控制台会报错),如在 data 中定义 name:this.name,后者的 name 就是 props 接收到的 name,然后去修改 data 中的数据

传递数据:

在父组件中使用子组件

<子组件 name="xxx" sex="女" :age="18">

接收数据:

在子组件中

<h2>{{name}}</h2>
<h2>{{sex}}</h2>
<h2>{{age+1}}</h2>

//简单声明接收
props:['name',sex','age']
//接收的同时对数据进行类型限制
props:{
    name:String,
    sex:String,
    age:Number
}
//接收的同时对数据进行类型限制 + 默认值的指定 + 必要性的限制
props:{
    name:{
        type:String, //name 的类型是字符串
        required:true  //name 是必要的
    },
    sex:{
        type:String,
        required:true
    },
    age:{
        type:Number, //name 的类型是字符串
        default:100
    }
}

mixin 混入

mixin(混入)可以把多个组件共用的配置提取成一个混入对象

使用方式:

(1)定义混合

{
    data(){...}
    methods:{...}
}

(2)使用混入

1)全局混入:在 main.js 中添加 
import {xxx} from '../mixin.js'
Vue.mixin(xxx)
2)局部混入:在组件中添加
import {xxx} from '../mixin.js'
mixins:['xxx']

创建 src/mixin.js 文件

export const mixin = {
    methods:{
        showName(){
            console.log(this.name)
        }
    },
    mounted(){
        console.log('hello')
    }
}
export const mixin2 = {
    data(){return{...}}
}

在组件中

import {mixin,mixin2} from '../mixin.js'

export default{
    name:'组件名',
    data(){return{...}}
    mixins:[mixin,mixin2]
}

组件中和混入中都有定义的东西会合并起来使用(若 data 或 methods 等重名了以组件中定义的为主),而同一生命周期函数两者定义的都会调用,且混合中定义的先调用

plugins 插件

插件用于增强 Vue

Vue 的插件本质是个对象,并且该对象中必须包含 install,install 的第一个参数是 Vue 构造函数,第二个及之后的参数是插件使用者传递的数据

新建文件 src/plugins.js 定义插件

export default {
    install(Vue,a,b,c){ //第一个参数是 Vue 构造函数
        console.log(a,b,c)
        //全局过滤器
        Vue.filter('xxx',function(value){...})
        //定义全局指令
        Vue.directive('xxx',{...})
        //定义混入
        Vue.mixin({...})
        //给 Vue 原型上添加一个方法(vm 和 vc 就都能用了)
        Vue.prototype.xxx = () => {...}
        Vue.prototype.$myMethod = function(){...}
        Vue.prototype.$myProperty = xxx
    }
}

在 main.js 文件中使用插件

import plugins from './plugins.js'
//应用/使用插件
Vue.use(plugins,1,2,3)

在组件中可直接使用插件中定义的东西

scoped 样式

scoped 样式让样式在局部生效,即在组件内部生效,防止各组件间样式的冲突

<style scoped></style>

组件化编码流程

(1)拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突

(2)实现动态组件:考虑好数据的存放位置

数据若是一个组件在用:放在组件自身即可
数据若是多个组件再用:放在它们共同的父组件上(状态提升)

(3)实现交互:从绑定事件开始

组件自定义事件

组件的自定义事件是一种组件间通信的方式,适用于子组件给父组件传数据

使用场景:A 是父组件,B 是子组件,B 想给 A 传数据就要在 A 中给 B 绑定自定义事件(事件的回调在 A 中),在 B 中触发该事件

绑定自定义事件

通过父组件给子组件传递函数类型的 props 实现子给父传递数据

在父组件中
<子组件 :参数名="父组件中的函数"/>
父组件中的函数(xxx){}

在子组件中
props:['收到的参数名']
this.收到的参数名(xxx) //调用父组件函数来给父组件传数据

通过父组件给子组件绑定一个自定义事件实现子给父传递数据(第一种写法,使用 @ 或 v-on)

在父组件中绑定自定义事件
<子组件 @自定义事件名="父组件中的函数"/>
//若想只触发一次:
//<子组件 @自定义事件名.once="父组件中的函数"/>
父组件中的函数(xxx){}

在子组件中触发自定义事件
this.$emit('父组件自定义的事件名',数据) //作为点击等事件的回调函数使用,就会触发父组件中自定义的事件从而执行父组件中的函数

通过父组件给子组件绑定一个自定义事件实现子给父传递数据(第二种写法,使用 ref)

在父组件中绑定自定义事件
<子组件 ref="子组件别名"/>
mounted(){
    this.$refs.子组件.$on('父组件自定义的事件名',this.父组件中的函数)
    //若想只触发一次:
    //this.$refs.子组件.$once('父组件自定义的事件名',this.父组件中的函数)
    //this.$refs.子组件.$on('父组件自定义的事件名',function(xx){...//这里的 this 是触发事件的子组件实例对象})
    //this.$refs.子组件.$on('父组件自定义的事件名',(xx)=>{...//这里的 this 是父组件实例对象})
}

父组件中的函数若要接收多个数据 父组件中的函数(name,...param){...},params 中就是第一个参数之后的所有参数都在 params 数组中

组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符

通过 this.$refs.子组件.$on('xxx',回调) 绑定自定义事件时,回调要么在 methods 中,要么用箭头函数,否则 this 指向会出问题

解绑自定义事件

解绑一个自定义事件

在子组件中
this.$off('父组件自定义的事件名')

解绑多个自定义事件

在子组件中
this.$off(['父组件自定义的事件名1','父组件自定义的事件名2'])

解绑所有自定义事件

在子组件中
this.$off()

注意事项

1、若在组件中 this.$destroy() 销毁当前组件实例,销毁后所有该实例自定义事件全都不奏效

2、若在子组件上绑定 click 事件 <子组件 @click="xxx"> 不会触发原生 click 事件,而是会把 click 看成自定义事件,需要在子组件中使用 this.$emit('click') 才能触发点击事件

3、若要给子组件绑定原生 click 事件可添加 .native,即 <子组件 @click.native="xxx"> 即可直接触发点击事件无需使用 $emit 触发

全局事件总线

全局事件总线是一种组件间通信的方式,适用于任意组件间通信

(1)安装全局事件总线:在 main.js 中

new Vue({
    beforeCreate(){
        Vue.prototype.$bus = this //安装全局事件总线
    }
})

(2)若组件1想接收数据,在组件1中绑定自定义事件到 $bus 组件实例对象上,事件的回调写在组件1自身

mounted(){
    this.$bus.$on('自定义事件名',(data)=>{
        console.log('组件1收到数据',data)
    })
}

或
methods(){
    demo(data){...}
}
mounted(){
    this.$bus.$on('自定义事件名',this.demo)
}

(3)在组件2中触发自定义事件给组件1传数据

this.$bus.$emit('自定义事件名',数据)

(4)在组件1组件实例销毁之前解绑当前组件用到的自定义事件

beforeDestroy(){
    this.$bus.$off('自定义事件名')
}

消息订阅与发布

消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信

消息订阅与发布借助于 pubsub-js 库

安装 npm install i pubsub-js -D

在接收数据的组件中订阅消息

import pubsub from 'pubsub-js'

mounted(){
    const pubId = pubsub.subscribe('消息名',(msgName,data)=>{ //第一个参数是消息名,第二个参数是数据
        consoe.log(this) //这里 this 是组件实例对象
    })
}

或
methods(){
    demo(msgName,data){...}
}
mounted(){
    const pubId = pubsub.subscribe('消息名',this.demo)
}

在发送数据的组件中发送数据

import pubsub from 'pubsub-js'
mounted(){
    pubsub.publish('消息名',数据)
}

在接收数据的组件中取消订阅

beforeDestroy(){
    pubsub.unsubscribe(this.pubId)
}

Vue 封装的过度与动画

过度与动画的作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名

动画

写法:

(1)准备好样式

元素进入的样式
v-enter:进入的起点
v-enter-acitve:进入过程中
v-enter-to:进入的终点

元素离开的样式
v-leave:离开的起点
v-leave-acitve:离开过程中
v-leave-to:离开的终点

(2)使用 <transition> 包裹要过度的元素,并配置 name 属性

(3)若有多个元素需要过度,则使用 <transition-group>,且每个元素都要指定 key 值

动画效果

例子:实现组件相左滑动离开页面以及从页面外向右滑动进来

方式一:使用 css 实现

@keyframes xxx{
    from{
        transform: translateX(-100%);
    }
    to{
        transform: translateY(0px);
    }
}
.come{
    animation: xxx 1s;
}
.go{
    animation: xxx 1s reverse;
}

方式二:在 Vue 中实现

<transition appear> <!--若添加 appear 属性则页面一渲染就会自动播放动画,真实 DOM 中没有 transition-->
    <h1 v-show="isShow">hello</h1>
</transition>

@keyframes xxx{
    from{
        transform: translateX(-100%);
    }
    to{
        transform: translateY(0px);
    }
}
//进入的过程
.v-enter-active{ //若在 <transition> 中设置了 name 属性为 xxxx,这里的类需要写成 .xxxx-enter-active
    animation: xxx 1s;
}
//离开的过程
.v-leave-active{ //若在 <transition> 中设置了 name 属性为 xxxx,这里的类需要写成 .xxxx-leave-active
    animation: xxx 1s reverse;
}

过度效果

<transition name="demo" appear>
    <h1 v-show="isShow">hello</h1>
</transition>

h1{
    transition: 1s linear;
}
//进入的起点
.demo-enter{
    transform: translateX(-100%);
}
//进入的终点
.demo-enter-to{
    transform: translateX(0);
}
//离开的起点
.demo-leave{
    transform: translateX(0);
}
//离开的终点
.demo-leave-to{
    transform: translateX(-100%);
}

上述样式可简写为

//进入的起点、离开的终点
.demo-enter,.demo-leave-to{
    transform: translateX(-100%);
}
//进入的终点、离开的起点
.demo-enter-to,.demo-leave{
    transform: translateX(0);
}
.demo-enter-active,.demo-leave-active{
    transition: 1s linear;
}

若多个元素有同样的过度效果

<transition-group name="demo" appear>
    <h1 v-show="isShow" key="1">hello</h1>
    <h1 v-show="isShow" key="2">world</h1>
</transition-group>

集成第三方动画

如第三方动画库 animate.css,官网

安装 npm install animate.css -D

引入

import 'animate.css'

使用

<transition-group 
    appear
    name="animate__animated animate__bounce"
    enter-active-class="animate__swing"
    leave-active-class="animate__backOutRight"
> <!--name 值是固定的,在 enter-active-class 中设置进入动画,在 leave-active-class 中设置离开动画,具体动画效果可在官网查看-->
    <h1 v-show="isShow" key="1">hello</h1>
    <h1 v-show="isShow" key="2">world</h1>
</transition-group>

利用 Vue 脚手架巧妙解决跨域问题

解决跨域的方法

(1)CORS:后端中在响应头中配置 CORS 来解决跨域问题,但这种方式不安全,使得所有人都能访问服务器
(2)jsonp:利用 script 标签中的 src 在引入资源时不用遵循同源策略,这种方式需要前端后端配合,但这种方法在开发中用的少
(3)代理服务器:借助一台与前端地址同源的代理服务器,前端向代理服务器发送请求,代理服务器转发给服务器,服务器响应数据给代理服务器再转发至前端,因为服务器与服务器之间的通信没有什么同源策略,ajax 只有在前端中才有
    开启代理服务器的方式:(1)nginx(2)借助 vue-cli

利用 Vue 脚手架开启代理服务器解决跨域问题

方式一

在 vue.config.js 文件中配置如下

devServer:{
    proxy:'http://localhost:3000', //目标服务器地址
}

在代码中发送请求时写的请求地址和前端同源,path 后缀和服务器接口一致即可,如前端在 http://localhost:8080,要访问服务器的 http://localhost:3000/xxx,在代码要写成 http://localhost:8080/xxx

注意:代理服务器不是把所有请求都转发给服务器,当请求的资源在代理服务器有的时候就不转发给服务器,代理服务器的根路径就是 public 文件夹所在位置

优点:配置简单,请求资源时直接发给前端即可

缺点:上面这种方式中不能配置多个代理,并且不能灵活地控制走不走代理(工作方式:只有当代理服务器中没有所要资源时转发给服务器(优先匹配前端资源))

方式二

在 vue.config.js 文件中配置如下

devServer:{
    proxy:{
        '/api':{
            target:'http://localhost:3000', //目标服务器地址
            pathRewrite:{'^/api':''},
            ws:true,  //用于支持 websocket
            changeOrigin: true //用于控制请求头中的 host 值,即若为 false,服务器显示收到的请求来自 http://localhost:8080,若为 true 显示的是来自 http://localhost:3000
            //changeOrigin 默认是 true
        },
        '/demo':{
            target:'http://localhost:3001',
            pathRewrite:{'^/demo':''},
        }
    }
}

如前端在 http://localhost:8080,要访问服务器的 http://localhost:3000/xxx,在代码要写成 http://localhost:8080/api/xxx,若向访问前端的 xxx 资源则是要不加 /api 就可以不走代理服务器

优点:可以配置多个代理,且可以灵活控制请求是否走代理

缺点:配置略微繁琐,请求资源时必须加前缀

发送 ajax 请求(vue-resource)

发送 ajax 请求的方式有 xhr、jQuery、axios、fetch,其中 jQuery 和 axios 都是对 xhr 的二次封装,fetch 是和 xhr 平级的

在 Vue 中还可以借助 vue-resource 插件库发送 ajax 请求,在 Vue 1.0 中用的多,官方已不维护。vue-resource 也是对 xhr 的封装

安装 npm i vue-resource -D

在 main.js 文件中

import vueResource from 'vue-resource'
Vue.use(vueResource)  //使用 vue-resource 插件库后 vm 和 vc 上都会出现 $http

在组件代码中使用 vue-resource,用法和 axios 相同

this.$http.get('url地址').then(
    response=>{...},
    error=>{...}
)

slot 插槽

作用:让父组件可以向子组件指定位置插入 html 结构,也是一种组件间通信的方式,适用于父组件向子组件传结构

分类:默认插槽、具名插槽、作用域插槽

默认插槽

子组件中

<div>
    <h1>xxx</h1>
    <slot>这里可以设置默认值,当没有传递具体结构时会出现该默认值</slot>
<div>

父组件中

<子组件>
    <h2>xxx</h2> <!--这部分内容会替换子组件中 slot 的位置-->
</子组件>

插槽中的样式可在父组件中定义(解析好父组件后把带样式的结构替换子组件中的 slot)也可在子组件中定义(解析后把结构传入子组件中的 slot 然后再付上样式)

具名插槽

当需要在一个组件中定义多个插槽,则需要给每个 slot 加上 name 属性

子组件中

<div>
    <h1>xxx</h1>
    <slot name="aaa">这里可以设置默认值,当没有传递具体结构时会出现该默认值</slot>
    <slot name="bbb">这里可以设置默认值,当没有传递具体结构时会出现该默认值</slot>
<div>

父组件中

<子组件>
    <h2 slot="aaa">xxx</h2>
    <a slot="bbb" href="xxxxx">xx</a>
</子组件>

注意若父组件中要放在 slot 中有多个元素可使用 <template> 把这些元素包裹起来,并且指定插槽时可使用 v-slot:aaa 来代替 slot="aaa"

<子组件>
    <template v-slot:aaa>
        <h2>xxx</h2>
        <h2>xxx</h2>
    </template>
</子组件>

作用域插槽

当数据在设置了插槽的子组件中,使用该组件的父组件若要用到这些数据就可以使用作用域插槽

即数据在组件自身,但根据数据生成的结构需要组件的使用者来决定

子组件中

<div>
    <h1>xxx</h1>
    <slot :data1="data1" msg="hello">这里可以设置默认值,当没有传递具体结构时会出现该默认值,这里也可以设置 name,在父组件中相应的使用 slot="xx"</slot>
<div>

data(){
    return{
        data1:xxx
    }
}

父组件中

<子组件>
    <template scope="somedata">
        <h2>{{somedata.data1}}</h2>
        <h2>{{somedata.msg}}</h2>
    </template>
</子组件>

<子组件>
    <template scope="{data1}">
        <h2>{{data1}}</h2>
    </template>
</子组件>

<子组件>
    <template slot-scope="{data1}">
        <h2>{{data1}}</h2>
    </template>
</子组件>

五、组件间通信

(1)父子组件:props

父传子数据

父组件中
`<子组件 变量名="值">`
子组件中
props:['变量名']

子传父

父组件中
`<子组件 变量名="函数名">`
子组件中
props:['变量名']
调用函数来修改父组件中的数据

(2)绑定自定义事件:$emit(多用于父子间子给父)

(3)全局事件总线(基于自定义事件)(任意组件间通信)这种方式用得比消息订阅发布的方式多

(4)消息订阅与发布(任意组件间通信)

(5)插槽(父给子传 html 结构)、作用域插槽(子中的数据父需要使用)

(6)Vue3 中祖与后代组件间通信的 provide 与 inject

六、Vuex

vuex 是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,

vuex 对 vue 应用中多个组件的共享状态进行集中式的管理(读/写)

vuex 也是一种组件间通信的方式,且适用于任意组件间的通信

适用场景

(1)多个组件依赖于同一状态

(2)来自不同组件的行为需要变更同一状态

工作原理

vuex

Vuex 的使用

(1)安装 npm i vuex@3

注意:vue2 只能使用 vuex3,vuex4 只能用在 vue3,这里先以 vue2 + vuex3 为例

(2)引入

在 main.js 中

import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'

new Vue({
    el:'#app',
    render: h=>h(App),
    store,
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

(3)创建 store

创建 src/store/index.js 文件用于创建 Vuex 中最核心的 store

import Vue from 'vue'
import Vuex from 'vuex'
//使用 Vuex 插件
Vue.use(Vuex)

//准备 actions 用于响应组件的动作
const actions = {}
//准备 mutations 用于操作 state 数据
const mutations = {}
//准备 state 用于存储数据
const state = {}
//准备 getters 用于将 state 中的数据进行加工
const getters = {}

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters
})

当 state 中的数据需要经过加工后再使用时,可使用 getters 加工(优点类似 computed)

(4)组件中读取/修改 vuex 数据

组件中读取 vuex 数据:this.$store.state.sumthis.$store.getters.sum,若在模板中读就不加 this

组件中修改 vuex 中的数据:this.$store.dispatch('action中的方法名',数据)this.$store.commit('mutations中的方法名',数据)

(5)组件中使用 mapState/mapGetters/mapMutations/mapActions

借助 mapState/mapGetters 生成计算属性,从 state/getters 中读取数据

借助 mapMutations/mapGetters 生成对应方法,方法中会调用 commit/dispatch 去联系 mutations/actions

注意:若没有网络请求或其他业务逻辑,组件中也可以越过 actions,即不写 dispatch,直接写 commit

例子

在 store.js 文件中

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const actions = {
    inc(context,value){ //第一个参数是上下文对象,该对象上有 $commit、$dispatch 等方法以及 state,第二个参数是 dispatch 传来的值
        console.log(context.state.sum)
        context.commit('INC',value) //会调用 mutations 中的 INC 函数
    }
}
const mutations = {
    INC(state,value){
        state.sum += value
    }
}
const state = {
    sum:0
}
const getters = {
    bigSum(state){
        return state.sum * 10
    }
}

export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters
})

在组件中

<h1>{{$store.state.sum}}</h1>
<h1>{{$store.getters.bigSum}}</h1>

methods:{
    increment(){
        console.log(this.$store.state.sum)
        this.$store.dispatch('inc',this.n) //会调用 actions 中的 inc 函数
    }
}

由于在 actions 中没有特别的业务逻辑,所以可以直接跳过 actions(不用 dispatch),由组件直接使用 commit 调用 mutation 里的函数,所以上述代码可简写为

在 store.js 文件中

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const actions = {
}
const mutations = {
    INC(state,value){
        state.sum += value
    }
}
const state = {
    sum:0
}

export default new Vuex.Store({
    actions,
    mutations,
    state
})

在组件中

<h1>{{$store.state.sum}}</h1>

methods:{
    increment(){
        console.log(this.$store.state.sum)
        this.$store.commit('INC',this.n) //会调用 actions 中的 inc 函数
    }
}

在组件里用上 mapState/mapGetters/mapMutations/mapActions

<button @click="{{increment(n)}}">{{sum}}</button>

import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'

computed:{
    ...mapState({sum:'sum',xxx:'xx'}) //对象写法,键为本组件中使用的名字,值为 state 中的变量名
    ...mapGetters({bigSum:'bigSum'})
    //或
    //...mapState(['sum','xxx']) //当组件中使用的名字和 state 中的变量名一致时可使用数组写法
    //...mapGetters(['bigSum'])
    //上述 mapState 写法相当于
    /*sum(){
        return this.$store.state.sum
    }
    xxx(){
        return this.$store.state.xxx
    }*/

    ...mapMutations({increment:'INC',decrement:'DEC'}) //对象写法
    //或
    //...mapMutations(['INC']) //对数组写法,此时在 @click 中应使用 INC 方法
    //上述 mapMutations 写法相当于
    /*increment(){
        return this.$store.commit('INC',this.n)
    }*/

    ...mapActions({increment1:'INC1',decrement1:'DEC1'}) //对象写法
    //或
    ...mapActions(['INC1']) //数组写法
    //上述 mapMutations 写法相当于
    /*increment1(){
        return this.$store.dispatch('INC1',this.n)
    }*/
}

vuex 模块化

在 store/index.js 中根据按功能将 actions、mutations、state、getters 包起来,也可以按功能将这些东西放到不同 js 文件中,然后在 store/index.js 中引入

const countOptions = {
    namespace:true,
    actions:{...},
    mutations{...},
    state:{...},
    getters:{...}
}
const persionsOptions = {
    namespace:true,
    actions:{...},
    mutations{...},
    state:{...},
    getters:{...}
}
export default new Vuex.Store({
    modules:{
        countAbout:countOptions,
        personAbout:persionsOptions
    }
})

在组件中

<button @click="{{increment(n)}}">{{sum}}</button>
<h1>{{sum}}</h1>

...mapState('countAbout',['sum','xxx'])
...mapGetters('countAbout',['bigSum'])
...mapMutations('countAbout',['INC'])

this.$store.getters['countAbout/bigSum']
this.$store.dispatch('countAbout/XXX',this.n)
this.$store.commit('countAbout/INC',this.n)

七、浏览器本地存储 webStorage

webStorage 存储内容大小一般支持 5MB 左右(不同浏览器可能不一样)

浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制

xxxxStorage.setItem('key','value')
该方法接收一个键和值作为参数,会把键值对添加到存储中,若键名存在,则更新其对应的值
xxxStorage.getItem('key')
该方法接收一个键名作为参数,返回键名对应的值
xxxStorage.removeItem('key')
该方法接收一个键名作为参数,并把该键名从存储中删除
xxxStorage.clear()
该方法会清空存储中的所有数据

sessionStorage 存储的内容会随着浏览器窗口关闭而消失

localStorage 存储的内容需要手动清除才会消失

xxxStorage.getItem(‘key’) 若 key 对应的 value 获取不到返回的是 null

Json.parse(null) 的结果依然是 null,不是 undefined

八、路由

路由就是一组 key(路径)-value(function 或 component) 的对应关系,多个路由需要经过路由器管理

路由分类

(1)后端路由

后端路由中 value 是 function,用于处理客户端提交的请求

工作过程:服务器接收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据

(2)前端路由

前端路由中 value 是 component,用于展示页面内容

工作过程:当浏览器的路径改变时,对应的组件就会显示

vue-router

vue-router 是 vue 的一个插件库,专门用来实现 SPA 单页面应用

单页 Web 应用中整个应用只有一个完整的页面,点击页面中的导航链接不会刷新页面,只会做页面的局部更新,数据需要通过 ajax 请求获取

注意事项

路由组件通常存放在 pages 文件夹,一般组件通常存放在 components 文件夹

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

每个组件都有自己的 $route 属性,里面存储着自己的路由信息

整个应用只有一个 router,可通过组件的 $router 属性获取到

使用

安装 npm i vue-router@3 -D

注意:vue2 只能使用 vue-router3,vue-router 4 只能用在 vue3 中使用,这里先以 vue2 + vuex3 为例,所以安装 vue-router 3

引入,在 main.js 中

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './router/index.js'
import store from './store/index.js'
Vue.use(VueRouter)
new Vue({
    el:'#app',
    render: h=>h(App),
    router
    store,
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

创建 src/router/index.js 文件专门用于创建整个应用的路由器

import VueRouter from 'vue-router'
import About from './components/about.vue'
import Home from './components/home.vue'
//创建并暴露一个路由器
export default new VueRouter({
    routes:[
        {
            path:'/about',
            component:About
        },
        {
            path:'/home',
            component:Home
        }
    ]
})

组件中使用 <router-link> 实现路由的切换(通过 active-class 配置高亮样式),使用 <router-view> 指定不同路由下组件的呈现位置

<router-link active-class="active" to="/about">About</router-link>
<router-link active-class="active" to="/home">Home</router-link>

<router-view></router-view>

嵌套(多级)路由

修改 src/router/index.js 文件

import VueRouter from 'vue-router'
import About from './components/about.vue'
import Home from './components/home.vue'
import News from './components/news.vue'
import Message from './components/message.vue'
//创建并暴露一个路由器
export default new VueRouter({
    routes:[
        {
            path:'/about',
            component:About
        },
        {
            path:'/home',
            component:Home,
            children:[
                {
                    path:'news', //子路由不加 /,这里相当于 /home/news
                    component:News
                },
                {
                    path:'message',
                    component:Message
                }
            ]
        }
    ]
})

在 Home 路由组件中

<router-link active-class="active" to="/home/news">News</router-link>
<router-link active-class="active" to="/home/message">Message</router-link>  <!--这里得写完整路径-->

<router-view></router-view>

路由的 query 参数

跳转路由携带 query 参数

<!to 的字符串写法-->
<router-link active-class="active" :to="/home/message/detail?id=${id}&title=${title}">{{title}}</router-link>

<!to 的对象写法-->
<router-link active-class="active" :to="{
    path:'/home/message/detail',
    query:{
        id:id,
        title:title
    }
}">{{title}}</router-link>

接收参数

this.$route.query.id
this.$route.query.title

命名路由

路由规则中

{
    name:'ddd'
    path:'detail',
    component:Detail
}

组件中

<router-link active-class="active" :to="{
    name:'ddd',
    query:{
        id:id,
        title:title
    }
}">{{title}}</router-link>

在没有 query 参数的组件中使用命名路由

<router-link active-class="active" :to="{name:'xxx'}">About</router-link>

路由的 params 参数

路由规则中

{
    name:'ddd'
    path:'detail/:id/:title',
    component:Detail
}

组件中

<!to 的字符串写法-->
<router-link active-class="active" :to="/home/message/detail/${id}/${title}">{{title}}</router-link>

<!to 的对象写法-->
<router-link active-class="active" :to="{
    name:'ddd', //注意这里只能用 name,不能用 path
    params:{
        id:id,
        title:title
    }
}">{{title}}</router-link>

接收参数

this.$route.params.id
this.$route.params.title

注意:路由携带 params 参数时,若使用 to 的对象写法,则不能使用 path 配置项,必须使用 name 配置

路由的 props 配置

路由的 props 配置让路由组件更方便的收到参数

写法一:路由规则中 props 值为对象

路由规则中

{
    name:'ddd'
    path:'detail/:id/:title',
    component:Detail,
    //写法一:值为 对象,该对象中的所有 key-value 都会以 props 的形式给 Detail 组件(这种写法用的少,因为写的都死数据)
    props:{a:1,b:'xxx'}
}

在 Detail 组件接收参数

props:['a','b']

写法二:路由规则中值为布尔值

路由规则中

{
    name:'ddd'
    path:'detail/:id/:title',
    component:Detail,

    //写法二:值为布尔值,若为真就会把该路由组就收到的所有 params 参数以 props 的形式传给 Detail 组件
    props:true
}

在 Detail 组件接收参数

props:['id','title']

写法三:路由规则中值为函数

路由规则中

{
    name:'ddd'
    path:'detail',
    component:Detail,

    //写法三:值为函数,该函数返回的对象中每一组 key-value 都会通过 props 传给 Detail 组件
    props($route){
        return {id:$route.query.id,title:$route.query.title}
    }
    //或直接解构赋值
    /*props({query:{id,title}}){
        return {id,title}
    }*/
}

在 Detail 组件接收参数

props:['id','title']

作用:控制路由跳转时操作浏览器历史记录的模式

浏览器的历史记录有两种写入方式:分别为 push 和 replace,push 是追加历史记录,replace 是替换当前记录,默认是 push

开启 replace 模式的方式:

<router-link replace>xxx</router-link>

编程式路由导航

由于 <router-link> 解析后都是以 a 标签的形式显示在页面中,若想要通过点击按钮来实现跳转则无法使用 <router-link> 来实现,又如要过一段时间后自动跳转路由也无法通过 <router-link> 实现

编程式路由导航作用:不借助 <router-link> 实现路由跳转,让路由跳转更加灵活

this.$router.push({
    name:'ddd',
    query:{  //也可以是 params
        id:'xx',
        title:'xxx'
    }
})

this.$router.replace({
    name:'ddd',
    query:{  //也可以是 params
        id:'xx',
        title:'xxx'
    }
})

this.$router.back() //后退

this.$router.forward() //前进

this.$router.go(整整)  //正整数前进 n 步,负整数后退 n 步

缓存路由组件

当某个组件中有用户输入的数据,当路由跳转后当前组件被销毁,用户输入的数据也消失了

缓存路由组件作用:让不展示的路由组件保持挂载不被销毁,通过在 <router-view> 外包裹 <keep-alive>

所有都缓存时

<keep-alive>
    <router-view></router-view>
</keep-alive>

缓存一个组件时

<keep-alive include="要缓存的组件名1">
    <router-view></router-view>
</keep-alive>

缓存多个组件时

<keep-alive :include="['要缓存的组件名1','要缓存的组件名2']">
    <router-view></router-view>
</keep-alive>

路由组件独有的两个新的生命周期钩子

若在被缓存的组件中开启定时器,当路由跳转时该定时器依然在运行

路由组件独有的两个新的生命周期钩子可用于捕获路由组件的激活状态

activated(){ //组件从无到有时被激活触发
    this.timer = setInterval(()=>{
        ...
    },1000)
},
deactivated(){ //当路由跳转时被失活触发
    clearInterval(this.timer)
}

路由守卫

路由守卫控制路由的权限

全局前置路由守卫

在 src/router/index.js 中添加

{
    name:'hhh'
    path:'/home',
    component:Home,
    meta:{isAuth:false} //在 meta 中配置自定义属性
}

//初始化时被调用,每次路由切换之前被调用
router.beforeEach((to,from,next)=>{
    //做一些判断是否允许路由正常跳转
    if(to.meta.isAuth){
        if(...){
            next() //正常跳转
        }else{...}
    }else{
        next() //正常跳转
    }
})

全局后置路由守卫

{
    name:'hhh'
    path:'/home',
    component:Home,
    meta:{isAuth:false,title:'xxx'} //在 meta 中配置自定义属性
}

//初始化时被调用,每次路由切换之后被调用
router.afterEach((to,from)=>{
    document.title = to.meta.title || '默认'  //修改页签
})

独享路由守卫

独享路由守卫即某个路由独享的路由守卫

{
    name:'hhh'
    path:'/home',
    component:Home,
    meta:{isAuth:false,title:'xxx'}
    beforeEnter:(to,from,next)=>{
        //这里可以完成如前置路由守卫中的逻辑
    }
}

注意:没有 afterEnter

组件内路由守卫

在组件中

//通过路由规则进入该组件时被调用(不是在页面中直接展示组件的方式进入组件)
beforeRouteEnter(to,from,next){
    //这里可以完成如前置路由守卫中的逻辑
    next() //允许进入
},
//通过路由规则离开该组件时被调用
beforeRouteLeave(to,from,next){
    next() //允许离开
}

history 模式和 hash 模式

对于 url 来说 # 及其后面的内容就是 hash 值

hash 值不会包含在 HTTP 请求中,即 hash 值不会带给服务器

hash 模式:

(1)地址中永远带着 # 号,不美观
(2)若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法
(3)兼容性较好

history 模式:

(1)地址干净美观
(2)兼容性和 hash 模式相比略差
(3)应用部署上线时需要后端人员支持,解决刷新页面服务器端 404 的问题

Vue 脚手架应用中默认是用 hash 模式

若要启用 history 模式,在 router/index.js 创建路由器时添加 mode:'history'

九、Vue UI 组件库

移动端常用 UI 组件库:VantCube UIMint UI

PC 端常用 UI 组件库:Element UIIView UI

注意:使用 Element UI 时按照文档中的按需引入配置会报错,其中安装完 babel-plugin-component 后,修改 babel.config.js 为

{
  "presets": [["@babel/preset-env", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

十、Vue 3

Vue3 在 2020 年 9 月发布正式版,Vue3 支持大多数的 Vue2 特性

Vue 3 带来了什么

(1)性能的提升

打包大小减少 41%

初次渲染快 55%,更新渲染快 133%

内存减少 54%

……

(2)源码升级

使用 Proxy 配合 Reflect 代替 Vue2 中 defineProperty 实现响应式

重写虚拟 DOM 的实现和 Tree-Shaking

……

(3)拥抱 TypeScript

Vue 3 可以更好的支持 TypeScript

(4)新的特性

— 1) Composition API(组合 API)

setup 配置
ref 与 reactive
watch 与 watchEffect
provide 与 inject
......

—2)新的内置组件

Fragment
Teleport
Suspense

—3)其他改变

新的生命周期钩子
data 选项应始终被声明为一个函数
移除 keyCode 支持作为 v-on 的修饰符
......

使用 vue-cli 创建 Vue3 工程

要创建 Vue3 项目,则 vue-cli 的版本必须在 4.5.0 以上

安装 npm install -g @vue/cli

创建项目 vue create 项目名

在项目文件夹下启动 npm run serve

使用 vite 创建

vite 是新一代前端构建工具,当前一般使用 webpack

vite 优势:

开发环境中,无需打包操作,可快速的冷启动
轻量快速的热重载(HMR)
真正的按需编译,不再等待整个应用编译完成

vite 与 webpack 区别:webpack 首先看入口文件,分析路由,然后分析各模块,再将所有东西进行一次打包形成一台准备好的服务器;而 vite 是先准备好空壳服务器,当发起 http 请求时从入口文件进入找到相应路由,然后分析该路由对应的那些模块,这样启动速度更快,东西现用现分析,动态引入和代码分割

创建 Vue3 项目:

创建工程 npm init vite-app 项目名

进入工程目录后安装依赖 npm install

运行 npm run dev

分析工程结构

在入口文件 main.js 中

//引入的不再是 Vue 构造函数了(构造函数首字母大写,需要通过 new 去调用),而是一个名为 createApp 的工厂函数(工厂函数无需通过 new 去调用)
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
//上行代码相当于
//const app = createApp(App) //创建应用实例对象 app,类似于 Vue2 中的 vm,但 app 比 vm 更轻,身上没有那么多函数
//app.mount('#app') //挂载

Vue3 组件中的结构可以没有根标签

常用的 Composition API(组合 API)

setup

setup 是 Vue3 中一个新的配置项,值为一个函数

setup 是所有 Composition API 的“表演舞台”

setup 函数的两种返回值

(1)若返回一个对象,则对象中的属性、方法在模板中共均可直接使用
(2)若返回一个渲染函数,则可以自定义渲染内容

注意:尽量不要与 Vue2 配置混用,Vue2 配置(data、methods、computed…)中可以访问到 setup 中的属性、方法,但 setup 中不能访问 Vue2 配置,如有重名,setup 优先

setup 不能是一个 async 函数,因为若加上 async 后返回值不再是 return 的对象,而是 promise,模板看不到 return 对象中的属性(注意:后期也可以返回一个 Promise 实例,但需要 Suspense 和 异步组件的配合)

例子

//import {h} from 'vue' //当 setup 要返回渲染函数时需要引入
export default{
    name:'App',
    //此处暂时不考虑响应式问题
    setup(){
        let name = 'xx'
        let age = 18
        function sayHello(){
            console.log(name,age)
        }
        //返回一个对象
        return {
            name,
            age,
            sayHello
        }
        //也可以返回一个渲染函数
        //return ()=>h('h1','hello')
    }
}

setup 执行的时机:在 beforeCreate 之前执行一次,this 是 undefined

setup 的参数:

props:值为对象,包含组件外部传递过来且组件内部声明接收了的属性
context:上下文对象
    context.attrs:值为对象,包含组件外部传递过来但没有在 props 配置中声明的属性,相当于 Vue2 中 this.$attrs
    context.slots:收到的插槽内容,相当于 this.$slots
    context.emit:分发自定义事件的函数,需要在触发父组件传来自定义事件的子组件中通过 emits:['事件名'] 来接收,并通过 context.emit('事件名',数据) 来触发,相当于 this.$emit

ref

ref 函数作用:定义一个(对象类型/基本类型)响应式的数据

语法:const xxx = ref(初始值),创建一个包含响应式数据的引用对象(即 reference 对象,简称 ref 对象)

操作数据时:xxx.value

模板中读取数据时不需要 .value,直接使用 {{xxx}}

接收的数据可以是基本类型,也可以是对象类型

基本类型的数据:响应式依然是靠 Object.defineProperty() 的 get 与 set 完成的

对象类型的数据:内部求助了 Vue3 的新函数 reactive 函数

<h1>{{name}}</h1> <!--自动读取 value 值-->
<h1>{{age}}</h1>

import {ref} from 'vue'
export default{
    name:'App',
    setup(){
        let name = ref('xx') //ref 返回引用实现(implement)的实例对象(RefImpl),变成响应式(通过 getter 和 setter 实现的响应式),name.value 是 Proxy 类型
        let age = ref(18)
        let job = ref({//对传入的对象加工后变成 Proxy 类型,这封装在了 reactive 函数中
            type:'前端',
            salary:'100'
        })
        function changeInfo(){
            name.value = 'xxx',
            age.value = 20,
            job.value.type = '工程师'
            job.value.salary = '200'
        }
        return {
            name,
            age,
            changeInfo
        }
    }
}

reactive 函数

作用:定义一个对象类型的响应式数据(基本类型别用它,用 ref 函数)

语法:const 代理对象 = reactive(Object源对象) 接收一个对象或数组,返回一个代理器对象(Proxy 的实例对象,简称 proxy 对象)

reactive 定义的响应式数据是深层次的(即对象中可有对象)

内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据,这种操作都能被 Vue 捕获到(即数据劫持),都是响应式的

import {ref,reactive} from 'vue'
export default{
    name:'App',
    setup(){
        let name = ref('xx')
        let age = ref(18)
        let job = reactive({ //reactive 返回 Proxy 类型数据变成响应式
            type:'前端',
            salary:'100'
        })
        function changeInfo(){
            name.value = 'xxx',
            age.value = 20,
            job.type = '工程师' //使用 reactive 就不用 job.value.type 了
            job.salary = '200'
        }
        return {
            name,
            age,
            changeInfo
        }
    }
}

响应式原理

Vue2 的响应式

实现原理:

— 对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)

— 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹,如 Vue 中对数组 push 先使用原生 js 的 push 修改数组,然后对页面进行更新)

//Vue2 中实现响应式
Object.defineProperty(person,'name',{
    get(){ 
        configurable:true, //配置后就可以 delete parson.name 来删除 name
        return person.name
    },
    set(value){
        person.name = value
    }
})
Object.defineProperty(person,'age',{
    get(){
        return person.age
    },
    set(value){
        person.age = value
    }
})
但上面只能读取和修改属性,无法捕获删除和添加属性

存在的问题(这些问题在 Vue3 中不存在):

— 新增属性、删除属性时界面不会更新,如不能用 对象.新属性 = 值 添加属性,也不能用 delete 对象.属性

— 直接通过下标修改数组,界面不会自动更新,如不能用 数组[下标] = 值 修改数组中的元素

解决:

增加属性时
this.$set(要添加属性的对象,'新属性名',属性值)
//或 Vue.set(要添加属性的对象,'新属性名',属性值)
删除属性时
this.$delete(要删除属性的对象,'属性名')
//或 Vue.delete(要删除属性的对象,'属性名')
修改数组时
this.$set(要修改的数组,下标索引,新值)
//或 Vue.set(要修改的数组,下标索引,新值)
//或 要修改的数组.splice(下标索引,1,新值)

Vue3 的响应式

实现原理:

— 通过 Proxy(代理):这是 window 上的内置对象,拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等

— 通过 Reflect(反射):这是 window 上的内置对象,对被代理对象(即源对象)的属性进行操作

let person = {
    name:'xx',
    age:18
}
const p = new Proxy(person,{  //让 p 映射 person,对 p 进行操作能被捕获到并更新 person
    //拦截读取属性值
    //读取 p 的某个属性时调用
    get(target,propName){ //第一个参数 target 即源对象 person,第二个参数是属性名
        return Reflect.get(target,propName) //相当于target[propName]
    },
    //拦截设置属性值或添加新属性
    //修改或添加 p 的某个属性时调用
    set(target,propName,value){
        Reflect.set(target,propName,value) //相当于target[propName] = value
    },
    //拦截删除属性
    //删除 p 的某个属性时调用
    deleteProperty(target,propName){
        return Reflect.deleteProperty(target,propName) //相当于delete target[propName]
    }
})
p.sex = '女' //给 person 添加 sex 属性

MDN 文档中描述的 ProxyReflect

Vue3 中的添加、删除、修改对象或数组元素:

let person = reactive({
    name:'xxx',
    hobby:['aa','bb','cc']
})

person.sex = '女'
delete person.name
person.hobby[0] = 'dd'

对比 reactive 与 ref:

(1)从定义数据角度对比

ref 用来定义基本类型数据
reactive 用来定义对象或数组类型数据
ref 也可以用来定义对象或数组类型数据,它内部会自动通过 reactive 转为代理对象(即 Proxy 实例对象)

(2)从原理角度对比

ref 通过 Object.defineProperty() 的 get 与 set 来实现响应式(数据劫持)
reactive 通过使用 Proxy 来实现响应式(数据劫持),并通过 Reflect 操作源对象内部的数据

(3)从使用角度对比

ref 定义的数据:操作数据需要 .value,读取数据时模板中直接读取不需要 .value
reactive 定义的数据:操作数据与读取数据均不需要 .value

计算属性 computed

Vue3 与 Vue2 中的 computed 配置功能一致

import {reactive,computed} form 'vue'
setup(){
    let person = reactive({
        firstName: 'x',
        lastName: 'xx'
    })
    //计算属性简写————没有考虑计算属性被修改的情况
    /*person.fullName = computed(()=>{
        return person.firstName + '-' + person.lastName
    })*/
    //计算属性完整版————考虑读和写
    person.fullName = computed({
        get(){
            return person.firstName + '-' + person.lastName
        },
        set(value){
            const nameArr = value.split('-')
            person.firstName = nameArr[0]
            person.lastName = nameArr[1]
        }
    })
    return {
        person
    }
} 

数据监视 watch

Vue3 与 Vue2 中的 watch 配置功能一致

首先要 import {watch} from 'vue',把 watch(...) 写在 setup 里

情况一:监视 ref 定义的一个响应式数据

watch(sum,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
},{immediate:true})

情况二:监视多个 ref 定义的响应式数据

watch([sum,msg],(newValue,oldValue)=>{
    console.log('sum 或 msg,newValue,oldValue 是数组,包含了 sum 和 msg 的值',newValue,oldValue)
},{immediate:true})

情况三:监视 ref 定义的对象类型的响应式数据

对象类型一般通过 reactive 定义,但若使用 ref 定义,则需使用如下两种方式的一种进行监视

watch(person.value,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
})  //通过 person.value 获取到的是 Proxy 类型的对象,而 Proxy 类型对象通过 reactive 生成,所以此时相当于监视一个 reactive 定义的响应式数据 person.value

watch(person,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
},{immediate:true,deep:true})

情况三:监视 reactive 定义的响应式数据

若 watch 监视的是 reactive 定义的响应式数据,则无法正确获得 oldValue,并且会强制开启深度监视(即对象中嵌套的对象中属性改变也能监测到)

watch(person,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
},{immediate:true,deep:false}) //此处的 deep 配置不再奏效

情况四:监视 reactive 定义的响应式数据中的某个属性

watch(()=>person.age,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
},{immediate:true}) //此处能拿到 oldValue

watch(()=>person.job,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
},{immediate:true,deep:true}) //此处由于监视的是 reactive 定义的对象中的属性,所以 deep 配置有效,并且此处无法拿到 oldValue,因为 job 也是 reactive 定义的

情况五:监视 reactive 定义的响应式数据中的某些属性

watch([()=>person.name,()=>person.age,(newValue,oldValue)=>{
    console.log(newValue,oldValue)
})

注意:监视 reactive 定义的响应式数据时,oldValue 无法正确获取,并强制开启了深度监视(deep 配置失效)

监视 reactive 定义的响应式数据中某个属性时 deep 配置有效,但是只有属性依然是 reactive 定义的时配置 deep 才有意义,当监视的是基本数据时无需配置 deep

watchEffect 函数

watch 的套路是既要指明监视的属性,也要指明监视的回调

watchEffect 的套路是不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性

watchEffect 有点类似于 computed,但 computed 注重的是计算出来的值(回调函数的返回值),所以必须要写返回值;而 watchEffect 更注重过程(回调函数的函数体),所以不再写返回值

首先要 import {watchEffect} from 'vue',把 watchEffect(...) 写在 setup 里

//watchEffect 所指定的回调中用到的数据只要发生变化,则直接重新执行回调
watchEffect(()=>{
    const x1 = sum.value //监视 sum
    const x2 = person.age //监视 person.age
})

Vue3 生命周期

Vue3生命周期

Vue3 中可继续使用 Vue2 中的生命周期钩子,但有两个被更名:
beforeDestory 改名为 beforeUnmount
destroyed 改名为 unmounted

Vue3 也提供了 Composition API 形式的生命周期钩子(注意用之前需要 import 引入),与 Vue2 中钩子的对应关系如下

beforeCreate ===> setup() 若通过配置项写 beforeCreate,则 setup 在 beforeCreate 之前执行,但若要用组合 API 的形式(即放到 setup 中),则 beforeCreate 被 setup 代替,无需写 beforeCreate
created ===> setup() //即 setup 相当于 created
beforeMount ===> onBeforeMount
mounted ===> onMounted
beforeUpdate ===> onBeforeUpdate
updated ===> onUpdated
beforeUnmount ===> onBeforeUnmount
unmounted ===> onUnmounted

这些组合 API 在 setup 中使用,并且需要传入一个回调函数,如

import {onBeforeMount} from 'vue'

setup(){
    onBeforeMount(()=>{})
}

若既写配置项中的生命周期也用组合式 API 的生命周期,则对应同一时期的钩子组合式 API 比配置项中的先执行

自定义 hook

hook 本质是一个函数,对 setup 函数中使用的 Composition API 进行了封装,类似于 Vue2 中的 mixin

自定义 hook 的优势:复用代码,让 setup 中的逻辑更清除易懂

新建 src/hooks/useXxx.js

import {reactive,onMounted,beforeUnmount}
export default function(){
    let xxx = ref(..)
    let xxxx = reactive(...)
    function xxxxx(){...}
    //生命周期钩子,如
    onMounted(()=>{...})
    beforeUnmount(()=>{...})
    return xxx
}

在组件中只需引入就可以复用该功能

import useXxx from '../hooks/useXxx'
然后可直接使用 useXxx

toRef

作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性值

语法:

import toRef from 'vue'
const name = toRef(person,'name') //此时若修改 person 中的 name 属性,则变量 name 的值也跟着变;若是直接赋值 const name = persom.name 则当 person.name 修改时变量 name 不变

应用:要将响应式对象中的某个属性单独提供给外部使用时

扩展:toRefs 与 toRef 功能一致,但可以批量创建多个 ref 对象,语法 toRefs(person)

当暴露数据时

<h1>{{name}}</h1>

setup(){
    retun {
        person,
        name:toRef(person,'name'), //把 person 里常用的一些属性单独暴露出来方便使用
        age:toRef(person,'age'),
        salary:toRef(person.job,'salary')
        //上述代码也可用 toRefs 替代,批量暴露
        //...toRefs(person)
    }
}

其他 Composition API

shallowReactive 与 shallowRef

shallowReactive 只处理对象最外层属性的响应式(浅响应式),而 reactive 可处理深层次的对象

shallowRef 只处理基本数据类型的响应式,不进行对象的响应式处理,若传入对象,则 value 是 Object 类型,不是 Proxy 类型,而 Ref 可处理基本数据类型或对象

使用场景:

若有一个对象数据,结构比较深,但变化时只是外层属性变化 ===> shallowReactive
若有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换 ===> shallowRef

readonly 与 shallowReadonly

readonly 让一个响应式数据变为只读的(深只读)

shallowReadonly 让一个响应式数据变为只读的(浅只读,即只有对象的第一层数据不能改,但对象中的对象的属性可改)

应用场景:不希望数据被修改时

import {ref,reactive,readonly,shallowReadonly} from 'vue'
let sum = ref(0)
let person = reactive({...})
person = readonly(person)
sum = readonly(sum)
//person = shallowReadonly(person)

toRaw 与 markRaw

toRaw:

作用:将一个由 reactive 生成的响应式对象转为普通对象,不能转 ref 定义的基本数据类型,否则会返回 undefined

使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作不会引起页面更新

import {toRaw} from 'vue'

const p = toRaw(person)

markRaw:

作用:标记一个对象,使其永远不会再成为响应式对象

应用场景:

(1)有些值不应该被设置为响应式的,例如复杂的第三方类库等
(2)当渲染具有不可变数据源的大列表时,跳过响应式转换可提高性能

import {markRaw} from 'vue'
let car = {name:'xx',price:50}
person.car = markRaw(car) //若直接追加属性 person.car 为 car 对象,则改属性自动为响应式,若无需响应式则加上 markRaw

customRef

作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

customRef 中需传入一个回调函数,且回调函数必须返回一个对象

实现防抖效果:

import {customRef} from 'vue'

setup(){
    ler keyWord = myRef('hello',1000)
    function myRef(value,delay){
        let timer,
        return customRef((track,trigger)=>{
            return {
                get(){ //当从 myRef 容器中读取数据时调用
                    track() //通知 Vue 追踪 value 的变化
                    return value
                },
                set(newValue){  //当修改 myRef 容器中的数据时调用
                    clearTimeout(timer)
                    timer = setTimeout(()=>{
                        value = newValue
                        trigger() //通知 Vue 去重新解析模板,这样会再次 get 获得值
                    },delay)

                }
            }
        })
    }
}

provide 与 inject

作用:实现祖与后代组件间通信

套路:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据

祖组件中

import {reactive,toRefs,provide} from 'vue'
setup(){
    ...
    let car = reactive({name:'xx',price:50})
    provide('car',car)
    ...
    return {...toRefs(car)}
}

后代组件中

import {inject} from 'vue'
setup(props,context){
    ...
    const car = inject('car')
    return {car}
}

响应式数据的判断 API

isRef:检查一个值是否为一个 ref 对象

isReactive:检查一个对象是否是由 reactive 创建的响应式代理

isReadonly:检查一个对象是否是由 readonly 创建的只读代理

isProxy:检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

Composition API 的优势

Options API (Vue2 中所使用的方式)存在的问题:

使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data、methods、computed 里修改

Composition API 的优势:

利用组合式 API 可以更加优雅的组织我们的代码、函数,让相关功能的代码更加有序的组织在一起(结合 hooks)

新组件

Fragment

在 Vue2 中组件必须有一个根标签

在 Vue3 中组件可以没有根标签,内部会自动将多个标签包含在一个 Fragment 虚拟元素中

好处:减少标签层级,减小内存占用

Teleport

Teleport 是一种能够将组件 html 结构移动到指定位置的技术

<Teleport to="内部要显示在的标签如 html、body、#id 等">
    <div v-if="isShow" class="mask">
        <div class="dialog">
            <h3>一个弹窗</h3>
            <button @click="isShow = false">关闭弹窗</button>
        </div>
    </div>
</Teleport>

Suspense

等待异步组件时渲染一些额外内容,让应用有更好的用户体验

若使用普通 import 静态引入组件则若子孙组件未加载完成则祖组件也都不会渲染,直到内部组件加载完成后一起渲染

而动态/异步引入则当内部组件未加载完成时可先渲染外部组件

使用步骤:

(1)异步引入组件

import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./components/Child.vue')) //动态/异步引入组件

(2)使用 Suspense 包裹组件,并配置好 default 与 fallback

<template>
    <div>
        <h1>xxx</h1>
        <Suspense>
            <template v-slot:default> <!--真正要展示的内容-->
                <Child/>
            </template>
            <template v-slot:fallback> <!--当真正要展示的内容加载不出来时显示-->
                <h1>加载中...</h1>
            </template>
        </Suspense>    
    </div>
</template>

注意:当使用了 Suspense 和异步引入后 setup 返回值就可以是一个 Promise 对象

在异步引入的组件中

async setup(){
    let p = new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve({sum})
        },1000)
    })
    return await p
}

全局 API 的转移

Vue2 有许多全局 API 和配置,如:注册全局组件、注册全局指令等

//注册全局组件
Vue.component('xx',{
    data:()=>({
        count:0
    }),
    template:'<button @click="count++">xx</button>'
})
//注册全局指令
Vue.directive('focus',{
    inserted:el => el.focus()
})

Vue3 中对这些 API 做出了调整,将全局 API,即 Vue.xxx 调整到应用实例(即 createApp 生成的 app)上

Vue2 全局 API(在 Vue 上) Vue3 实例 API(在 app 上)
Vue.config.xxx app.config.xxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties

其他改变:

data 选项应是始终被声明为一个函数

过度类名的更改

Vue2 写法
.v-enter,.v-leave-to{
    opacity:0;
}
.v-leave,.v-enter-to{
    opacity:1;
}

Vue3 写法
.v-enter-from,.v-leave-to{
    opacity:0;
}
.v-leave-from,.v-enter-to{
    opacity:1;
}

移除 keyCode 作为 v-on 的修饰符,同时不再支持 config.keyCodes

Vue2 中
@keyup.13 标识按下回车
Vue.config.keyCodes.huiche = 13 //定义别名按键

移除 v-on.native 修饰符

父组件中绑定事件
<子组件 v-on:close="函数" v-on:click="函数"/>

子组件中声明自定义事件
export default{
    emits:['close'] //此处没声明 click则父组件中给子组件绑定的 click 为原生事件
}

移除过滤器:过滤器需要一个自定义语法,打破大括号内表达式是“只是 JavaScript” 的假设,有学习成本和实现成本,建议用方法调用或计算属性替换过滤器

其他

js 表达式和 js 代码/语句

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

a
a+b
demo(1)
x === y ? 'a' : 'b'

代码/语句,如

if(...){...}
for(...){...}

表达式是一种特殊的语句

vscode 中 vue 相关插件

Vue 3 Snippets 会提示代码

Vetur 对符合 Vue 的代码有高亮

cookie图示

在设置 cookie 时重要的 cookie 要设置 HttpOnly,这样就不能通过 document.cookie 获取

v-model

注意使用 v-model 绑定的值不能是 props 传来的值,因为 props 是不可以修改的

props

props 传来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做,因为 Vue 主要监视的是对象值的地址是否发生变化

$nextTick

$nextTick 的回调会在 DOM 更新后再执行

this.$nextTick(function(){...})

作用:$nextTick 在下一次 DOM 更新结束后执行其指定的回调

适用场景:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 $nextTick 所指定的回调函数中执行

如原本 xxx DOM 元素的 v-show 为 false,即不在页面上显示,在某函数中将 v-show 改为 true 并获取该 DOM 元素焦点,但是 Vue 会在函数执行完后才会重新解析模板重新渲染,所以函数中对 DOM 元素获取焦点的操作无效,因为那时页面中并没有相应 DOM 元素,因此可以借助 $nextTick 的回调使得当 v-show 改为 true 页面重新渲染, DOM 元素出现在页面中后再执行对它获取焦点的操作

当然 $nextTick 也可以通过利用不指定时间定时器来代替该函数

setTimeout(()=>{...})

其他

插值语法中可以展示 data、props、computed 中的数据

Vue3 + TS 相关笔记可参考尚硅谷的课程笔记

Vue 中数据代理、数据劫持、计算属性的原理都用到 Object.defineproperty