路由参数解耦
一般在组件内使用路由参数,大多数人会这样做:
1export default { 2 methods: { 3 getParamsId() { 4 return this.$route.params.id 5 } 6 } 7} 8
在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。
正确的做法是通过 props 解耦
1const router = new VueRouter({ 2 routes: [{ 3 path: /user/:id , 4 component: User, 5 props: true 6 }] 7}) 8
将路由的 props 属性设置为 true 后,组件内可通过 props 接收到 params 参数
1export default { 2 props: [ id ], 3 methods: { 4 getParamsId() { 5 return this.id 6 } 7 } 8} 9
另外你还可以通过函数模式来返回 props
1const router = new VueRouter({ 2 routes: [{ 3 path: /user/:id , 4 component: User, 5 props: (route) => ({ 6 id: route.query.id 7 }) 8 }] 9}) 10
函数式组件
函数式组件是无状态,它无法实例化,没有任何的生命周期和方法。创建函数式组件也很简单,只需要在模板添加 functional 声明即可。一般适合只依赖于外部数据的变化而变化的组件,因其轻量,渲染性能也会有所提高。
组件需要的一切都是通过 context 参数传递。它是一个上下文对象,具体属性查看文档。这里 props 是一个包含所有绑定属性的对象。
函数式组件
1<template functional> 2 <div class="list"> 3 <div class="item" v-for="item in props.list" :key="item.id" @click="props.itemClick(item)"> 4 <p>{{item.title}}</p> 5 <p>{{item.content}}</p> 6 </div> 7 </div> 8</template> 9
父组件使用
1<template> 2 <div> 3 <List :list="list" :itemClick="item => (currentItem = item)" /> 4 </div> 5</template> 6
1import List from @/components/List.vue 2export default { 3 components: { 4 List 5 }, 6 data() { 7 return { 8 list: [{ 9 title: title , 10 content: content 11 }], 12 currentItem: 13 } 14 } 15} 16
样式穿透
在开发中修改第三方组件样式是很常见,但由于 scoped 属性的样式隔离,可能需要去除 scoped 或是另起一个 style 。这些做法都会带来副作用(组件样式污染、不够优雅),样式穿透在css预处理器中使用才生效。
我们可以使用 >>> 或 /deep/ 解决这一问题:
1<style scoped> 2外层 >>> .el-checkbox { 3 display: block; 4 font-size: 26px; 5 6 .el-checkbox__label { 7 font-size: 16px; 8 } 9} 10</style> 11
1<style scoped> 2/deep/ .el-checkbox { 3 display: block; 4 font-size: 26px; 5 6 .el-checkbox__label { 7 font-size: 16px; 8 } 9} 10</style> 11
watch高阶使用
立即执行
watch 是在监听属性改变时才会触发,有些时候,我们希望在组件创建后 watch 能够立即执行
可能想到的的方法就是在 create 生命周期中调用一次,但这样的写法不优雅,或许我们可以使用这样的方法
1export default { 2 data() { 3 return { 4 name: Joe 5 } 6 }, 7 watch: { 8 name: { 9 handler: sayName , 10 immediate: true 11 } 12 }, 13 methods: { 14 sayName() { 15 console.log(this.name) 16 } 17 } 18} 19
深度监听
在监听对象时,对象内部的属性被改变时无法触发 watch ,我们可以为其设置深度监听
1export default { 2 data: { 3 studen: { 4 name: Joe , 5 skill: { 6 run: { 7 speed: fast 8 } 9 } 10 } 11 }, 12 watch: { 13 studen: { 14 handler: sayName , 15 deep: true 16 } 17 }, 18 methods: { 19 sayName() { 20 console.log(this.studen) 21 } 22 } 23} 24
触发监听执行多个方法
使用数组可以设置多项,形式包括字符串、函数、对象
1export default { 2 data: { 3 name: Joe 4 }, 5 watch: { 6 name: [ 7 sayName1 , 8 function(newVal, oldVal) { 9 this.sayName2() 10 }, 11 { 12 handler: sayName3 , 13 immaediate: true 14 } 15 ] 16 }, 17 methods: { 18 sayName1() { 19 console.log( sayName1==> , this.name) 20 }, 21 sayName2() { 22 console.log( sayName2==> , this.name) 23 }, 24 sayName3() { 25 console.log( sayName3==> , this.name) 26 } 27 } 28} 29
watch监听多个变量
watch本身无法监听多个变量。但我们可以将需要监听的多个变量通过计算属性返回对象,再监听这个对象来实现“监听多个变量”
1export default { 2 data() { 3 return { 4 msg1: apple , 5 msg2: banana 6 } 7 }, 8 compouted: { 9 msgObj() { 10 const { msg1, msg2 } = this 11 return { 12 msg1, 13 msg2 14 } 15 } 16 }, 17 watch: { 18 msgObj: { 19 handler(newVal, oldVal) { 20 if (newVal.msg1 != oldVal.msg1) { 21 console.log( msg1 is change ) 22 } 23 if (newVal.msg2 != oldVal.msg2) { 24 console.log( msg2 is change ) 25 } 26 }, 27 deep: true 28 } 29 } 30} 31 32
事件参数$event
$event 是事件对象的特殊变量,在一些场景能给我们实现复杂功能提供更多可用的参数
原生事件
在原生事件中表现和默认的事件对象相同
1<template> 2 <div> 3 <input type="text" @input="inputHandler( hello , $event)" /> 4 </div> 5</template> 6
1export default { 2 methods: { 3 inputHandler(msg, e) { 4 console.log(e.target.value) 5 } 6 } 7} 8
自定义事件
在自定义事件中表现为捕获从子组件抛出的值
my-item.vue :
1export default { 2 methods: { 3 customEvent() { 4 this.$emit( custom-event , some value ) 5 } 6 } 7} 8复制代码 9
App.vue
1<template> 2 <div> 3 <my-item v-for="(item, index) in list" @custom-event="customEvent(index, $event)"> 4 </my-list> 5 </div> 6</template> 7
1export default { 2 methods: { 3 customEvent(index, e) { 4 console.log(e) // some value 5 } 6 } 7} 8 9
自定义组件双向绑定
组件 model 选项:
允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。
input 默认作为双向绑定的更新事件,通过 $emit 可以更新绑定的值
1<my-switch v-model="val"></my-switch> 2
1export default { 2 props: { 3 value: { 4 type: Boolean, 5 default: false 6 } 7 }, 8 methods: { 9 switchChange(val) { 10 this.$emit( input , val) 11 } 12 } 13} 14
修改组件的 model 选项,自定义绑定的变量和事件
1<my-switch v-model="num" value="some value"></my-switch> 2
1export default { 2 model: { 3 prop: num , 4 event: update 5 }, 6 props: { 7 value: { 8 type: String, 9 default: 10 }, 11 num: { 12 type: Number, 13 default: 0 14 } 15 }, 16 methods: { 17 numChange() { 18 this.$emit( update , this.num++) 19 } 20 } 21} 22 23
监听组件生命周期
通常我们监听组件生命周期会使用 $emit ,父组件接收事件来进行通知
子组件
1export default { 2 mounted() { 3 this.$emit( listenMounted ) 4 } 5} 6
父组件
1<template> 2 <div> 3 <List @listenMounted="listenMounted" /> 4 </div> 5</template> 6
其实还有一种简洁的方法,使用 @hook 即可监听组件生命周期,组件内无需做任何改变。同样的, created 、 updated 等也可以使用此方法。
1<template> 2 <List @hook:mounted="listenMounted" /> 3</template> 4
程序化的事件侦听器
比如,在页面挂载时定义计时器,需要在页面销毁时清除定时器。这看起来没什么问题。但仔细一看 this.timer 唯一的作用只是为了能够在 beforeDestroy 内取到计时器序号,除此之外没有任何用处。
1export default { 2 mounted() { 3 this.timer = setInterval(() => { 4 console.log(Date.now()) 5 }, 1000) 6 }, 7 beforeDestroy() { 8 clearInterval(this.timer) 9 } 10} 11
如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
我们可以通过 $on 或 $once 监听页面生命周期销毁来解决这个问题:
1export default { 2 mounted() { 3 this.creatInterval( hello ) 4 this.creatInterval( world ) 5 }, 6 creatInterval(msg) { 7 let timer = setInterval(() => { 8 console.log(msg) 9 }, 1000) 10 this.$once( hook:beforeDestroy , function() { 11 clearInterval(timer) 12 }) 13 } 14} 15
使用这个方法后,即使我们同时创建多个计时器,也不影响效果。因为它们会在页面销毁后程序化的自主清除。
手动挂载组件
在一些需求中,手动挂载组件能够让我们实现起来更加优雅。比如一个弹窗组件,最理想的用法是通过命令式调用,就像 elementUI 的 this.$message 。而不是在模板中通过状态切换,这种实现真的很糟糕。
先来个最简单的例子:
1import Vue from vue 2import Message from ./Message.vue 3 4// 构造子类 5let MessageConstructor = Vue.extend(Message) 6// 实例化组件 7let messageInstance = new MessageConstructor() 8// $mount可以传入选择器字符串,表示挂载到该选择器 9// 如果不传入选择器,将渲染为文档之外的的元素,你可以想象成 document.createElement()在内存中生成dom 10messageInstance.$mount() 11// messageInstance.$el获取的是dom元素 12document.body.appendChild(messageInstance.$el) 13
下面实现一个简易的 message 弹窗组件
Message/index.vue
1<template> 2 <div class="wrap"> 3 <div class="message" :class="item.type" v-for="item in notices" :key="item._name"> 4 <div class="content">{{item.content}}</div> 5 </div> 6 </div> 7</template> 8
1// 默认选项 2const DefaultOptions = { 3 duration: 1500, 4 type: info , 5 content: 这是一条提示信息!, 6} 7let mid = 0 8export default { 9 data() { 10 return { 11 notices: [] 12 } 13 }, 14 methods: { 15 add(notice = {}) { 16 // name标识 用于移除弹窗 17 let _name = this.getName() 18 // 合并选项 19 notice = Object.assign({ 20 _name 21 }, DefaultOptions, notice) 22 23 this.notices.push(notice) 24 25 setTimeout(() => { 26 this.removeNotice(_name) 27 }, notice.duration) 28 }, 29 getName() { 30 return msg_ + (mid++) 31 }, 32 removeNotice(_name) { 33 let index = this.notices.findIndex(item => item._name === _name) 34 this.notices.splice(index, 1) 35 } 36 } 37} 38
1.wrap { 2 position: fixed; 3 top: 50px; 4 left: 50%; 5 display: flex; 6 flex-direction: column; 7 align-items: center; 8 transform: translateX(-50%); 9} 10 11.message { 12 --borderWidth: 3px; 13 min-width: 240px; 14 max-width: 500px; 15 margin-bottom: 10px; 16 border-radius: 3px; 17 box-shadow: 0 0 8px #ddd; 18 overflow: hidden; 19} 20 21.content { 22 padding: 8px; 23 line-height: 1.3; 24} 25 26.message.info { 27 border-left: var(--borderWidth) solid #909399; 28 background: #F4F4F5; 29} 30 31.message.success { 32 border-left: var(--borderWidth) solid #67C23A; 33 background: #F0F9EB; 34} 35 36.message.error { 37 border-left: var(--borderWidth) solid #F56C6C; 38 background: #FEF0F0; 39} 40 41.message.warning { 42 border-left: var(--borderWidth) solid #E6A23C; 43 background: #FDF6EC; 44} 45
Message/index.js
1import Vue from vue 2import Index from ./index.vue 3 4let messageInstance = null 5let MessageConstructor = Vue.extend(Index) 6 7let init = () => { 8 messageInstance = new MessageConstructor() 9 messageInstance.$mount() 10 document.body.appendChild(messageInstance.$el) 11} 12 13let caller = (options) => { 14 if (!messageInstance) { 15 init(options) 16 } 17 messageInstance.add(options) 18} 19 20export default { 21 // 返回 install 函数 用于 Vue.use 注册 22 install(vue) { 23 vue.prototype.$message = caller 24 } 25} 26
main.js
1import Message from @/components/Message/index.js 2 3Vue.use(Message) 4
使用
1this.$message({ 2 type: success , 3 content: 成功信息提示 , 4 duration: 3000 5}) 6
