大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 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 / inject
API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
$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