vue学习笔记
$watch
1. 声明式watch
vue中的watch可以用来监听响应式属性,prop的变动,通常我们可以再 组件的option的watch中定义对属性的监听:
export default Demo {
data() {
return {
msg: "hello world",
}
},
watch:{
msg(newVal, oldVal) {
console.log(newVal);
// do something ...
}
}
}
以上是一个简单的用法,watch中的属性代表这对data上的某个属性的监听配置操作:
-
当watch中的属性是一个函数的时候,vue会监听data上名称与这个函数相同的属性的变动,并将这个函数作为数据变更动作的回调函数(handler)执行。
-
当watch中的属性为一个对象的时候,vue会已这个属性名为路径,去监听data上的对应数据,并将这个对象中的handler方法作为数据变更后的回调。
-
当watch中的属性值是一个对象的时候,这个对象中可以定义一些watch的行为,例如是否深度监听,是否立即执行
-
当watch中的属性值是一个字符串的时候,vue会将组件实例上以这个字符串为名称的属性作为监听变化的回调函数执行。
因此,watch可以有一下使用方式:
export default Demo {
data() {
return {
user:{
age: 18,
name:"xiaoming",
},
address:"天安门",
description:"帅气"
}
},
methods:{
handleDescChanged(newDesc, oldDess) {
console.log("description changed", newDesc);
}
},
watch:{
address(newAddr, oldAddr) {
// do something ...
},
"address,description":function([newAddr, newDesc], [oldAddr,oldDesc]) {
// 同时监听两个属性,任意一个变更都会触发
},
user:{
deep:true, // 是否需要深度监听,为true时会监听到对象里面的属性变化
immediate: true, // 是否立即触发,为true时,会在组件创建,属性初始化的时候触发,
handler(newUser, oldUser) {
}
},
"user.age":function(newAge, oldAge) {
// 根据数据路径去监听age
},
description: 'handleDescChanged'
}
}
2. 编程式watch 和 单次watch
有时候我们无法在编码时预知需要监听的属性的访问路径,而是需要在运行时动态决定需要监听的属性,在vue组件的实例上有一个$watch函数,允许我们运行时监听数据,例如:
export default {
data() {
return { msg: "hello" }
},
methods:{
handleMsgChanged(newMsg) {
console.log(newMsg);
}
},
created() {
this.$watch('msg', this.handleMsgChanged);
}
}
$watch函数返回一个unwatch函数,允许我们在合适的时机取消对数据的监听, 例如下面的代码演示了如何一次性监听某个数据变更:
export default {
data() {
return { msg: "hello" }
},
created() {
const unwatch = this.$watch('msg', (newVal) => {
console.log(newVal);
if(unwatch) {
unwatch();
}
});
}
}
computed 计算属性
有时候我们的组件某个状态数据并不是简单的值或者对象,而是依赖其他组件状态数据计算得到的,当依赖的状态少的时候,我们可以可以通过组件状态配合watch实现,但是当依赖的状态很多时,我们需要写很多相似的watch, vue中,我们可以使用computed计算属性非常简洁的实现这个需求。
computed中的属性和data中的属性一样,最终都会作为组件实例的响应式状态暴漏在组件实例上。
computed中的属性的可以为为一个函数,这个函数的返回值将作为计算属性的值,当函数内部依赖的响应式数据变动后,vue将会自动重新计算计算属性的值,并触发依赖更新。
export default Demo {
data:{
name:'xiaoming',
age:18,
address: '天安门'
},
template:`<div>{{ desc }}</div>`,
computed:{
desc() {
return `我叫${this.name}, 今年${this.age}岁, 我家住在${this.address}`
}
}
}
computed中属性的值也可以是一个包含set,get的对象,这个对象的get函数将会作为计算属性的getter, vue会手机这个set函数内部的依赖并用作计算属性的更新触发条件,给计算和属性赋值时,将会触发set函数。
asyncComputed
vue原生的计算属性要求get函数同步返回一个值,如果需要异步计算属性可以使用 vue-async-computed
组件hook事件
有时候,我们需要监听子组件的生命周期变化,通常我们可以在子组件的生命周期内部对外发布事件,但是对于第三方组件,这显然是不可能的。vue为我们提供了hookEvent, 我们可以通过监听组件的hook:lifeCycle 事件,例如:
const Demo = {
template:"<h1 >hahahh</h1>"
}
export default {
components:{ Demo },
template:"<Demo @hook:mounted='onDemoCompMounted' />"
methods:{
onDemoCompMounted() {
console.log('demo mounted');
}
}
}
vm.$set, Vue.set 与 Vue.observable
vm.$set 与 Vue.set 的用法相同,都可以往某个对象上添加一个响应式属性,
有时候,由于一些不恰当的操作,或者没有预期到的场景,我们需要为对象添加一个属性,并且让视图能够相应其变化,这时,我们就可以使用$set:
this.$set(this.user, 'father', "xiyangyang");
Vue.observable
Vue.observable可以将一个普通的对象转换成一个响应式对象,返回的结果可以用于视图绑定,计算属性或者监听属性,或者渲染函数。返回对象的属性值变化会出发视图更新和其他响应式计算。
有几个场景Vue.observable 特别合适:
-
当我们需要给一个已有的响应式对象重新赋默认值时,如果一个一个属性去赋值会很麻烦,如果直接赋值一个普通的对象,数据会丢失响应式特性,这个时候可以使用Vue.observable将默认值对象转换成响应式对象后直接付给原来的变量:
const Demo = { data() { return { user: { age:18, name:'xiaoming', } } }, methods:{ func() { // 下面这种方式会丢失响应式特性,视图中无法继续响应age,name的更新 this.user = { age:20, name:"xiaohong" } // 这种方式则不会存在这种问题: this.user = Vue.observable({ age:20, name:"xiaohong" }); // 当然你也可以使用 vm.$set或者Vue.set来达到相同目的: this.$set(this,'user',{ age:20, name:"xiaohong" }) } } }
-
实现简单的状态管理
有时候,我们需要一些全局的,跨组件的响应式数据,我们期望这些数据变更后,任何使用他的组件都可以触发响应式更新,对于一些大型应用,我们可以使用VueStore或者Redux等一些状态管理库,但是对于一些小的应用,我们可以直接使用Vue.observable来生成一个全局的响应式对象。组件直接依赖这个对象即可。
const state = Vue.observable({ name: 'dahuang',age: 20 }); const Demo1 = { render(h) { return h('button', { on: { click:() => ++state.age } } ,state.age); } },
组件之间的通信
props 、路由参数
组件之间通信常用方式有 props绑定,以及路由传参,不细说
provide/inject
props绑定的局限很明显,他必须存在确定的父子组件关系,并且只能由父组件往子组件传,如果子组件的子组件也需要这个值,那么就需要子组件中再绑定一次。当组件嵌套层级比较深的时候,这个做法就很不优雅,耦合度也非常高。
vue提供provideApi, 允许父组件向任意层级的子组件注入属性,子组件如果需要使用这个属性,只需要使用inject api声明接受被注入的属性即可:
const Child = {
inject:['locale'],
template:'<h1> {{ locale }} </h1>'
}
const Parent = {
provide() {
return { locale: this.locale }
},
data:{
locale: 'zh-CN',
},
template:"<Child></Child>"
}
使用provide/inject 需要注意的一点是:当注入的数据为简单数据时,子组件内部是无法相应父组件中的数据更新的。但是如果需要注入相应式数据,我们可以传入一个响应式对象:
const Child = {
inject:['injections'],
template:'<h1> {{ injections.locale }} </h1>'
}
const Parent = {
provide() {
return { locale: this.locale }
},
data:{
injections:{ locale: 'zh-CN' },
},
template:"<Child></Child>"
}
这个时候,父组件修改了locale后,子组件就会实时相应数据的变更了。
全局状态管理
Vue.observable或者使用VueStore
组件事件
某些情况下,子组件需要的数据并不是为了在视图上展示,而是要通过数据进行一些业务计算,如果使用provide/inject的话,需要我们手动的去监听数据变更,然后进行计算,这样比较麻烦。vue提供组件事件来实现父子组件之间数据状态变化的消息机制。
组件事件和props有着相同的弊端,并且事件只能由子组件发送到父组件,某些场景下很有用,但是在一些需要双向数据流的场景,就不适用。
全局消息总线
对于上面说的,双向数据流的情况下,我们除了使用全局状态管理外,还可以使用全局事件。全局事件其实是一个取巧的方法,最终还是调用了vue的事件机制:
let eventBus = new Vue();
Vue.prototype.$eventBus = eventBus;
我们通过在引用初始化的时候给vue原型上放一个空的Vue实例对象,由于所有组件都是vue的实例,那么所有组件上其实都有一个 e v e n t B u s 对 象 , 后 面 组 件 可 以 通 过 这 个 对 象 的 eventBus对象,后面组件可以通过这个对象的 eventBus对象,后面组件可以通过这个对象的emit方法往总线上发布事件,也可以通过$on方法监听总线上的事件。
$nextTick
$nextTick 接受一个回调函数,这个函数将会在下一次DOM更新执行完后执行,因此,在回调函数调用之前,某些响应式数据的更新,可能无法获取到对应的视图状态, 例如:
const Demo = {
template:`<div>
<h1 v-if="show">h1 shown </h1>
<h2 v-else ref="h2">this is a h2</h2>
<button @click="func">click me</button>
</div>`,
data:{
show: true,
},
methods:{
func() {
this.show = false;
console.log(this.$refs['h2']); // 这里可能获取到的是null,应为视图可能还没有更新完
this.$nextTick(function() {
console.log(this.$refs['h2']); // 这里可以取到期望的结果,应为nextTick保证了回调函数在下一次DOM更新完后执行
})
}
}
}
nextTick在支持Promise的环境下,如果没有传入回调函数,会返回一个promise.
动态组件
动态组件可以理解为一个占位符,与router-view 类似。通过给is属性绑定不同的值,可以控制站位部分显示不同的组件,这在一些简单的多组件切换页面使用起来会比vue-router方便很多。
动态组件上的绑定的属性会全部绑定到实际显示的组件上。
const Comp1 = { template:"<h1>comp 1</h1>" };
const Comp2 = { template:"<h1>comp 2</h1>" }
const App = {
template:"<div><component is='compName' :some-prop /> <button @click='change'>change</button></div>",
data:{
compName:'Comp1'
},
components:{
Comp1, Comp2
},
methods:{ change() { this.compName = "Comp2" } }
}
函数式组件
对于一些简单的静态表现组件,既没有状态数据,也没有监听任何其他组件提供的状态更新,也没有生命周期等等,只是由单纯的静态视图组成的组件,我们可以将他标记为函数式组件。
函数是组建因为无需关心状态(没有响应式数据),也没有实例(没有this上下文),在渲染时无需关心状态更新,因此,它的性能很好。
函数式组件的render函数除了有createElement形参外,还有一个context形参,表示这个组件的上下文环境。并且自vue 2.3后,所有组件的attrs都被封装到render函数中的隐式参数props中:
const Demo = {
functional: true,
render(h, context) {
return h('div', context.data, context.$childern)
}
}
render函数的第二个参数context中包含了组件的props, listeners, slots等等数据。
render函数
vue视图代码通常使用html写在template中,或者直接写在html中后期通过挂在的方式与实例关联。
通常情况下,vue提供的指令足够开发出满足需求的组件,但是,在一些高级场景,他可能还是没有javascript那样灵活。render函数是tempplate的另一种替代方案,允许通过JavaScript动态的创建最终的DOM视图,他可以完全的发挥js的灵活性等优势。
render函数有一个参数,createElement函数(通常也被定义成h),他可以创建一个VNode,render函数要求返回一个只有一个根节点的VNode,
createElement函数的第一个参数是VNode的tagName, 第二个参数是要给这个VNode绑定的数据对象, 第三个参数是这个VNode的子节点,例如:
export default App {
render(h) {
return h('div', { attrs:{ title:"div" }, on:{ click:() => console.log('clicked...') } },
[ h("h1", null , 'this is a div created by render function') ]
);
}
}
Vue.compile
Vue.compile 可以将一段html模板字符串编译成一个render函数。
绝大多数情况下我们并不需要使用Vue.compile函数,但是,在某些情况下,我们需要改变一段模板编译的上下文环境,或者某些UI框架需要VNode作为参数的时候,他就派上用场了。
例如我们想要使用antd的Modal临时的弹出一个表单,让用户填写一些信息,这个表单的使用场景可能很少,功能也很简单,所以我们不太希望他的代码混主业务的模板中,那么我们可以这么做:
let content = `<a-form :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="警情地点">
<a-input default-value="${jqxx.jqdz}" />
</a-form-item>
</a-form>`;
antd.Modal.confirm({
title: 'title',
content: Vue.compile(content).render()
})
// Vue.compile(content, {}, this)返回一个带有能返回模板字符串中定义的DOM结构的render函数
异步组件
某些情况下,组件的代码并不在本地,而是在一个远程的服务器上,或者组件的某些必须资源在某个服务器上,并且我们需要在使用组件的时候采取加载这些资源,这时候就可以使用异步组件。
在局部注册组件的时候,我们可以直接提供一个返回Promise的函数,例如:
const App = {
conponents:{
Demo: () => import('./demo-async-component')
}
}
使用Vue.component定义全局组件的时候,Vue.component允许你提供一个工厂函数,函数在调用时会传入一个resolve函数,你可以在工厂函数内部做一些异步操作,然后再合适的时机通过resolve将组件的时机内容交给Vue, 或者你也可以在工厂函数内部直接返回一个Promise
Vue.component('demo-comp', function(resolve) {
setTimeout(() => {
resolve({
template:'demo async component'
})
})
})
Vue.component('demo-comp1', function(resolve) {
return Promise.resolve({
template:'demo async component'
})
})
slot
概叙
插槽,可以理解为模板中的一个占位符,在组件开发过程中,允许在组件中定义一系列的slot, 调用者可以在使用组件的时候通过某种方式来详细的定义组件的显示行为。
例如:
ButtonComponent.vue
<template>
<button>
<slot></slot>
</button>
</template>
App.vue
<template>
<Button>
click me !
</Button>
</template>
<script>
import Button from '@/ButtonComponent.vue'
export default App {
component:{ Button }
}
</script>
当组件渲染完成时,ButtonComponent.vue中的 位置的内容会被替换为 ‘click me !’。
这里,并不局限与文本,可以是任意的组件允许的模板内容,例如可以是一个组件,也可以是复杂的html模板代码。或者组件使用这的数据插值,以及任意vue 指令:
<template>
<Button>
<i if="showIcon" class="fa fa-star"></i>
click me !
<Badge :count="5"></Badge>
</Button>
</template>
<script>
import Button from '@/ButtonComponent.vue'
import Badge from '@/BadgeComponent.vue'
export default App {
data:{
showIcon: true,
},
component:{ Button, Badge }
}
</script>
插槽默认内容
有时候一个组件的内部并不一定需要使用者提供UI行为,组件本身就可以提供一些默认行为,slot标签内部允许组件自己提供备用显示内容,:
<template>
<button>
<slot>Submit</slot>
</button>
</template>
当组件使用的时候没有提供插槽内容的时候,插槽渲染时就会使用组件自己提供的备用内容
命名插槽
有时候我们的组件里面不仅仅时只有插槽内容,还有我们自己定义的其他结构内容,而且我们想要为用户提供更多的可定制性: 我们可以在不通的地方提供不同的插槽,以允许使用这能更详细的定义组件渲染的细节,这个时候就需要用到命名插槽。
例如,有这样一个组件模板:
<template>
<div class="modal">
<header>这里是modal-header</header>
<section>
<slot>这里是modal-body</slot>
</section>
<footer>这里是modal-footer</footer>
</div>
</template>
上面这个例子,我们希望使用者可以同时自己定义header,body, footer的内容,如果三个地方都向section中一样使用slot,很明显,最终渲染的结果是header,body ,footer的内同相同。对于这种情况Vue slot为我们提供了一个name属性,允许我们定义不同的插槽:
<template>
<div class="modal">
<header>
<slot name="header">这里是modal-header</slot>
</header>
<section>
<slot>这里是modal-body</slot>
</section>
<footer>
<slot name="footer">这里是modal-footer</slot>
</footer>
</div>
</template>
slot如果不指定name属性,那么默认的name就是default。
使用组件时,在向命名插槽分发内容的时候, 我们可以在一个template元素上使用v-slot指令来置顶要将当前节点元素的内容分发到那个插槽,v-slot指令的值对应目标插槽的name属性的名称:
<template>
<modal>
<template slot="header">this is custom modal header</template>
<template slot="footer">this is custom modal footer</template>
<div>
modal body
</div>
</modal>
</template>
使用时,插槽的顺序不做任何要求,任何没有对应到某个具体的命名插槽的内容都会被默认分发到默认插槽里面。
插槽作用域
再来看一下插槽的数据绑定的问题。
上面我们演示过,可以在插槽中使用任何vue指令,函数,甚至绑定响应式数据,在使用插槽的地方,插槽内部的上下文为当前使用这个组件的组件实例的上下文:
<template>
<Button>
{{ buttonName }}
</Button>
</template>
<script>
export default Demo {
data:{
buttonName: 'cancel'
}
}
</script>
即上面这个Demo中, Button 里面的默认插槽的作用域为Demo组件的实例,插值表达式 {{ buttonName }} 在渲染时获取demo的实例上读取buttonName属性。
作用域插槽
上面介绍了插槽的作用域,可以发现默认情况下,在给插槽分发内容的时候没有办法使用组件内部的数据。考虑下面这种情况:
TodoList.vue
<template>
<div v-for="item in todos">
<slot name="header"></slot>
<slot></slot>
</div>
</template>
<script>
export default TodoList {
props:['todos'],
}
</script>
有时候使用者希望,在向插槽分发内容的时候,插槽的内容实际上时根据每个具体的todo-item动态的生成的,现在问题时,只有TodoList组件能访问到item, 因此,需要想办法让用户在使用插槽的时候能访问到 item 对象,这个时候就要使用到作用域插槽.
Vue允许我们在定义组件slot时给slot绑定组件内部的数据,和普通模板节点绑定数据一样,同样可以使用v-bind来给slot绑定数据:
<template>
<div v-for="item in todos">
<slot name="header"></slot>
<slot :todoItem="item"></slot>
</div>
</template>
<script>
export default TodoList {
props:['todos'],
}
</script>
然后我们可以在向插槽分发内容时这样读取组件内部绑定的内容:
<template>
<TodoList>
<template slot="header" scope="props">
{{ props.todoItem.title }}
</template>
</TodoList>
</template>
组件给插槽上绑定的内容最终都被聚合到一个变量上,也就是这里的props, 你可以把props理解为函数的形参,因此,你可以任意修改这个变量的名字。
render函数中使用插槽
render函数中,在使用createElement函数创建VNode的时候,我们可以在第二个参数中使用slot属性来将当前VNode定义为一个插槽,slot属性的值就是这个插槽的名称。
组件在渲染时,所有插槽的内容将被聚合到组件实例的$slots属性上:
export default Modal {
render(h) {
return h('div', { attrs:{ class:"modal" } }, [
h('header', this.$scopedSlots.header({ user: this.user }) ),
h('section', this.$slots.default ),
h('footer', this.$slots.footer || 'modal footer' ),
])
}
}
这个例子演示里如果在render函数中使用命名插槽,默认插槽, 作用域插槽以及如何给插槽提供默认内容。
vm. p r o p s / v m . props / vm. props/vm.attrs /vm.$listeners
vm.$props 包含当前组件实例从父级作用域接收到的,同时在组件的props配置中有声明的props对象。这些props对象可以直接在vm实例上找到对应的代理属性。
vm. a t t r s : 包 含 父 级 作 用 域 中 给 组 件 绑 定 的 所 有 属 性 中 没 有 在 p r o p s 中 声 明 的 对 象 属 性 , 如 果 组 件 没 有 声 明 任 何 p r o p s , v m . attrs : 包含父级作用域中给组件绑定的所有属性中没有在props中声明的对象属性,如果组件没有声明任何props, vm. attrs:包含父级作用域中给组件绑定的所有属性中没有在props中声明的对象属性,如果组件没有声明任何props,vm.attrs就会包含所有父组件给实例绑定的所有属性。
vm. l i s t e n e r s : 与 v m . listeners: 与vm. listeners:与vm.attrs类似,包含父级作用域在组件上的所有事件监听器。
一般开发情况下,我们都会使用带具体某个prop或者事件监听器上,因此通常我们不太会去使用这三个api,。
但是,当我们在开发一些高阶组件,或者在对组件进行二次封装的时候,这三个api就很有用,我们可以通过v-bind=" a t t r s " 将 父 级 作 用 域 绑 定 的 属 性 传 入 给 被 封 装 的 组 件 中 , 可 以 通 过 v − o n = " attrs"将父级作用域绑定的属性传入给被封装的组件中, 可以通过v-on=" attrs"将父级作用域绑定的属性传入给被封装的组件中,可以通过v−on="listeners"将父级监听的所有事件绑定到被封装的组件上。
vm. e m i t / v m . emit / vm. emit/vm.off / vm. o n / v m . on / vm. on/vm.once
vm.$once: 只监听某个事件一次。
vm.$forceUpdate
vm.$forceUpdate强制更新DOM, 只能更新本组件的dom,无法更新子组件的DOM
vm.$destory
销毁组件实例
keep-alive
缓存组件的状态,在使用v-if, router-view或者动态组件切换组件时,保证组件不会被销毁,组件里面的状态被保留。