【面试题】面试不面试,你都必须得掌握的vue知识_数据

前言

   因为是自己的理解,所以难免会出现错误。如果大家发现了错误,或者有些问题需要交流,​​欢迎​​​在评论区下留言。由于最近​​项目加急​​​,还有很多事情需要处理,剩下的​​渲染函数​​​、​​路由守卫​​​、​​vue2.x底层原理​​会在后续抽空更完,在此向大家说声抱歉。有兴趣继续读下去的朋友们可以先​​收藏吃灰​​,哈哈哈。如果本篇文章对您有帮助,烦请大家一键三连哦, 蟹蟹大家~

1. vue常用知识点总结(vue群演)

a. $watch的使用

【面试题】面试不面试,你都必须得掌握的vue知识_插槽_02

```传送门(vue3):https://staging-cn.vuejs.org/api/component-instance.html#attrs```
```传送门(vue2):https://cn.vuejs.org/v2/api/#vm-watch```

总结:

```1. 监听一个属性名```
this.$watch('a', (newVal, oldVal) => {})

```2. 监听对象的属性```
this.$watch('a.b', (newVal, oldVal) => {})

```3. 监听getter函数的值```
this.$watch(
// 每一次这个 `this.a + this.b` 表达式生成一个
// 不同的结果,处理函数都会被调用
// 这就好像我们在侦听一个计算属性
// 而不定义计算属性本身。
() => this.a + this.b,
(newVal, oldVal) => {}
)

```4. 停止该侦听器```
const unwatch = this.$watch('a', cb)
unwatch()

```5. 排除对象某些属性的监听```
mounted() {
Object.keys(this.params)
.filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
.forEach((_) => {
this.$watch((vm) => vm.params[_], handler, {
deep: true,
});
});
},
data() {
return {
params: {
a: 1,
b: 2,
c: 3,
d: 4
},
};
},
watch: {
params: {
deep: true,
handler() {
this.getList;
},
}
}

b. 计算属性的用法

`1.计算属性中添加getter和setter:`
sizeCheckAll: {
get() {
return this.selectSizeList?.length == this.allSizes?.length
},
set(bool) {
if(bool) this.selectSizeList = this.allSizes
else this.selectSizeList = []
}
}

`2.计算属性中解构引用data中定义的变量:`
text({ loading }) {
return !loading ? this.$t('page.folder.upload') : this.$t('page.folder.uploading')
}

`3.在计算属性中使用闭包的思想传递参数:`
(1) 这种情况不推荐,无法回收变量,会造成内存泄漏,需要改成方法;
(2) 除这种情况外,建议使用计算属性,因为有缓存(除非依赖的变量变化才会重新执行)。当其他变量改变视图更新时,
方法会重新执行,而计算属性不会;

getSku() {
return (type, row) => {
return row.customProductList[row.$sizeIndex].customSpecificProductList?.map((item) => item[type])
}
}

`4.计算属性中的mapState(存储在vuex中的state数据)和mapGetters(对state中的数据进行包装的数据)`
import { mapState,mapGetters } from 'vuex'

computed: {
...mapState(['categoryList','productList']),
...mapGetters(['finalData'])
}

对于解构出来的`mapMutations和mapActions`需要定义在`methods`中,并在`methods`中进行调用
methods: {
...mapMutations(['add','addN']),
...mapActions(['awaitAdd'])
}

`5.计算属性中不能执行异步操作`

`6. 重新执行计算属性的方法:`

(1) 计算属性中可以`解构非关联的属性`,当`非关联属性属性变化`时,会`重新获取`计算属性的值(`小技巧`,但不推荐)

(2) 利用计算属性的原理(_computedWatchers.计算属性名称.dirty,不推荐)
计算属性是通过`dirty`来判断是否要进行`重新计算`
`dirty``false`, `缓存`计算结果,
`dirty``true`, `重新获取计算属性的值`

this._computedWatchers.计算属性名称.dirty = true
this.$forceUpdate()
this.$nextTick(()=> {
...
})

c. 关于路由跳转

1. 使用`$router.push`进行`vue页面`间的`跳转`(会向`vue的history`中添加记录):
(1) 使用`name`进行跳转(推荐,因为`path`可能会移动和变化,但我们一般不会改变路由名称`name`的值)
`在路由上显示传参,可以通过this.$route.query.id获取参数:`
this.$router.push({ name: "detail", query: { id } })

`不会在路由上显示传参,可以通过this.$route.params.id获取参数`
this.$router.push({ name: "detail", params: { id } })

(2) 使用`path`进行跳转(不推荐)

`在路由上显示传参,可以通过this.$route.query.id获取参数:`
this.$router.push(`/product/detail?id=${id}`)
this.$router.push({ path: "/product/detail", query: { id } })

`不会在路由上显示传参,可以通过this.$route.params.id获取参数`
this.$router.push({ path: "/product/detail", params: { id } })

2. 使用`router.replace`跳转路由(`不会记录路由的history`)
this.$router.replace(`/product/detail?id=${id}`)

this.$router.replace({ name: "detail", query: { id } })
this.$router.replace({ name: "detail", params: { id } })

this.$router.replace({ path: "/product/detail", query: { id } })
this.$router.replace({ path: "/product/detail", params: { id } })

3. 使用`router-link`跳转路由:
<router-link :to="{ name: 'detail', params: { id } }">
<router-link :to="{ name: 'detail', query: { id } }">
<router-link :to="{ path: '/product/detail', params: { id } }">
<router-link :to="{ path: '/product/detail', query: { id } }">
复制代码

d. 关于vue中的通讯方式

  1. 全局通讯:

   a. ​​vueX​

   b. ​​$root​​​(获取​​根组件​​​,即​​App.vue​​的数据)

   c. ​​eventbus(事件总线)​

    ​​三心哥之事件总线​

`为避免内存泄漏,需要销毁监听的自定义事件,有两种解决方案:`

(1) 在组件的`beforeDestroy`钩子函数中销毁监听的自定义事件:

beforeDestroy() {
//销毁监听事件
this.$bus.off("addProduct");
}

(2) 每次$on之前$off需要销毁的事件名称;

   d. ​​localStorage详解​

   e. ​​页面的路由传参​

  2. 父子组件之间通讯:

   a. 父组件使用​​props​​​向子组件传值, 子组件定义​​props​​​接收父组件传递过来的值。​​特别注意:​​​ 如果​​props​​​是​​基本数据类型​​​, 那么在子组件中,不能直接修改父组件传递过来的​​props​​;

   b. 子组件使用​​$emit​​​向父组件传递事件和值,父组件使用​​@事件名​​接收事件和参数

1. 父子组件使用`$emit``@自定义事件`传递方法:

(1) `父组件中的方法按序接收传递过来的参数`

子组件传递多个参数的情况:
this.$emit('test', 1, 2, 3)

父组件接收子组件的自定义事件:

@test="test"

test(a, b, c) {
console.log('我是接收过来的参数', a, b, c)
}

`或`

test(...params) {
console.log('我是接收过来的参数', params[0], params[1], params[2])
}

(2) `父组件使用函数中内置的arguments伪数组(且必须为这个内置参数),接收传递过来的参数`

this.$emit('test', 1, 2, 3)

@test="test(arguments)"

test(params) {
console.log('我是接收过来的参数', params[0], params[1], params[2])
}

(3) `使用对象的方式组装数据`

this.$emit('test', { age, sex, city })

@test="test"

test(params) {
console.log('我是接收过来的参数', params.age, params.sex, params.city)
}

2. `$emit`的扩展: 使用`$on监听本组件的自定义事件`, 后文会讲到可以`使用$once只监听一次本组件的自定义事件`

mounted() {
`// 在钩子函数中定义了一个方法,用于closeModal调用时再去执行`
`// 至于$on调用的方法和父组件从子组件接收来的自定义方法执行的快慢就看它们的执行机制
(同步状态下,处理相同条件,父组件更快,一方异步一方同步的状态下,同步的那方先执行,都是异步看谁先执行完)`
this.$on('closeModal',res=>{
console.log(res);
})
},

destoryed() {
`// 使用$off移除事件监听
1)如果没有提供参数,则移除所有的事件监听器;
2)如果只提供了事件,则移除该事件所有的监听器;
3)如果同时提供了事件与回调,则只移除这个回调的监听器。
`
this.$off("closeModal");
},

closeModal(){
this.$emit('closeModal')
}

3. `$emit`的扩展:
`this.$emit('update:visible',false)`, 使用双向绑定的`语法糖`,在父组件中使用`.sync`对传入的`props`进行
双向绑定,更新父组`visible``prop`;

   c. 父组件使用​​this.$refs.子组件的ref名称​​​获取​​子组件的vue实例​​​,​​this.$refs.子组件的ref名称.$el​​​是获取组件的​​dom​​元素;

   d. 父组件使用​​this.$children​​​(包含所有​​子组件​​​(不包含孙子组件)的 ​​VueComponent 对象数组​​​) 获取子组件的数据,例如​​this.$children[0].someMethod()​​​执行子组件的方法。对于子组件,则直接使用​​this.$parent​​获取父组件的值。

   e. ​​插槽​

  3. 祖先跨级通讯:

   a. 祖先组件使用​​provide​​​返回需要传递的参数,后代组件使用​​inject​​接收参数

// 祖先组件
provide() {
return {
`// keyName: { name: this.name }, // value 是对象才能实现响应式,也就是引用类型`
keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]
`// keyName: 'test' value 如果是基本类型,就无法实现响应式`
}
},

data() {
return {
name:'张三'
}
},

methods: {
changeValue(){
this.name = '改变后的名字-李四'
}
}

// 后代组件
inject:['keyName']
create() {
`因为是函数,所以得执行才能获取响应式的数据,改变后的名字-李四`
const keyName = this.keyName()
}

   b. 使用​​$attrs​​​和​​$listeners​​​实现祖先的跨级通讯(​​详见本文第一点​​)

e. 插槽

vue插槽官方文档

1. 在子组件A中使用`<slot></slot>`进行`占位`,父组件引入子组件A,在A下`添加的内容``自动转移``插槽占位的地方`
2. 如果`<slot></slot>`中有内容,如果在父组件不给子组件添加内容,那么就会展示插槽的默认内容;
3. 如果未给`<slot></slot>`提供名称,那么该插槽的默认名称是`default`;
4. 如果需要给`插槽`指定名称,直接对子组件使用`name`命名即可;
`<slot name="footer"></slot>`

如果在父组件中,需要给对应`插槽`添加内容,则可以使用如下三种写法:
<template v-slot:footer>
<!-- footer 插槽的内容放这里 -->
</template>

<template #footer>
<!-- footer 插槽的内容放这里 -->
</template>

<template slot="footer">
<!-- footer 插槽的内容放这里 -->
</template>


5. `插槽`是有`作用域`的,父组件中的`插槽`内容无法访问子组件的内容,除非通过`作用域插槽`的方式进行传递:

子组件:
`<slot name="footer" :row="row"></slot>`

父组件:
<template v-slot:footer="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>

<template #footer="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>

<template slot="header" slot-scope="{ row }">
<!-- footer 插槽的内容放这里 -->
</template>

在这里`扩展`一个代码的优化点,`v-if`可以使用`template`包裹,`语义`会更加清晰。

6. 如果组件只有一个`插槽`,则在父组件上,可以直接使用`插槽`语法,而不需要`template`标签嵌套。
7. 自定义组件内部的`$scopedSlots`记载了对应的`作用域插槽信息`,以`key(插槽名)-value(对应函数。指定key值,
执行得到Vnode数组,对应$slots,一般更推荐使用$scopedSlots)`的形式出现。因此,根据这个特性,
`有多个具有插槽的组件`定义在`一个自定义组件中`时,可以通过`遍历的方式动态添加插槽`

<template>
<div class="el-tree-select_component">
<el-select
ref="select"
:class="[defaultText && 'has-default-text']"
v-bind="selectProps"
:value="valueTitle"
:popperClass="concatPopperClass"
>
<template v-for="(val, key) in allSlots.inputScopedSlots" #[key]>
<slot :name="key"></slot>
</template>
<el-option :label="valueTitle" :value="valueId">
<el-tree
ref="selectTree"
:data="options || []"
:node-key="props.value"
:default-expanded-keys="defaultExpandedKey"
v-bind="treeProps"
@node-click="handleNodeClick"
>
<template v-for="(val, key) in allSlots.treeScopedSlots" #[key]="scope">
<slot :name="key" v-bind="scope"></slot>
</template>
</el-tree>
</el-option>
</el-select>
<span class="default-text" v-if="defaultText"> {{ defaultText }} </span>
</div>
</template>

const INPUT_SLOT_LIST = ['prefix', 'empty']

computed: {
// 获取select组件和tree组件的插槽
allSlots({ $scopedSlots }) {
const inputScopedSlots = {}
const treeScopedSlots = {}
for (let key in $scopedSlots) {
const val = $scopedSlots[key]
if (INPUT_SLOT_LIST.includes(key)) inputScopedSlots[key] = val
else treeScopedSlots[key] = val
}
return {
inputScopedSlots,
treeScopedSlots
}
}
}

f. vueX

  vueX黑马笔记

`在平时的项目中,为了代码看上去不是那么臃肿,一般会使用多个store文件来维护vueX,比如product.js, order.js...`
`并可以通过函数的方式拿到vueX中存储的数据`

computed: {
...mapState({
has_image_gallery: (state) => state.customizer.has_image_gallery,
library: (state) => state.myImage.library.list,
meta: (state) => state.myImage.library.pagination,
last_page: (state) => state.myImage.library.pagination.last_page
})
}

g. 指令(以回到顶部组件说明)

  指令的官方介绍

`自定义指令中的第三个参数vnode的context记载了组件的一些信息,这个是我们比较需要关注的`

`1. 使用自定义指令,实现回到顶部的效果:`

`添加全局公共样式:`
.scroll-top-class {
position: fixed;
bottom: 120px;
right: 30px;
opacity: 0;
height: 40px;
width: 40px;
line-height: 40px;
font-size: 30px;
text-align: center;
color: #ddd;
opacity: 0;
z-index: 2021;
cursor: pointer;
border-radius: 50%;
box-shadow: 0px 0px 8px 1px #ccc;
background-color: rgba($color: #666, $alpha: 0.5);
transition: all 1s;
}

`指令挂载方法:在有滚动条的容器上,添加v-scrollTop指令, 并提供相应的值即可。如果不提供,则使用默认值`
<div class="topic-page" v-scrollTop> </div>

`指令注册方法: 同第k点, 先install, 在本文件暴露出去。然后在main.js文件中引入,并使用vue.use(引入的名称)全局注册`

`第一种方法:直接使用binding.value判断回到顶部图标出现的位置(相对推荐)`

Vue.directive('scrollTop', {
inserted(el, binding) {
`如果未设置binding.value的值,则默认为200`
`滚动条移动超过200px的距离就显示,反之则隐藏`
if (!binding.value) binding.value = 200

el.style.scrollBehavior = 'smooth'

const backEl = document.createElement('div')
backEl.className = 'scroll-top-class el-icon-top'
el.appendChild(backEl)
backEl.addEventListener('click', () => (el.scrollTop = 0))

el.addEventListener('scroll', () => {
if (el.scrollTop >= binding.value) backEl.style.opacity = 1
else backEl.style.opacity = 0
})
}
})

`第二种方法:使用binding.value,根据滚动条的总高度所占比例,间接判断回到顶部图标出现的位置`
`(不推荐,因为在产品列表无限滚动情况下,滚动条高度是动态变化的,无法适用,而且倍数不好控制)`

// 滚动条指令
Vue.directive('scrollTop', {
inserted(el, binding) {
if (binding.value >= 1 || binding.value <= 0) return new Error('v-scrollTop的绑定值需要介于0到1之间')

`获取元素的整体高度`
const elH = el.offsetHeight

`也可以给visibilityHeight定值(不推荐,无法兼容所有需要滚动条的页面)`
let visibilityHeight = 0
if (binding.value) visibilityHeight = binding.value * elH
`阈值默认为滚动区域整体高度的0.2倍`
else visibilityHeight = 0.2 * elH

`为滚动条返回顶部添加平滑的过渡效果`
el.style.scrollBehavior = 'smooth'

const backEl = document.createElement('div')
backEl.className = 'scroll-top-class el-icon-top'
`将创建的回到顶部图标作为孩子插入到el中`
el.appendChild(backEl)

backEl.addEventListener('click', () => (el.scrollTop = 0))

el.addEventListener('scroll', () => {
if (el.scrollTop >= visibilityHeight) backEl.style.opacity = 1
else backEl.style.opacity = 0
})
}
})

`2. 自定义组件,实现回到顶部的效果:`
`使用这种方式,需要在每个有回到顶部需求的文件中引入该自定义组件,并指定高度阈值及滚动条dom容器对应的字符串`

<template>
<el-button v-show="visible" class="back" @click="backTop">top</el-button>
</template>

<script>
export default {
props: {
height: {
required: false,
type: Number,
default: 200
},

target: {
required: false,
type: String,
default: '.topic-page'
}
},

data() {
return {
container: false,
visible: false
}
},

mounted() {
this.container = document.querySelector(this.target)
if (!this.container) throw new Error('target is not existed: ' + this.target)
this.container.style.scrollBehavior = 'smooth'
`最保险的做法是,使用this.$nextTick包裹下面的代码,因为vue是异步更新机制,dom可能还未更新`
this.container.addEventListener('scroll', this.scrollToTop)
this.$once('hook:beforeDestory', () => {
this.container.removeEventListener('scroll', this.scrollToTop)
})
},

methods: {
backTop() {
this.container.scrollTo({
top: 0,
behavior: 'smooth'
})
},

scrollToTop() {
this.visible = this.container.scrollTop > this.height ? true : false
}
}
}
</script>

<style lang="scss" scoped>
.back {
position: fixed;
bottom: 100px;
right: 100px;
}
</style>

h. 使用install和use进行全局注册

`lodopPrintPdf.js`

import Vue from 'vue'
import PrintBtn from './printBtn'
import merge from 'element-ui/src/utils/merge'

export default async function lodopPrintPdf(option) {
const ExtendPrintBtn = Vue.extend(PrintBtn)
`继承打印组件并初始化vue实例`
const vm = new ExtendPrintBtn({})
`合并option,等价于Object.assign(vm, option)`
`相当于遍历添加传入vm的prop参数`
merge(vm, option)
`调用实例的方法,js动态加载完成`
return vm.printHandler()
}
复制代码
`globalConst.js`

import lodopPrintPdf from '@/components/lodopPrint/lodopPrintPdf.js'

export default {
install(Vue) {
`在vue的原型对象上挂载$lodopPrintPdf,并暴露出去`
Vue.prototype.$lodopPrintPdf = lodopPrintPdf //lodop打印pdf
}
}
`main.js`

`Vue.use的用法: 安装Vue插件。
如果插件是一个对象,必须提供 install 方法。
如果插件是一个函数,它会被作为 install 方法。`

import globalConst from '@/commons/globalConst'

Vue.use(globalConst)
复制代码
`$lodopPrintPdf方法的使用`

`传入的五个参数就是前面定义的函数所接收的option值,相当于调用打印组件,传入对应的五个props`
this.$lodopPrintPdf({
type: 'html',
printable: this.$refs.label.$el,
paperSize: [841.89, 595.28],
onSuccess: this.resetLoading,
onError: this.resetLoading
})

i. 混入 && 继承

  混入: 对于具有相同逻辑的​​vue​​​文件,其实可以抽取成一个​​混入​​​,存放公共的​​js​​​代码。在使用​​混入​​​的​​vue​​​文件中,可以定义相同的变量或者方法来​​覆盖​​混入中的变量或者方法。

  继承: 相比​​混入​​​,​​继承​​​更加霸道,可以​​继承​​​整个​​vue​​​文件。同时在​​继承​​​文件中,可以添加一些额外的​​js​​​代码。如果在​​被继承​​​的组件中存在这些​​js​​​变量和方法,那么​​继承​​​组件就会覆盖这些变量和方法,如果不存在则为添加。如果在​​继承​​​组件中添加​​html​​​和​​css​​​代码,不管这些代码之前是否和​​被继承​​​组件的​​html​​​和​​css​​​代码冲突,​​继承​​​组件的​​html​​​和​​css​​​代码都会以自身代码为主,不会继承​​被继承​​​组件的​​html​​​和​​css​​代码。

<script>
import dialog from '@/extend/components/dialog/index'

export default {
extends: dialog

}

</script>

j. $props三兄弟和inherits属性

  $props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。

  $attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。

  $listeners:包含了父作用域中的(不含.native 修饰器的)v-on事件监听器。

  ​​inherits属性​​的作用是禁止传入的属性添加到组件的根元素上。默认为true,即将传入的属性添加到组件的根元素上。

   应用:​​v-bind="$attrs"​​​和​​v-notallow="$listeners"​​​一般用于组件封装上,必须得绑定在组件上。​​v-bind="$attrs"​​相当于一个展开运算符,将从父组件传来的​​props​​​且未被当前组件​​props​​​接收的​​prop​​​挂载到组件上,使得组件具有可扩展性。如果未绑定,孙子组件可以通过​​this.$attrs​​​拿到子组件的​​props​​​,但是无法拿到父组件的​​props​​​。如果要拿到父组件的​​props​​​,则需要在子组件上绑定​​v-bind="$attrs"​​​,这样孙子组件中的​​this.$attrs​​​就指向父组件的​​props​​。


k. v-model语法糖

官网双向绑定原理详解

l. 修饰符的顺序及理解

  


m. render函数 && 函数式组件

n. 路由守卫

2. vue的冷知识(vue间谍,大部分内容来自Sunshine_Lin的掘金博客。部分内容有自己的思考和扩展, 以扩展两字进行标注)

a. 为什么不建议v-for和v-if同时存在?

<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
{{item}}
</div>

`拓展:`
`vue2中的v-for优先级高于v-if, vue3则相反。首先会把7个元素都遍历出来,然后再一个个判断是否为3,并把3的dom给隐藏掉,
这样的坏处就是,渲染了无用的3节点,增加无用的dom操作,建议使用computed来解决这个问题:`

<div v-for="item in list">
{{item}}
</div>

computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}

b. 为什么不建议用index做key,为什么不建议用随机数做key?

<div v-for="(item, index) in list" :key="index">{{item.name}}</div>

list: [
{ name: '小明', id: '123' },
{ name: '小红', id: '124' },
{ name: '小花', id: '125' }
]

渲染为
<div key="0">小明</div>
<div key="1">小红</div>
<div key="2">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' })

渲染为
<div key="0">小林</div>
<div key="1">小明</div>
<div key="2">小红</div>
<div key="3">小花</div>


新旧对比

<div key="0">小明</div> <div key="0">小林</div>
<div key="1">小红</div> <div key="1">小明</div>
<div key="2">小花</div> <div key="2">小红</div>
<div key="3">小花</div>

可以看出,如果用index做key的话,其实是更新了原有的三项,并新增了小花,虽然达到了渲染目的,但是损耗性能

现在我们使用id来做key,渲染为

<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

现在我执行 list.unshift({ name: '小林', id: '122' }),渲染为

<div key="122">小林</div>
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>

新旧对比

<div key="122">小林</div>
<div key="123">小明</div> <div key="123">小明</div>
<div key="124">小红</div> <div key="124">小红</div>
<div key="125">小花</div> <div key="125">小花</div>

可以看出,原有的三项都不变,只是新增了小林这个人,这才是最理想的结果

  用​​index​​​和用​​随机数​​​都是同理,​​随机数​​​每次都在变,做不到专一性,很​​渣男​​​,也很消耗性能,所以,拒绝​​渣男​​​,选择​​老实人​

c. 为什么data是个函数并且返回一个对象呢?

`data`之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行`data函数`并返回新的数据对象。
这样,可以避免多处调用的`数据污染`

d. vue2的内部指令

【面试题】面试不面试,你都必须得掌握的vue知识_vue.js_03

`拓展:`

1. `v-text`的用法:

本质其实和`插值表达式`一样。不同的是,`v-text`后面必须得`赋值`,它只与`赋予的变量的值`有关。比如:

<div v-text="123"> {{ unsignedCountries }} </div>

此处只`渲染123`, 而不会渲染`计算属性`的值。

2. `v-once`:指令所绑定的`html标签``组件`中的变量只会渲染一次,`不会随着变量的变化而变化`,利用好这一点可以优化

e. vue2的生命周期

【面试题】面试不面试,你都必须得掌握的vue知识_javascript_04

 

`拓展:`
`1. vue生命周期钩子函数的组成:`

`vue的生命周期`是一个`组件/实例``创建到销毁`的过程中自动执行的函数,主要分为:`创建、挂载、更新、销毁`四个模块。

`挂载``$el``$mount`都是为了将`实例化后的vue挂载`到指定的`dom元素`中,但是`$el`的优先级要高于`$mount`

如果`实例化vue`的时候指定`el`,则`vue`将会渲染到`此el对应的dom`中,
反之,若没有指定`el`,则`vue实例`会处于一种`未挂载`的状态,此时可以通过`$mount`来手动`执行挂载`

`2. 被keep-alive缓存的组件的生命周期:`

第一次进入,`created` -> `mounted` -> `activated`
退出时触发`deactivated`。当再次进入时,只触发`activated`

f. 如何设置动态class,动态style?

(1) 动态​​class​​(对象形式):

​<div :class="{ 'is-active': true, 'red': isRed }"></div>​

(2) 动态​​class​​(数组形式):

拓展: ​​<div :class="['is-active', isRed && 'red' ]"></div>​

(3) 动态​​style​​​对象(其中的​​css属性​​​必须使用​​驼峰命名​​):

​<div :style="{ color: textColor, fontSize: '18px' }"></div>​

(4) 动态​​style​​​数组(使用多个​​对象​​​包裹,​​对象​​​之间的​​css属性​​​使用​​逗号​​隔开):

​<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>​

g. 如何处理非响应式数据?

  在我们的Vue开发中,会有一些数据,从始至终都​​未曾改变过​​​,这种​​死数据​​​,既然​​不改变​​​,那也就​​不需要对他做响应式处理​​​了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的​​死数据​​,如果都进行响应式处理,那会消耗大量性能。

// 方法一:将数据定义在data之外
data () {
`拓展:这种非响应式的数据可以直接定义,包括在methods方法里`
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}

// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}

h. 父子组件的生命周期?

  挂载阶段: ​​父beforeCreate​​​ >​​父created​​​ > ​​父beforeMount​​​ > ​​子beforeCreate ​​​>​​子created​​​> ​​子beforeMount​​​ > ​​子mounted​​​ >​​父mounted​

  扩展:

  子组件更新过程: ​​父beforeUpdate​​​ > ​​子beforeUpdate​​​ > ​​子updated​​​ > ​​父updated​

  父组件更新过程(影响到子组件): ​​父beforeUpdate​​​ > ​​子beforeUpdate​​​ > ​​子updated​​​ > ​​父updated​

  父组件更新过程(不影响子组件): ​​父beforeUpdate​​​ > ​​父updated​

  销毁过程: ​​父beforeDestroy​​​ > ​​子beforeDestroy​​​ > ​​子destroyed​​​ > ​​父destroyed​

h. Vue采用的是异步更新的策略,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才会节省性能。

i. 关于props的自定义校验

props: {
num: {
default: 1,
`// type:Number, 指定props的类型`
`// required: true, 指定props是否为必填项`
validator: function (value) {
// 返回值为false则验证不通过,报错
return [1, 2, 3, 4, 5].indexOf(value) !== -1
}
}
}

`拓展:`

`如果props没有默认值,一般的写法是:`
props: {
num: Number
}

`如果props有默认值,一般的写法是:`
props: {
num: {
type: Number,
default: 1
}
}

`特别地,如果需要给对象或者数组指定默认值,一般的写法是:`
props: {
selectList: {
type: Array,
default: () => []
}
}

默认值使用`函数的形式`返回的原因,其实和`data为什么需要返回一个函数`大同小异。
多个`父组件`引用`同一个子组件`时,引用类型`props``相互隔离`

i. 使用this.$options.data()获取vue文件data函数返回的初始对象状态

j. 自定义v-model

  默认情况下,​​v-model​​​ 是 ​​@input 事件侦听器​​​和 ​​:value 属性​​​上的语法糖。但是,你可以在你的​​Vue组件​​​中指定一个​​模型属性​​​来定义​​使用什么事件​​​和​​value属性​​——非常棒!

export default: {
model: {
event: 'change',
prop: 'checked'
}
}

k. 给组件绑定动态key值,当key值变化时,可以刷新组件,重新走组件的生命周期

l. 动态指令和动态参数的使用

`拓展:使用 @[响应式变量名] 实现动态自定义事件, 使用 :[响应式变量名] 实现动态props`

<template>
...
<aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
...
data(){
return{
...
someEvent: someCondition ? "click" : "dbclick",
someProps: someCondition ? "num" : "price"
}
},
methods: {
handleSomeEvent(){
// handle some event
}
}
</script>

m. (拓展) hook和$once的使用

`1. $once的介绍:

(1) $once是一个函数,可以为Vue组件实例绑定一个自定义事件,但该事件只能被触发一次,触发之后随即被移除。
(2) $once有两个参数,第一个参数为字符串类型,用来指定绑定的事件名称,第二个参数设置事件的回调函数。
(3) $once可以多次为同一个事件绑定多个回调,触发时,回调函数按照绑定顺序依次执行。
(4) once可以作为修饰符,.once只会触发一次 `

<template>
<div>
<button @click="$emit('clickHander')">按钮</button>
</div>
</template>

<script>
export default {
mounted() {
`在按钮第一次点击时,会先后调用两次回调函数`
`此后再点击按钮,不会触发回调函数`
this.$once('clickHander', () => {
console.log('第一次:该事件只能够被触发一次,触发后立刻被移除');
});
this.$once('clickHander', () => {
console.log('第二次:该事件只能够被触发一次,触发后立刻被移除');
});
}
}
</script>

`2. 使用$once清除定时器:`

通常的代码:`使用这种方式会多定义一个响应式变量timer,而且需要分别在两个生命周期里定义定时器以及清除定时器。`

export default{
data(){
timer: null
},
mounted(){
this.timer = setInterval(() => {
//具体执行内容
console.log('1')
},1000)
}
beforeDestory(){
clearInterval(this.timer)
this.timer = null
}
}

`使用$once的方法解决问题会更加优雅:`
export default{
mounted(){
let timer = setInterval(() => {
//具体执行内容
console.log('1')
},1000);
this.$once('hook:beforeDestroy',() => {
clearInterval(timer)
timer = null
})
}
}

`3. 使用监听生命周期钩子的hook进行父子组件之间的事件传递:`

通常的代码:

//父组件
<rl-child @childMounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},

// 子组件
mounted () {
this.$emit('childMounted')
},

`使用hook写出的优雅代码:`

//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
}

n. (拓展) v-for的使用 ( 非必须,指定key值方便diff算法对新旧虚拟dom进行比较,提升效率)

1. `v-for在数组中的遍历(item 是数组的每一项,index 对应数组的索引,同时支持解构)`
<div v-for="({name, id}, index) in cityArr" :key="id"> </div>

2. `v-for在对象中的遍历(value, key, index分别对应值、键、索引)`
<div v-for="(value, key, index) in form" :key="key"> </div>

3. `v-for迭代数字(从1开始打印,1 2 3 4)`
<div v-for="count in 4" :key="count"> </div>

4. `v-for迭代数字(输出每一个字符)`
<div v-for="str in 'hello'"> {{ str }} </li>

o. (拓展) vue2组件为什么只能有一个根节点?

`结论`: `vue2`组件只能有一个根节点,但是在`vue3组件`中,可以有`多个根节点`

`原因:`

(1) `vue2``虚拟dom`是一颗`单根树形结构``patch方法`在遍历的时候`从根节点开始遍历和比较`
它要求组件只有一个`根节点`,组件会转换为一个`虚拟dom`

(2) `vue3`引入了`Fragment`的概念。这是一个抽象的节点,如果发现组件有多个根,就创建一个`Fragment节点`
`多个根节点`作为它的`children`

p. (拓展) 使用watch监听路由变化

watch: {
'$route.query.id'() {
...
},
//或者:
'$route.query.id': {
handler(new, old) {

},
immediate: true
...
},
$route(to, from) {
// 监听路由变化
console.log("watch", to.query.id)
}
}

q. (拓展) 使用vue.config全局配置,在开发阶段获取组件的错误信息

`vue.config是一个对象,包含vue的很多全局配置,这里不一一展开介绍,有兴趣的朋友请移步vue官网`
`不过vue已经对组件名称进行处理,我们并不清楚具体指代,所以这个扩展我们看看就好,知道就行,最好的是sentry配置`

.`main.js`中进行`全局配置`:

Vue.config.errorHandler = function(err, vm, info) {
console.log(`组件${vm.$vnode.tag}发生错误:${err.message},${info}`)
}

      

【面试题】面试不面试,你都必须得掌握的vue知识_前端_05

`sentry上注册一个账号,并创建一个vue项目`

1.`在main.js中,引入配置sentry的index文件, 获取对应的dsn地址`
import '@/sentry'

2.`index.js配置(参照官网)`

import Vue from 'vue'
import router from '../router'
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
const needSentry = ['pro'].includes(process.env.VUE_APP_ENV_STAGE)

// Sentry会捕获用户操作系统过程中在控制台上产生的错误信息
// 判断是否为生产环境,只有在生产环境才配置
if (!needSentry) return
Sentry.init({
Vue,
dsn: '填写Sentry对应的dsn地址',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
// VUE_APP_BASE_URL对应后端api地址
tracingOrigins: [process.env.VUE_APP_BASE_URL]
})
],
tracesSampleRate: 1.0
})

3.vue2.x的底层原理(vue刺客)

a. vue2.x在初始化的过程中做了什么事?


`总结:`
1. `选项合并`,处理组件的配置内容,将`传入的options``构造函数本身的options`进行合并
(`用户选项和系统默认的选项进行合并`,与本文第一点:`vue常用知识点总结`中的`第k点`中的`merge`类似)
(初始化做了一些`性能优化`,将组件配置对象上的一些深层次属性放到`vm.$options`选项中,以提高代码的执行效率)
2. `初始化组件实例的关系属性`,如`$parent``$children``$root``$refs`
3.`初始化自定义组件事件的监听`, 若存在父监听事件, 则添加到该实例上
4. 初始化`render渲染`所需的`slots``渲染函数`等。
(其实就两件事:`插槽的处理``$createElm的声明`,也就是`render函数中的h的声明`)
5. `调用beforeCreate钩子函数`,在这里就能看出一个组件在创建前和后分别做了哪些初始化
6. `初始化注入数据`,隔代传参时先`inject`
(作为一个组件,在要给后辈组件提供数据之前,需要先把`祖辈传下来的数据注入`进来)
7. `props`,`methods`,`data`,`computed`,`watch`进行初始化,包括`响应式的处理`
8. `把祖辈传下来的数据注入`进来以后, `初始化provide`
9. 调用`created`钩子函数,初始化完成后,就可以执行挂载,进入挂载阶段

b. vue2.x在挂载的过程中做了什么?

c. vue2.x的响应式原理?

d. vue2.x的diff算法?