1.v-show和v-if的区别
v-show通过CSS display控制显示和隐藏,v-if是组件的渲染和销毁,
所以频繁切换显示状态用v-show,否则用v-if
2.为何在v-for中使用key?
必须使用key,且最好不用index,diff算法中通过tag和key来判断是否是相同节点,目的是减少渲染次数,提高性能
key的作用
key是Vue中vnode的唯一标记,通过这个标记可以使diff操作更高效
3.双向数据绑定v-model的实现原理
v-model是v-bind绑定value变量,和v-on绑定input事件的结合,
通过v-bind绑定value变量,每次输入内容时候触发input事件,通过事件对象参数获取内容然后赋值给value变量,value发生变化之后input输入框发生变化,,从而实现双向绑定。
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
自定义组件的双向绑定:
父组件给子组件添加v-model,子组件通过prop的value属性获取数据,子组件内部想要修改value属性的时候通过$emit触发input事件并且携带最新的值给父组件,父组件把值同步到value变量上。
//parent
<custom-v-model v-model="text"></custom-v-model>
//child
<input :value="text" @input="$emit('input', $event.target.value)" />
props: {
text: String
},
model: {
prop: 'text',
event: 'input'
},
4.为什么组件的data必须是个函数?
因为.vue文件中定义的组件是一个类,每个地方使用该组件的时候都是对组件的实例化,如果data是个对象,组件复用会相互影响。
5.何时使用keepalive?
缓存组件,不需要重复渲染,
多个静态tab页的切换
优化性能
6.何时需要使用beforeDestory?
解绑自定义事件event.$off, 清除定时器,解绑自定义DOM事件,如window.scroll等
7.对vue的MVVM的理解
MVVM分为三个部分,
Model模型层,负责业务数据
View视图层, 负责视图相关的,html和css
ViewModel是View和model连接的桥梁,负责监听模型层和视图层的修改,
MVVM支持双向绑定,意思是当模型层数据进行修改时,VM层会检测到变化,并通知视图层进行更新,反之修改视图层也会通知模型层进行修改。
8.怎样理解Vue的单向数据流
prop让父子组件之间形成了一个单向绑定,父组件的prop更新单向流动到子组件,反过来则不行,这样防止子组件改变父组件的状态从而造成混乱。如果子组件想修改,只能通过$emit派发一个自定义事件通知父组件修改。
有两种常见尝试修改prop的情况:
1.prop传递一个初始值,子组件想将其作为一个本地的数据使用,可以定义一个本地变量,将prop值作为初始值。
2.prop值作为初始值传入并且需要进行转换,可以定义一个计算属性。
9.computed和watch的区别和应用场景?
computed计算属性:如果依赖的属性没有发生变化,调用函数的 时候会从缓存中取值,使用场景是当一个属性受多个属性影响时使用,比如购物车结算。
watch监听属性:每次监听的属性发生变化是执行回调,使用场景是一个属性影响多个属性时,比如搜索框。
10.直接给一个数组项赋值,Vue能检测到吗?
由于js的限制,直接给数组的索引位置赋值并不会触发Vue的更新机制
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
复制代码// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
11.Vue生命周期的理解?
指的是Vue实例从创建,初始化数据,编译模板,挂载,渲染,更新,卸载的过程。
beforeCreate:组件实例开始创建
created:组件实例已创建,属性和方法已初始化
beforeMount:在挂载之前被调用,模板编译完成,render函数首次被调用。
mounted:挂载完成,dom树已经渲染到页面上,可以进行dom操作。
beforeUpdate:组件数据更新之前调用,发生在虚拟dom打补丁之前。
updated:组件数据更新之后
activated:keepalive专用,组件被激活时调用
deactivated:组件销毁时调用
beforeDestory:组件销毁前调用
destory:组件销毁后调用
12.Vue的父组件和子组件生命周期钩子函数的执行顺序?
执行顺序分为四类:
加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程:
父 beforeUpdate -> 父 updated
销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
13.在哪个生命周期中调用异步请求?
‘可以在created和beforeMount,mounted中进行,因为此时data和方法都已经创建,推荐在creatde中进行,因为这样可以减少加载事件。
14.父组件可以监听子组件的生命周期吗?
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
方法二
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
15.谈谈对keep-alive的理解
keepalive可以使被包含的组件保留状态避免被重新渲染。
一般结合路由和动态组件一起使用,用于缓存组件,
有include和exclude属性,都支持字符串和正则表达式,include表示缓存的组件,exclude表示不被缓存的组件,,其中exclude优先级比include高。
对应的两个钩子函数activated和deactivated,当组件被激活时触发activated,组件被移除时触发deactivated。
16.多个组件有相同逻辑,如何抽离?(mixin, 以及mixin缺点)
17.你使用过vuex吗?
vuex是一个状态管理模式,集中存储管理所有组件的状态,每一个vuex的核心是store,就是一个容器,包含大部分状态。
Vux的存储是响应式的,放store中状态发生改变,相应的组件也会发生更新。
改变状态的唯一方法是commit mutions。
可以用于大型项目的组件之间的状态管理,小型项目推荐使用localstorage作为数据之间的传递。
vuex的核心概念有state,getter,mutation,action,module
state存放的是状态
mutations存放如何更改状态,且必须是同步函数
getters是从state中派生出状态,比如state中获取某个状态进行过滤后获取新的状态。
actions,通过commit mutations中的方法来改变状态,重点是可以进行异步操作。
module,可以把容器分成几个模块,把状态和方法分类,让代码更清晰。
18.Vue怎么用vm.$set()解决对象新增属性不能响应的问题
vm.$set实现原理是:
如果目标是数组,直接使用数组的splice方法触发响应式
如果目标是对象,会先判断属性是否存在,对象是否是响应式,最后如果对属性进行响应式处理,则是通过调用defineReactive方法进行响应式处理(defineReactive方法就是Vue在初始化对象时,给对象属性采用Obejct.defineProperty动态添加getter和setter的功能所调用的方法)
19.你有对Vue项目进行哪些优化?
代码层面:
v-if和v-show
computed和watch区分使用场景
v-for必须添加key,且不能与v-if同时用
事件销毁
图片懒加载
路由懒加载
按需引入插件
webpack层面优化:
webpack对图片进行压缩
减少es6转为es5的冗余代码
提取公共代码
模板预编译
Vue项目的编译优化
基础的web技术的优化
开启gzip压缩
浏览器缓存
cdn的使用
Vue响应式
1.监听data变化的核心api是什么?
Object.defineProperty(Vue3.0后使用proxy)
接受三个参数(更新的对象,属性名,配置项)
作用是直接在一个对象上定义一个新属性,或修改一个已存在的属性
它的缺点:
1.监听不到对象新增和删除属性
2.监听不到数组下标的变化,导致通过数组下标添加元素无法实时响应
3.性能问题,需要为每个属性都添加setter和getter,当监听的数据较大时,会带来一定性能问题
writable: 是否可重写
value: 当前值
get: 读取时内部调用的函数
set: 写入时内部调用的函数
enumerable: 是否可以遍历
configurable: 是否可再次修改配置项
get:当有人读取n属性时,get函数会被调用,返回n的值,
set:当哦有人修改n属性时,set函数会被调用,会受到要修改的值,可以实现数据的联动效果。
基本使用
const data = {}
let n = 'chenrui'
Object.defineProperty(data, 'n', {
get: function() {
console.log('get');
return n
},
set: function(newVal) {
console.log('set');
n = newVal
}
})
// 测试
console.log(data.n);
data.n = 'wuwuwu'
console.log(data);
2.深度监听data变化
// 通过Object.defineproperty深度监听数组,对象
// 触发更新视图
function updateView() {
console.log('视图更新');
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
observer(value)
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newVal) {
if(value !== newVal) {
observer(newVal)
// 注意:value一直在闭包中,此处设置完之后,再get时也是最新的值
value = newVal
// 触发视图更新
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if(typeof target !== 'object' || target === null) {
return target //不是数组或对象
}
if(Array.isArray(target)) {
target.__proto__= arrProto
for (let i = 0; i < target.length; i++) {
observer(target[i])
}
}
// 重新定义各个属性(for in也可以遍历数组)
for(let key in target) {
defineReactive(target, key, target[key])
}
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// Object.create 创建新对象,原型指向oldArrayProperty,扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty)
const methodsArr = ['push', 'pop', 'shift', 'unshift', 'splice']
methodsArr.forEach((methodName) => {
arrProto[methodName] = function() {
updateView()
console.log('监听数组成功', methodName);
oldArrayProperty[methodName].call(this, ...arguments)
}
})
// 准备数据
let data = {
name: 'chenrui',
info: {
address: '北京'
},
nums: [1,2,3,65]
}
// 监听数据
observer(data)
data.name = 'rt'
data.n = 'nnnn' //新增属性,监听不到,---Vue.set
delete data.name //删除属性,监听不到 ---Vue.delete
data.info.address = 'shanghai ' //深度监听
data.nums.push(3232)
2.模板编译(渲染和模板编译的关系)
是将模板字符串转换为rander函数的过程。具体说是当组件的生命周期执行到created和beforemounted之间时,Vue将模板编译成rander函数,它是一个纯js函数,由with语句构成,接受一个Vue组件实例参数,render函数执行时会调用h函数,生成虚拟dom。
vdom: patch(elem, vnode) 和patch(vnode, newVnode)
3.组件渲染/更新的过程
初次渲染过程:
解析模板为render函数,(或在开发环境已完成)触发响应式,监听data属性getter setter, 执行render函数,生成vnode, 并且执行patch(elem, vnode)
更新过程:
修改data,触发setter,(此前在getter中已被监听),重新执行render函数,生成newVnode,执行patch(vnode, newVnode),diff算法更新视图
相关的三个考点:渲染和响应式的关系,渲染和模板编译的关系,渲染和vdom的关系
4.描述响应式原理(Obejct.defineProperty + 渲染更新过程)
通过Object.defineproperty遍历对象的属性,把每个属性转换成带有getter和setter的属性,读取属性时调用getter,修改属性时调用setter。
当运行render函数时,用到了data中的数据时,就会调用该数据的getter函数,然后用watcher记录下来,当数据发生改变时,就会调用setter函数,watcher也会记录下来,然后通知render函数重新运行,然后生成虚拟dom树。
5.什么是虚拟dom?
虚拟dom是一棵以js对象为基础的树,用对象来描述节点(vnode)。
如果组件内有响应的数据,数据发生改变时,render函数会生成一个新的虚拟dom,新的虚拟dom和旧的虚拟dom进行对比(这个对比过程就是diff算法),最后把差异更新到真实dom中。
虚拟dom的优缺点
优点:
保证性能的下限,虚拟dom需要适配任何api可能产生的操作,它的dom操作必须是普遍适用的,虽然性能不是最优的但比起直接操作dom的性能要好,所以是保证性能的下限。
无需手动操作dom,虚拟dom的diff和patch都是在一次更新中自动进行的
跨平台:虚拟dom本质是js对象,而dom与平台强相关, 相比之下虚拟DOM可以进行更方便地跨平台操作 。
缺点:
无法进行极致优化。
6.为什么需要虚拟dom?
虚拟dom把整个dom树抽象成一个js对象,建立了一一对应的关系,每次dom的更改,通过找到相应对象,就找到了相应的dom节点,然后对其更新,可以节省性能,并且js对象的查询比对整个dom树的查询消耗的性能要多,可以直接操作js对象,而不需要频繁操作dom。
7.简述diff算法过程
diff算法的用途是在新老虚拟dom之间找到最小更新的部分,将该部分进行dom更新,
只会在同层级进行比较
diff算法
1.通过patch判断新旧节点是否相同(通过选择器sel和key唯一标识判断),不同则删除旧节点
2.相同就通过patchVnode函数进一步比较,在这个方法内部,先判断新旧节点石头相同如果相同,直接return,如果不同,要分情况对比,对比原则是以新虚拟节点的结果为准,有以下几种情况:
1.新旧节点都有文本节点,就用新的文本节点代替旧文本节点
2.旧节点没有子节点,新节点有子节点,就直接添加新节点
3.旧节点有子节点,新节点没有子节点,直接删除旧子节点
4.新旧节点都有子节点,就执行updateChildren方法
8.异步渲染
$nexttick等dom渲染完成之后再回调,页面渲染会将data的修改做整合,一次性更新视图,目的是减少dom操作次数,提高性能
9.Vue如何实现数据双向绑定(原理 )?
双向绑定指的是:数据变化更新视图,视图变化更新数据。
主要通过四个步骤实现双向绑定:
1.监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineproperty()对属性加上setter和getter,这样的话能监听到数据变化。
2.解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化视图,并且将每个节点都绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
3.订阅者Watcher:watcher订阅者是Observer和Compile之间通信的桥梁,主要任务是订阅Observer中属性值变化的消息,当收到属性变化的消息时,触发解析器Compile中对应的更新函数。
4.订阅器Dep:订阅器采用发布-订阅的模式,收集订阅者watcher,对监听器Observer和订阅者watcher进行统一管理。
vue router
1.路由传参的三种方式
路由传参方式可以分为params传参和query传参,而params传参又可以分为在url中显示参数和不显示参数两种方式,这就是vue路由传参的三种方式。
方式一:params传参(显示参数)
params传参(显示参数)分为声明式和编程式两种:
a.声明式router-link
//子路由配置
{
path: '/child/:id',
component: Child
}
//父路由组件
<router-link :to="/child/123">进入Child路由</router-link>
b.编程式this.$router.push
//子路由配置
{
path: '/child/:id',
component: Child
}
//父路由编程式传参(一般通过事件触发)
this.$router.push({
path:'/child/${id}',
})
//在子路由中这样获取参数
this.$route.params.id
方法二:params传参(不显示参数)
也分为声明式和编程式,与方式一不同的是通过路由的name属性进行传值
这种params传值不显示参数的方法会导致在刷新页面时,传递的值会丢失。
a.声明式
<router-link :to="{name:'Child',params:{id:123}}">进入Child路由</router-link>
b.编程式
//子路由配置
{
path: '/child,
name: 'Child',
component: Child
}
//父路由编程式传参(一般通过事件触发)
this.$router.push({
name:'Child',
params:{
id:123
}
})
方式三:query传参(显示参数)
a.声明式
//子路由配置
{
path: '/child',
name: 'Child',
component: Child
}
//父路由组件
<router-link :to="{name:'Child',query:{id:123}}">进入Child路由</router-link>
b.编程式
//子路由配置
{
path: '/child',
name: 'Child',
component: Child
}
//父路由编程式传参(一般通过事件触发)
this.$router.push({
name:'Child',
query:{
id:123
}
})
query传参,name和path都行
2.hash和history的对比:
hash特点:
通过location.hash实现
hash永远不会提交到服务端
hash变化会触发页面跳转,即浏览器的前进,后退,
hash变化不会刷新页面,spa必需的特点,
可以通过hashchange事件监听hash值的变化进行页面跳转
history特点:
h5 history通过history.pushState和window.onpopstate监听
h5 history需要后端支持
两者选择:
B端:推荐使用hash,简单易用,对url规范不敏感
C端:可以用h5 history,但需要服务端支持