前言
本篇博客不在赘述基础的用法,主要回顾一下日常开发中常用的知识点,如计算属性、过滤器、指令函数、slot插槽、组件间通信等。
面试回答
1.watch和computed的区别:computed是计算属性,可以产生新变量,具备缓存性,就跟data一样,它是为了简化template里面的模版字符串,防止太过冗余。在值不变的情况下可以直接读取缓存复用。而watch只能监听已有的数据,更多的是用回调函数处理业务逻辑,并且能够处理异步任务。本质上watch也是通过Object.defineProperty来监听属性的变化。
2.自定义指令:指令一般都会进行全局注册,注册时用vue.directive完成,其中第一个参数为指令名,第二个参数是个对象,对象里有bind、inserted、update三个函数。如果要实现一个v-model的指令,可以先注册一个名为v-model的指令,然后在bind、update回调函数中接收element、binding两个参数,将binding的value与element的value在两个回调函数中互相赋值来实现。当然,这是用指令的方式,其本质还是:value和v-on事件进行处理。
3.自定义事件:自定义事件常用于组件间的通信,比如当父组件中引用了子组件,在子组件上添加自定义事件,假设是点击事件那么就是@事件名=方法名,这个方法是在父组件中使用的,接下来就是子组件内部,在指定元素上添加对应的点击事件,然后可以在点击事件的方法里通过$emit去传值给父组件。如果需要传值给子组件,那么直接:自定义属性名=传入属性名,在子组件中可以通过props获取,不过为了保证数据的单项流动,props不能直接修改,必须是父组件修改后传递给子组件。父子组件间的通信也可以通过ref来实现,在子组件内部通过ref调用父组件方法,然后将数据传给父组件。
4.组件封装:以input组件为例,封装UI组件我们只需要做好展示以及数据定义即可,一般用props去处理数据,用emit或refs去处理事件,用slot去处理插槽,在完成input组件功能之后,需要在组件库的主文件index.js中实现按需引入,也就是给封装好的input组件添加一个install方法,在该方法中通过Vue.component完成注册,最后export出来,其他组件用相同的方式实现。然后在pageage.json中添加脚本,可以用vue-server-cli - build命令将封装的组件库进行打包,然后用npm publish --access public 去推送。
知识点
过滤器、指令函数仅用于了解,slot插槽、组件间通信、计算属性等开发常用则需进一步了解
1.计算属性
计算属性可以当作属性来使用,类似data中的msg,他实际上就是一个对象,computed属性的属性值一般是函数,此时默认会走get方法,不过在需要时你也可以提供一个 setter,如下:
computed:{
fullName:{
set(){
},
get(){
return 'abc'
}
}
}
//或
computed:{
fullName(){
return 'abc'
}
}
计算属性还有以下性质:
- 支持缓存,只有依赖数据发生改变,才会重新进行计算
- 不支持异步,当computed内有异步操作时无效,无法监听数据的变化
- computed属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值
计算属性与监听属性(watch)的区别:
- watch不支持缓存,数据发生改变,会直接触发相应的操作;computed支持缓存。
- watch支持异步,computed不支持。
- watch监听函数能够接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
- 监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数,
- immediate:组件加载立即触发回调函数执行,
- deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变。deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到。
- 如果一个值依赖多个属性(多对一),用computed肯定是更加方便的;如果一个值变化后会引起一系列操作,或者一个值变化会引起一系列值的变化(一对多),用watch更加方便一些。
- computed会产生新的属性,产生的新的属性与data中原有的属性功能没有区别,用法一样;watch不会产生新的属性。
- computed会在vue实例化过程中执行一次;watch在vue初始化时,不会执行。
- watch也是通过Object.defineProperty来监听属性的变化。
2.过滤器
过滤器就是把一些不必要的东西过滤掉,实质不改变原始数据,可以理解为函数,对数据进行加工处理后返回过滤后的数据,一般应用于某些需要处理格式的数据,比如时间、金额等。由于Vue3中已废弃filter,所以过滤器模块仅做了解,接下来看看全局过滤器以及局部过滤器的用法
-
全局过滤器
//在main.js中 import moment from 'moment' import Vue form 'vue' //定义全局过滤器--时间格式化 Vue.filter('format',function(val,arg){ if(!val)return; val = val.toString() return moment(val).format(arg) }) //如果所有过滤器均写在一个js中,可以用循环添加filter中,如下: import * filter from './filters' Object.keys(filters).forEach(key=>{ Vue.filter(key,filters[key]) }) //在任意组件中 <template> <div>{{time | format("YYYY-MM-DD HH:mm:ss")}}</div> </template> <script> export default{ data(){ return { time:'2020-10-08T04:04:04Z' } } } </script>
-
局部过滤器:局部过滤器优先级高于全局过滤器
<template> <div>{{ message | test("6666")}}</div> </template> <script> data(){ return { message:'zxp' } }, filters:{ test(val){ return value } } </script>
过滤器相比于普通方法的优点:
- 复用性高,可以在全局注册一个过滤器,然后再各个组件中使用,如果用全局函数,用的组件都需要引用这个函数
- 可以通过多个管道串联,写法较为优雅,其执行顺序从左往右,如 {{message | filterA | filterA}}。
- 没有缓存,被调用时才计算
3.指令函数
指令函数也就是自定义指令,指令函数也同样分为全局注册与局部注册,通过Vue.directive(name, [definition] )注册指令,第一个参数为自定义指令名称(指令名称不需要加v-前缀,默认是自动加上前缀的,使用指令的时候一定要加上前缀),第二个参数可以是对象数据,也可以是一个指令函数。 具体用法如下:
-
全局注册
//在main.js中 Vue.directive('copy',{ //当被当定的元素插入到DOM中时... inserted:function(el){ el.focus()//聚焦元素 } }) //如果所有指令均写在一个js中,可以用循环注册指令,如下: import directives from './directives' Object.keys(directives).forEach(key=>{ Vue.directive(key,directives[key]) }) //在任意组件中 <template> <div v-copy>zxp</div> </template>
-
局部注册
<div id="app" class="demo"> <!-- 局部注册 --> <input type="text" placeholder="我是局部自定义指令" v-focus2> </div> <script> expor default { directives: { focus2: { inserted: function(el){ el.focus(); } } } } </script>
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
指令钩子函数会被传入以下参数:
- el: 指令所绑定的元素,可以用来直接操作 DOM,就是放置指令的那个元素。
- binding: 一个对象,里面包含了几个属性,这里不多展开说明,官方文档上都有很详细的描述。
- vnode:Vue 编译生成的虚拟节点。
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
顺带说一下,cm-directives依赖挺好用的。
4.slot插槽
简单来说插槽就是子组件中的提供给父组件使用的一个坑位,用slot表示,父组件可以在这个坑位中填充任何模板代码替换子组件中的slot。插槽有这么几种形式:默认插槽、具名插槽、动态插槽、作用域插槽,下面我们逐一来看
- 默认插槽:当父组件没有替换子组件时,所展示的内容
//子组件Child
<template>
<div>
<slot>我是默认内容</slot>
</div>
</template>
//父组件:没有替换内容,展示我是默认内容
<template>
<div>
<Child></Child>
</div>
</template>
<script>
import Child from './Child.vue'
</script>
//父组件:有替换内容,展示Hello zxp
<template>
<div>
<Child>Hello zxp</Child>
</div>
</template>
<script>
import Child from './Child.vue'
</script>
- 具名插槽:目的让父组件的插槽替换子组件中指定的slot,多应用于多个插槽的情况。
//子组件Child
<template>
<div>
<slot name="red">red</slot>
<slot name="green">green</slot>
<slot name="yellow">yellow</slot>
</div>
</template>
//父组件
<template>
<Child>
<!-- #green 为v-slot:green的缩写 -->
<template #green>
222
</template>
<template #yellow>
333
</template>
<template v-slot:red>
111
</template>
</Child>
</template>
<script>
import Child from './Child.vue'
</script>
- 动态插槽:插槽名变成了变量,可以随时修改这个变量从而展示不同的效果。写法是v-slot:[变量名]、缩写为#[变量名]。
//父组件
<template>
<Child>
<template #[slotName]>
红
</template>
<template #yellow>
黄
</template>
<template #green>
绿
</template>
</Child>
</template>
<script>
import Child from './Child.vue'
import { ref } from 'vue'
const slotName = ref('red')
</script>
- 作用域插槽:有些需求父组件需要从子组件的插槽上获取属性,此时需要用到作用域插槽,即在子组件的标签上绑定属性,然后在父组件中取用。
//子组件Child
<template>
<div>
<slot childName="zxp" childAge="26"></slot>
</div>
</template>
//父组件
<template>
<Child v-slot="slotProps">
我叫 {{ slotProps.childName }} ,今年 {{ slotProps.childAge }} 岁了。
</Child>
</template>
<script>
import Child from './Child.vue'
</script>
当然,大部分情况用具名插槽更为准确,具名作用域插槽的用法如下:
//子组件Child.vue
<template>
<slot name="color" colorCode="red"></slot>
</template>
//父组件
<template>
<Child>
//本质:v-solt:color="colorProps"
<template #color="colorProps">
{{ colorProps.colorCode }}
</template>
</Child>
</template>
<script>
import Child from './Child.vue'
</script>
5.组件间通信
参考文章:https://juejin.cn/post/6844903845642911752
组件间通信有以下几种方式,
1.props/$emit
该方式适用于直接父子间的数据传递,通过父组件绑定自定义事件,子组件通过this.emit(‘自定义事件’,value)传值。
//子组件Child
<div>
<p>{{getParent}}</p>
<button @click="ToParent">点击传值</button>
</div>
<script>
export default{
props:["getParent"],
methods:{
ToParent(){
this.$emit('changeMsg','zxc') // 第一个参数为自定义事件名,第二个参数为需要传递的值
}
}
}
</script>
//父组件
<div>
//这边的changeMsg就是子组件emit中的自定义事件名称
<child :getParent="test" @changeMsg="getChild"></child>
</div>
<div>{{msgfromChild}}</div>
<script>
import child from './child'
export default {
name:'father',
data(){
return {
test:"father nice",
msgfromChild:''
}
},
components:{
child
},
methods:{
getChild(val){
this.msgfromChild = val
}
}
}
</script>
2.$parent
、 $children
、 $ref
该方式同样适用于直接父子间的数据传递,但是这种方式仅用于取值使用而不能像$emit
一样改变数据互相传递。ref如果绑定在DOM元素上,则指向的就是DOM元素;如果绑定在组件上,则指向组件实例。
//子组件Child
<div>
<p>{{$parent.father}}</p>
</div>
<script>
export default{
data(){
return {
child:'222'
}
}
}
</script>
//父组件
<child ref="childA"></child>
//下面两个获取的值是一致的
<div>{{$children.child}}</div>
<div>{{$refs.childA.child}}</div>
<script>
import child from './child'
export default {
name:'father',
data(){
return {
father:"111",
}
}
}
</script>
3.$emit
/$on
EventBus这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件(emit)和监听事件(on),来实现了任何组件间的通信,适用于父子、兄弟、跨级组件之间的数据传递。
//公共实例文件bus.js,作为公共数控中央总线
import Vue from 'vue';
export default new Vue();
//第一个组件first.vue
import Bus from './bus.js'
export default {
name:'first',
data(){
return {
value:'我来自first.vue组件!'
}
},
methods:{
add(){
Bus.$emit('txt',this.value);
}
}
}
//第二个组件second.vue
import Bus from './bus.js'
export default {
name:'second',
data(){
return {
}
},
mounted:function(){
//因为有时不确定何时会触发事件,一般会在mounted或created钩子函数中来监听。
Bus.$on('txt',function(val){
console.log(val)
});
}
}
-
$attrs
/$listeners
:多级组件嵌套需要传递数据时,且仅传递数据,而不做中间处理,使用vuex或者$parent/$children
麻烦,则可以考虑使用这种方式。 -
$attrs
:包含了父作用域中不被prop所识别、获取的特性绑定,它可以通过 v-bind=“$attrs” 传入内部组件。 -
$listeners
:包含了父作用域中的 (不含.native修饰器的) v-on事件监听器,它可以通过 v-on=“$listeners” 传入内部组件即
$attrs
与$listeners
是两个对象,$attrs
里存放的是父组件中绑定的非Props属性,$listeners
里存放的是父组件中绑定的非原生事件。
// index.vue
<template>
<div>
<child-com1
:foo="foo"
:boo="boo"
:coo="coo"
:doo="doo"
title="前端工匠"
></child-com1>
</div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
components: { childCom1 },
data() {
return {
foo: "Javascript",
boo: "Html",
coo: "CSS",
doo: "Vue"
};
}
};
</script>
// childCom1.vue
<template class="border">
<div>
<p>foo: {{ foo }}</p>
<p>childCom1的$attrs: {{ $attrs }}</p>
<child-com2 v-bind="$attrs"></child-com2>
</div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
components: {
childCom2
},
inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
props: {
foo: String // foo作为props属性绑定
},
created() {
console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
}
};
</script>
// childCom2.vue
<template>
<div class="border">
<p>boo: {{ boo }}</p>
<p>childCom2: {{ $attrs }}</p>
<child-com3 v-bind="$attrs"></child-com3>
</div>
</template>
<script>
const childCom3 = () => import("./childCom3.vue");
export default {
components: {
childCom3
},
inheritAttrs: false,
props: {
boo: String
},
created() {
console.log(this.$attrs); // { "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
}
};
</script>
// childCom3.vue
<template>
<div class="border">
<p>childCom3: {{ $attrs }}</p>
</div>
</template>
<script>
export default {
props: {
coo: String,
title: String
},
created() {
console.log(this.$attrs); // { "doo": "Vue" }
}
};
</script>
- provide/inject:这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。简单来说,祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。 它的使用场景主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
// Parent.vue
export default {
provide: {
name: 'zxp'
}
}
// Child.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // zxp
}
}
需要注意的是:provide和inject绑定并不是可响应的。
provide与inject实现数据响应式有两种办法:
- provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods
- 使用2.6最新API Vue.observable 优化响应式 provide(推荐)
我们来看个例子:孙组件D、E和F获取A组件传递过来的color值,并能实现数据响应式变化,即A组件的color变化后,组件D、E、F会跟着变。
// A 组件
<div>
<h1>A 组件</h1>
<button @click="() => changeColor()">改变color</button>
<ChildrenB />
<ChildrenC />
</div>
......
data() {
return {
color: "blue"
};
},
// provide() {
// return {
// theme: {
// color: this.color //这种方式绑定的数据并不是可响应的
// } // 即A组件的color变化后,组件D、E、F不会跟着变
// };
// },
provide() {
return {
theme: this//方法一:提供祖先组件的实例
};
},
methods: {
changeColor(color) {
if (color) {
this.color = color;
} else {
this.color = this.color === "blue" ? "red" : "blue";
}
}
}
// 方法二:使用2.6最新API Vue.observable 优化响应式 provide
// provide() {
// this.theme = Vue.observable({
// color: "blue"
// });
// return {
// theme: this.theme
// };
// },
// methods: {
// changeColor(color) {
// if (color) {
// this.theme.color = color;
// } else {
// this.theme.color = this.theme.color === "blue" ? "red" : "blue";
// }
// }
// }
// F 组件
<template>
<div class="border2">
<h3 :style="{ color: injections.theme.color }">F 组件</h3>
</div>
</template>
<script>
export default {
inject: {
theme: {
//函数式组件取值不一样
default: () => ({})
}
}
};
</script>
常见的组件封装一般也就做好展示以及数据定义即可,一般用props去处理数据,用emit或refs去处理事件,用slot去处理插槽,在完成input组件功能之后,需要在组件库的主文件index.js中实现按需引入,也就是给封装好的input组件添加一个install方法,在该方法中通过Vue.component完成注册,最后export出来,其他组件用相同的方式实现。然后在pageage.json中添加脚本,可以用vue-server-cli - build命令将封装的组件库进行打包,然后用npm publish --access public 去推送。
最后
// } else {
// this.theme.color = this.theme.color === “blue” ? “red” : “blue”;
// }
// }
// }
// F 组件
F 组件
常见的组件封装一般也就做好展示以及数据定义即可,一般用props去处理数据,用emit或refs去处理事件,用slot去处理插槽,在完成input组件功能之后,需要在组件库的主文件index.js中实现按需引入,也就是给封装好的input组件添加一个install方法,在该方法中通过Vue.component完成注册,最后export出来,其他组件用相同的方式实现。然后在pageage.json中添加脚本,可以用vue-server-cli - build命令将封装的组件库进行打包,然后用npm publish --access public 去推送。
最后
走过路过,不要错过,点赞、收藏、评论三连~