父子组件的生命周期
父组件Home
,子组件List
- 挂载
Home beforeCreate -> Home created -> Home beforeMount -
-> List beforeCreate -> List created -> List beforeMount -> List mounted -
-> Home mounted
- 销毁
Home beforeDestroy -> List beforeDestroy -> List destroyed -> Home destroyed
路由参数解耦
this.$route.params.xxx
的用法会使组件与$route
高度耦合,从而使组件只能再某些特定URL
上使用,限制了其灵活性;
正确的做法应当是配置路由参数props
,达到解耦。它支持三种配置方式:
-
props: true
对应组件内可以通过props
接收params
参数。
// 动态路由的 :xxx 其实会作为 params 参数
const router = new VueRouter({
routes: [{
path: '/user/:id',
component: User,
props: true
}]
})
// 组件User
export default {
props: ['id'],
}
- 对象
routes: [{
// ...
props: { newsletterPopup: false }
}]
- 函数:回调的第一个参数就是
$route
routes: [{
// ...
props: (route) => ({
id: route.query.id
})
}]
面包屑
利用 this.$route.matched
可得到路由匹配的数组,按顺序解析可得到路由层次关系
// Breadcrumb.vue 面包屑组件
watch: {
$route() {
console.log(this.$route.matched)
}
}
自定义v-model
v-model
其实只是一个语法糖,默认会向组件传递一个名为 value
的props
,并注册一个名为 input
的事件。
这个 value
绑定的值就是 v-model
的值,input
事件用于修改v-model
绑定的变量值,从而形成双向绑定,也避免了props
的单向传递(子组件不能直接修改props
中的变量值)
<input type="text" v-model="price" />
# --> 等效于(语法糖):
<input type="text" :value="price" @input="price=$event.target.value">
当然,可以通过配置 model
属性,修改默认的value 、input
,通过 this.$emit('xxx', v)
手动触发事件。
在自定义组件上使用 v-model
- 父组件
<checkbox v-model="chkStatus" @change="onChangedListener" /> // 可以显示注册 change 事件,做一些额外处理 // 但 change 事件会优先修改 v-model 绑定的变量,不需要手动修改chkStatus
- 子组件
Checkbox .vue
<template> <input type="checkbox" v-bind:checked="state" v-on:change="setChange" /> </template> <script> export default { model: { prop: 'state', // 默认是 value event: 'change', }, props: { state: Boolean // 默认是value,model中修改为state }, methods: { setChange(evt) { this.$emit("change", evt.target.checked); } } }
$emit()
与 $on()
的本质是:谁监听,谁派发
<FormItem>
<KMInput />
</FormItem>
- 子组件
KMInput
this.$parent.$emit('xxx', xxx) //通过父组件派发事件
- 父组件
FormItem
mounted() { // 注册事件 this.$on('xxx', () => { // ... }) }
校验库:async-validator
,element-ui
也是用这个库
修饰符sync
sync
与 v-model
颇为相似,都是为了优雅而不粗鲁的实现父子组件间的双向绑定。但.sync
比 v-model
更灵活,一个组件上可以有多个.sync
,但只能有一个v-model
- 父组件:
:xxx.sync="myProp"
<Child :visible.sync="showDlog" />
- 子组件:
this.$emit('update:xxx', value);
<button @click="updeShow">更新</button> props: { visible: { type: Boolean, default: false } } updeShow() { this.$emit('update:visible',false); // 分发以 update: 为前缀的事件 }
综上可知,:xxx.sync="myProp"
其实就是 @update:xxx="value => myProp = value"
和 :xxx="myProp"
的语法糖。
典型应用:Dialog
对话框
函数式组件
函数式组件
函数式组件没有管理任何状态(响应式数据),没有实例(this
),也没有监听任何传递给它的状态,也没有生命周期方法。
实际上,它只是一个接受一些 prop 的函数。
组件标记为 functional
,因为是一个函数,所以渲染效率高于普通组件。
-
模板上标记
<template functional> </template>
-
属性标记
export default { name: 'MenuItem', functional: true, props: { icon: { type: String, default: '' }, title: { type: String, default: '' } }, render(h, context) { // 返回一个VNode const { icon, title } = context.props // 支持JSX语法 const vnodes = [] if (icon) { vnodes.push(<svg-icon icon-class={icon}/>) } if (title) { vnodes.push(<span slot='title'>{(title)}</span>) } return vnodes } }
样式穿透
在开发中修改第三方组件的样式时,但由于 scoped
属性的样式隔离,可能需要另起一个<style>
。但这种做法都会带来副作用(组件样式污染、不够优雅),样式穿透在css
预处理器中使用才生效。
- less
<style scoped lang="less"> .content /deep/ .el-button { height: 60px; } </style>
- scss
<style scoped lang="scss"> .content ::v-deep .el-button { height: 60px; } </style>
- stylus
<style scoped ang="stylus"> 外层 >>> .el-checkbox{ font-size: 20px; } </style>
watch
- 只监听对象的某个或一些属性,而不是用
deep: true
深度监听所有属性watch: { 'obj.a': { handler(newV, oldV) { console.log('obj.a changed'); }, immediate: true, // 立即执行一次handler方法 } }
- 让
handler
指向methods
中的方法watch: { studen: { handler: 'sayName', immediate: true // 创建组件后立即执行一次 } }, methods: { sayName() { console.log(this.studen) } }
- 触发多个监听方法,使用数组可以设置多项,形式包括字符串、函数、对象
watch: { name: [ 'sayName1', function(newVal, oldVal) { // ... }, { handler: 'sayName2', immaediate: true } ] }, methods: { sayName1() { console.log('sayName1==>', this.name) }, sayName2() { console.log('sayName2==>', this.name) } }
- 同时监听多个属性的变化:把多个变量包装成
compouted
compouted: { msgObj() { const { msg1, msg2 } = this return { msg1, msg2 } } }, watch: { msgObj: { handler(newVal, oldVal) { if (newVal.msg1 != oldVal.msg1) { console.log('msg1 is change') } if (newVal.msg2 != oldVal.msg2) { console.log('msg2 is change') } }, deep: true // 深度监听 } }
监听子组件的生命周期
- 通过
$emit
通知父组件// 子组件List export default { mounted() { this.$emit('listenMounted') } } // 父组件 <template> <div> <List @listenMounted="listenMounted" /> </div> </template>
- 更优雅的方式:使用
@hook:
即可监听子组件生命周期// 子组件无需做任何改变,父组件直接监听子组件的生命周期方法 <template> <List @hook:mounted="listenMounted" /> </template>
程序化的事件侦听器
以清除定时器为例:在mounted
中注册定时器,在beforeDestroy
中清除定时器;
可以使用$on('hook:')
或 $once('hook:')
来简化生命周期的注册。
export default {
mounted() {
// 连续注册两个定时器,无需额外变量,也不担心忘记清除
this.creatInterval('hello')
this.creatInterval('world')
},
creatInterval(msg) {
let timer = setInterval(() => {
console.log(msg)
}, 1000)
// 监听组件的销毁
this.$once('hook:beforeDestroy', () => {
clearInterval(timer)
})
}
}
mixins
mixins:混入,扩展组件的data
属性和方法。
- 也是为了实现代码逻辑复用
- 当多个组件中出现业务逻辑重复时我们就可以抽离重复代码片段,写成一个混入对象
- 父组件直接引入这个对象
slot
slot:插槽,分为普通插槽,具名插槽(对多个slot
进行命名),作用域插槽(父组件可以接收来自子组件的 slot
传递过来的参数值)。
- 该组件被多个地方使用
- 每个父组件中对该组件的内部有一部分需要特殊定制
-
slot
可以让更好地复用组件的同时并对其定制化处理 - 可以理解为父组件向子组件传递了一段
html
文本
要求:
1.子组件模板包含至少一个 插槽<slot></slot>
2.父组件整个内容片段将插入到slot
所在的DOM
位置,并替换掉slot
标签本身
provide / inject
- 父组件向所有子孙组件传递数据,不管孙组件有多深。
- 但却难以实现响应式,这是设计者刻意为之。
- 适合传递方法、初始化的值。
如果传递一个引用型数据,如数组arr
,只要父组件不主动修改 arr
的指向,而是通过 push、splice
这些方法去修改数组元素,孙组件是可以响应的!
把所有props传到子组件
<template>
<childComponent v-bind="$props" />
</template>
listeners
$attrs
与 $listeners
的主要应用是实现多层嵌套传递,爷孙组件
-
$attrs
父组件在使用子组件时,通常会在子组件上声明一些属性,而子组件会使用prop
接收这些属性。
子组件中的this.$attrs
就是在子组件上声明的静态属性(attr="xxx")
和动态属性(:attr="xxx")
的对象集合,其中不包括class 和 style
两个属性。
最重要的是,子组件可以通过v-bind="$attrs"
向下继续传递!这在父组件与孙组件通信时非常有用!
但如果子组件中声明了prop
,this.$attrs
将去除prop
中声明的属性。 -
$listeners
包含了父组件中的v-on (@xxx="xxx")
事件监听器,但不包括.native
修饰器的。它可以通过v-on="$listeners"
继续向下传给孙组件。
孙组件通过this.$emit('xxx', args)
可以直接给父组件发送事件。 -
inheritAttrs
默认情况下,父组件在子组件上声明的一系列属性,但没有被子组件的props
声明,这些属性会包含在this.$attrs
中,但同时也会作为普通字符串应用在子组件的根元素上。
inheritAttrs
应用在子组件中,默认为true
,设置为false
会去除默认行为,不再把this.$attrs
中的属性以普通字符串的形式应用在自己的根节点上!
注意:不会影响class
和style
属性
# 子组件:KMInput
<div>
<input :value="value" v-bind="$attrs" @input="$emit('input', e.target.value)">
</div>
export default {
inheritAttrs: false, // 根组件上不继承未被 props 声明的属性
props: {
value: {
type: String,
default: ''
}
}
}
# 父组件:
<KMInput v-model="pwd" type="password" class="test-km" />
# 映射到子组件-->
<div class="test-km">
<input :value="value" type="password" @input="$emit('input', e.target.value)">
</div>
Vue.use 与 Vue.component
它们都是用于注册全局组件/插件的,不同的是,Vue.component()
每次只能注册一个组件,功能很单一。
Vue.component('draggable', draggable)
-
Vue.use()
内部调用的仍是Vue.component()
去注册全局组件/插件,但它可以做更多事情,比如多次调用Vue.component()
一次性注册多个组件,还可以调用Vue.directive()、Vue.mixins()、Vue.prototype.xxx=xxx
等等,其第二个可选参数又可以传递一些数据。
Vue.use({
install:function (Vue, options) {
// 接收传递的参数: { name: 'My-Vue', age: 22 }
console.log(options.name, options.age)
Vue.directive('my-directive',{
inserted(el, binding, vnode) { }
})
Vue.mixin({
mounted() { }
})
Vue.component('draggable', draggable)
Vue.component('Tree', Tree)
}
}, { name: 'My-Vue', age: 22 })
$createElement
this.$createElement
方法用来创建和返回虚拟节点。例如,利用它在可以通过v-html
指令传递的方法中使用标记。
在函数组件中,此方法将作为渲染函数render
中的第一个参数进行访问。
JSX
Vue CLI 3
默认支持JSX
。使用JSX
可以很方便地编写函数式组件。如果尚未使用Vue CLI 3
,则可以使用插件babel-plugin-transform-vue-jsx
获得JSX
得支持。
require.context
一个Webpack
的API,通过 require.context()
获取一个特定的上下文(创建自己的context
),主要用来实现自动化导入模块。
它会遍历文件夹中的指定文件,然后自动化导入,而不需要每次都显式使用 import/require
语句导入模块!
在前端工程中,如果需要一个文件夹引入很多模块,则可以使用 require.context()
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
-
directory {String}
读取目录的路径 -
useSubdirectories {Boolean}
是否递归遍历子目录 -
regExp {RegExp}
匹配文件的正则
读取home
目录下的.vue
文件
const path = require("path");
const files = require.context("./home", false, /\.vue$/)
const modules = {}
files.keys().forEach(item => {
const name = path.basename(item, ".vue")
// .default 是因为Vue组件是通过 export default 导出的
modules[name] = files(item).default
})
// ---> 批量引入,或者全局挂载
export default {
components: {
...modules
},
不管是.vue
文件,还是 .js
、.json
文件,都是前端项目的一个模块,都可以通过API实现自动化导入。
require-context
context module
require.context
返回值是一个 require function
function webpackContext(req) {
return __webpack_require__(webpackContextResolve(req));
}
function webpackContextResolve(req) {
var id = map[req];
if(!(id + 1)) { // check for number or string
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return id;
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
}
这个function
有三个属性:resolve、keys、id
-
resolve
是一个函数,返回被解析后得到的模块id
-
keys
函数,返回一个数组,由所有可能被解析的req
对象组成;在批量导入文件时会很有帮助;const req = require.context('./svg', false, /\.svg$/) function requireAll (r) { r.keys().forEach(r); } requireAll(req)
-
id
上下文模块的ID
,在使用module.hot.accept
时会用到。
高精度全局权限处理
权限控制由前端处理时,通常使用
v-if / v-show
控制元素对不同权限的响应效果。这种情况下,就会导致很多不必要的重复代码,不容易维护,因此可以造一个小车轮,挂在全局上对权限进行处理。
// 注册全局自定义指令,对底层原生DOM操作
Vue.directive('permission', {
// inserted → 元素插入的时候
inserted(el, binding){
// 获取到 v-permission 的值
const { value } = binding
if(value) {
// 根据配置的权限,去当前用户的角色权限中校验
const hasPermission = checkPermission(value)
if(!hasPermission){
// 没有权限,则移除DOM元素
el.parentNode && el.parentNode.removeChild(el)
}
} else{
throw new Error(`need key! Like v-permission="['admin','editor']"`)
}
}
})
// --> 在组件中使用 v-permission
<button v-permission="['admin']">权限1</button>
<button v-permission="['admin', 'editor']">权限2</button>
小知识点
- data:组件的data必须是一个函数,因为一个组件可能被多次引用,它们之间的data应该是独立的,所以使用函数每次返回一个新的data
-
<component>:vue中的一个动态组件标签,其
v-bind:is
属性可以绑定不同的组件,实现动态切换组件。 -
vue-loader:
.vue
文件称为单文件组件,通过webpack
的一个loader --- vue-loader
将单文件组件转为JavaScript
模块。 默认支持ES2015
-
模板编译:
<template>
会被编译成AST
语法树,再经过generate
得到render
函数,其返回值为VNode --- Vue的虚拟DOM节点
-
parse过程
:将template
利用正则转化成AST
抽象语法树。 -
optimize过程
:标记静态节点,diff
过程跳过静态节点,提升性能。 -
generate过程
:生成render
字符串。
司徒大佬的文章: 前端模板的原理与实现
-
- Vue采用
Virtual DOM
的原因
一方面是出于对性能的考量:- 创建真实DOM的代价很高:真实DOM节点实现的属性有很多,而VNode仅仅实现一些必要的属性。
- 触发多次浏览器重绘及回流:使用VNode相当于加了一个缓冲,让一次数据变动所带来的所有Node变化,先在VNode中修改,再
diff
之后对所有产生差异的节点集中一次对DOM Tree
进行修改,以减少浏览器的重绘和回流。
然而,性能受场景的影响是非常大的,不同的场景可能造成不同实现方案之间成倍的性能差距,所以依赖细粒度绑定及
Virtual DOM
哪个性能更好不是一个容易下定论的问题。
更重要的是为了解耦HTML依赖,有两个好处:
1. 不再依赖HTML解析器进行模板解析,可以进行更多的AOT
工作提高运行时的效率:通过模板AOT
编译,Vue的运行时体积可以进一步压缩,运行时效率可以进一步提升。
2. 可以渲染到DOM以外的平台,实现SSR、同构渲染
这些高级特性,Weex
等框架应用的就是这一特性。
综上所述,Virtual DOM
在性能上的收益并不是最主要的,更重要的是,它使Vue具备了现代框架应用的高级特性。