前端-VUE重点笔记
问题1:介绍一下vue-router
vue-router是Vue的一个官方路由插件,路由用于设定访问路径,将路径和组件对应起来。在vue-router的单页面应用中,路径的切换就是组件的切换。
可以通过两种方式来使用(vue-router的路由的跳转方式)
第一种是使用router-link和router-view,它们是vue的两个内置组件可以直接使用。我们在router-link上设置to属性为某一个路径,router-view会根据当前的路径,动态渲染不同的组件。其中router-link默认会被渲染成一个a标签,我们可以通过tag属性来指定它被渲染成什么标签。
第二种是通过JS来控制路由的切换,组件对象上都会有router的push方法和replace方法,将路径作为参数传进去来实现路由跳转。通过push方法跳转的路由还可以使用浏览器的回退按钮回到上一个页面,但是replace不可以,它是直接替换掉的。
如果我们需要把某种模式匹配到的所有路由,全都映射到同个组件,就可以使用动态路由。动态路由在配置时需要我们在前边加一个冒号,比如user路径后边加/:id。然后在组件中使用$route对象的params再点上对应的路由名称就可以获取这个参数。
问题2:介绍一下vuex
当遇到多个组件需要共享状态时,就可以把组件的共享状态抽取出来。vuex就是这样一个工具,它采用集中式存储管理组件的状态,并且保证状态的变化是响应式的。在这种模式下,任何组件都能获取状态或者触发行为。我们可以通过$store.state
属性的方式来访问状态,通过$store对象的commit方法显示提交mutation来修改状态。
vuex的核心概念有五个,分别是state、getters、mutations、actions、modules。state对象中保存着组件之间需要共享的状态,而getter类似于计算属性,当我们需要从state中派生出一些状态时,就可以使用getter,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,通过store对象的dispatch来调用分发action。当整个应用的状态变得复杂时,就可以使用module分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。
问题3:介绍一下axios
axios是一个基本promise,用于浏览器的http客户端,支持在浏览器中发送XMLHttpRequest请求,支持promise的api,可以拦截请求和相应。
问题4:事件总线
我们可以在main.js文件中使用Vue.prototype.bus = new Vue(),来创建一个事件总线。它就相当于一个组件共用的事件中心,可以向该中心注册发送事件或接收事件。我们可以使用$emit方法来发送事件,使用$on方法来接收事件。
问题5:vue中兄弟组件通信
- 子传父,父再传子
- vuex
- 事件总线
问题6:vue中的过滤器
vue允许我们自定义过滤器,可被用于 一些常见的文本格式化,过滤器可以用在双花括号中,也可以用在v-bind表达式中。我们可以使用Vue.filter定义全局的过滤器,也可以在组件中定义本地的过滤器,过滤器函数接收表达式的值作为第一个参数。过滤器可以串联,此时前一个过滤器的返回值将作为后边一个过滤器的参数。
问题7:vue中的动态组件
vue提供了一个特殊的元素<component>
,我们可以使用is属性来动态挂载不同的组件。is属性的值可以是组件的名字或者是组件对象。如果在组件切换的过程中,我们想要保持组件的状态,以避免反复重渲染导致的性能问题。就可以使用<keep-alive>
组件,它会将不活动的组件缓存起来,而不是销毁他们。
问题8:vue中的异步更新队列
Vue并不是会在数据发生变化之后立即更新 DOM,而是按照一定的策略进行异步执行。在侦听到数据变化后,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。然后在下一个事件循环中,vue会刷新队列并执行其中已经去重的工作。多数情况我们不需要关心这个过程,但是如果想基于更新后的 DOM 状态来做点什么事情,就可以在组件内使用$nextTick
方法,$nextTick
方法的回调函数将在 DOM 更新完成后被调用。
问题9:你的vue商城项目中封装的比较难的组件有哪些
有一个顶部导航栏的组件,导航栏分为左中右三个部分,这个组件会在多个页面中用到,封装的时候使用了插槽slot。
还有一个是商品信息展示的组件,通过父子组件通信将商品的数据传到这个组件中,展示出图片、文字等。
还有一些其它的组件,这个项目确实是采用组件化的思想进行开发了,代码没有全部写到一个文件中,不同的小功能都分配一个组件,然后导入,再通过组件通信传递数据进行使用。
问题10:vue数据的响应式原理
Vue2采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,vue在初始化时,会调observe方法对用户的数据进行监听,在observe方法中会去判断当前数据是否已经被观测过了,如果没又被观测过就会new Observer去观测这个对象,如果在new Observer时传入的是对象则执行walk方法,对对象的属性进行循环遍历,并调用defineReactive方法,这个方式是vue中的一个核心方法,用来定义响应式,在defineReactive方法中实例化了一个Dep(发布者),通过Object.defineProperty对数据进行拦截,把这些 property 全部转为 getter/setter。当页面中使用到某一个属性时,会在getter中收集依赖,通过使用dep实例的depend方法收集watcher。如果数据发生了变化会通过dep.notify发布通知,通知watcher,更新视图。在defineReactive方法中,如果对象的某一个属性依然是对象或者在改变数据后变成了一个新的对象,会继续调用observe方法,重新走一遍这个流程。
如果对象的某个属性是数组的话,会首先把这个数组的原型重新设置为我们自定义的对象。vue内部支持数组响应式的方法只有7个,分别是push、pop、shift、unshift、splice、sort、reverse,vue内部以Array.prototype对象为原型创建一个新的对象,我们使用Object.defineProperty方法在这个新的对象上定义上边的那7个方法,我们在重定义的时候就可以做一些事情,增加一些我们自己的逻辑,实现数组更新时的响应式。另外数组在调用方法,比如push方法的时候,它依然调用的是Array.prototype上的方法。然后在new Observer的时候把数组的原型设置为我们自定义的这个。
在vue2中使用getter和setter有一些不足之处,当对象新增或删除属性的时候是检测不到变化的,可以使用vue.set和vue.delete去更新数据。
问题11:vue的虚拟DOM和diff算法
JS在查找和修改真实的DOM结构时,是比较耗费时间的,但是JS直接操作对象却很快。在jquery时代,数据改变之后会直接去操作真实的DOM来引起视图的更新。
DOM是个与语言无关的API,它在浏览器中的接口是用JavaScript实现的,浏览器通常会把DOM和JavaScript独立实现。这对性能意味着什么?把DOM和JavaScript各自想象成一个岛屿,它们之间用收费桥梁连接。JS每次访问DOM都要途径这座桥,访问DOM的次数越多,费用也就越高。因此推荐的做法是尽可能减少过桥的次数,这也就是我们常说的要尽量避免和DOM的交互。所以在vue中就引入了虚拟DOM,虚拟DOM就是使用JS对象模拟真实的DOM结构和它的层次结构,DOM中的属性在虚拟DOM中都有对应的属性。数据改变之后,会把新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出最小量更新,最后反映到真正的DOM上。vue中虚拟DOM的实现是参考的snabbdom库。
在内部使用h函数来生成虚拟节点Vnode,由vnode组成结构就是虚拟DOM。vnode有很多优点,vnode的兼容性强,不受环境的影响,因为vnode是JS对象,不管是浏览器还是Node都可以进行操作。另外,使用vnode可以减少操作真实的dom,数据的变化先在虚拟节点上进行操作对比,在最后一步挂载更新DOM
,从而提高页面性能。h函数可以嵌套使用,从而生成虚拟DOM树。
diff算法是最小量更新算法,可以对两个虚拟DOM进行精细化比较,最后反映到真实的DOM上。只有是同一个虚拟节点,diff才进行精细化比较,否则就是暴力删除旧的、插入新的。
使用h函数时,传入的选择器和key都相同的时候,认为是同一个虚拟节点。key很重要,key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。如果没有key,它会销毁旧的,重新创建新的。另外,diff只进行同层比较,不会进行跨层比较。
问题12:vue中的混入mixin
当组件中有很多可复用的功能时,就可以使用mixin混入,mixin是一个对象,这个对象可以包含任意组件选项,可以有data,methods,生命周期函数等等。接下来在组件就可以使用这个混入对象,此时所有混入对象的选项将被“混合”进入该组件本身的选项。
当组件和混入对象有同名选项时,这些选项将以恰当的方式进行“合并”:
- data中的数据发生重名冲突时会以组件中的数据优先。
- 如果生命周期函数同名了,它们都会被调用,混入对象中定义的生命周期函数会在组件自身的生命周期函数之前调用。
- 对于值是对象的选项,比如methods,它们将被合并为同一个对象,如果两个对象的键名有冲突,以组件中的优先。
大多数情况下我们在组件内使用混入,混入也可以使用Vue.mixin({})进行全局注册,但是一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。比如我们在全局混入中定义了一个生命周期函数created,那么这个函数将在每一个vue组件被创建的时候调用。所以对于这个全局混入要谨慎使用。
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
问题13:vuex中的mapGetters
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性(computed中)
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount', // getter的名字
'anotherGetter',
// ...
])
}
}
// 于是就可以使用this.xxx的形式直接访问vuex中的getter
mapGetters函数的大致实现原理:
let getters = {
a() {
return 1;
},
b() {
return 2;
},
c() {
return 3;
}
};
function mapGetters(keys) {
let data = {};
keys.forEach(key => {
if (getters.hasOwnProperty(key)) {
data[key] = getters[key];
}
});
return data;
}
let computed = {
...mapGetters(['a','b'])
};
问题14:Vue3 为什么要用 Proxy 代替 Object.defineProperty 实现响应式
https://www.jianshu.com/p/8cde476238f0
问题15:vue中的高阶组件
高阶组件的定义:一个函数接收一个组件作为参数,返回一个新的组件。是非侵入式的,没有修改原组件,而是在新组件中渲染了原组件。在 Vue 的世界里,组件是一个对象,所以高阶组件就是一个函数接受一个对象,返回一个新的包装好的对象。
问题16:MVVM思想
https://juejin.cn/post/6879300070962003982
问题17:父子组件的生命周期顺序
https://juejin.cn/post/6844904113914773518
问题18:编写vue组件
来源:字节面试
动态显示和隐藏图片组件
功能描述:编写一个BlinkImg组件,调用方式如下,组件标签上传入interval作为时间参数,每次间隔interval的毫秒数就将图片动态显示和隐藏
<BlinkImg :interval="1000">
<img alt="Vue logo" src="../assets/logo.png" />
</BlinkImg>
组件代码:
<template>
<div>
<div :class="{ show: isShow }">
<slot></slot>
<p>hello</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: true,
};
},
props: {
interval: {
type: Number,
default: 1000,
},
},
mounted() {
this.showImg();
},
methods: {
showImg() {
this.timer = setInterval(() => {
this.isShow = !this.isShow;
}, this.interval);
},
},
beforeDestroy() {
clearInterval(this.timer);
},
};
</script>
<style scoped>
.show {
/* opacity: 0; */
/* display: none; */
visibility: hidden;
}
</style>
问题来啦!
问题一:为什么不将timer变量提前定义在data中,而是直接添加到this上,这两种方式有什么区别?
原因:vue内部实现双向数据绑定时会遍历data中的所有属性,使用defineProperty方法为每一个属性添加getter和setter,但是,我们这个组件中timer是不需要和视图进行挂钩的,所以将其变成响应式也是无用,那么就没有必要添加到data中了。
问题二:实现显示和隐藏的样式中,可以用opacity: 0;
、 display: none;
、visibility: hidden;
,这三种方式有什么区别?
- 首先,用display:none是不合适的,如果这个组件下方有其他元素,当组件隐藏的时候其dom元素会被移除,它下方的元素会上移。
- 如果用opacity: 0;和visibility: hidden;它们两个都会将元素隐藏,并且不会从文档流中移除。区别是,如果给该元素绑定了事件,使用opacity时,元素隐藏也可以正常触发事件,而visibility不会触发事件。
使用opacity,注意当它隐藏的时候我进行点击
使用visibility,当它隐藏的时候我进行点击
问题三:你看看你写的,为什么要再创建一个div用来包裹slot?直接在最外层的div上写动态类名也是可以的,这样就少渲染一个div,减少一些性能的损耗
自定义组件内部的渲染方式
描述:有一个列表,编写一个List组件,他有自己的默认渲染方式,如按照ul、li的方式渲染。但是也可以在使用组件的时候自定义渲染数据的方式。
考察的是插槽和作用域插槽
List组件代码:
<template>
<div>
<slot :languages="languages">
<ul>
<li v-for="item in languages" :key="item.id">{{ item.value }}</li>
</ul>
</slot>
</div>
</template>
<script>
export default {
data() {
return {
languages: [
{ id: 1, value: "java" },
{ id: 2, value: "javascript" },
{ id: 3, value: "python" },
{ id: 4, value: "c++" },
],
};
},
};
</script>
使用该组件:注意我这里使用作用域插槽时使用的是v-slot指令,没有用slot-scope
<!-- 默认渲染方式 -->
<List></List>
<!-- 自定义渲染 -->
<List>
<template v-slot:default="slotData">
<p v-for="item in slotData.languages" :key="item.id">
{{ item.value }}
</p>
</template>
</List>
问题19:vue项目打包优化
面试题:vue项目在打包后生成的dist文件夹如果比较大,怎么做一些优化?