1、说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
2、v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
3、Class 与 Style 如何动态绑定?
Class 可以通过对象语法和数组语法进行动态绑定:
-
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div> data: { isActive: true, hasError: false } -
数组语法:
data: { activeClass: 'active', errorClass: 'text-danger' }
Style 也可以通过对象语法和数组语法进行动态绑定:
-
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> data: { activeColor: 'red', fontSize: 30 } -
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div> data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } }
4、怎样理解 Vue 的单向数据流?
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop 的情形 :
-
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } } -
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
5、computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
-
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
-
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
5.1computed中的getter和setter
说一下computed中的getter和setter。
问题描述
很多情况,我问到这个问题的时候对方的回答都是vue的getter和setter、订阅者模式之类的回答,我就会直接说问的并不是这个,而是computed,直接让对方说computed平时怎么使用,很多时候得到的回答是computed的默认方式,只使用了其中的getter,就会继续追问如果想要再把这个值设置回去要怎么做,当然一般会让问到这个程度的这个问题他都答不上来了。
期望答案
```
<!--直接复制的官网示例-->
当监听到的fullName发生变化,则set中对应的firstName和lastName也发生相应的操作
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]
}
}
}
```
5.2watch监听对象
问题
如何watch监听一个对象内部的变化。
问题描述
这个问题我感觉是一个不应该不会的问题,可是我遇到的人大部分都没有给出我所期望的答案,有些人会说直接监听obj,好一点的会说直接点出来监听obj.key,但是很少有人回答deep,开始我还会去问immediate,但是太多人不知道了,所以后来我就只问监听对象了,只有回答出deep的才会去问immediate的作用。
期望答案
如果只是监听obj内的某一个属性变化,可以直接obj.key进行监听。
```
watch: {
'obj.question': function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
}
```
如果对整个obj深层监听
```
watch: {
obj: {
handler: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
},
deep: true,
immediate: true
}
}
```
immediate的作用:当值第一次进行绑定的时候并不会触发watch监听,使用immediate则可以在最初绑定的时候执行。
6、nextTick知道吗,实现原理是什么?
问题描述
$nextTick用过吗,有什么作用。
问到这个问题时,很多人都会说到可以处理异步,而往下追问为什么要用nextTick,他解决了什么问题,不用他会怎么样的时候就很多人说不上来了。
期望答案
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。(官网解释)
解决的问题:有些时候在改变数据后立即要对dom进行操作,此时获取到的dom仍是获取到的是数据刷新前的dom,无法满足需要,这个时候就用到了$nextTick。
```
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
```
原理
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
(关于宏任务和微任务以及事件循环可以参考我的另两篇专栏)
(看到这你就会发现,其实问框架最终还是考验你的原生JavaScript功底)
7、常用的事件修饰符
- .stop:阻止冒泡
- .prevent:阻止默认行为
- .self:仅绑定元素自身触发
- .once: 2.1.4 新增,只触发一次
- passive: 2.3.0 新增,滚动事件的默认行为 (即滚动行为) 将会立即触发,不能和.prevent 一起使用
- .sync 修饰符
从 2.3.0 起vue重新引入了.sync修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。示例代码如下:
<comp :foo.sync="bar"></comp>
会被扩展为:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:
this.$emit('update:foo', newValue)
8、vue如何获取dom
先给标签设置一个ref值,再通过this.$refs.domName获取,例如:
<div ref="test"></div>
const dom = this.$refs.test
9、v-on可以监听多个方法吗?
是可以的,来个例子:
<input type="text" v-on="{ input:onInput,focus:onFocus,blur:onBlur, }">
10、assets和static的区别
这两个都是用来存放项目中所使用的静态资源文件。
两者的区别:
assets中的文件在运行npm run build的时候会打包,简单来说就是会被压缩体积,代码格式化之类的。打包之后也会放到static中。
static中的文件则不会被打包。
建议:将图片等未处理的文件放在assets中,打包减少体积。而对于第三方引入的一些资源文件如iconfont.css等可以放在static中,因为这些文件已经经过处理了。
11、vue初始化页面闪动问题
使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。
首先:在css里加上以下代码
[v-cloak] {
display: none;
}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
12、直接给一个数组项赋值,Vue 能检测到变化吗?
由于 JavaScript 的限制,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)
13、谈谈你对 Vue 生命周期的理解?
(1)生命周期是什么?
共分8个阶段:
beforeCreate
在实例初始化之后,数据观测者
data observer和event/watcher事件配置之前调用
created
在实例创建完成后立即调用,此时,实例已完成:观测者,属性和方法的运算,
watch/event事件回调,挂载阶段还没开始,$el属性目前不可见。
beforeMount
在挂载开始之前调用,相关的
render函数首次调用。
mounted
el被新创建的vm.$el替换,并且在挂载到实例上之后再调用该钩。如果root实例挂载了一个文档内元素,当调用mounted时vm.$el也在文档内。
beforeUpdate
在数据更新时调用,发生在虚拟
dom重新渲染和打补丁之前。
updated
由于数据更改导致的虚拟
dom重新渲染和打补丁,在这之后会调用该钩子。
beforeDestroyed
在实例销毁之前调用,在这一步,实例仍然完全可用。
destroyed
在vue.js实例销毁后调用,vue.js实例指示的所有东西都会解除绑定,所有的事件监听会被移除,所有的子实例也会被销毁。
如果使用组件的keep-alive功能时,增加两个周期:
activated在keep-alive组件激活时调用;deactivated在keep-alive组件停用时调用。
<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive>是一个抽象组件,它自身不会渲染一个DOM元素,也不会出现在父组件链中。
当在<keep-alive>内切换组件时,它的activated和deactivated这两个生命周期钩子函数将会执行。
<keep-alive>
<component :is="view"></component>
</keep-alive>
增加一个周期钩子:ErrorCaptured表示当捕获一个来自子孙组件的错误时调用。
(2)Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分
-
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
-
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
-
父组件更新过程
父 beforeUpdate -> 父 updated
-
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
(3)在哪个生命周期内调用异步请求?
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
(4)在什么阶段才能访问操作DOM?
在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。vue 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。
(5)父组件可以监听到子组件的生命周期吗?
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
(6)谈谈你对 keep-alive 的了解?
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
14、Vue.extend
1、Vue.extend( options ) : 基础Vue构造器
参数是一个包含组件选项的对象。
data选项是特例,在Vue.extend()中它必须是函数,为了保证引用数据不乱来。
<div id='app'>
<span>MyApp</span>
</div>
<script>
var Profile = Vue.extend({
template: '<p>目标:绑定到id为app的元素下面 打印data中的数据{{p1}},{{p2}}</p>',
data:function(){
return {
p1:'打印记录1',
p2:'打印记录2'
}
}
})
//进行绑定
new Profile().$mount('#mount-point');
</script>
2、为什么要使用extend ?
常规办法,我们的所有页面都是用router进行管理,组件也是通过import进行局部注册,也会存在不足的地方。
例如:假设我们需要从接口动态渲染组件的情况?如何实现一个类似window.alert()类似的提示组件,要求调用js一样使用?
3、如何使用extend构造一个自定义弹窗
1.编写弹窗组件
// 测试的话 建议直接复制粘贴
<template>
<div v-if="show" ref="modal" class="ek-modal_wrap">
<div class="ek-modal-content">
<div class="modal-title-wrap">
<div class="modal-title">{{ title }}</div>
<slot name="description"></slot>
</div>
<div class="modal-button">
<a v-if="confirmVisible" class="contral-btn confirm-btn" href="javascript:;" @click="confirm">{{
confirmText
}}</a>
<a v-if="cancleVisible" class="contral-btn cancle-btn" href="javascript:;" @click="cancle">{{ cancleText }}</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
title: '', // 标题
confirmText: '确定', // 确认文字
confirmVisible: true, // 是否展示确认按钮
onConfirm: () => { // 确认执行函数
this.$emit('confirm')
},
cancleText: '取消', // 取消文字
cancleVisible: true, // 是否展示取消按钮
onCancle: () => { // 取消执行函数
this.$emit('cancle')
}
}
},
methods: {
confirm() {
this.onConfirm()
this.close()
},
cancle() {
this.onCancle()
this.close()
},
close() {
this.show= false
if (this.$refs.modal) {
this.$refs.modal.remove() // 关闭时候直接移除当前元素
}
}
}
}
</script>
第二步:在vue的原型上绑定方法。
import Vue from 'vue';
import dialog from './components/Dialog.vue';
function showDialog(options) {
const Dialog = Vue.extend(dialog); // 返回一个vue子类
//创建实例并且挂载
const app = new Dialog().$mount(document.createElement('div'));
//初始化参数
for (let key in options) {
app[key] = options[key];
}
//将元素插入body中
document.body.appendChild(app.$el);
}
Vue.prototype.$showDialog = showDialog; //将方法放在原型上。
第三步:在vue页面中调用方法既可
....某vue页面
mounted() {
console.log(this.$showDialog);
this.$showDialog({
title: '测试弹窗',
confirmText: '希望弹出窗口正确调用',
cancelVisible: false,
});
},
... 欢迎各位尝试体验
15、自定义指令,从底层解决问题
什么是指令?指令就是你女朋友指着你说,“那边搓衣板,跪下,这是命令!”。开玩笑啦,程序员哪里会有女朋友。
通过上一节我们开发了一个loading组件,开发完之后,其他开发在使用的时候又提出来了两个需求
- 可以将
loading挂载到某一个元素上面,现在只能是全屏使用 - 可以使用指令在指定的元素上面挂载
loading
有需求,咱就做,没话说
1.开发v-loading指令
import Vue from 'vue'
import LoadingComponent from './loading'
// 使用 Vue.extend构造组件子类
const LoadingContructor = Vue.extend(LoadingComponent)
// 定义一个名为loading的指令
Vue.directive('loading', {
/**
* 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
* @param {*} el 指令要绑定的元素
* @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
*/
bind(el, binding) {
const instance = new LoadingContructor({
el: document.createElement('div'),
data: {}
})
el.appendChild(instance.$el)
el.instance = instance
Vue.nextTick(() => {
el.instance.visible = binding.value
})
},
/**
* 所在组件的 VNode 更新时调用
* @param {*} el
* @param {*} binding
*/
update(el, binding) {
// 通过对比值的变化判断loading是否显示
if (binding.oldValue !== binding.value) {
el.instance.visible = binding.value
}
},
/**
* 只调用一次,在 指令与元素解绑时调用
* @param {*} el
*/
unbind(el) {
const mask = el.instance.$el
if (mask.parentNode) {
mask.parentNode.removeChild(mask)
}
el.instance.$destroy()
el.instance = undefined
}
})
2.在元素上面使用指令
<template>
<div v-loading="visible"></div>
</template>
<script>
export default {
data() {
return {
visible: false
}
},
created() {
this.visible = true
fetch().then(() => {
this.visible = false
})
}
}
</script>
3.项目中哪些场景可以自定义指令
-
为组件添加
loading效果 -
按钮级别权限控制
v-permission -
代码埋点,根据操作类型定义指令
-
input输入框自动获取焦点
-
其他等等。。。
指令
16、组件中 data 为什么是一个函数?
为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?
// data
data() {
return {
message: "子组件",
childName:this.name
}
}
// new Vue
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
17、v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
使用.sync,更优雅的实现数据双向绑定
在Vue中,props属性是单向数据传输的,父级的prop的更新会向下流动到子组件中,但是反过来不行。可是有些情况,我们需要对prop进行“双向绑定”。上文中,我们提到了使用v-model实现双向绑定。但有时候我们希望一个组件可以实现多个数据的“双向绑定”,而v-model一个组件只能有一个(Vue3.0可以有多个),这时候就需要使用到.sync。
.sync与v-model的异同
相同点:
- 两者的本质都是语法糖,目的都是实现组件与外部数据的双向绑定
- 两个都是通过属性+事件来实现的
不同点(个人观点,如有不对,麻烦下方评论指出,谢谢):
- 一个组件只能定义一个
v-model,但可以定义多个.sync v-model与.sync对于的事件名称不同,v-model默认事件为input,可以通过配置model来修改,.sync事件名称固定为update:属性名
自定义.sync
在开发业务时,有时候需要使用一个遮罩层来阻止用户的行为(更多会使用遮罩层+loading动画),下面通过自定义.sync来实现一个遮罩层
<!--调用方式-->
<template>
<custom-overlay :visible.sync="visible" />
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
18、scoped属性作用
在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。
scoped属性的实现原理是给每一个dom元素添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom。
<template>
<div class="box">dom</div>
</template>
<style lang="scss" scoped>
.box{
background:red;
}
</style>
vue将代码转译成如下:
.box[data-v-11c6864c]{
background:red;
}
<template>
<div class="box" data-v-11c6864c>dom</div>
</template>
19、scoped样式穿透
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性。
-
使用/deep/
//Parent <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> .wrap /deep/ .box{ background: red; } </style> //Child <template> <div class="box"></div> </template> -
使用两个style标签
//Parent <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> //其他样式 </style> <style lang="scss"> .wrap .box{ background: red; } </style> //Child <template> <div class="box"></div> </template>
20、Vue 组件间通信有哪几种方式?
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1)props / $emit 适用 父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref 与 $parent / $children 适用 父子组件通信
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent/$children:访问父 / 子实例
(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4)$attrs/$listeners 适用于 隔代组件通信
$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on="$listeners"传入内部组件
(5)provide / inject 适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
原理:
provide用于向子组件(或子组件中的子组件,无限嵌套)提供自身的一些数据,或者将自身所有属性全部提供,但是提供的数据均为非响应式数据。
inject用于引入父级组件所提供的数据。
1、实用技巧;子(或更低层)组件中直接调用父组件(或更高层)的方法
下面代码为:父组件代码
<template>
<div>
<inject-test></inject-test>
</div>
</template>
<script>
import InjectTest from "./injectTest";
export default{
components: {InjectTest},
name: 'ProvideTest',
provide(){
return {
parentTest:this//通过provide提供自身所有属性
}
},
methods: {
printMessage(msg){
alert(msg)
}
}
}
</script>
<template>
<div>
<button @click="handleClick">点击我调用父组件方法</button>
</div>
</template>
<style scoped>
</style>
<script>
export default{
name: 'InjectTest',
inject:['parentTest'],
data(){
return {}
},
methods: {
//子组件调用父组件方法
handleClick(){
this.parentTest.printMessage("message!")
}
}
}
</script>
可以看到,子组件通过inject父组件提供的provide的key直接调用父组件的方法,这点在实际开发当中是个非常实用的技巧。
(6)Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
21、你使用过 Vuex 吗?
1、Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
2、怎么在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
3、组件中重复使用mutation
使用mapMutations辅助函数,在组件中这么使用
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)
4、mutation和action有什么区别
-
action 提交的是 mutation,而不是直接变更状态。mutation可以直接变更状态
-
action 可以包含任意异步操作。mutation只能是同步操作
-
提交方式不同
action 是用this.store.dispatch('ACTION_NAME',data)来提交。 mutation是用this.$store.commit('SET_NUMBER',10)来提交 -
接收参数不同,mutation第一个参数是state,而action第一个参数是context,其包含了
{ state, // 等同于 `store.state`,若在模块中则为局部状态 rootState, // 等同于 `store.state`,只存在于模块中 commit, // 等同于 `store.commit` dispatch, // 等同于 `store.dispatch` getters, // 等同于 `store.getters` rootGetters // 等同于 `store.getters`,只存在于模块中 }
5、在v-model上怎么用Vuex中state的值?
需要通过computed计算属性来转换。
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
22、代码优化
1.页面需要导入多个组件,如何避免挨个写入,比如把一个目录下所有的组件一次性全部引入,require.context()
1.场景:如页面需要导入多个组件,原始写法:
import titleCom from '@/components/home/titleCom'
import bannerCom from '@/components/home/bannerCom'
import cellCom from '@/components/home/cellCom'
components:{titleCom,bannerCom,cellCom}
2.这样就写了大量重复的代码,利用 require.context 可以写成
const path = require('path')
const files = require.context('@/components/home', false, /\.vue$/)
const modules = {}
files.keys().forEach(key => {
const name = path.basename(key, '.vue')
modules[name] = files(key).default || files(key)
})
components:modules
这样不管页面引入多少组件,都可以使用这个方法
2.components和 Vue.component的区别
components:局部注册组件
//1.创建组件构造器
var obj = {
props: [],
template: '<div><p>{{extendData}}</p></div>',//最外层只能有一个大盒子,这个和<tempalte>对应规则一致
data: function () {
return {
extendData: '这是component局部注册的组件',
}
},
};
var Profile = Vue.extend(obj);
//3.挂载
new Vue({
el: '#app',
components:{
'component-one':Profile,
'component-two':obj
}
});
Vue.component:全局注册组件
1.将通过 Vue.extend 生成的扩展实例构造器注册(命名)为一个全局组件,参数可以是Vue.extend()扩展的实例,也可以是一个对象(会自动调用extend方法) 2.两个参数,一个组件名,一个extend构造器或者对象
//1.创建组件构造器
var obj = {
props: [],
template: '<div><p>{{extendData}}</p></div>',//最外层只能有一个大盒子,这个和<tempalte>对应规则一致
data: function () {
return {
extendData: '这是Vue.component传入Vue.extend注册的组件',
}
},
};
var Profile = Vue.extend(obj);
//2.注册组件方法一:传入Vue.extend扩展过得构造器
Vue.component('component-one', Profile)
//2.注册组件方法二:直接传入
Vue.component('component-two', obj)
//3.挂载
new Vue({
el: '#app'
});
//获取注册的组件 (始终返回构造器)
var oneComponent=Vue.component('component-one');
console.log(oneComponent===Profile)//true,返回的Profile构造器
3.mixins 在项目中使用场景
场景:有些组件有些重复的 js 逻辑,如校验手机验证码,解析时间等,mixins 就可以实现这种混入 mixins 值是一个数组
var mixin={
data:{mixinData:'我是mixin的data'},
created:function(){
console.log('这是mixin的created');
},
methods:{
getSum:function(){
console.log('这是mixin的getSum里面的方法');
}
}
}
var mixinTwo={
data:{mixinData:'我是mixinTwo的data'},
created:function(){
console.log('这是mixinTwo的created');
},
methods:{
getSum:function(){
console.log('这是mixinTwo的getSum里面的方法');
}
}
}
var vm=new Vue({
el:'#app',
data:{mixinData:'我是vue实例的data'},
created:function(){
console.log('这是vue实例的created');
},
methods:{
getSum:function(){
console.log('这是vue实例里面getSum的方法');
}
},
mixins:[mixin,mixinTwo]
})
//打印结果为:
这是mixin的created
这是mixinTwo的created
这是vue实例的created
这是vue实例里面getSum的方法
对于混入组件 和 主组件中的相关冲突:
1. 对于方法来说,主组件中的方法会覆盖掉混入组件的方法(名字相同时)
2. 对于生命周期相同时,会将其合并在一个里面,对应周期里的名字相同时,也会被覆盖
3. 混合对象里的钩子函数在主组件里的钩子函数之前调用;
4. 钩子函数相同时,混入对象函数中的console会在主组件里的钩子函数之前调用;
与vuex的区别
经过上面的例子之后,他们之间的区别应该是:
vuex:用来做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。
Mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的,值的修改在组件中不会相互影响。
4.extends
extends用法和mixins很相似,只不过接收的参数是简单的选项对象或构造函数,所以extends只能单次扩展一个组件
var extend={
data:{extendData:'我是extend的data'},
created:function(){
console.log('这是extend的created');
},
methods:{
getSum:function(){
console.log('这是extend的getSum里面的方法');
}
}
}
var mixin={
data:{mixinData:'我是mixin的data'},
created:function(){
console.log('这是mixin的created');
},
methods:{
getSum:function(){
console.log('这是mixin的getSum里面的方法');
}
}
}
var vm=new Vue({
el:'#app',
data:{mixinData:'我是vue实例的data'},
created:function(){
console.log('这是vue实例的created');
},
methods:{
getSum:function(){
console.log('这是vue实例里面getSum的方法');
}
},
mixins:[mixin],
extends:extend
})
//打印结果
这是extend的created
这是mixin的created
这是vue实例的created
这是vue实例里面getSum的方法
5.Vue.use()
场景:我们使用 element时会先 import,再 Vue.use()一下,实际上就是注册组件,触发 install 方法; 这个在组件调用会经常使用到; 会自动组织多次注册相同的插件.
23、你有对 Vue 项目进行哪些优化?
如果没有对 Vue 项目没有进行过优化总结的同学,可以参考本文作者的另一篇文章《 Vue 项目性能优化 — 实践指南 》,文章主要介绍从 3 个大方面,22 个小方面详细讲解如何进行 Vue 项目的优化。
一、代码层面的优化
1.1、v-if 和 v-show 区分使用场景
v-if 是 真正 的条件渲染;v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
1.2、computed 和 watch 区分使用场景
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
-
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
-
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
1.3、v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
(1)v-for 遍历必须为 item 添加 key
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
(2)v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
推荐:
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
1.4、长列表性能优化
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
1.4、Object.freeze使用场景
场景:一个长列表数据,一般不会更改,但是vue会做getter和setter的转换 用法:是ES5新增的特性,可以冻结一个对象,防止对象被修改 支持:vue 1.0.18+对其提供了支持,对于data或vuex里使用freeze冻结了的对象,vue不会做getter和setter的转换 注意:冻结只是冻结里面的单个属性,引用地址还是可以更改
new Vue({
data: {
// vue不会对list里的object做getter、setter绑定
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
},
mounted () {
// 界面不会有响应,因为单个属性被冻结
this.list[0].value = 100;
// 下面两种做法,界面都会响应
this.list = [
{ value: 100 },
{ value: 200 }
];
this.list = Object.freeze([
{ value: 100 },
{ value: 200 }
]);
}
})
1.5、事件的销毁
Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() {
addEventListener('click', this.click, false)
},
beforeDestroy() {
removeEventListener('click', this.click, false)
}
1.6、图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:
(1)安装插件
npm install vue-lazyload --save-dev
(2)在入口文件 man.js 中引入并使用
import VueLazyload from 'vue-lazyload'
然后再 vue 中直接使用
Vue.use(VueLazyload)
或者添加自定义选项
Vue.use(VueLazyload, {
preLoad: 1.3,
error: 'dist/error.png',
loading: 'dist/loading.gif',
attempt: 1
})
(3)在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:
<img v-lazy="/static/img/1.png">
以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址。
1.7、路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
路由懒加载:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
1.8、第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:
(1)首先,安装 babel-plugin-component :
npm install babel-plugin-component -D
(2)然后,将 .babelrc 修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
(3)在 main.js 中引入部分组件:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
1.9、优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
(3)基础的 Web 技术的优化
-
开启 gzip 压缩
-
浏览器缓存
-
CDN 的使用
-
使用 Chrome Performance 查找性能瓶颈
24、说说你使用 Vue 框架踩过最大的坑是什么?怎么解决的?
本题为开放题目,欢迎大家在评论区畅所欲言,分享自己的踩坑、填坑经历,提供前车之鉴,避免大伙再次踩坑 ~~~
1、打包之后文件、图片、背景图资源不存在或者路径错误的问题
先看下项目的config文件夹下的index.js文件,这个配置选项就好使我们打包后的资源公共路径,默认的值为‘/’,即根路径,所以打包后的资源路径为根目录下的static。由此问题来了,如果你打包后的资源没有放在服务器的根目录,而是在根目录下的mobile等文件夹的话,那么打包后的路径和你代码中的路径就会有冲突了,导致资源找不到。
所以,为了解决这个问题,你可以在打包的时候把上面这个路径由‘/’的根目录,改为‘./’的相对路径。
这样的的话,打包后的图片啊js等路径就是‘./static/img/asc.jpg’这样的相对路径,这就不管你放在哪里,都不会有错了。但是,凡是都有但是~~~~~这里一切正常,但是背景图的路径还是不对。因为此时的相对就变成了static/css/文件夹下的static/img/xx.jpg,但是实际上static/css/文件夹下没有static/img/xx.jpg,即static/css/static/img/xx.jpg是不存在的。此时相对于的当前的css文件的路径。所以为了解决这个问题,要把我们css中的背景图的加个公共路径‘../../’,即让他往上返回两级到和index.html文件同级的位置,那么此时的相对路径static/img/xx.jpg就能找到对应的资源了。那么怎么修改背景图的这个公共路径呢,因为背景图是通过loader解析的,所以自然在loader的配置中修改,打开build文件夹下的utils文件,找到exports.cssLoaders的函数,在函数中找到对应下面这些配置:
找到这个位置,添加一上配置,就是上图红框内的代码,就可以把它的公共路径修改为往上返回两级。这样再打包看下,就ok了!
2、打包后生成很大的.map文件的问题
项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。 而生成的.map后缀的文件,就可以像未加密的代码一样,准确的输出是哪一行哪一列有错可以通过设置来不生成该类文件。但是我们在生成环境是不需要.map文件的,所以可以在打包时不生成这些文件:
在config/index.js文件中,设置productionSourceMap: false,就可以不生成.map文件
3、路由问题
路由 TypeError: Cannot read property 'matched' of undefined 的错误问题
找到入口文件main.js里的new Vue(),必须使用router名,不能把router改成Router或者其他的别名
// 引入路由
import router from './routers/router.js'
new Vue({
el: '#app',
router, // 这个名字必须使用router
render: h => h(App)
});
Vue里面router-link在电脑上有用,在安卓上没反应怎么解决
Vue路由在Android机上有问题,babel问题,安装babel polypill插件解决
Vue2中注册在router-link上事件无效解决方法
<li >
<router-link to="/one" @click="change('color1')" :class="{'bgred':fff['color1']}">
页面一</router-link>
</li>
使用@click.native。原因:router-link会阻止click事件,.native指直接监听一个原生事件
RouterLink在IE和Firefox中不起作用(路由不跳转)的问题
-
只用a标签,不使用button标签
-
使用button标签和Router.navigate方法
Admin
使用<button>标签和Router.navigate方法
<button type="button" clrDropdownItem (click)="gotoAdmin()">
Admin
</button>
import { Router } from '@angular/router';
...
export class MainComponent {
constructor(
private router: Router
) {}
gotoAdmin() {
this.router.navigate(['/admin']);
}
}
下篇:
整理自
1、30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度) (juejin.cn)