大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
Vuex中actions和mutations有什么区别
题目分析
mutations和actions是vuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。- 我们只需记住修改状态只能是
mutations,actions只能通过提交mutation修改状态即可
回答范例
- 更改
Vuex的store中的状态的唯一方法是提交mutation,mutation非常类似于事件:每个mutation都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action类似于mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态 - 开发时,包含异步操作或者复杂业务组合时使用
action;需要直接修改状态则提交mutation。但由于dispatch和commit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。调用dispatch和commit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果 - 实现上
commit(type)方法相当于调用options.mutations[type](state);dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了
实现
我们可以像下面这样简单实现commit和dispatch,从而辨别两者不同
class Store { constructor(options) { this.state = reactive(options.state) this.options = options } commit(type, payload) { // 传入上下文和参数1都是state对象 this.options.mutations[type].call(this.state, this.state, payload) } dispatch(type, payload) { // 传入上下文和参数1都是store本身 this.options.actions[type].call(this, this, payload) } }
如何在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex' export default{ computed:{ ...mapGetters(['total','discountTotal']) } }
Vue组件之间通信方式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信 :
父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信
组件传参的各种方式
组件通信常用方式有以下几种
props / $emit适用 父子组件通信
- 父组件向子组件传递数据是通过
prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
ref与$parent / $children(vue3废弃)适用 父子组件通信
ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例$parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
EventBus ($emit / $on)适用于 父子、隔代、兄弟组件通信
- 这种方法通过一个空的
Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
$attrs / $listeners(vue3废弃)适用于 隔代组件通信
$attrs:包含了父作用域中不被prop所识别 (且获取) 的特性绑定 (class和style除外 )。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定 (class和style除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合inheritAttrs选项一起使用$listeners:包含了父作用域中的 (不含.native修饰器的)v-on事件监听器。它可以通过v-on="$listeners"传入内部组件
provide / inject适用于 隔代组件通信
- 祖先组件中通过
provider来提供变量,然后在子孙组件中通过inject来注入变量。provide / injectAPI 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
$root适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用Vuex适用于 父子、隔代、兄弟组件通信
Vuex是一个专为Vue.js应用程序开发的状态管理模式。每一个Vuex应用的核心就是store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 改变
store中的状态的唯一途径就是显式地提交 (commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。
根据组件之间关系讨论组件通信最为清晰有效
- 父子组件:
props/$emit/$parent/ref - 兄弟组件:
$parent/eventbus/vuex - 跨层级关系:
eventbus/vuex/provide+inject/$attrs + $listeners/$root
下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯
1. 父子组件通信
使用
props,父组件可以使用props向子组件传递数据。
父组件vue模板father.vue:
<template> <child :msg="message"></child> </template> <script> import child from './child.vue'; export default { components: { child }, data () { return { message: 'father message'; } } } </script>
子组件vue模板child.vue:
<template> <div>{{msg}}</div> </template> <script> export default { props: { msg: { type: String, required: true } } } </script>
回调函数(callBack)
父传子:将父组件里定义的method作为props传入子组件
// 父组件Parent.vue: <Child :changeMsgFn="changeMessage"> methods: { changeMessage(){ this.message = 'test' } }
// 子组件Child.vue: <button @click="changeMsgFn"> props:['changeMsgFn']
子组件向父组件通信
父组件向子组件传递事件方法,子组件通过
$emit触发事件,回调给父组件
父组件vue模板father.vue:
<template> <child @msgFunc="func"></child> </template> <script> import child from './child.vue'; export default { components: { child }, methods: { func (msg) { console.log(msg); } } } </script>
子组件vue模板child.vue:
<template> <button @click="handleClick">点我</button> </template> <script> export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } } } </script>
2. provide / inject 跨级访问祖先组件的数据
父组件通过使用provide(){return{}}提供需要传递的数据
<template> <button @click="handleClick">点我</button> </template> <script> export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } } } </script>
子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数
<template> <p>曾孙组件</p> <p>{{message}}</p> </template> <script> export default { // inject 注入/接收祖先组件传递的所需要的数据即可 //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}} inject: [ "message","say"], mounted() { this.say(); }, }; </script>
3. ������+parent+children 获取父组件实例和子组件实例的集合
this.$parent可以直接访问该组件的父实例或组件- 父组件也可以通过
this.$children访问它所有的子组件;需要注意$children并不保证顺序,也不是响应式的
<!-- parent.vue --> <template> <div> <child1></child1> <child2></child2> <button @click="clickChild">$children方式获取子组件值</button> </div> </template> <script> import child1 from './child1' import child2 from './child2' export default { data(){ return { total: 108 } }, components: { child1, child2 }, methods: { funa(e){ console.log("index",e) }, clickChild(){ console.log(this.$children[0].msg); console.log(this.$children[1].msg); } } } </script>
<!-- child1.vue --> <template> <div> <button @click="parentClick">点击访问父组件</button> </div> </template> <script> export default { data(){ return { msg:"child1" } }, methods: { // 访问父组件数据 parentClick(){ this.$parent.funa("xx") console.log(this.$parent.total); } } } </script>
<!-- child2.vue --> <template> <div> child2 </div> </template> <script> export default { data(){ return { msg: 'child2' } } } </script>
4. �����+attrs+listeners多级组件通信
$attrs包含了从父组件传过来的所有props属性
// 父组件Parent.vue: <Child :name="name" :age="age"/> // 子组件Child.vue: <GrandChild v-bind="$attrs" /> // 孙子组件GrandChild <p>姓名:{{$attrs.name}}</p> <p>年龄:{{$attrs.age}}</p>
$listeners包含了父组件监听的所有事件
// 父组件Parent.vue: <Child :name="name" :age="age" @changeNameFn="changeName"/> // 子组件Child.vue: <button @click="$listeners.changeNameFn"></button>
5. ref 父子组件通信
// 父组件Parent.vue: <Child ref="childComp"/> <button @click="changeName"></button> changeName(){ console.log(this.$refs.childComp.age); this.$refs.childComp.changeAge() } // 子组件Child.vue: data(){ return{ age:20 } }, methods(){ changeAge(){ this.age=15 } }
6. 非父子, 兄弟组件之间通信
vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props和$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:
// Bus.js // 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
<template> <button @click="toBus">子组件传给兄弟组件</button> </template> <script> export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } } } </script>
另一个组件也在钩子函数中监听on事件
export default { data() { return { message: '' } }, mounted() { this.$bus.$on('foo', (msg) => { this.message = msg }) } }
7. $root 访问根组件中的属性或方法
- 作用:访问根组件中的属性或方法
- 注意:是根组件,不是父组件。
$root只对根组件有用
var vm = new Vue({ el: "#app", data() { return { rootInfo:"我是根元素的属性" } }, methods: { alerts() { alert(111) } }, components: { com1: { data() { return { info: "组件1" } }, template: "<p>{{ info }} <com2></com2></p>", components: { com2: { template: "<p>我是组件1的子组件</p>", created() { this.$root.alerts()// 根组件方法 console.log(this.$root.rootInfo)// 我是根元素的属性 } } } } } });
8. vuex
- 适用场景: 复杂关系的组件数据传递
- Vuex作用相当于一个用来存储共享变量的容器
state用来存放共享变量的地方getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值mutations用来存放修改state的方法。actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作
小结
- 父子关系的组件数据传递选择
props与$emit进行传递,也可选择ref - 兄弟关系的组件数据传递可选择
$bus,其次可以选择$parent进行传递 - 祖先与后代组件数据传递可选择
attrs与listeners或者Provide与Inject - 复杂关系的组件数据传递可以通过
vuex存放共享的变量
Vue为什么需要虚拟DOM?优缺点有哪些
由于在浏览器中操作
DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因。Vue2的Virtual DOM借鉴了开源库snabbdom的实现。Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点,是对真实DOM的一层抽象
优点:
- 保证性能下限 : 框架的虚拟
DOM需要适配任何上层API可能产生的操作,它的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限; - 无需手动操作 DOM : 我们不再需要手动去操作
DOM,只需要写好View-Model的代码逻辑,框架会根据虚拟DOM和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率; - 跨平台 : 虚拟
DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、weex开发等等。
缺点:
- 无法进行极致优化:虽然虚拟
DOM+ 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化。 - 首次渲染大量
DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
- 用
JavaScript对象模拟真实DOM树,对真实DOM进行抽象; diff算法 — 比较两棵虚拟DOM树的差异;pach算法 — 将两个虚拟DOM对象的差异应用到真正的DOM树。
说说你对虚拟 DOM 的理解?回答范例
思路
vdom是什么- 引入
vdom的好处 vdom如何生成,又如何成为dom- 在后续的
diff中的作用
回答范例
- 虚拟
dom顾名思义就是虚拟的dom对象,它本身就是一个JavaScript对象,只不过它是通过不同的属性去描述一个视图结构 - 通过引入
vdom我们可以获得如下好处:
- 将真实元素节点抽象成
VNode,有效减少直接操作dom次数,从而提高程序性能
- 直接操作
dom是有限制的,比如:diff、clone等操作,一个真实元素上有许多的内容,如果直接对其进行diff操作,会去额外diff一些没有必要的内容;同样的,如果需要进行clone那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到JavaScript对象上,那么就会变得简单了 - 操作
dom是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象VNode进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
- 方便实现跨平台
- 同一
VNode节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是dom元素节点,渲染在Native( iOS、Android)变为对应的控件、可以实现SSR、渲染到WebGL中等等 Vue3中允许开发者基于VNode实现自定义渲染器(renderer),以便于针对不同平台进行渲染
vdom如何生成?在vue中我们常常会为组件编写模板 -template, 这个模板会被编译器 -compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。
- 挂载过程结束后,
vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图
为什么要用vdom?案例解析
现在有一个场景,实现以下需求:
[ { name: "张三", age: "20", address: "北京"}, { name: "李四", age: "21", address: "武汉"}, { name: "王五", age: "22", address: "杭州"}, ]
将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用jQuery实现如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="container"></div> <button id="btn-change">改变</button> <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script> const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; //渲染函数 function render(data) { const $container = $('#container'); $container.html(''); const $table = $('<table>'); // 重绘一次 $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>')); data.forEach(item => { //每次进入都重绘 $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`)) }) $container.append($table); } $('#btn-change').click(function () { data[1].age = 30; data[2].address = '深圳'; render(data); }); </script> </body> </html>
- 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,
table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。 - 因此我们采用JS对象模拟的方法,将
DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。 - 当然有人说虚拟DOM并不比真实的
DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在js中diff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。
如下DOM结构:
<ul id="list"> <li class="item">Item1</li> <li class="item">Item2</li> </ul>
映射成虚拟DOM就是这样:
{ tag: "ul", attrs: { id: "list" }, children: [ { tag: "li", attrs: { className: "item" }, children: ["Item1"] }, { tag: "li", attrs: { className: "item" }, children: ["Item2"] } ] }
使用snabbdom实现vdom
这是一个简易的实现
vdom功能的库,相比vue、react,对于vdom这块更加简易,适合我们学习vdom。vdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM上
简单介绍一下这两个函数的用法:
h('标签名', {属性}, [子元素]) h('标签名', {属性}, [文本]) patch(container, vnode) // container为容器DOM元素 patch(vnode, newVnode)
现在我们就来用snabbdom重写一下刚才的例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="container"></div> <button id="btn-change">改变</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script> <script> let snabbdom = window.snabbdom; // 定义patch let patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]); //定义h let h = snabbdom.h; const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; data.unshift({name: "姓名", age: "年龄", address: "地址"}); let container = document.getElementById('container'); let vnode; const render = (data) => { let newVnode = h('table', {}, data.map(item => { let tds = []; for(let i in item) { if(item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')); } } return h('tr', {}, tds); })); if(vnode) { patch(vnode, newVnode); } else { patch(container, newVnode); } vnode = newVnode; } render(data); let btnChnage = document.getElementById('btn-change'); btnChnage.addEventListener('click', function() { data[1].age = 30; data[2].address = "深圳"; //re-render render(data); }) </script> </body> </html>
你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销
vue中使用
h函数生成虚拟DOM返回
{ tag: "ul", attrs: { id: "list" }, children: [ { tag: "li", attrs: { className: "item" }, children: ["Item1"] }, { tag: "li", attrs: { className: "item" }, children: ["Item2"] } ] }
怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多
思路
- 缓存用
keep-alive,它的作用与用法 - 使用细节,例如缓存指定/排除、结合
router和transition - 组件缓存后更新可以利用
activated或者beforeRouteEnter - 原理阐述
回答范例
- 开发中缓存组件使用
keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive> <component :is="view"></component> </keep-alive>
- 结合属性
include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>
- 缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) },
actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){ this.getData() // 获取数据 },
keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行
vue-router 动态路由是什么
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个
User组件,对于所有ID各不相同的用户,都要使用这个组件来渲染。那么,我们可以在vue-router的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果
const User = { template: "<div>User</div>", }; const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: "/user/:id", component: User }, ], });
问题: vue-router 组件复用导致路由参数失效怎么办?
解决方法:
- 通过
watch监听路由参数再发请求
watch: { //通过watch来监听路由变化 "$route": function(){ this.getData(this.$route.params.xxx); } }
- 用
:key来阻止“复用”
<router-view :key="$route.fullPath" />
回答范例
- 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
- 例如,我们可能有一个
User组件,它应该对所有用户进行渲染,但用户ID不同。在Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数 - 路径参数 用冒号
:表示。当一个路由被匹配时,它的params的值将在每个组件中以this.$route.params的形式暴露出来。 - 参数还可以有多个,例如/
users/:username/posts/:postId;除了$route.params之外,$route对象还公开了其他有用的信息,如$route.query、$route.hash等
阿里前端常考vue面试题汇总(二):https://developer.aliyun.com/article/1415112



