Vue面试必刷题
一、实现原理
数据劫持:Vue是采用数据劫持+发布-订阅者模式实现的,使用Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时,通知给各个订阅者,触发相应的回调。
二、生命周期
beforeCreate:创建之前,此时还没有data与method
created: data与method有值,但还没有el(undefined)
beforeMount: 页面渲染之前,此时el有值,即挂载的dom元素
Mounted: 页面渲染之后,此时会用数据替换掉dom元素双括号内的变量
beforeUpdate: data改变,对应的组件重新渲染之前
Updated: 对应的组件重新渲染之后
beforeDestroy: 实例销毁之前,此时实例仍然可以使用
Destoryed: 实例销毁之后
<body>
<div id="app">
<p>{{message}}</p>
<button @click="changeMsg">改变</button>
</div>
</body>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------挂载前---------')
console.log(this.message)
console.log(this.$el)
},
mounted () {
console.log('------挂载完成---------')
console.log(this.message)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
</script>
生命周期钩子函数的执行顺序?
-
首先是数据加载渲染到页面,包括初始化阶段和挂载阶段
Vue实例化(new Vue())-> 父组件beforeCreate -> 父组件 created -> 父组件 beforeMount -> 子组件 beforeCreate -> 子组件 created -> 子组件 beforeMount -> 真实DOM挂载完毕 -> 子组件 mounted -> 父组件 mounted -
其次当data里的数据发生了变化,进入更新阶段
父组件 beforeUpdate -> 子组件 beforeUpdate -> 子组件 updated -> 父组件 updated -> 获取更新后的真实DOM -
最后当$destroy()被调用进入销毁阶段
父组件 beforeDestroy -> 子组件 beforeDestroy -> 实例销毁后 -> 子组件 destroyed -> 父组件 destroyed
三、Vue.nextTick(callback)
应用:
- 在数据改变后需要执行某些操作,而这些操作需要使用随数据改变而改变的DOM结构时,这些操作都需要放在nextTick回调函数中。
- 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。
- 通常ajax发送请求,放在created函数中
<div class="app">
<!--使用 ref 可以获取页面dom元素 this.$refs.msgDiv-->
<div ref="msgDiv">{{msg}}</div>
<div v-if="msg1">Message got outside : {{msg1}}</div>
<div v-if="msg2">Message got inside : {{msg2}}</div>
<div v-if="msg3">Message got outside : {{msg3}}</div>
<button @click="changeMsg">
Change the Message
</button>
</div>
<script>
new Vue({
el: '.app',
data: {
msg: 'Hello Vue.',
msg1: '', // 空字符串为false
msg2: '',
msg3: ''
},
methods: {
changeMsg() {
this.msg = "Hello world."
this.msg1 = this.$refs.msgDiv.innerHTML
this.$nextTick(() => {
this.msg2 = this.$refs.msgDiv.innerHTML
})
this.msg3 = this.$refs.msgDiv.innerHTML
}
}
})
</script>
四、Computed VS Watch
相同点:Computed与watch 都以Vue的依赖追踪机制为基础,当一个数据发生变化时,依赖这些数据的相关数据也会自动发生变化。
不同点:
- computed 计算属性。当它依赖的数据(可以是单个,也可以是多个)发生变化时,会重新调用get来计算,默认只有get, 如果需要也可以自己加上set;watch是监听器,可以监听某一数据,当数据发生变化时触发相应的操作。
- computed支持缓存,当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值;watch不支持缓存,当对应属性发生变化的时候,响应执行。
- computed不支持异步,有异步操作时无法监听数据变化;watch支持异步操作。
- computed第一次加载时就监听;watch默认第一次加载时不监听。
var vm = new Vue({
el: '#app',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
watch: {
firstName: function (newVal,oldVal) {
this.fullName = newVal + ' ' + this.lastName
console.log(newVal+'========='+oldVal)//Fooaaaa=========Foo
},
lastName: function (newVal,oldVal) {
this.fullName = this.firstName + ' ' + newVal
console.log(newVal+'========='+oldVal)//Baraaaa=========Bar
}
}
})
五、组件
定义:组件的本质就是自定义标签。将一些公共的模块(比如头部、尾部、上传图片等)抽取出来,写成单独的组件,在需要的页面中直接引入即可,提高了代码的复用率。每一个.vue文件都可以视为一个组件。
- 全局组件
所有Vue实例都可使用。
语法:Vue.component(‘组件名’,{options}); 组件名使用驼峰命名法,如testName,
标签使用test-name.
<div id="app">
<test-name></test-name>
</div>
<!--定义外部模板-->
<template id='temp'>
<div> <!--如果模板中有多个标签定义,必须用父元素包裹-->
<h1>名字:{{name}}</h1>
<p @click="changeName()"> 改变名字 </p>
</div>
</template>
<script>
// 注册全局组件
Vue.component('testName', {
template:'#temp', //template: '<h1>{{name}}</h1>'
data() { // data在组件中必须使用函数形式
return {
name:'小明'
}
},
methods:{
changeName(){ //注意函数写法:函数名(){函数体}
console.log(111)
this.name="小红";
}
}
})
// 创建根实例
new Vue({
el: '#app'
})
</script>
- 局部组件
只在当前vue实例有效,使用components属性
<div id="app">
<demo></demo>
</div>
<!--定义外部模板-->
<template id='temp'>
<div> <!--如果模板中有多个标签定义,必须用父元素包裹-->
<h1>名字:{{name}}</h1>
<p @click="changeName()"> 改变名字 </p>
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
"demo":{
template:'#temp',
data(){
return { name : "小青" }
},
methods:{
changeName(){
this.name="小紫";
}
}
}
}
})
</script>
- 父子组件
组件嵌套组件。
注意:父子组件之间的作用域完全独立,数据不互通
<div id="app">
<parent-item></parent-item>
</div>
<!--父组件模板-->
<template id='parent'>
<div>
<h1>父组件:{{data}}</h1>
<!--子组件必须在父组件模板中调用-->
<son-item></son-item>
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
"parentItem":{
template:'#parent',
data(){
return { data : "父组件数据" }
},
components:{
"sonItem":{
template:"<h2>子组件:{{data}}</h2>",
data(){
return { data : "子组件数据" }
}
}
}
}
}
})
</script>
六、如何进行父子组件之间的通信?
父组件通过props向子组件传递数据,子组件通过事件向父组件发送数据
- 父向子传递数据
第一步:子模板内定义变量。 {{msg}}
第二步:子组件添加props属性。[“msg”]
第三步:父模板内,向子组件定义的变量传参。 v-bind:msg 简写 :msg
v-bind 绑定元素属性,并为其传参
<div id="app">
<parent-item></parent-item>
</div>
<!--父组件模板-->
<template id='parent'>
<div>
<h1>父组件:{{data}}</h1>
<!— v-bind:msg1 简写形式 :msg1 ,msg1 msg2是传参,msg3是死数据-->
<!—第3步:传参 -->
<son-item :msg1="msg1" v-bind:msg2="msg2" msg3="数据3"></son-item>
</div>
</template>
<template id='son'>
<div>
<h2>子组件:{{data}}</h2>
<p>{{msg1}}</p> //第一步: 子模板内定义变量
<p>{{msg2}}</p>
<p>{{msg3}}</p>
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
"parentItem":{
template:'#parent',
data(){
return {
data : "父组件数据" ,
msg1:"数据11",
msg2:"数据22",
}
},
components:{
"sonItem":{
template:"#son",
data(){
return { data : "子组件数据" }
},
props:["msg1","msg2","msg3"]
//第二步:子组件中加入props属性
}
}
}
}
})
</script>
- 子向父传递数据
第一步:子组件通过this.$emit(“事件名”,参数),触发事件。
第二部: 父组件通过 v-on:事件名 =‘callback’, 监听事件,触发回调函数 (简写:@事件名)
<div id="app">
<parent-item></parent-item>
</div>
<!--父组件模板-->
<template id='parent'>
<div>
<h1>父组件:{{data}}</h1> // 3
<son-item :msg1="msg1" :msg2="msg2" :msg3="msg3" v-on:change="change" ref=”aaa”></son-item>
</div>
</template>
<template id='son'>
<div>
<h2>子组件:{{data}}</h2>
<p>{{msg1}}</p>
<p>{{msg2}}</p>
<p>{{msg3}}</p>
<p @click="changeData">改变父组件数据</p> // 1
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
"parentItem":{
template:'#parent',
data(){
return {
data : "父组件数据" ,
msg1:"数据11",
msg2:"数据22",
msg3:"数据33",
}
},
methods:{
// 4
change(newdata1,newdata2){
console.log(newdata1,newdata2);
this.data=newdata2;
//this.$refs.aaa.data 访问子组件
}
},
components:{
"sonItem":{
template:"#son",
data(){
return {
data : "子组件数据",
data2:"修改父组件数据2",
data3:"修改父组件数据3"
}
},
props:["msg1","msg2","msg3"],
methods:{
//2
changeData(){
console.log(this.data2);
this.$emit("change",this.data2,this.data3);
}
}
}
}
}
}
})
</script>
父组件可以通过refs访问子组件。
vue使用自己的本身的属性,前面加$
七、兄弟组件之间传参
- 事件总线
在main.js中定义$bus
beforeCreate(){
Vue.prototype.$bus = this //安装全局事件总线,$bus 就是当前应用的vm
}
然后可以通过this.
b
u
s
.
bus.
bus.on()监听事件,This.
b
u
s
,
bus,
bus,emit() 发起一个事件
2. Vuex
Vuex是专为vue开发的一个状态管理模式,为了解决不同vue组件间的共享数据问题。
适用场景:多个视图行为要更改统一数据,同一数据要被多个视图使用,(中大型单页面应用)
状态 data 视图 template 操作 methods
单一状态树
State 存放公共数据的地方
Getter 从state中派生出一些状态(根据一些业务场景,对state进行处理)
Mutations 不能直接对state进行修改,必须通过提交mutations来进行修改,同步的
Actions 异步处理,通过分发操作触发mutations
Mudule 将store分割,减少代码臃肿
Ajax 如果需要,一般写在actions里面
兄弟组件之间有大量通信的,建议一定要用vuex
八、插槽
作用:在组件内部定义一些标签,这些标签将无法显示,如果要显示需要在组件模板内使用接受
类型:默认插槽,具名插槽,作用域插槽(带数据的插槽)
<div id="app">
<runoob>
<h2>插槽内容1</h2>
<h2>插槽内容2</h2>
<h2 slot='slot1'>具名插槽</h2>
</runoob>
</div>
<template id='temp'>
<div>
<h1>父组件:{{data}}</h1>
<!--<slot></slot> --> <!--默认插槽-->
<slot><h2>插槽备胎</h2></slot>
<slot name='slot1'></slot>
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
'runoob':{
template:'#temp',
data(){
return {
data:'父组件数据'
}
}
}
}
})
</script>
作用域插槽:带数据的插槽,子组件负责提供数据,父组件负责数据的显示格式。也是子组件向父组件传递数据的一种方法。
注意:使用作用域插槽,需要Vue版本在2.6以后。
<div id="app">
<parent></parent>
</div>
<!--父组件-->
<template id='parentTemp'>
<div>
<h1>父组件:{{data}}</h1>
<son> <!—必须用template包围-->
<template slot-scope="propName">
<ul>
<li v-for="color in propName.data">{{color}}</li>
</ul>
</template>
</son>
</div>
</template>
<!—子组件模板-->
<template id='sonTemp'>
<div>
<h1>子组件:{{data}}</h1>
<slot :data='arr'></slot>
</div>
</template>
<script>
new Vue({
el: '#app',
components:{
'parent':{
template:'#parentTemp',
data(){
return {
data:'父组件数据'
}
},
components:{
'son':{
template:'#sonTemp',
data(){
return {
data:'子组件数据',
arr:["red","blue","green","black"]
}
}
}
}
}
}
})
</script>
九、Vue代码优化方式
- 区分使用v-if 和v-show
相同点:v-if和v-show都可用来控制这个控件显示或不显示
区别:
(1)v-if 多用于条件判断,每次一切换都会重新渲染,重新走一次vue生命周期。具有较高的切换代价;v-show 用于条件隐藏,它只是把控件的样式display:none, 不会增删dom元素,也不会重走生命周期,可以提高性能;
(2)v-if更适合于带有权限的操作,渲染时判断权限数据,有则展示该功能,没有则删除;v-show更适合于日常使用,可以减少数据的渲染,减少不必要的操作 - 区别使用computed 和watch
- 使用Object.freeze() 冻结vue中不需要修改的属性,从而阻止数据劫持,减少组件初始化时间。
- 图片懒加载,当图片滚动到可视区域时再去加载
- 路由懒加载
十、插件
插件与组件的区别:
vue 插件通常是为了给vue增加全局功能,是对vue功能的增强或补充,比如 Vue-router;组件是为了构成业务模块的。将一些公共的模块抽取出来,封装成组件。
Vue-router主要用于在单页面应用中,每个视图的展示都需要配备一个特殊的URL来实现切换效果,页面的刷新,前进,后退都不会向服务器发送请求,都需要通过这个特殊的URL来实现
实现过程:
1.定义组件
2.将路径与组件匹配起来 【{path:’/home’,component:home},】创建routers对象
3.使用路由,借助于router-link、router-view>使用路由,router-link会默认渲染成a标签,router-view会根据当前的路径,动态渲染出不同的组件。页面切换其实就是router-view挂载的dom元素的切换
实现单页面应用路由有两种形式:hash和history,vue-router默认使用的是hash实现的
路由懒加载,就是将路由对应的组件打包成一个个代码块,在这个路由被访问的时候才进行加载
ES6实现路由懒加载: const Home=()=>impot(‘…/components/Home.vue’)
十一、MVC 与MVVM
- MVC
Model 模型,是程序用于处理数据逻辑的部分,模型对象通常负责在数据库中存取数据
View:视图,负责依据模型数据渲染页面
Controller :控制器,是模型数据与视图之间通信的桥梁,负责从视图读取数据,控制用户输入,并向模型发送数据。同时负责将Model的数据用View显示出来.
优点:耦合度低,重用性高,代码部署快,可维护性高
缺点:比较适用于大中型项目。视图与模型虽然分离,但联系密切,妨碍了它们的独立重用。降低了视图对模型数据的访问,依据模型操作接口的不同,视图可能多次调用才能获得足够的显示数据 - MVVM
Model 模型,数据部分
View 视图,页面
ViewModel 视图模型,是mvvm的核心,是model与view之间的桥梁。负责
模型转化为视图,主要通过数据绑定来实现的;视图转化为模型,主要通过DOM事件监听来实现的。两种转化都实现,被称之为双向数据绑定。
数据绑定:当model数据改变时,不需要手动操作DOM元素改变view的显示,view会自动更新。(典型例子:Vue)
低耦合,可重用,可测试 - 比较
MVVM主要解决了MVC中大量的DOM操作使页面渲染性能降低,加载速度变慢,影响用户体验,vue数据驱动,通过数据来显示视图层而不是节点操作。数据操作比较多的场景,需要大量操作DOM元素时,采用MVVM的开发方式,会更加便捷。MVC和MVVM的区别并不是VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。