[toc]
Vue
- 在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问
全局挂载事件 this.$root.$on('eventName', () => {}) this.$root.$emit('eventName')
移除自定义事件监听器 this.$root.$off('eventName')
vue3 实际已经禁止使用这个了 https://v3.vuejs.org/guide/migration/events-api.html
vue-2
__ob__: Observer
vue设置的数据监听器,一般不可枚举重置数据到初始化状态
/* 只能用assign 否者会报错 Use nested data properties instead */
Object.assign(this.$data , this.$options.data())
/* 当data数据中有用到 this.props this.methods中的方法 需要重新绑定 this; 因为这些属性没有挂载在$options上,所以直接赋值会是undefined*/
Object.assign(this.$data, this.$options.data.call(this));
Vue.component(id, 组件对象)
注册或获取全局组件虚拟 DOM
- 需要适配上层 API 对 dom 的操作,所以需要具有普适性,所以不是最优性能实现,但是比所有的都直接操作DOM要更好,保障了性能的下限
- 跨平台,因为本质是js对象,,可以做服务端渲染 weex 等
- 有些高性能应用中,无法极致优化,比如vscode手动操作DOM进行的性能优化
自定义组件用 v-model
参考 base-dialog
<input :value="value" @input="input"> // 等价于下面方式
<input v-model="value"> // 语法糖
后面为了满足非输入元素(也就是自定义组件)父子通信有了如下方式
父组件:
<ChildComponent v-model="select"></ChildComponent>
data() {
return {
select: 0
}
}
子组件:()
model: {
prop: 'sel', // 与下面 props 中的 sel 一致
event: 'eventName' // 用于 this.$emit('eventName', '参数') 触发
},
props: {
value: '', // v-model 默认子组件接收就是用的 value 事件触发用的 this.$emit('input', '参数');如果要换其它名称就用上面的方式
sel: {
type: Number, // 用于接收父组件 v-model 中的值
default: 0
},
}
计算属性
computed 不能传参
默认只有getter
computed: {
testComputed: {
get() {
return this.name // name变动时调用 getter
},
set(v) {
this.name = v
}
}
}
this.testComputed = 'new name' // 触发setter
事件处理
<button @click="fn('some string', $event)"></button>
// 如果这里 fn 没有参数传递 那么在 method 中的 fn 默认第一个参数为 even
// 获取 原始 DOM 事件
methods: { fn(str, event) {}}
组件中,可以用 $on,$once 监听所有的生命周期钩子函数,如监听组件的 destroyed 钩子函数可以写成 this.$on('hook:destroyed', () => {})
其他周期函数同理
父组件监听子组件生命周期函数
// 方式一
// 父组件
<SomeCustomComponent @created="someFn" />
// 子组件
created() {
this.$emit('created')
}
// 方式二
@hook:created 监听组件的 created 生命钩子函数 同理 其它周期函数也可以这样 监听
<SomeCustomComponent @hook:created="someFn" />
自定义事件
自定义事件名,会被转换为全小写;camelCase 或 PascalCase 与 kebab-case,永远不会相同;推荐使用 kebab-case 命名
定义的组件
如果要使用原生事件(click等) 需要加上.native
;
watch 处理
- immediate: 表示初始化监听时,立即触发一次,从而不必在 created 等钩子函数中去获取第一次传入的 prop 值
- deep: 当监听的是对象时,并且父组件只是修改了对象某个属性值,并没有修改引用地址,则需要设置deep进行监听,当修改了对象引用,则不需要
无论何时,绑定的数据对象上 message 属性发生了改变,插值处的内容都会更新;通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。
过滤器函数总接受表达式的值作为第一个参数。
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。
Vue 2.x 中,过滤器只能在 mustache 绑定和 v-bind 表达式(从 2.1.0 开始支持)中使用,因为过滤器设计目的就是用于文本转换。为了在其他指令中实现更复杂的数据变换,你应该使用计算属性。
然而,不同的是计算属性是基于它们的依赖进行缓存的,计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
v-if v-show
说明- v-else 元素必须紧跟在 v-if 或者 v-else-if 元素的后面——否则它将不会被识别
- v-if 条件块内的事件监听器和子组件适当地被销毁和重建
- v-show 元素总是会被渲染,并且只是简单地基于 CSS 进行切换
- elementUI 的 dialog 使用的是 v-show
v-model后不能跟表达式
v-bind="$attrs" v-on="$listeners"
$attrs:
包含了父作用域中 不作为 prop 被识别和获取的属性,在孙一级中定义prop获取或者不设置prop,直接用this.$attrs获取,两者只能选其一(使用示例参考 vue-admin componentCommunicate)
$listeners:
包含了父作用域中传入的 v-on 事件监听器,除了原生事件,可以是 click 等事件也可以是自定义事件,可以 this.$listeners.click() this.$emit('click') 两种方式调用(使用示例参考 vue-admin componentCommunicate)
可用于透传参数,比如二次封装某些第三方组件时,<el-upload v-bind="$attrs">
那自己封装的组件就可以直接设置 第三方的组件属性
生命周期
- created 非相应式的data数据可以赋初始值
data: { a: {} }; this.a.b=1
- destroyed : 如果有定时器,在该钩子函数中务必清除
参考: https://www.jianshu.com/p/a20f2023c78a
beforeCreate:在实例初始化之后,data observer 和 event/watcher事件配置之前被调用,此时data、watcher、methods没有。
vue实例什么都没有,但$route对象是存在的,可以根据路由信息进行重定向之类的操作。
created:在实例已经创建完成之后被调用。在这一步,实例已完成以下配置:数据观测(data observer) ,属性和方法的运算, watch/event 事件回调。挂载阶段还没开始,$el属性目前不可见。
此时 this.$data 可以访问,watcher、events、methods也出现了,若根据后台接口动态改变data和methods的场景下,可以使用。
beforeMount:在挂载开始之前被调用,相关的 render 函数 首次被调用。但是render正在执行中,此时DOM还是无法操作的。此时的vue实例对象,相比于created生命周期,此时只是多了一个$el的属性,但其值为undefined。
页面渲染时所需要的数据,应尽量在这之前完成赋值。
mounted:在挂载之后被调用。在这一步 创建vm.$el并替换el,并挂载到实例上;此时元素已经渲染完成了
beforeUpdate:$vm.data更新之后,虚拟DOM重新渲染 和打补丁之前被调用。
你可以在这个钩子中进一步地修改$vm.data,这不会触发附加的重渲染过程。
updated:虚拟DOM重新渲染 和打补丁之后被调用。
当这个钩子被调用时,组件DOM的data已经更新,所以你现在可以执行依赖于DOM的操作。但是不要在此时修改data,否则会继续触发beforeUpdate、updated这两个生命周期,进入死循环!
beforeDestroy:实例被销毁之前调用。在这一步,实例仍然完全可用。
destroyed:Vue实例销毁后调用。此时,Vue实例指示的所有东西已经解绑定,所有的事件监听器都已经被移除,所有的子实例也已经被销毁。
mixin
import mixIn from 'mixin.js';
{
mixins: [mixIn]
}
data对象的数据会进行递归合并,并在发生冲突时以组件数据优先
同名钩子函数将合并为一个数组,都将被调用。混入对象的钩子将在组件自身钩子之前调用
值为对象的选项,例如 methods、computed、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
可以将一些公共方法,比如获取store中数据,放入mixin中,抽取成公共的mixin使用
过滤器
https://v2.cn.vuejs.org/v2/guide/filters.html
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
props:
同步且不变,可以直接在created等钩子函数直接获取值,而不是得到子组件设置的默认 porp 值;
异步,只能在watch中监听prop,从而获取值
当子组件中需要修改当前的prop时,可以利用watch监听其变化,在data中设置一个新的变量,以供子组件修改赋值
修改props 两种方式
- 通过 $emit 回掉父组件的函数修改
- 通过 .sync 修饰符方式 (父子组件是时刻也是必须保持一致)
.sync
一般用于简单数据处理 复杂数据用自定义事件接收,这里只是语法糖,业务逻辑还是要根据不同情况去添加其它逻辑代码 参考
<!-- 相当于对下面的代码进行语法糖,利用prop传递值给子组件 子组件通过emit修改父组件的数据 -->
<ChildComponent v-bind:someValue="sValue" v-on:update:count="sValue = $event" />
实际用法示例
<Parent>
<span>{{ sValue }}</span> <!-- 这里的值也会跟着一起修改 -->
<ChildComponent :someValue.sync="sValue" />
data () {
return { sValue: 1 }
}
</Parent>
// ChildComponent 组件中
props: {
someValue: {
type: Number,
default: 0
}
}
someClick() {
/* 执行后 父组件的 sValue 值会改变 */
this.$emit('update:someValue', this.someValue + 1) // 这里的update:someValue 必须与父组件定义的 prop 名称一致
}
插槽 slot
v-slot:name 简写 #name
<slot>这里的内容是默认内容,当父级有内容传入时,将会被替换</slot>
作用域插槽:
- 让父组件可以使用子组件插槽绑定的数据
- v-slot 只能添加在
<template>
上
v-slot:default 设置了后,如果使用非具名 slot 接收父组件传递的默认文本,不会被渲染到 slot 中 反而会将绑定了 v-slot:default 的template多渲染一次
定义 slot 组件
主要操作:
- 设置插槽名称
- 绑定作用域插槽数据
<template>
<div>
<slot >这是默认接收</slot>
<slot v-bind:data1="data1">这是默认作用域</slot>
<!-- 具名插槽 -->
<slot name="name"></slot>
<!-- 作用域插槽:让父级可以使用子插槽组件中的数据 -->
<slot name="scopedSlotData1" v-bind:data1="data1">
{{ data1 }}
</slot>
<slot name="scopedSlot" v-bind:data1="data1" v-bind:data2="data2">
{{ data1.name }}{{ data1.name }}
</slot>
</div>
</template>
<script>
export default { data () { data1: { name: 'warren1' } data2: { name: 'warren2' } } }
</script>
引用 slot
为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)
主要操作:
- 使用哪个插槽
- 是否使用子组件的数据
<template>
<div>
<MySlot>
<!-- 使用具名插槽 v-slot:name 简写 #name -->
<template v-slot:name>new name</template>
<!-- 作用域插槽简写 v-slot:default="data" 等同于 v-slot="data" -->
<template v-slot="data">
{{ data }}
<!-- 这里显示显示 { data1: { name: 'warren1' } } 没有具名的slot (即上面定义的第二个slot)-->
</template>
<template v-slot:scopedSlotData1="data">
{{ data }}
<!-- 这里显示显示 { data1: { name: 'warren1' } } -->
</template>
<template v-slot:scopedSlot="data">
<!-- 这里的 data 可以修改为任意名称 -->
<!-- 下面的 {{}} 将会被替换为 { "data1": { "name": "warren1" }, "data2": { "name": "warren2" } } -->
:<b>{{ data }}</b>
</template>
</MySlot>
</div>
</template>
<script>
import MySlot from './mySlot.vue'
export default { components: { MySlot }, data () { } }
</script>
Vue Test Utils
https://vue-test-utils.vuejs.org/zh/
mocha:对测试过程进行描述 (语句中的describe和it等方法)
chai:断言库,即各种判断(expect方法)
Karma: 一个启动浏览器运行测试并生成报告的测试运行器
sinon: 模拟与其它系统或函数对接,主要有三个方法 spy stub mock
cypress: Fast, easy and reliable testing for anything that runs in a browser.
vue-loader
在style中用别名引入scss文件时,如果报错,在别名前
~ :~@/asset/xxx
在scss中@是变量定义的前缀,所以需要加上~以示区分独立的 .scss 文件中引入图片,图片的相对地址是根据,引入该样式的 vue 文件的相对地址决定的
@import '@/x/x/x.scss'
; 引用文件时,不加后缀名可能导致编译错误深度作用域
.a >>> .b (实际使用中效果没有deep好)
.a /deep/ .b (容易报错)
.a ::v-deep .b (最新版,vue/cli 4.4.1,只有这个有效)
.parent :deep(.need) vue3 用法
深入响应式原理
- 非侵入性的响应式系统
- 数据模型为js对象,对其修改时,视图更新
如何追踪变化
vue将接收的data全部用Object.defineProperty把属性转为getter/setter(导致不支持ie8以及一下)
属性被访问和修改时通知变化
每个组件实例都有对应的watcher实例对象(它会在组件渲染的过程中把属性记录为依赖,当依赖的setter被调用时,会通知watcher重新计算,从而使相关组件更新)
检测变化的注意事项
只有在data对象上的属性才是响应式的
改变对象和数组的一些情况不会被检测到更新
要用到的状态,提前在data对象中声明
异步更新队列
vue router
路由的加载,管理,匹配都是 vueRouter 创建的实例属性 router.matcher 处理
vueRouter 的两个方法 match addRouter 都是 matcher 暴露的方法
$route.matched()
匹配当前页面路由所在的层级 比如地址是 /a/b/c b是a的child一级 c是b的child一级 那匹配的就是 a 和 a/b 和 a/b/c 的路由信息
beforeRouteUpdate 处理使用同一个组件页面 但是路由参数变化的情况 /:id 或 /?id=1
相关的路由钩子函数,必须只能在路由上挂载的组件中才有效,内部其它组件中无效
new Router({
/* 这里的键名必须是 routes */
routes: baseRouter
})
router-view 与路由表中的 children 有关
router.push 改为了 Promise
参数传递:
/* params 针对的是 定义的path形如 /xx/xx/:id */
// 注意如果 path是 /xx/xx/ 的话,刷新页面 传递的数据会掉
router.push({ name: 'user', params: { id: '123' }})
// path 和 params 同时设置 将无效, 实际使用时 { path: '/user:is', params: { id: '123' }} 看起来path也很怪异
router.push({ path: '/user', params: { id: '123' }})
// name 和 path 都可以和 query 一起用
// 定义的路由 path: '/user' 以下两种方式跳转后url都是: /user?id=123
router.push({ name: 'user', query: { id: '123' }})
router.push({ path: '/user', query: { id: '123' }})
- 导航守卫的钩子中
next() // 进行管道中的下一个钩子
next({ path: '/' }) // 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
// 以上两个的区别,前者表示,路由完成,显示对应页面;后者表示,需要开启一个新的路由过程,与跳转路由相似
keep-alive
- include 名称匹配的组件会被缓存,包含的是组件的name属性值
- 通过同一个 router-view 进入的路由间切换, keep-alive 都有效,都会缓存页面
- 只要通过keep-alive下的路由(前提是要包含在include中) ,每次都会触发activated, 只有第一次进入会触发mounted(切换过router-view入口 再进入也会触发mounted)
- 注意 include 如果用字符串值,后面名称与逗号之间不要有空格
- exclude 优先级更高
beforeRouteLeave (to, from, next) {
/** 这里主要是重置数据,当要去往的地址是新增、编辑、详情这些时,即可以不用重置数据 */
if (!['routeName'].includes(to.name)) { this.key = '数据重置了' }
next()
},
activated () { /** @des 这里更新表格数据等 */ },
created () { /** 这里请求不需要频繁更新的数据 */ },
处理保存页面状态
- 最好一个模块有一个单独的 router-view
- 在 activated 中请求需要实时更新的数据
- 在 beforeRouteLeave 中处理和当前需要保存状态页面走同一个 router-view 的页面,否则在这些页面间切换,页面的状态也会被保留(data中的数据)
使用 dart-sass
替换 node-sass; 安装 dart-sass sass sass-loader
启动后,如果报错 this.getOptions is not a function
将 sass-loader版本改为10.1.1 (/deep/ 和 >>>)
替换为 ::v-deep
// vue.config.js
css: {
loaderOptions: {
sass: {
implementation: require('sass'),
},
},
}
vuex
vue3 官方已经推荐 Pinia
实践使用参见
vue-admin/src/store
vuex esm. js sub is not function
报错 是vue浏览器插件问题,重启或关闭,升级可以解决
// state
this.$store.state
computed: {
...mapstate(['user']) // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
// ...mapgetters 同上
}
// getters 返回函数 可以获取参数 第二个参数为其它 getter
(state, getters) => (param) => {}
// mutation
(state, payload) => {}
// actions
({ state, commit, dispatch, rootState }, payload) => { }
// rootState 返回绑定的根节点下的所有modules
公用数据,在组件中获取store中数据,若store中没有则等待其从服务器拉去数据
async mounted () {
try {
/* actionGetSomeData 中有异步数据请求 */
await this.actionGetSomeData()
} catch (error) {
console.log(error)
}
/* 这里调用的方法需要等待store中的数据来改变界面显示 */
this.someMethodsUpdateView()
},
- Mutation 同步操作
// 用法一
this.$store.commit('name', payload) // 这里不用管是否 是提交的某个模块 commit 只要名称对应即可,所以 mutation 定义的名称要保证全局唯一
// 用法二
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment', payload)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment', payload)`
})
}
- Action 内部包含异步操作 异步原因:
- 区分 actions 和 mutations 为了能用 devtools 追踪状态变化
- 当用到相同的数据时,如果采用【异步获取数据+mutation】模式,会造成在很多地方写重复代码,就可以抽取到action中统一管理 还能防止重复发起http请求
如果用 Pinia 就没有这个问题了,它只提供了 actions
定义的action方法会被包裹一层,返回的是一个 Promise,所以触发 action 要在 then 中去接收返回值;
如果action中有数据请求,需要同步执行,必须有如下两种操作之一,推荐第二种:
/* 1. */
actionSome ({ commit }) {
return new Promise((resolve, reject) => { resolve() })
}
/* 2. */
async actionSome ({ commit }) {
try {
await requestSome()
} catch (error) {
}
}
this.$store.dispatch('xxx')
methods: {
...mapActions([ // 默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间, 除了加命名空间, 所以注意名称全局不重复
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment', payload)`
]),
...mapActions('模块名称', ['f1', 'f2']) // 使用了 namespace
...mapActions(['f1', 'f2']) // 没使用 namespace
}
namespaced: true
命名空间
用法 store.dispatch('模块名/actionSetUserInfo')
elementUI
表单重置
this.$refs.ruleForm.resetFields()
只会清除 新输入的数据 ;当在data上配置表单数据时设置了初始化值时,该默认值是不会被清除的,并且修改该默认值,重置后的值也是初始值当表单数据有多层嵌套对象时,在设置prop时要将 嵌套关系用字符串形式 赋值
初始化时给表单赋值,触发了校验,赋值后还是给出了错误提示,检查是否是在 created 生命周期中进行的赋值,如果不是请保证在 created 中进行初始化赋值
upload 组件 :accept="image/png, image/jpeg, image/jpg"
v-for 中使用 ref 注意 获取
this.$refs.name
返回的是数组
el-cascader
需要点击两次才能选中 (1) 其中一个原因是,多次赋值操作;解决办法为,保证只有在初始化时进行一次赋值操作 (2) 如果是对该组件进行了封装,且用 v-model 进行父子组件参数传递,且数据是异步,可以在 watch 监听时,判断 oldvalue 如果存在值 就不进行赋值操作,防止重复赋值 (3) 加个
emitPath: false
属性,便能搞定, 注意这样设置后 change事件返回的value 不是数组解决 tooltip 文字内容过多导致显示不正常, 注意这里是全局修改,可以给一个较大的 max-width 值
.el-popper.is-dark, .el-tooltip__popper.is-dark { max-width: calc(100vw - 300px); }
.el-popper__popper.is-dark { max-width: calc(100vw - 300px); }
select change 三种方式传参 后两种是传递了当前选择的值和额外添加的参数
<!-- option 的value直接传对象 -->
<el-select v-model="name" placeholder="xxx" @change="fn($event,i)">
<el-select v-model="name" placeholder="xxx" @change="(val) => fn(val,i)">
tree
- 没有采用 checkbox 需要设置选中且高亮 使用 setCurrentKey
有checkbox的情况 单选
<el-tree
ref="refDepTree"
:data="treeData"
check-strictly
node-key="id"
default-expand-all
highlight-current
:expand-on-click-node="false"
show-checkbox
:props="{ children: '', label: '', }"
@check="check"
/>
check (v) {
if (v.id === this.currentSelect) {
this.currentSelect = ''
} else {
this.$refs.refDepTree.setCheckedKeys([])
this.$nextTick(() => {
this.$refs.refDepTree.setCheckedKeys([v.id])
})
this.currentSelect = v.id
}
}
表格
表格设置 height 属性, 如果需要动态修改,初始值不能填 0 或 能够判断为 true 的值; 源码:
if (!el && (value || value === 0)) return Vue.nextTick(() => this.setHeight(value, prop));
动态表格列,需要用
template
来包裹
<template v-if="judge"> <el-table-column prop="prop" label="name"></el-table-column> </template>
在表格中使用 dropdown 的基础形式 也就是没有用 split-button 需要把 v-slot="scope" 加上
动态表格添加项时 一定要注意把字段加上,如果表格中需要的字段,在是初始化时没有指定,在修改其值后 页面不会及时更新显示,再次操作时才会显示
表格合并 注意被合并的单元格的起始单元格设置对应的行列合并即可,其他被合并的单元格应该将行列设置为0(隐藏这些单元格) rowspan是合并的行数:为1表示不变;为0表示去除该单元格,后面的单元格会向上往这格填; colspan同理
固定列时,加了自定义滑动条,固定列和旁边列可能会出现行错位的问题 element-plus 用的scrollbar组件所以不存在该问题
.el-table__fixed-body-wrapper .el-table__body {
padding-bottom: 10px; /* 这里的数值和设置的滑动条高度一致 */
}