近况
本来打算10月份学cloud,但是找到特别好的vue网课总不能不学吧,比之前的网课新了很多,发现vue中有很多
内容还不懂,所以具体什么时间学完vue,开始去学cloud还不清楚,定个小目标,10月15号之前,学完那个网课
并且写几篇关于vue3的博客吧,学一个技术就应该扎稳脚步,不能急功近燥,下面是我了解的一些关于vue的
基本原理,就当个记录吧。
v-for中key的原理
我们都知道,使用v-for遍历元素时,在标签上应该加上key属性,并且key属性必须要是唯一的,这是为什么呢?
下面是一段代码关于v-for使用:
<div id="app">
<button @click="addPerson">点击增加老刘</button>
<ul>
<li v-for="p in arr">
{{p.name}} - {{p.age}} <input type="text">
</li>
</ul>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
arr: [
{id: '001',name: '张三',age: 18},
{id: '002',name: '李四',age: 19},
{id: '003',name: '王五',age: 20}
]
},
methods: {
addPerson(){
this.arr.unshift({id: '004',name: '老刘',age: 30})
}
}
});
</script>
这里我没给li标签加上key属性,则Vue会自动给li标签加上key属性,且属性值是数组的index,如果单纯的展示
数组的数据没有问题,但是如果对数组进行了修改,并且打断了原来的index顺序,就会造成错误,什么意思呢?
你最开始遍历数组,张三,李四,王五,生成三个li标签,每个标签,Vue给你增加了一个key属性,属性值是index
所以张三这个标签key为0,李四是1,王五是2,后来你点击了按钮,为数组增加了新的元素,并且放在数组最前面
这就导致,新增的老刘是0,张三是1,李四是2,王五是3,这就叫做打乱了原有的index顺序,这种方式会产生一些错误!
下面有张图,可以看到具体的错误:
当我们点击新增数据之前,我们在每个标签的input中输入对应的值,但是当我们新增数据后,发现新增的老刘的
input框后有张三的数据,并且下面的数据都不正常了,这显然是不对的,我们希望新增老刘后的input是空的
所以为什么会存在这种错误呢?
key的原理:key是用来给Vue做标识的,即给标签加上个唯一标识,可以认为是id,Vue拿到原来数据,
首先会根据数据生成一个虚拟DOM,页面上看到的标签是真实DOM,虚拟DOM是放在内存中,虚拟DOM中
就会使用到key,标识虚拟DOM的每一个标签,然后虚拟DOM会转成真实DOM,从而被我们看得到,
当我们遍历时没有指定key,或者key=index时,新增数据老刘后生成的新虚拟DOM中key值就会更改,
key值的主要作用就是用于新虚拟DOM与旧虚拟DOM对比,决定是生成新真实DOM还是复用旧真实DOM,
例如老刘的key是0,会找到key为0的旧虚拟DOM即张三,对比虚拟DOM内部,老刘与张三不相等,生成新的真实DOM即老刘
而新虚拟DOM中存在input,此时旧虚拟DOM张三中也有input,则会复用旧虚拟DOM中input的旧真实DOM
因为旧虚拟DOM生成过一整套真实DOM,所以将张三后面的input对应的input旧真实DOM拿过去复用,
所以我们看到老刘-30后面是张三的input框,并且其中的数据还在,这里的数据是旧真实DOM中的,后面几个input框错用也是如此。
总结一下:
在本案例中,
新虚拟DOM就是根据增加后的数据所生成的虚拟DOM,而根据新虚拟DOM生成的自然是新真实DOM
旧虚拟DOM就是根据原来数据生成的虚拟DOM,而根据旧虚拟DOM生成的自然是旧真实DOM
key就是虚拟DOM的标识,或者说id,根据新虚拟DOM的key找到相同的旧虚拟DOM,然后对比其中的内容
如果内容相同,则复用对应的旧真实DOM,如果内容不相同,则生成新真实DOM
下面是一张图片总结:
Vue监测数据的原理
Vue是如何做到响应式的,说的就是Vue响应式原理或者说v-model原理,这常常在面试中被问到,
Vue实现响应式得做到两点:
- Vue是如何知道数据被改变了?说的就是Vue是怎么监测数据的,即本小节所要讲的原理
- Vue是怎么更新页面的,vue需要知道哪些DOM使用到了被修改的数据,并及时更新这些DOM
而没有被修改数据的DOM页面不需要更改,这说的是发布订阅原理,留在以后讲。
Vue2监测数据通过Object.definedProperty
方法,通过该方法我们可以监测到对其他对象的属性的读改操作
这被称为数据劫持,即,只要别人读取修改这个数据,我们能拦截到,当然数据劫持在另一个角度来看也像
数据代理,下面看看具体代码:
<script>
let data = {
name: '万一',
age: 23
}
let _data = {}
Object.defineProperty(_data,'name',{
get(){
return data.name
},
set(val){
data.name=val
}
})
console.log(_data)
/*
输出结果如下:
name: (…)
get name: ƒ get()
set name: ƒ set(val)
[[Prototype]]: Object
*/
</script>
/*
当我们通过Object.definedProperty给_data对象中新增name属性时,我们同样给这个name属性增加getter和setter方法,
从打印_data对象就可以看到,确实有getter和setter方法,其次在getter中返回data的name数据,
setter中将修改数据写到data对象的name中,这就使得_data中的name属性和data中的name属性存在了关联,
这种通过修改_data中name的值达到修改data中name的值,就叫做数据代理
我们获取data中name只需要通过_data.name实现,我们修改data中name只需要通过_data.name="xxx"这就是数据代理,
数据代理也被叫做数据劫持,因为我们可以在_data中name的getter和setter方法中做一些其他的事,相当于劫持了对data的读改操作。
*/
同理,Vue中也存在数据代理或者说数据劫持,我们的数据作为属性放在data对象中,而这里面的数据都会被
放到Vue实例的_data
属性中,并且每一个属性都有getter和setter方法,不仅如此,为了方便我们使用
Vue将_data
属性中的数据又做了一层代理,将_data
中数据全部作为了Vue实例的属性,以后我们每次修改
Vue实例的数据就相当于修改_data
属性中的数据,修改_data
属性中的数据就相当于修改data的数据
下面有一张数据代理或者说数据劫持的图片:
上图中省略了setter和getter方法,通过代理,我们直接在浏览器按f12,输入vm.name="万一"
,就能修改掉原来
data中的name数据,因为数据被最终代理到Vue的实例上,我们在模板中使用{{name}}
,就能访问到data中的
值,不然只做一层代理的话,你需要{{_data.name}}
才能访问得到data中的name值,显然简化了很多。
至此,Vue就是通过这种方式去监测data数据的,不管是访问还是修改data数据,都会调用到Vue中getter和
setter方法,在这两个方法里可以做一些发布和订阅的事,这个之后再讲,总之现在data中数据都是响应式的了
因为被代理后,都有了相应的getter和setter方法,但是有些时候,data中的数据并不是响应式的。
在某些情况下,data中数据不是响应式:
当Vue实例创建完毕,你动态向data的属性中增加某个属性时,则该属性不是响应式的,例如:
//初始数据: data: { message:'加油!', student: { name: 'wanyi', age: 23 } } //你中途给school新增属性: this.student.sex='男' 你会发现你新增数据成功了,但打开_data中数据一看,sex属性并没有getter和setter方法 这说明该属性并不是响应式,直接影响是虽然数据更新了,但页面DOM中用到school.sex的地方没有任何变化 注意点: 1)在模板中通过{{time}}访问data中的直接属性,如果没有time属性则程序直接报错 如果通过{{school.sex}}访问data属性的属性,即school的不存在的属性却并不报错。 2)在Vue初始化完毕后,你不能中途向data中增加直接属性,你可以给data的属性增加属性 但是这个新增的属性如果是向上面的方式添加,则这个属性不是响应式的,即没有getter和setter方法
直接修改data中数组中的元素,而不通过指定方法修改数组,会使用修改后的元素不是响应式的,例如:
<div id="app"> <ul> <li v-for="p in persons" :key="p.id"> {{p.name}} - {{p.age}} - {{p.sex}} </li> </ul> </div> <script> let app = new Vue({ el: '#app', data: { message:'加油!', persons: [ {name: '周杰伦',age: 30,id: '001',sex: '男'}, {name: '马冬梅',age: 18,id: '002',sex: '女'}, {name: '周冬雨',age: 25,id: '003',sex: '女'}, {name: '胡歌',age: 35,id: '004',sex: '男'} ] }, }); </script>
当我们中途修改数组中元素时,则修改后的元素并不是响应式的,没有被数据代理,直接影响是,虽然你修改了数据
但是页面没有响应修改,下面是一张修改数据的过程:
那么如何才能使得中途新增的属性(该新增的属性不能是data的直接属性)是响应式的呢?如何使得修改数组中元素是响应式的呢?
对于中途新增属性,我们可以使用Vue.set()
方法,或者app.$set
,这两者是等同的,使用方法如下:
Vue.set(target,propertyName/index,value)
app.$set(target,propertyName/index,value)
测试原始数据还是和上面新增对象属性的例子一样,下面是使用上面两个方法响应式增加属性:

对于修改数据属性:我们可以通过调用这7个方法修改数组的元素,这样才能使得修改后的元素是响应式的
这七个方法分别是:push(),pop(),shift(),unshift().splice(),sort(),reverse()
值得注意的是,这7个方法都是Vue包装后的方法,并不是原生数组方法,通过包装原生数组方法,增加getter,setter方法
除了使用这7个方法修改数组的元素,你也可以使用上面两个方法:Vue.set()或app.$set()
修改数组元素。
最后有一个悲伤的事,在Vue3中Vue.set和app.$set都被弃用了,连上面监测属性使用的Object.definedProperty方法也被弃用了。
生命周期函数
生命周期函数也叫钩子函数,顾名思义,我们写好钩子函数,等待vue去上钩,呸,等待vue去调用,
vue的生命周期分为创建,挂载,更新,销毁,四个阶段,分别对应8个钩子函数,下面是两张生命周期图片
我们在不同的钩子函数中可以做点不同的事情,当vue运行到了某个时期就去调用:
Vue中Dom的异步渲染
<body>
<div id="app">
<ul>
<li v-for="a in arr">{{a}}</li>
</ul>
{{sum}}
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
data:{
arr:[1,2,3]
},
created(){
// 如果在created改变数据,那么模板编译进行一次,如果在mounted中改数据,会再次编译模板
},
beforeMount(){
/*
在beforeMount之前编译模板,编译原有数据的3个li,存在内存中(形成虚拟DOM)
当数据在beforeMount更新时,立即将内存中的模板(虚拟DOM)编译成4li
最后挂载到真实的DOM上,不会引发beforeUpdate函数的调用
*/
//this.arr=[1,2,3,4];
},
computed:{
sum(){
// 在挂载的过程中,执行的这个函数
console.log(1);
return 100;
}
},
/*
在mounted之前改数据,不会调用beforeUpdate这个函数
进入mounted函数表示DOM已经挂载完毕
如果在mounted函数中修改数据,则立即将内存中的模板编译成4li,再次把最新虚拟DOM挂载到真实的DOM上
会引发beforeUpdate函数的调用
*/
mounted(){
this.arr=[1,2,3,4];
/*
VUE中DOM的更新是异步的
如何在mounted中操作最新的真实DOM呢?
$nextTick需要传入一个回调函数,会当真实的DOM再次更新完成,会立即调用这个回调函数
*/
this.$nextTick(()=>{
let oLis = document.getElementsByTagName("li")
console.log(oLis.length); //4
});
},
beforeUpdate(){
console.log("beforeUpdate");
}
}).$mount("#app");
</script>
</body>
</html>
还有另外三个钩子函数:activated,deactivated,errorCaptured,
讲这三个钩子函数就不得不讲vue中两个内置组件,component组件,keep-alive组件
component内置组件: 每次能动态显示一个组件,当切换下一个组件时,当前组件要销毁
也就是说,当组件被激活时(被我们点击看到),组件执行Created函数,当组件失活时,调用destroy函数等
使用方式:<component is=“组件名”>
keep-alive:用于缓存组件,如果该组件还会再启用,那么可以使用keep-alive进行组件缓存和优化,提高性能
缓存的组件不需要销毁,也不需要再创建
使用方式:<keep-alive><component :is=“组件名”></keep-alive> //如果是动态属性,那么会去data中取值
当然你也可以在路由中使用:<keep-alive><router-view></keep-alive>
activated和deactivated两个钩子函数在路由篇讲过,组件被缓存后,这两个钩子函数就代替了created和destroy
而errorCaptured则是当子孙组件出错时,会调用这个钩子函数,这个函数能接收三个参数:
errorCaptured(a, b, c) {
/*
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:
错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。
此钩子可以返回false,以阻止该错误继续向上传播。
*/
console.log(a);
console.log(b);
console.log(c);
console.log(“子组件报错”)
}
生命周期具体笔记:
1,生命周期函数中的this都是vue实例或者组件实例对象
2,除了更新阶段的钩子函数,其他钩子函数只执行一次
3,执行this.$destroy()方法销毁vue实例,直接执行beforeDestroy函数,再是destroyed函数
在beforeDestroy函数中虽然可以操作数据,但不会触发更新操作了,没有回头路了
4,在mounted函数中通常用来发送ajax请求,启动定时器,绑定自定义事件,订阅消息等初始化操作
5,在beforeDestroy函数中通常用来清除定时器,解绑自定义事件,取消订阅消息等收尾工作
6,调用destroy销毁vue后,自定义事件会失效(子组件向父组件通信),但原生DOM事件依然有效(click等自带的事件)
组件与Vue原型对象的关系
还记得我们是怎么创建一个组件的嘛?
//方式1
let mycon = Vue.extend({
template:`
<div>
<h3>万一爱野马</h3>
</div>
`
});
/*
拿到mycon组件,你可能会放到别的组件的component属性中,局部组件
或者通过Vue.component('mycon',mycon);,注册全局组件
*/
//方式2,或者你直接导入组件
import App from './App.vue'
以上我们拿到的mycon,App,都是组件,或者说都是VueComponent构造函数,但这两个VueComponent并不相等
当我们在模板中使用组件标签时,会根据VueComponent构造函数创建相应的组件实例对象,所以我们在组件中的this都是组件实例对象
说回来,标题中,组件与Vue原型对象有什么关系呢?下面看张图片:
这也就解释了,为什么我们通过Vue.prototype.$xxx = xxx
后,我们在所有组件中都可以能拿到this.$xxx
这里VueComponent原型对象相当于一个Vue实例,所以组件实例能拿到VueComponent原型对象中的数据
也能拿到Vue原型对象中的数据,解释完毕,华丽退场!
render函数
我们在使用vue脚手架创建vue项目的时候,可能让你选择runtimeCompiler与runtimeOnly两种构建项目方式,
runtimeCompiler方法构建的vue项目允许你在app.vue中使用template属性,这种方式构建的项目。项目中引用
的vue内部包括了vue核心+模板解析器,模板解析器就是专门用来解析app.vue中的template属性的,
包含模板解析器的vue被称为完整版的vue,当我们输入import Vue from 'vue'
时,它引入node_modules/vue/dist/vue.js
,
这个文件就是完整版的vue文件,当使用webpack打包vue项目时,自然是把vue包含的模板解析器打包了
而如果使用runtimeOnly方式构建项目,则使用render函数代替template属性项目中使用的vue,
它引入的vue文件是node_modules/vue/dist/vue.runtime.esm.js
,这个vue文件不包含模板解析器,
所以当这个项目被webpack打包后,要比runtimeCompiler方法构建的vue项目打包后,小大概10KB,
render函数本质就是用来创建app标签元素的,作用和template一样。
总结:如果你的项目app.vue中需要使用template属性,使用runtimeCompiler方式构建项目
如果你的项目app.vue中使用render函数,则使用runtimeOnly方式构建项目