一、Vue2
请求数据放在哪个生命周期钩子中比较合适?
答案是:created
解析:
-
在created阶段props、data等已经创建,但还没有el选项,因此在这里请求后端接口比较合适,如果接口响应速度快,在执行到beforeMount阶段时创建了el选项并读取data中的数据进行渲染(也就是说此时首次渲染就已经是接口数据),如果放在mounted中请求,此时组件已经渲染完成,等到接口数据请求回来之后又会触发一次渲染,导致二次渲染问题。
-
在created阶段请求接口能更快的获取到服务端数据,因为vue diff过程是递归进行的(同步),如果组件树层级过于复杂,等到mounted阶段时已经耗时挺久了,而请求接口是异步的,不会阻断后面代码的执行,因此也适合放在created阶段。
-
在created阶段进行请求有助于一致性,因为SSR不支持beforeMount、mounted钩子,后续项目如果改造成SSR可以平滑过渡。
如何在beforeCreate钩子里获取data?
这个问题看起来有点无聊,毕竟工作中不怎么会关注这个钩子,但这是一个很有意思的面试题。按我们正常的想法,在beforeCreate时data等选项还没进行挂载,因此data肯定是拿不到的,这没什么问题,那么这个问题的考点在哪里?
-
异步方式获取:$nextTick或者setTimeout都行,vue初始化生命周期并按顺序执行生命周期钩子,这是同步的,在beforeCreate钩子中使用异步方式来获取data,来相当于在初始化前告诉容器,等执行完了再跑里面的代码。借助下一次tick来获取data,这种方式可以拿data,也可以拿更新后的dom
-
同步方式获取:需要了解一下框架内部原理,beforeCreate时,所有的options(data、props、methods等)都会先存到
vm.$options中,此时data是一个函数,需要调用该data函数来获取内部return的数据,因此可以通过this.$options.data()["属性名"]来获取数据,如果data中的初始值是简单的string,那直接this.$options.data()["属性名"]就好。涉及到复杂的情况,建议看看源码里是怎么处理的,具体在core/instance/state.js中的initData(vm)里。在beforeCreate之后,将$options里的data、props、methods等一个个挂载到vm上,然后再触发created钩子。所以在beforeCreate的时候,通过this.xxx是拿不到值的,在created的时候就能通过this.xxx拿到值了。
所谓的同步获取data数据,面试官的考的就是候选人对event loop的理解和是否了解vue的初始化过程。
computed、watch、method怎么用?
-
computed:会将计算结果缓存,与watch一样会监听依赖数据的改变,依赖发生变化时才会重新触发计算,是提升性能的好帮手,一般情况下自动触发计算,如果需要手动触发其他操作则需要设置get和set,在set中加入自己要执行的操作。
-
watch:与computed相同的是都会监听依赖变化,不同的是watch没有缓存能力,并且需要手动设置回调,在回调中执行相应操作,一般用于监听数据变化后执行某些异步操作,如请求数据。
-
method:普通方法,没有缓存也不能监听依赖变化,调用即返回结果,作为普通函数使用即可。
如何在 Vue 之外创建一个具有响应性的变量?
如果你想创建一个具有响应性的变量又不想将它放在组件的data、props、computed中,可以使用Vue提供的observable方法来创建,例如:
import Vue from 'vue'
// 可以完全在 Vue 组件之外完成
const reactiveVariable = Vue.observable('test');
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:
const state = Vue.observable({ count: 0 })
const Demo = {
render(h) {
return h('button', {
on: { click: () => { state.count++ }}
}, `count is: ${state.count}`)
}
}
怎么“窃取props类型”?
窃取 = 偷
偷 = 走捷径,快速达到目的
为什么要快速达到目的?=> 因为懒
这是在封装组件时容易遇到的场景,假设现在有一个Icon组件,其中有一大堆的props,如下所示:
export default {
props: {
iconType: {
type: String,
required: true,
},
iconSize: {
type: String,
default: 'medium',
validator: size => [
'small',
'medium',
'large',
'x-large'
].includes(size),
},
iconColour: {
type: String,
default: 'black',
},
heading: {
type: String,
required: true,
},
},
};
现在需要创建一个A组件来复用Icon组件并且加入一些新功能,同时要让A组件支持Icon组件中的props验证和自身的props验证,我们需要手动将Icon组件中的props都复制过来,如下所示:
<template>
<div>
<h2>{{ title }}</h2>
<Icon
:type="iconType"
:size="iconSize"
:colour="iconColour"
/>
</div>
</template>
<script>
import Icon from './Icon';
export default {
components: { Icon },
props: {
iconType: {
type: String,
required: true,
},
iconSize: {
type: String,
default: 'medium',
validator: size => [
'small',
'medium',
'large',
'x-large'
].includes(size),
},
iconColour: {
type: String,
default: 'black',
},
title: {
type: String,
required: true,
},
},
};
</script>
这样做无疑是非常麻烦的且不好维护,当Icon组件中的props被更新,往往会忘记同时更新A组件中的props,最终导致一些莫名其妙的报错。
因此我们需要有一种方式来巧妙的避开这些问题,做法是在A组件中拿到Icon组件实例上的props并添加到自己的props中,代码如下:
import Icon from './Icon';
export default {
components: { Icon },
props: {
...Icon.props, // 获取Icon组件的props并添加到自己的props上
title: {
type: String,
required: true,
},
},
};
现在,如果Icon组件中的props被修改,我们的A组件将保持最新状态。
怎么实现slot透传?
场景举例:业务组件A使用了B、C等组件,B、C组件都有自己的slot,现在D组件使用了A组件,需要将slot传给B组件。
vue为我们提供了$scopedSlots这个API,用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 数组。
因此,可以在业务组件A中将slot分发给B、C组件,代码如下:
<!-- 实现插槽透传 -->
<!-- 调用子组件的插槽,向子组件传递内容 -->
<template>
<B>
<template v-for="(_, name) in $scopedSlots" v-slot:[name]="data">
<!-- 定义插槽用于接受其父级组件的插槽内容 -->
<slot :name="name" v-bind="data"></slot>
</template>
</B>
<C>
<template v-for="(_, name) in $scopedSlots" v-slot:[name]="data">
<!-- 定义插槽用于接受其父级组件的插槽内容 -->
<slot :name="name" v-bind="data"></slot>
</template>
</C>
</template>
怎么快速重置data中的属性为初始值(以重置表单为例)?
假设现在有如下表单,当我填写完所有的表单项后,想要一键重置除了调用el-form提供的重置初始值方法外,能否借助vue自身来完成呢?
data() {
return {
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: ''
}
}
}
当然可以,原理为获取data初始值覆盖现有值,代码如下:
onReset() {
Object.assign(this.$data, this.$options.data())
},
这里借助了几个api
- Object.assign()
- this.$data
- this.$options.data()
生命周期不展开讲了,vm.$options.data() 方法可以获取data的初始状态(没有被Object.defineProperty代理),vm.$data指向当前组件的data(已经被代理),可以验证一下
this.$data 打印结果为(可以看到打印的属性上有get和set表示已经被代理):
this.$options.data() 打印结果为:
而 Object.assign() 方法下面附一张图
解释一下也就是说Object.assign()传入target对象和sources对象时,sources对象中的属性将会覆盖target对象中的同名属性,此时target对象(vm.$data)中的属性已经被代理,当被修改为初始值(this.$options.data())时也会触发响应式更新,因此就达到了“一键重置”的效果。
二、Vue3
怎么实现slot透传?
原理跟vue2版本相同,只不过在vue3中所有的插槽都是函数,统一暴露在$slots中,我们可以看做vue2的$scopedSlots
以封装一个el-dialog组件为例,dialog组件代码如下:
<template>
<el-dialog v-bind="$attrs" class="global-dialog">
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽透传 -->
<template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]="slotProps">
<slot :name="key" v-bind="{...slotProps}"></slot>
</template>
</el-dialog>
</template>
除了更改了组件名以外其它的全部按照el-dialog文档配置即可,使用示例:
<GlobalDialog v-model="dialogVisible" :title="dialogTitle" width="400">
<div class="dialog-content">
content
</div>
<!-- 可以通过这种方式来接收slot props -->
<!-- <template #footer="footerProps"> {{ footerProps }} </template> -->
<template #footer>
<div class="dialog-footer">
<el-button size="default" @click="dialogVisible = false" plain>取消</el-button>
<el-button size="default" type="primary" @click="dialogVisible = false">确认</el-button>
</div>
</template>
</GlobalDialog>
为什么组件有多个根节点时会导致切换页面白屏?
场景:有A、B两个页面,A为正常,B为错误
- 第一次直接进入错误页B,B页面正常显示,从错误页B切换到正常页A,正常页A空白,再切回错误页B,B依然空白
- 第一次进入正常页A,不进入错误页B,均正常显示,切换任意非错误页B的页面都显示正常
- 只要点过错误页面B,都会导致正常页显示异常
这个问题目前我发现两种情况,虽然vue3已经不再限制template只能有一个根节点了,但如果在router-view中给内置component组件传了参数或者指令,就会出现这种问题,例如:
layout组件:
- 第一种情况是:组件template中写了多个根节点,vue会在运行时抛出一个警告。
路由页面组件:
警告:
此时就会发现切换路由时符合以上场景的情况会出现页面空白。
- 第二种情况为:在根节点同级添加了注释,导致vue把他当成
fragment渲染了,例如:
<template>
<!-- 在此处写了注释内容 -->
<div class="page"></div>
</template>
此时也会出现切换路由时页面空白的情况,并且不会有任何报错和警告。
vue官网给出的解释是:
官文链接:cn.vuejs.org/guide/compo…
因此解决方式有以下几种:
- 在组件内部显示绑定传递过来的属性。
- 保证template只存在一个根节点。
- 注释内容写在根节点内或者template之外。
怎么快速重置data中的属性为初始值(以重置表单为例)?
以如下表单为例:
vue3中不能像在vue2中那样通过调用$options.data()来获取初始值,因此就需要换一种方式来实现。
首先创建一个class,定义好初始值
class FormState {
risk_level = 0
app_ids = ''
strategy_name = ''
domain = ''
client_ip = ''
code = 0
date = ''
}
通过reactive或者ref创建form,初始值为FormState类的实例
const form = reactive(new FormState())
// or
const form = ref(new FormState())
reactive方式:
const onReset = () => {
console.log(form)
Object.assign(form, new FormState()) // Object.assign原理可参考上面vue2的案例
console.log(form)
}
ref方式:
const onReset = () => {
console.log(form.value)
form.value = new FormState()
console.log(form.value)
}
随便填写一些数据点击重置打印结果:
这种方式下,new FormState() 是一个新实例,不需要担心引用问题,如果你使用了ts,class不仅可以通过new来作为值用,也可以使用interface作为类型用,算是进可攻退可守,一举两得。