vue
基本概念
标签数据
- , msg也可以是js表达式,但只能包含**单个**表达式
一个表达式会产生一个值,它可以放在任何需要一个值的地方
语句可以理解成一个行为.循环语句和if语句就是典型的语句
需要语句的地方,你可以使用一个表达式来代替.这样的语句称之为表达式语句 v-model=”msg”建立双向绑定
1
<input type="text" v-model="name">
相当于:
1
<input type="text" :value="name" @input="name = $event.target.value">
v-once指令: 执行一次性地插值,当数据改变时,插值处的内容不会更新
1
<span v-once>这个将不会改变: {{ msg }}</span>
编译html元素 v-html
v-text
标签属性
1 | <img src="{{url}}" alt="" /> |
内联样式
1 | <img v-bind:style="{color:'',fontsize:data+'px'}"> |
class
1 | a:'red', |
组件内部引入第三方的css文件只在当前组件生效的办法
vue样式穿透 ::v-deep https://www.jb51.net/article/188038.htm
修改vantUI样式,直接在 中编写的话只会影响当前组件内的样式,但如果去掉scoped话又会影响全局样式。
如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:
上述代码将会编译成:.a[data-v-f3f3eg9] .b { /* … */ }可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作。
指令
v-for和虚拟DOM
1 | v-for="(val,index) in array" |
打个🌰。把F元素插入到A B C D E中。
其实是这么插的:新的dom和旧的dom比较, 第一个原来是A,更新之后还是A,所以就不变,第二个是B,更新之后还是B,所以还是不变,第三个是C,更新之后变成了F。 然后后面的都变化了
但是如果给每一个列表渲染的元素加上了唯一标识符。列表更新之后,编译器通过标识符知道第一个元素是A。第三个是C,就不会更新成F。就像下图。
vue和react的虚拟DOM的Diff算法大致相同
如果dom树有三层,在没加ID的情况下。
先比较第一层。比较一次
再比较第二层。比较第一层第一个节点和第二层第一个节点,第一层第一个节点和第二层第二个节点,比较第一层第二个节点和第二层两个节点。比较了四次。
算法复杂度,2的n次方。
如果加上ID。
比较第一个节点。再比较第二个节点。再比较第三个节点。再比较第四个节点。再比较第五个节点。一直比到第n个节点。
算法复杂度为n。
v-if和v-show
https://blog.csdn.net/zg0601/article/details/123632608
v-if
1 | v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 值的时候被渲染。 |
当它们处于同一节点,v-for
的优先级比 v-if
更高,这意味着 v-if
将分别重复运行于每个 v-for
循环中。当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:
1 | <li v-for="todo in todos" v-if="!todo.isComplete"> |
v-show
1 | <div v-show="a"> |
区别
既然 v-show 和 v-if 这两个指令都可以控制DOM元素的行为,那么它们有什么区别呢?
1、控制手段不同
v-show指令设置隐藏是给绑定的DOM元素添加CSS样式:display:none,但是DOM元素仍然存在;
v-if指令设置隐藏是将DOM元素整个删除,此时DOM元素不再存在。
2、编译过程不同
v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;而 v-show 只是简单的基于CSS切换,不管初始条件是什么,元素总是会被渲染。
3、编译条件不同
v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
v-if 由false变为true时,触发组件的beforeCreate、create、beforeMount、mounter钩子,由true变为false时,触发组件的
beforeDestory、destoryed方法。v-if 是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;
v-if 也是惰性的,如果初始渲染时条件为假,则什么也不做——直到为真时才开始渲染条件块。
4、性能消耗不同
v-show 由更高的初始渲染消耗, v-if 有更高的切换消耗。
使用场景
如果需要非常频繁地切换,则使用v-show较好;
如果在运行时条件很少改变,则使用v-if较好。
v-cloak
自定义指令
Vue.directive(指令名称,function(参数){
this.el -> 原生DOM元素
});
<div v-red="参数"></div>
指令定义函数提供了几个钩子函数(可选):
- bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
- inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
- update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
- componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
- unbind: 只调用一次, 指令与元素解绑时调用。
例子:https://juejin.cn/post/6906028995133833230#heading-5
computed和watch
如果一个值依赖多个属性(多对一),用computed肯定是更加方便的。如果一个值变化后会引起一系列操作,或者一个值变化会引起一系列值的变化(一对多),用watch更加方便一些
计算属性computed
- computed是计算属性,用来计算一个属性的值。
- 调用的时候不需要加括号,可以直接当属性来用
- 根据依赖自动缓存,依赖不变的时候,值不会重新计算
- computed的getter和setter
- computed的属性可以读取和设值。因此,在computed中可以分为getter(读取)和setter(设值).
- 一般情况下没有setter,computed只预设了getter,只能读取,不能设值。所以,computed默认格式(是不表明getter函数的).
- 当赋值给计算属性的时候,将调用setter函数。
计算属性是为了模板中的表达式简洁,易维护
1 | <p id="app">{{ myname.substring(0,1).toUpperCase() + myname.substring(1) }}</p> |
运算过于复杂,冗长,且不好维护,因此我们对于复杂的运算应该 使用计算属性的方式去书写。
1 | <template> |
变量不在 data中定义,而是定义在computed中
计算属性基于响应式依赖进行缓存。如其中的任意一个值未发生变化,它调用的就是上一次 计算缓存的数据,而不是从新计算。因此提高了程序的性能。而methods中每调用一次就会重新计算一次,为了进行不必要的资源消耗,选择用计算属性
支持缓存,只有依赖数据发生改变,才会重新进行计算
不支持异步,当computed内有异步操作时无效,无法监听数据的变化
侦听器 watch
watch的意思是监听,当发生变化时,监听并且执行。
- immediat:ture表示让值最初时候watch就执行
- deep表示对对象里面的变化进行深度监听
- 不支持缓存,数据变,直接会触发相应的操作
1 | var vm = new Vue({ |
不支持缓存,数据变,直接会触发相应的操作;
watch支持异步;
监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
当一个属性发生变化时,需要执行对应的操作;一对多;
监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数
- immediate:组件加载立即触发回调函数执行,
- deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变,注意监听数组的变动不需要这么做。注意:deep无法监听到数组的变动和对象的新增
不应该使用箭头函数来定义 watcher 函数,因为箭头函数没有 this,它的 this 会继承它的父级函数,但是它的父级函数是 window,导致箭头函数的 this 指向 window,而不是 Vue 实例
区别
- 功能上:computed是计算属性,watch是监听一个值的变化,然后执行对应的回调。
- 是否调用缓存:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调。
- 是否调用return:computed中的函数必须要用return返回,watch中的函数不是必须要用return。
- computed默认第一次加载的时候就开始监听;watch默认第一次加载不做监听,如果需要第一次加载做监听,添加immediate属性,设置为true(immediate:true)
- 使用场景:computed—-当一个属性受多个属性影响的时候,使用computed—–购物车商品结算。watch–当一条数据影响多条数据的时候,使用watch—–搜索框.
tip
vue中,如何解决watch的新值和旧值是一样的
https://juejin.cn/post/6898347237173100558
事件
使用方法
1 | <!-- 完整语法 --> |
1 | //使用原生的事件 |
键盘
@keydown $event ev.keyCode
1 | 常用键: |
.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
1 | <!-- 即使 Alt 或 Shift 被一同按下时也会触发 --> |
系统按键修饰符
你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
.ctrl
.alt
.shift
.meta
修饰符
修饰符串联
1 | <!-- 修饰符可以串联 --> |
lazy
在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件_之后_进行同步。
意思是什么呢,就是说当我们在input输入框输入数据时,v-model绑定的值不会发发生变化,但是当我们停止输入,输入框失去焦点或者按下回车时,v-model绑定的值才会发生变化,即在“change”时而非“input”时更新
native
@click.native是给组件绑定原生事件,否则会认为监听的是来自组件自定义的事件
trim
用户输入的前后的空格去掉
stop
stop防止事件冒泡
1 | @click.stop="show()" |
阻止冒泡:
a). ev.cancelBubble=true;
b). @click.stop 推荐
c). event.stopPropagation();
prevent
1 | <!-- 阻止默认行为,提交事件不再重载页面 --> |
capture
1 | <!-- 添加事件监听器时使用事件捕获模式 --> |
self
1 | <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> |
target
1 | <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> |
once
1 | <!-- 点击事件将只会触发一次 --> |
passive
1 | <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> |
exact
.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
1 | <!-- 即使 Alt 或 Shift 被一同按下时也会触发 --> |
sync
日常开发时,我们总会遇到需要父子组件双向绑定的问题,但是考虑到组件的可维护性,vue中是不允许子组件改变父组件传的props值的。那么同时,vue中也提供了一种解决方案.sync修饰符。sync修饰符,与我们平常使用$emit实现子组件向父组件通信没有区别,只不过是写法上方便一些。
$emit
子组件使用$emit向父组件发送事件:
1
this.$emit('update:title', newTitle)
父组件监听这个事件并更新一个本地的数据title:
1
2
3
4<text-document
:title="title"
@update:title="val => title = val"
></text-document>.sync修饰符
只需要修两个地方:
- 子组件内触发的事件名称以“update:title”命名
- 父组件v-bind:title 加上.sync修饰符,即 v-bind:title.sync
这样父组件就不用再手动绑定@update:title事件了。
1
2
3
4
5
6
7
8
9// 子组件
...
methods: {
onInput(e) {
this.$emit("update:title", e.target.value)
}
}
// index.vue组件
<info :title.sync="title"></info>
filter
1 | <p>1.msg|filterA</p> |
数据配合使用过滤器:
limitBy 限制几个
limitBy 参数(取几个)
limitBy 取几个 从哪开始
1 | filterBy 过滤数据 |
过渡动画
https://cn.vuejs.org/v2/guide/transitions.html
过渡条件
Vue 提供了 transition
的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用
v-if
) - 条件展示 (使用
v-show
) - 动态组件
- 组件根节点
过渡的类名
在进入/离开的过渡中,会有 6 个 class 切换。
v-enter
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to
:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时v-enter
被移除),在过渡/动画完成之后移除。v-leave
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to
:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时v-leave
被删除),在过渡/动画完成之后移除。
实例
1 | <div id="demo"> |
过渡组件transition
https://cn.vuejs.org/v2/api/#transition
Prop
name
- string,用于自动生成 CSS 过渡类名。例如:name: 'fade'
将自动拓展为.fade-enter
,.fade-enter-active
等。默认类名为"v"
appear
- boolean,是否在初始渲染时使用过渡。默认为false
。css
- boolean,是否使用 CSS 过渡类。默认为true
。如果设置为false
,将只通过组件事件触发注册的 JavaScript 钩子。type
- string,指定过渡事件类型,侦听过渡何时结束。有效值为"transition"
和"animation"
。默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。mode
- string,控制离开/进入过渡的时间序列。有效的模式有"out-in"
和"in-out"
;默认同时进行。duration
- number | {enter
: number,leave
: number } 指定过渡的持续时间。默认情况下,Vue 会等待过渡所在根元素的第一个transitionend
或animationend
事件。enter-class
- stringleave-class
- stringappear-class
- stringenter-to-class
- stringleave-to-class
- stringappear-to-class
- stringenter-active-class
- stringleave-active-class
- stringappear-active-class
- string
事件
before-enter
before-leave
before-appear
enter
leave
appear
after-enter
after-leave
after-appear
enter-cancelled
leave-cancelled
(v-show
only)appear-cancelled
用法
<transition>
元素作为单个元素/组件的过渡效果。<transition>
只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。
1 | <!-- 简单元素 --> |
1 | new Vue({ |
- 参考:过渡:进入,离开和列表
混入mixin
https://cn.vuejs.org/v2/guide/mixins.html
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
与组件的区别
组件:在父组件中引入组件,相当于在父组件中给出一片独立的空间供子组件使用,然后根据props来传值,但本质上两者是相对独立的。
Mixins:则是在引入组件之后与组件中的对象和方法进行合并,相当于扩展了父组件的对象与方法,可以理解为形成了一个新的组件。
特点
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
选项为data :在发生冲突时以组件数据优先。
值为函数的选项,如生命周期钩子函数created,mounted等,就会被合并调用,混合对象里的钩子函数在组件里的钩子函数之前调用
值为对象的选项,例如 methods、components和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
方法和参数在各组件中不共享
vm实例
当一个 Vue 实例被创建时,它将 data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
1 | var data = { a: 1 } |
当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于 data
中的 property 才是响应式的。也就是说如果你添加一个新的 property,比如:
1 | vm.b = 'hi' |
那么对 b
的改动将不会触发任何视图的更新。
全局Api
Vue.component
Vue.use( plugin )
安装 Vue.js 插件。如果插件是一个对象,必须提供 install
方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
该方法需要在调用 new Vue()
之前被调用。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。
property
vm.$options
1 | var vm=new Vue({ |
vm.$data
Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象 property 的访问。
1 | var data = { a: 1 } |
vm.$props
当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象 property 的访问。
vm.$el
获取Vue实例挂载的元素节点
vm.$refs
一个对象,持有注册过 ref
attribute 的所有 DOM 元素和组件实例。
vm.$parent
父实例,如果当前实例有的话。子组件可以通过this.$parent.fn
(父组件的函数)去调用父组件的函数
vm.$children
当前实例的直接子组件。需要注意 $children
并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children
来进行数据绑定,考虑使用一个数组配合 v-for
来生成子组件,并且使用 Array 作为真正的来源。
vm.$root
当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
方法
vm.$watch
vm.$watch( expOrFn, callback, [options] )
观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。
vm.$set
vm.$set( target, propertyName/index, value )
🌹调用方法:this.$set( target, key, value )
🌹 target:要更改的数据源(可以是对象或者数组)
🌹 key:要更改的具体数据
🌹 value :重新赋的值
当你发现你给对象加了一个属性,在控制台能打印出来,但是却没有更新到视图上时,也许这个时候就需要用到this.$set()这个方法了
给data定义的对象新增属性
,同时又要视图实时更新
,除了用Vue.$set()
方法,也可以通过Object.assign()
实现
vm.$once
$once有两个参数,第一个参数为字符串类型,用来指定绑定的事件名称,第二个参数设置事件的回调函数
自定义事件
1 | <template> |
生命周期
1 | let timer = setInterval(()=>{ |
生命周期
vm.$mount
挂载,将数据转化为dom
vm.$forceUpdate()
nextTick
https://www.jianshu.com/p/a7550c0e164f
https://mp.weixin.qq.com/s/lf9uKtTAKNplJeSwFSM_Uw
在下次 DOM 更新循环结束之后执行延迟回调。
1 | // 修改数据 |
$nextTick和setTimeout区别(宏任务微任务)
https://blog.csdn.net/u010565037/article/details/125757087
nextTick
在vue 源码中是利用 Promise.resolve()
实现的。该问题实际就是Promise
与setTimeout
的区别,本质是Event Loop
中微任务与宏任务的区别。
nextTick
:在下次 DOM 更新循环结束之后执行延迟回调。虽然DOM更新了,但是由于v-if此时并没有执行,所以获取不到其中的元素
组件
组件命名
kebab-case(短横线分隔命名)
1
Vue.component('my-component-name', { /* ... */ })
PascalCase (首字母大写命名)
1
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说
<my-component-name>
和<MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
创建组件
局部注册
- 定义组件
1 | //vue2.0组件定义 |
- 组件使用
1 | //导入组件 |
全局组件
1 | Vue.component('component-a', { /* ... */ }) |
如果你恰好使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context
只全局注册这些非常通用的基础组件
1 | import Vue from 'vue' |
全局注册的行为必须在根 Vue 实例 (通过 new Vue
) 创建之前发生。这里有一个真实项目情景下的示例。
内置组件
插槽:slot
https://v2.cn.vuejs.org/v2/guide/components-slots.html
原理
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot>
元素作为承载分发内容的出口。
编译作用域
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
1 | <navigation-link url="/profile"> |
具名插槽
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout>
组件:
对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
1 | //<base-layout> |
一个不带 name
的 <slot>
出口会带有隐含的名字default。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
1 | <base-layout> |
现在 <template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot
的 <template>
中的内容都会被视为默认插槽的内容。
然而,如果你希望更明确一些,仍然可以在一个 <template>
中包裹默认插槽的内容:
1 | <base-layout> |
任何一种写法都会渲染出:
1 | <div class="container"> |
注意 v-slot
只能添加在 <template>
上 (只有一种例外情况),这一点和已经废弃的 slot
attribute 不同。
作用域插槽
让父组件插槽内容能够访问子组件中的数据
我们想在父组件中使用子组件中定义的user变量
1 | <current-user> |
然而上述代码不会正常工作,因为只有 <current-user>
组件可以访问到 user
,而我们提供的内容是在父级渲染的。为了让 user
在父级的插槽内容中可用,我们可以将 user
作为 <slot>
元素的一个 attribute 绑定上去:
1 | <span> |
绑定在 <slot>
元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
1 | <current-user> |
动态插槽
1 | <base-layout> |
动态组件
keep-alive
1 | <component v-bind:is="currentTabComponent"></component> |
如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
用一个
Props:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
用法:
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和<transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。当组件在
<keep-alive>
内被切换,它的activated
和deactivated
这两个生命周期钩子函数将会被对应执行。- 页面第一次进入,钩子的触发顺序created-> mounted-> activated
- 退出时触发deactivated
- 当再次进入(前进或者后退)时,只触发activated。
props
1 | <!-- 在 HTML 中是 kebab-case(短横线分隔命名) 的 --> |
每个 prop 都可以指定的值类型
1 | props: { |
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。
组件手动更新
v-if
1
2
3
4
5
6
7
8
9<div v-if="isUpdate"></div>
// 移除组件
this.isUpdate = false;
//在组件移除后,重新渲染组件
//this.$nextTick可实现在DOM 状态更新后,执行传入的方法。
this.$nextTick(() => {
this.isUpdate = true;
});:key
1
2<div v-if="isUpdate" :key="test || Math.random() || new Data()"></div>
this.test++调用强制更新方法this.$forceUpdate()会更新视图和数据,强制触发vue的update方法
this.$set
组件通信
https://segmentfault.com/a/1190000019208626
父组件向子组件通信
1 | 父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。 |
子组件向父组件通信
emit
emit用于子组件调用父组件的方法并传递数据;子组件可以使用“$emit”触发父组件的自定义事件,触发事件后附加参数都会传给监听器回调,语法为“vm.$emit(事件, arg )”。
1 | <div id="box"> |
sync修饰符
自定义组件的v-model
https://blog.csdn.net/weixin_47232046/article/details/109738816
一个组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
attribute 用于不同的目的。model
选项可以用来避免这样的冲突:
1 | Vue.component('base-checkbox', { |
现在在这个组件上使用 v-model
的时候:
1 | <base-checkbox v-model="lovingVue"></base-checkbox> |
这里的 lovingVue
的值将会传入这个名为 checked
的 prop。同时当 <base-checkbox>
触发一个 change
事件并附带一个新的值的时候,这个 lovingVue
的 property 将会被更新。
中央事件总线
缺点:A组件触发子组件B的事件时,需要B组件已经渲染出来,如果给B组件v-if,那么A触发后,B组件没有创建,B就不会执行
这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
1 | var Event = new Vue(); 相当于又new了一个vue实例,Event中含有vue的全部方法; |
1 | //准备一个空的实例对象 |
provide/inject
允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
$parent
/ $children
与 ref
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/$children
:访问父 / 子实例- 这两种方法的弊端是,无法在跨级或兄弟间通信。
1 | // component-a 子组件 |
1 | // 父组件 |
使用场景
- 父子通信:
父向子传递数据是通过 props,子向父是通过 events($emit
);通过父链 / 子链也可以通信($parent
/ $children
);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
- 兄弟通信:
Bus;Vuex
- 跨级通信:
Bus;Vuex;provide / inject API、$attrs/$listeners
组件懒加载
异步方法
1
2
3components:{
"One-com":resolve=>(['./one'],resolve)
}const方法
1
2
3
4const One = ()=>import("./one");
components:{
"One-com":One
}
注入
Vue.use
和Vue.prototype
没有本质区别,Vue.use
就是在Vue.prototype
基础上又封装了一层而已,他们实现的原理都是在Vue.prototype
上添加了一个方法,Vue.prototype
适合于注册Vue生态外的插件,Vue.use
适合于注册Vue生态内的插件。
依赖注入
1 | <google-map> |
在这个组件里,所有 <google-map>
的后代都需要访问一个 getMap
方法,以便知道要跟哪个地图进行交互。不幸的是,使用 $parent
property 无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provide
和 inject
。
provide
选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是 <google-map>
内部的 getMap
方法:
1 | provide: function () { |
然后在任何后代组件里,我们都可以使用 inject
选项来接收指定的我们想要添加在这个实例上的 property:
1 | inject: ['getMap'] |
Vue.use
Vue.use(Object | Function)
Object ,必须提供 install 方法
1
Function,它会被作为 install 方法
1
2
3
4
5
6
7
8
9import moment from "moment";
const momentPlugin = {
install: function (Vue: any) {
Object.defineProperty(Vue.prototype, "$moment", { value: moment });
},
};
export default momentPlugin;
install 方法调用时,会将 Vue 作为参数传入。 该方法需要在调用 new Vue() 之前被调用。当 install 方法被同一个插件多次调用,插件将只会被安装一次。
还是看代码比较直接,新建plugin文件夹,文件夹下新建plugin.js
1 | var install = function(Vue) { |
main.js导入
1 | // 测试插件 |
使用插件
1 | this.$Plugin() |
总结:
Vue.use主要是执行install方法,而install主要也是执行Vue.prototype方法。所以,其实Vue.use()方法的核心就是Vue.prototype,只不过又封装了一层,更加的灵活,扩展性更好
饿了么
用饿了么UI举例
1 | import Vue from 'vue' |
饿了么部分源码
1 | //types/element-ui.d.ts |
ue.use
就是要运行这个install
对应的函数
总结
- Vue的插件是一个对象, 就像
Element
. - 插件对象必须有
install
字段. install
字段是一个函数.- 初始化插件对象需要通过
Vue.use()
Vue.prototype.$xxx
如果需要设置全局变量,在main.js中,Vue实例化的代码里添加。 不想污染全局作用域。这种情况下,你可以通过在 原型 上定义它们使其在每个Vue实例中可用。
1 | vue.prototype.$echarts = echarts |
这样$echarts
就在所有Vue实例中可用了,变量前加上$,是防止被组件中的变量意外覆盖。
vue生命周期
Vue 实例在被创建时都要经过一系列的初始化过程 , 编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等 。 在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
1 | beforeCreate |
不要在生命周期函数或者回调上使用箭头函数, 因为箭头函数并没有 this
,this指向调用它的VUE实例
比如 created: () => console.log(this.a)
或 vm.$watch('a', newValue => this.myMethod())
vue-router路由
hash和history
1 | const router = new VueRouter({ |
原理
hash
hash模式下,它指 # 号之后的所有字符,但是他虽然包含在url中,但是不包含在http请求中,所以改变hash值不会重新加载页面,对传给后端的url没有任何影响,因此不会重新加载页面。它每次改变都会触发hashchange事件,可以通过给window加上hashchange事件进行监听。它是单页面的标配。
history
利用了HTML5 History Interface中新增的pushState和replaceState方法。这两个方法应用于浏览器的历史记录栈, 而不会引起页面的刷新 ,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。 history模式下有一个问题,就是当页面刷新时,他会实实在在的发送请求,把url给传送过去,因此,如果后端没有做处理的话,就会因找不到资源而报404错误,因此使用history模式时可以跟后端进行配合。
下面阐述几种 HTML5
新增的 history API
。具体如下表:
API | 定义 |
---|---|
history.pushState(data, title [, url]) | pushState主要用于往历史记录堆栈顶部添加一条记录。各参数解析如下:①data会在onpopstate事件触发时作为参数传递过去;②title为页面标题,当前所有浏览器都会忽略此参数;③url为页面地址,可选,缺少时表示为当前页地址 |
history.replaceState(data, title [, url]) | 更改当前的历史记录,参数同上; 上面的pushState是添加,这个更改 |
history.state | 用于存储以上方法的data数据(即state对象),如果当前URL不是通过pushState或者replaceState产生的,那么history.state是null。 |
window.onpopstate | 响应pushState或者replaceState的调用。 |
总结
hash
- url中有#
- 原理是onhashchange事件
- 仅 hash 符号之前的内容会被包含在请求中
- hash修改的url是同文档的url
- hash不会修改浏览器历史记录栈
- 生成二维码、微信分享页面的时候都会自动过滤掉#后面的参数
history
url中没有#,美观
原理是popstate事件,浏览历史(即history对象)出现变化时,就会触发popstate事件。history.pushState用于在浏览历史中添加历史记录,history.replaceState修改浏览历史中当前纪录,但是并不触发页面刷新
1
2
3
4
5
6
7history.pushState({color:'red'}, 'red', 'red'})
window.onpopstate = function(event){
console.log(event.state)
if(event.state && event.state.color === 'red'){
document.body.style.color = 'red';
}
}全路径内容会被包含在请求中
history修改的url可以是同域的任意url
history会修改浏览器历史记录栈
history模式往往需要后端支持,如果后端nginx没有覆盖路由地址,就会返回404
配置路由
手动配置路由
直接vue add router
,或者
下载vue-router模块
1
npm install vue-router
创建文件夹,存放路由配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import Vue from "vue";
import VueRouter from "vue-router";
import login from "../components/login.vue";
import register from "../components/register.vue"
Vue.use(VueRouter);
const routes = [
{
path: "/",
component: login,
},
{
path: "/register",
component: register,
},
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
export default router;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//4.0
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})main.js文件中引入router
1
2
3
4
5
6
7
8
9
10
11
12import Vue from "vue";
import App from "./App.vue";
import VueRouter from "vue-router";
import router from "./router/index";
Vue.config.productionTip = false;
Vue.use(VueRouter);
new Vue({
router: router,
render: (h) => h(App),
}).$mount("#app");1
2
3
4
5
6
7
8
9
10// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router)
app.mount('#app')
// 现在,应用已经启动了!
自动配置路由
根据文件夹自动配置路由
1 | //index.js |
1 | //activity/index.js |
标签
router-view
router-view
将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局。<router-view>
渲染的组件还可以内嵌自己的 <router-view>
,根据嵌套路径,渲染嵌套组件。
因为它也是个组件,所以可以配合 <transition>
和 <keep-alive>
使用。如果两个结合一起用,要确保在内层使用 <keep-alive>
:
1 | <transition> |
router-link
1 | <!-- 使用 router-link 组件来导航. --> |
要注意,当 <router-link>
对应的路由匹配成功,将自动设置 class 属性值 .router-link-active
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
1 | <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link> |
这跟代码调用 router.push()
是一回事:
1 | router.push({ name: 'user', params: { userId: 123 } }) |
路由用法
动态路由匹配
模式 | 匹配路径 | $route.params |
---|---|---|
/user/:username | /user/evan | { username: 'evan' } |
/user/:username/post/:post_id | /user/evan/post/123 | { username: 'evan', post_id: '123' } |
除了 $route.params
外,API 文档
当使用路由参数时,例如从 /user/foo
导航到 /user/bar
,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route
对象
1 | const User = { |
或者,使用 beforeRouteUpdate
导航守卫,它也可以取消导航:
1 | const User = { |
捕获所有路由
1 | //当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后 |
当使用一个通配符时,$route.params
内会自动添加一个名为 pathMatch
参数。它包含了 URL 通过通配符被匹配的部分:
1 | // 给出一个路由 { path: '/user-*' } |
嵌套路由
嵌套路由的现象:点击了路由跳转之后父路由组件的内容一直呈现;子路由的内容进行切换,地址栏的路径也随之改变。
1 | // 嵌套路由 |
1 | //TravelPage.vue |
命名路由
有时候,通过一个名称来标识一个路由显得更方便一些(有些路径很长,直接写太麻烦),特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes
配置中给某个路由设置名称。
1 | const router = new VueRouter({ |
要链接到一个命名路由,可以给 router-link
的 to
属性传一个对象:
1 | <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link> |
这跟代码调用 router.push()
是一回事:
1 | router.push({ name: 'user', params: { userId: 123 } }) |
这两种方式都会把路由导航到 /user/123
路径。
命名视图
命名视图只需两步:第一在router-view添加name属性,第二在路由中用components。
有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
。
1 | <router-view class="view one"></router-view> |
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components
配置(带上 s):
1 | const router = new VueRouter({ |
重定向和别名
重定向
重定向也是通过 routes
配置来完成,下面例子是从 /a
重定向到 /b
:
1 | const router = new VueRouter({ |
重定向的目标也可以是一个命名的路由:
1 | const router = new VueRouter({ |
甚至是一个方法,动态返回重定向目标:
1 | const router = new VueRouter({ |
别名
“重定向”的意思是,当用户访问 /a
时,URL 将会被替换成 /b
,然后匹配路由为 /b
,那么“别名”又是什么呢?
/a
的别名是 /b
,意味着,当用户访问 /b
时,URL 会保持为 /b
,但是路由匹配则为 /a
,就像用户访问 /a
一样。
上面对应的路由配置为:
1 | const router = new VueRouter({ |
“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
路由传参
方式一:通过 params 传参
编程式:
- ```
data:{
username: ‘’
},
login() {
…
this.$router.push({
})name: 'home', //注意使用 params 时一定不能使用 path params: { username: this.username },
}1
2
3
4
5
- 声明式:
- ```
<router-link :to="{ name: 'home', params: { username: username } }">
- ```
取值:
this.$route.params.username
方式二:通过 query 传参
编程式:
- ```
data:{
username: ‘’
},
login() {
…
this.$router.push({
})path: '/home', query: { username: this.username },
}1
2
3
4
5
- 声明式:
- ```
<router-link :to="{ path: '/home', query: { username: username } }">
- ```
取值:
this.$route.query.username
params 传参后,刷新页面会失去拿到的参数。所以路由参数要修改为 '/login/:username'
(官方称为动态路由)
路由守卫
路由守卫就是路由跳转过程中的一些钩子函数 ,在路由跳转的时候,做一些判断或其它的操作。 类似于组件生命周期钩子函数 。
分类
全局路由守卫(全局路由守卫,就是小区大门,整个小区就这一个大门)
beforeEach(to, from, next) 全局前置守卫,路由跳转前触发
beforeResolve(to, from, next) 全局解析守卫 在所有组件内守卫和异步路由组件被解析之后触发
afterEach(to, from) 全局后置守卫,路由跳转完成后触发路由独享守卫
beforeEnter(to,from,next) 路由对象单个路由配置 ,单个路由进入前触发
组件路由守卫(跟 methods: {}等同级别书写,组件路由守卫是写在每个单独的 vue 文件里面的路由守卫)
beforeRouteEnter(to,from,next) 在组件生命周期beforeCreate阶段触发
beforeRouteUpdadte(to,from,next) 当前路由改变时触发
beforeRouteLeave(to,from,next) 导航离开该组件的对应路由时触发
参数
to: 即将要进入的目标路由对象
from: 即将要离开的路由对象
next:一定要调用该方法来 resolve 这个钩子。执行效果依赖 next
方法的调用参数。
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed
(确认的)。next(false)
: 中断当前的导航。如果浏览器的URL
改变了 (可能是用户手动或者浏览器后退按钮),那么URL
地址会重置到from
路由对应的地址。next('/')
或者next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next
传递任意位置对象,且允许设置诸如replace: true
、name: 'home'
之类的选项以及任何用在router-link
的to
prop 或router.push
中的选项。next(error)
: (2.4.0+) 如果传入next
的参数是一个Error
实例,则导航会被终止且该错误会被传递给router.onError()
注册过的回调。。
路由前置守卫
1 | const router = new VueRouter({ ... }) |
确保 next
函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错
以一个简单的例子来解释router.beforeEach
假设我们现在做一个这样的需求,用户在未登录的时候进入任意页面,我们就让用户跳转到登录页面,在已登录的时候让用户正常跳转到点击的页面。
1 | // BAD |
组件内的守卫
beforeRouteEnter
beforeRouteUpdate
(2.2 新增)beforeRouteLeave
1 | beforeRouteEnter(to, from) { |
API
router.push
注意:在 Vue 实例内部,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
。
router.push
这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 <router-link>
时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(...)
声明式 | 编程式 |
---|---|
<router-link :to="..."> |
router.push(...) |
1 | // 字符串 |
注意:如果提供了 path
,params
会被忽略,上述例子中的 query
并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name
或手写完整的带有参数的 path
:
1 | const userId = '123' |
router.replace
跟 router.push
很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
声明式 | 编程式 |
---|---|
<router-link :to="..." replace> |
router.replace(...) |
router.go
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
。
例子
1 | // 在浏览器记录中前进一步,等同于 history.forward() |
vuex
开始
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
1 | import Vue from 'vue' |
结构
state
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态
1 | //main.js |
1 | //当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性 |
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
1 | computed: mapState([ |
getters
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
1 | computed: { |
1 | const store = new Vuex.Store({ |
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
1 | import { mapGetters } from 'vuex' |
如果你想将一个 getter 属性另取一个名字,使用对象形式:
1 | ...mapGetters({ |
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 **回调函数 (handler)**。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
1 | const store = new Vuex.Store({ |
你可以向 store.commit
传入额外的参数,即 mutation 的载荷(payload):
1 | //两种方式提交 |
Mutation 必须是同步函数
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作(请求在这里操作)。
1 | const store = new Vuex.Store({ |
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。
1 | actions: { |
Action 通过 store.dispatch
方法触发:
1 | store.dispatch('increment') |
Actions 支持同样的载荷方式和对象方式进行分发:
1 | // 以载荷形式分发 |
可以在 action 内部执行异步操作
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
1 | const moduleA = { |
vuex项目
1 | ├── index.html |
index.js
1 | import Vue from 'vue' |
getters.js
1 | const user = { |
user.js
1 | /* eslint-disable no-unused-vars */ |
调用
1 | computed: { |
权限
路由权限
- 静态路由:固定的路由,没有权限。如login页面
- 动态路由:根据不同的角色,后端返回不同的路由接口。通过meta中的roles去做筛选
store存储路由
1 | //地址:store/modules/permission |
router添加路由
将store中的动态路由使用addRoute添加(最新版本去掉了addRoutes只能使用addRoute添加路由)。
1 | //地址:router/index |
菜单权限
路由遍历,通过store路由权限中的permission.state.routes去做处理
按钮权限
准备:存储按钮标识
1 | //地址:store/modules/user |
指令
通过模拟传入按钮标识的属性,去判断按钮是否隐藏或者禁用
1 | //地址:directive/permission/index |
1 | //地址:directive/permission/permissionBtn |
//应用
1 | <template> |
函数
1 | /** |
1 | <template> |
vue-class-component
vue2.x 对 TS 的支持并不友好,所以 vue2.x 跟 TS 的整合,通常需要基于 vue-class-component 来用基于 class(类) 的组件书写方式。
然后现在 vue3.x 已出,对 TS 很友好的支持,所以使用 vue3.x 的话暂时就不需要这种写法了。
构建
https://www.jianshu.com/p/adfe275b731e
源码
API
createDecorator
vue-property-decorator 依赖 vue-class-component 实现,主要用了内部提供的 createDecorator 方法。
在 vue-class-component 中提供了工具函数 createDecorator 允许添加其他额外的装饰函数,统一挂载在 Component.decorators 上,并把 options 传过去,对 options 增加需要的属性,实际上会调用这些装饰函数,让这些函数有机会处理 options。
createDecorator
接收一个回调函数作为其第一个参数,而该回调函数将会接收以下参数:
options
:Vue 组件选项对象。对这个对象的改动影响到提供的组件。key
:该修饰符应用的属性或方法的键名。parameterIndex
:被装饰的参数的索引值,如果该自定义装饰器作为一个参数被使用时。
1 | function createDecorator(factory) { |
createDecorator 调用后会返回一个函数,这个函数可以作为装饰器函数,接收的 target 如果是函数类型,说明作为类装饰器,target 就是被装饰的类;否则,得到的是原型,通过 constructor 拿到构造函数。
向要装饰的类上添加静态属性 **decorators**,存入一个函数,获得 options。
watch
现在来看 vue-property-decorator 中 watch 装饰器的源码,代码地址
1 | function Watch(path, options) { |
传入 createDecorator 的回调函数,会接受两个参数,componentOptions 为一个对象,就是在上面 componentFactory 中调用 Component.decorators,传入的对象,目的是向这个对象添加或增加 watch 属性,给要装饰的类使用;handler 是函数名字;
这样使用:
1 | @Component |
经过 @watch 装饰器处理后,选项对象上会增加一段数据:
1 | { |
data
1 | //将class实例的属性放入mixin中 |
由上面可得到data由两部分组成
比如原来有个组件:
1 | @Components({ |
现在有个需要渲染的组件,要把上面定义在 Home 中的 message 写在现有组件的 data 中:
1 | const App = Vue.extend({ |
本质就是初始类得到实例,拿属性组成对象,混合到渲染的组件中。
基本
Vue Class Component 是一个可以让你使用Class风格语法编写Vue组件的库
1 | <template> |
可以使用通过@Component
装饰器标注Class, 来用直观和标准的Class语法定义组件的data和方法. 你可以简单地使用Class风格的组件代替组件定义, 因为它等价于普通的使用对象定义的组件.
通过使用Class风格定义的组件, 你不但要改变语法, 还要利用一些ECMAScript语法特性, 比如Class继承和装饰器. Vue Class Component 也提供了一个mixins
帮助 来继承mixin, 以及一个 createDecorator
方法来简单地创建你自己的修饰器.你或许也需要使用 Vue Property Decorator 提供的 @Prop
和 @Watch
装饰器.
Class 组件
@Componen
装饰器使你的类成为一个Vue组件:
1 | import Vue from 'vue' |
Data
使用Class属性来初始化 data
:
1 | <template> |
The above component renders 上面的组件会在<div>
中的组件data message
中渲染Hello World!
注意如果初始化的值是 undefined
, Class属性将不是响应式的, 意思就是当其发生修改后, 将不会被侦测到:
1 | import Vue from 'vue' |
为了防止这种情况, 你需要使用 null
来赋值, 或者使用 data
钩子来代替:
1 | import Vue from 'vue' |
Methods
组件 methods
将直接定义在Class方法属性中:
1 | <template> |
Computed计算属性
计算属性可以通过Class属性的 getter / setter 定义:
1 | <template> |
额外的钩子
如果你使用Vue的插件, 比如Vue Router, 你或许需要Class组件来解决它们提供的钩子. 在这个例子中, Component.registerHooks
允许你注册这些钩子:
1 | // class-component-hooks.js |
在注册完这些钩子后, 就可以在Class组件中把它们当作Class属性方法来使用:
1 | import Vue from 'vue' |
推荐在单独的文件中写注册钩子的代码, 因为你需要在其他组件定义前注册它们. 你可以在文件顶部使用 import
引入:
1 | // main.js |
扩展 和 Mixins
扩展
你可以扩展一个存在的Class组件, 类似于原生的Class继承. 想象你有下面的名为Super的Class组件:
1 | // super.js |
你可以扩展他, 通过使用原生的Class继承语法:
1 | import Super from './super' |
注意 名为Super的Class组件必须是一个Class组件. 换句话说, 它需要继承作为原本的Vue
构造器以及被 @Component
装饰器装饰.
Mixins
Vue Class Component 提供 mixins
助手函数来在Class风格中使用 mixins. 通过使用mixins
助手, TypeScript 可以推断mixin类型以及在组件类型中继承它们.
例子中定义了名为Hello
和 World
的 mixins :
1 | // mixins.js |
在Class组件中使用它们:
1 | import Component, { mixins } from 'vue-class-component' |
和名为Super的Class组件一样, 所有的mixins 必须被定义为一个 Class 组件.
装饰器
https://github.com/kaorun343/vue-property-decorator#readme
https://zhuanlan.zhihu.com/p/191443950
https://blog.csdn.net/yusirxiaer/article/details/112364800
vue-property-decorator 是在 vue-class-component
上增强了更多的结合 Vue
特性的装饰器
Prop
@Prop(options: (PropOptions | Constructor[] | Constructor) = {})
@Prop
装饰器接收一个参数,这个参数可以有三种写法:
Constructor
,例如String,Number,Boolean
等,指定prop
的类型;Constructor[]
,指定prop
的可选类型;PropOptions{}
,可以使用以下选项:type,default,required,validator
1 | import { Vue, Component, Prop } from 'vue-property-decorator' |
PropSync
@PropSync
装饰器与@prop
用法类似,二者的区别在于:
@PropSync
装饰器接收两个参数:-
propName: string
表示父组件传递过来的属性名; -
options: Constructor | Constructor[] | PropOptions
与@Prop
的第一个参数一致;
-
@PropSync
会生成一个新的计算属性。
1 | import { Vue, Component, PropSync } from 'vue-property-decorator' |
注意: @PropSync 需要配合父组件的 .sync 修饰符使用
会被扩展为:
<comp :propA=”bar” @update:propA=”val => bar = val”>
当子组件需要更新 propA的值时,它需要显式地触发一个更新事件:
this.$emit(‘update:propA’, newValue)
Model
https://blog.csdn.net/weixin_47232046/article/details/109738816
@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})
@Model
装饰器允许我们在一个组件上自定义v-model
,接收两个参数:
event: string
事件名。options: Constructor | Constructor[] | PropOptions
与@Prop
的第一个参数一致。
1 | import { Vue, Component, Model } from 'vue-property-decorator' |
上面例子中指定的是change
事件,所以我们还需要在template
中加上相应的事件:
1 | <template> |
watch
@Watch(path: string, options: WatchOptions = {})
@Watch
装饰器接收两个参数:
path: string
被侦听的属性名;- options可以包含两个属性 :
immediate?:boolean
侦听开始之后是否立即调用该回调函数;deep?:boolean
被侦听的对象的属性被改变时,是否调用该回调函数;
1 | import { Vue, Component, Watch } from 'vue-property-decorator' |
监听路由
1 | @Watch('$route') |
Emit
@Emit(event?: string)
@Emit
装饰器接收一个可选参数,该参数是$Emit
的第一个参数,充当事件名。如果没有提供这个参数,$Emit
会将回调函数名的camelCase
转为kebab-case
,并将其作为事件名;@Emit
会将回调函数的返回值作为第二个参数,如果返回值是一个Promise
对象,$emit
会在Promise
对象被标记为resolved
之后触发;@Emit
的回调函数的参数,会放在其返回值之后,一起被$emit
当做参数使用。
1 | import { Vue, Component, Emit } from 'vue-property-decorator' |
ref
@Ref(refKey?: string)
@Ref
装饰器接收一个可选参数,用来指向元素或子组件的引用信息。如果没有提供这个参数,会使用装饰器后面的属性名充当参数
1 | import { Vue, Component, Ref } from 'vue-property-decorator' |
自定义装饰器
https://devpress.csdn.net/vue/632ca0af357a883f870c7ef9.html
Vue Class Component 提供 createDecorator
帮助创建自定义装饰器. createDecorator
接受一个回调函数作为第一个参数, 回调将接受下面的参数:
options
: Vue 组件选项. 改变这个对象将影响所提供的组件.key
: 装饰器所需要的属性或方法的键.parameterIndex
: 如果自定义装饰器用于参数,则装饰参数的索引.
下面例子是创建一个 Log
装饰器, 当修饰器方法被调用时, 打印log信息, 包括方法名和参数:
1 | // decorators.js |
作为方法修饰器使用它:
1 | import Vue from 'vue' |
在上面代码中, 当 hello
被调用, 并传入 42
, 将会打印处下面的log:
1 | Invoked: hello( 42 ) |
Tip
在属性中初始化this
的值
如果你在类的属性中定义一个箭头函数, 箭头函数中访问 this
时, 将无法获取实例. 这是因为当初始化Class属性时, this
仅仅时Vue实例的代理:
1 | import Vue from 'vue' |
通常使用生命周期函数代替constructor
由于原始构造函数被调用来收集初始组件数据, 建议不要自己声明 constructor
:
1 | import Vue from 'vue' |
上面的代码打算在组件初始化的时候用fetch来获取post列表, 但是fetch将会被调用两次, 因为Vue Class Component的运作
所以推荐写在生命周期函数里, 比如用created
代替 constructor
:
1 | import Vue from 'vue' |
Vue3快速上手
Vue3带来了什么
性能的提升
打包大小减少41%
初次渲染快55%, 更新渲染快133%
内存减少54%
……
源码的升级
使用Proxy代替defineProperty实现响应式
重写虚拟DOM的实现和Tree-Shaking
……
拥抱TypeScript
- Vue3可以更好的支持TypeScript
新的特性
Composition API(组合API)
- setup配置
- ref与reactive
- watch与watchEffect
- provide与inject
- ……
新的内置组件
- Fragment
- Teleport
- Suspense
其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除keyCode支持作为 v-on 的修饰符
- ……
API 风格
Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API
官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html
选项式 API
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。
1 | export default { |
组合式 API
通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 script setup
搭配使用。这个 setup
attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与 <script setup>
改造后和上面的模板完全一样的组件:
1 | <script setup> |
setup钩子
https://cn.vuejs.org/api/composition-api-setup.html
setup()
钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
在 setup()
函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup()
暴露的属性
1 | <script> |
在模板中访问从 setup
返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value
。当通过 this
访问时也会同样如此解包。
setup()
自身并不含对组件实例的访问权,即在 setup()
中访问 this
会是 undefined
。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。
setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
setup的两个注意点
- 尽量不要与Vue2.x配置混用
- Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
- 如果有重名, setup优先。
- setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
setup执行的时机
- 在beforeCreate之前执行一次,this是undefined。
setup的参数
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于
this.$attrs
。 - slots: 收到的插槽内容, 相当于
this.$slots
。 - emit: 分发自定义事件的函数, 相当于
this.$emit
。
- attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于
访问 Props
setup
函数的第一个参数是组件的 props
。和标准的组件一致,一个 setup
函数的 props
是响应式的,并且会在传入新的 props 时同步更新。
1 | export default { |
请注意如果你解构了 props
对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx
的形式来使用其中的 props。
如果你确实需要解构 props
对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:
1 | import { toRefs, toRef } from 'vue' |
Setup 上下文
传入 setup
函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup
中可能会用到的值:
1 | export default { |
该上下文对象是非响应式的,可以安全地解构:
1 | export default { |
attrs
和 slots
都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x
或 slots.x
的形式使用其中的属性。此外还需注意,和 props
不同,attrs
和 slots
的属性都不是响应式的。如果你想要基于 attrs
或 slots
的改变来执行副作用,那么你应该在 onBeforeUpdate
生命周期钩子中编写相关逻辑。
暴露公共属性
expose
函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose
函数暴露出的内容:
1 | export default { |
与渲染函数一起使用
setup
也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:
1 | import { h, ref } from 'vue' |
返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。
我们可以通过调用 expose()
解决这个问题:
1 | import { h, ref } from 'vue' |
此时父组件可以通过模板引用来访问这个 increment
方法。