VUE进阶知识
组件通信常用方式
-
props
-
event
-
vuex
边界情况
-
$parent
-
$root
-
$children
-
$refs
-
provide/inject
⾮prop特性
-
$attrs
-
$listeners
$parent/$root
兄弟组件之间通信可通过共同祖辈搭桥,$parent或$root。
// 兄弟组件1
sayBai() {
// this.$parent.$emit('handle', '我是老大');
this.$root.$emit('handle', '我是老大');
}
// 兄弟组件2
// this.$parent.$on('handle', message => {
this.$root.$on('handle', message => {
console.log(message);
})
但是这方式,存在耦合过高问题,因为一旦组件层级发生了改变,那么就会有问题,特别是$parent。
例如当我们自己封装组件时(自己封装表单),有嵌套关系,很多人会直接梭哈使用$parent,但是如果后面重构层级关系变了,那么很多逻辑都会改,导致很大麻烦。那么官方是怎么做的呢?
我们来查看下element中怎么做的呢:
// 广播: 从上到下派发事件
function broadcast(componentName, eventName, params) {
// componentName: 组件的componentName名
// eventName:事件名
// params: 参数,需要是一个数组
// 遍历所有的子组件:树形的向下遍历,只要名字相同,就都派发事件
this.$children.forEach(child => {
var name = child.$options.componentName;
// 如果子组件的componentName和传入的componentName名字相同,就派发事件
// 需要注意: 组件需要写componentName(和我们在组件中写的name相似)
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
// 从下到上派发事件(类似于冒泡)
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
// 向上查找,直到找到componentName和传入的componentName名字相同的组件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
// 如果找到,就派发事件
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
$children
⽗组件可以通过$children访问⼦组件实现⽗⼦通信。
// 在子组件2中,存在一个状态msg
// 父组件中
changeChildren1Msg() {
this.$children[1].msg='变';
}
注意:
- 组件中,$children只能访问到自定义组件。
- $children访问到的是一个数组,并且不能保证⼦元素顺序,例如:如果异步加载组件,那么顺序就会发生变化。
$attrs
当父组件传递数据到子组件时,如果没有在props中声明(声明了的就不能通过$attrs访问),那么就会被他们所绑定,通过 v-bind="$attrs" 传⼊到子组件,这在我们创建⾼级别的组件时⾮常有⽤。
// 父组件中
<Children2 name='pyy' />
// 子组件2中
<span>子组件2: {{msg}} - {{$attrs.name}}</span>
$listeners
例如当我们封装组件时,在组件中有个回调函数,但是这个回调函数设置是在父组件中设置的,在这个组件中只是负责触发它,不负责相应的实现逻辑,这个时候,我们就可以使用到$listeners。
// 在父组件中
<Children2 name='pyy' @click="onClickHandle" />
onClickHandle(){
console.log('父组件中: onClickHandle');
this.$children[2].msg='变变变';
}
// 在子组件中
<span v-on="$listeners">子组件2: {{msg}} - {{$attrs.name}}</span>
v-on="$listeners"
解析: $listeners本身是个对象(可以使用v-on展开),键值对的形式,键是父组件中所有事件监听器的名称,在这儿,父组件Children2上有个click事件,那么在子组件Children2中,$listeners中有个键就叫click,值就是父组件中设置的回调函数,也就是说,在子组件Children2中,这个span标签上有一个click事件=父组件中设置的回调函数。这样在子组件中不需要关心这个回调函数的处理,只需要绑定并触发它,在封装组件库时比较常用。
$refs
这是我们常用的获取⼦节点引⽤。
父组件中:
<Children2 ref="CR2" />
changeChildren1Msg() {
this.$children[1].msg='变'; // 只会找到第二个子组件修改它状态
this.$refs.CR2.msg = '变2';
},
provide/inject
provide/inject能够实现祖先和后代之间传值,当我们不使用vuex时,vue提供给了我们这种原生接口的方式来实现隔代传值。
例如:
// 在app.vue中:
provide(){ // 提供的意思
// 隔代传参,用法类似于data
return {
foo: 'foo',
}
},
// 在需要的后代组件中
inject: ['foo'], // 注入,注入需要的属性
注意:
-
如果传递的是基本数据类型,那么这种方式,不是响应式的。只有是引用数据类型-对象,并且这个对象是响应式的,那么传递下去的时候,才会是响应式的。
-
如果在子组件data中,已经声明了相同的属性,那么子组件中的属性才会生效(就近原则)。
-
如果在子组件data中,已经声明了相同的属性,那么怎么使用provide提供的属性呢?这个时候我们需要改造inject
inject: { foo1: 'foo' // 使用别名foo1 }
代码
// app.vue中
<template>
<div id="app">
<Father />
</div>
</template>
<script>
import Father from './components/Father.vue'
export default {
provide(){ // 提供的意思
// 隔代传参,用法类似于data
return {
foo: 'foo',
}
},
name: 'App',
components: {
Father
}
}
</script>
// Father.vue中
<template>
<div>
<span>父组件</span>
<Children1 />
<button @click="changeChildren1Msg">我是父组件按钮</button>
<Children2 name='pyy' @click="onClickHandle" />
<Children2 ref="CR2" />
</div>
</template>
<script>
import Children1 from "./Children1";
import Children2 from "./Children2";
export default {
methods: {
changeChildren1Msg() {
this.$children[1].msg='变'; // 只会找到第二个子组件修改它状态
this.$refs.CR2.msg = '变2';
},
onClickHandle(){
console.log('父组件中: onClickHandle');
this.$children[2].msg='变变变';
}
},
components: {
Children1,
Children2
}
};
</script>
// Children1.vue中
<template>
<div>
<button @click="sayBai">子组件1</button>
</div>
</template>
<script>
export default {
methods: {
sayBai() {
// this.$parent.$emit('handle', '我是老大');
this.$root.$emit('handle', '我是老大');
}
},
mounted () {
// this.$parent.$on('handle', message => {
this.$root.$on('handle', message => {
console.log(message);
})
},
}
</script>
// Children2.vue中
<template>
<div>
<!-- $listeners/$attrs -->
<span v-on="$listeners">子组件2: {{msg}} - {{$attrs.name}}</span>
<!-- provide/inject -->
<span>------{{foo}} </span>
</div>
</template>
<script>
export default {
// inject: ['foo'], // 注入,注入需要的属性
inject: {
foo: 'foo'
},
data() {
return {
msg: 'msg',
}
},
// 监听事件
mounted () {
// this.$parent.$on('handle', message => {
this.$root.$on('handle', message => {
console.log(message);
})
},
}
</script>
过滤器filters
作用: 过滤处理数据的格式,可被用于一些常见的文本格式化。
使用场景:过滤器可以用在两个地方:双花括号插值和v-bind表达式,注意过滤器要被添加在表达式的尾部,由“管道”符号|
表示。
语法:
<!-- 在双花括号中 -->
<div>{{ msg | 函数名 }}</div>
<!-- 在 `v-bind` 中 -->
<div v-bind:id="msg | 函数名"></div>
// 过滤器
filters: {
函数名(msg) {
return 过滤结果
}
}
例如:
<div id="app">
<ul>
<li v-for='item of goodList'>
<!-- 不使用过滤器时这么写,但是货币符号是固定的 -->
<!-- {{item.name}} - ${{item.price}} -->
{{item.name}} - {{item.price | symbol}}
</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
}
},
filters: {
symbol: function(value) {
return '$' + value;
}
}
});
把上面的例子稍微改造一下,符号可以动态传递而不是写死的。
<div id="app">
<ul>
<li v-for='item of goodList'>
<!-- 和方法一样调用,传递参数 -->
{{item.name}} - {{item.price | symbol('¥')}}
</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
}
},
filters: {
symbol: function(value, sym = '$') { // 第一个参数,理解为上面的item.price 第二个参数,就是('¥')中传递过来的符号, 为了容错处理 默认值给个$
return sym + value;
}
}
});
</script>
自定义指令
除了核心功能默认内置的指令 ,Vue 允许注册自定义指令。有的情况下,仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
例如官方输入框自动获取焦点例子:
<div id="app">
<input type="text" v-focus>
</div>
<script src="vue.js"></script>
<script>
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
// binding很重要,详细信息参看官网
inserted: function (el, binding) {
// 聚焦元素
el.focus()
}
});
new Vue({
el: '#app',
});
</script>
然后我们再来自定义做一个,根据当前登陆用户级别,来做权限设置。
<div id="app">
<input type="text" v-focus>
<!-- 特别需要注意: 指令里,""中是表达式,如果需要传递字符串,则需要加上字符串 -->
<button v-permission="'superAdmin'">删除</button>
</div>
<script src="vue.js"></script>
<script>
// 假设当前登陆用户是会员
const user = 'member';
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
// binding很重要,详细信息参看官网
inserted: function (el, binding) {
// 聚焦元素
el.focus()
}
});
// 第一个参数: 指令名,注意使用时要加上v-
// 第二个参数: 配置项
Vue.directive('permission', {
inserted: function (el, binding) {
console.log(binding);
// 若指定用户角色和当前用户角色不匹配则删除当前指令绑定的元素
if (user !== binding.value) {
el.parentElement.removeChild(el)
}
}
});
new Vue({
el: '#app',
});
</script>
渲染函数
Vue 推荐在绝大多数情况下使用模板来创建HTML。然而在一些场景中,真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
基础:
render: function (createElement) {
// createElement函数返回结果是VNode(虚拟DOM)
return createElement( // 接收三个参数
tagname, // 标签名称
data, // 传递数据
children // 子节点数组
)
}
基于官网的例子:
<div id="app">
<!-- 用render实现一个组件 : 实现标题 -->
<!-- level是指需要生成h1-6哪一个标签 -->
<my-head :level='1' :title='title'>{{title}}</my-head>
<my-head :level='3' :title='title'>我是另一个我</my-head>
<!-- <h2 :title='title'>
{{title}}
</h2> -->
</div>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
props: ['level', 'title'],
// render函数接收一个 createElement参数,我们一般简写为h h === createElement
// 因为Vdom底层的算法是snabbdom算法,这个算法里面生成虚拟dom的方法名就叫h
render(h){
// 注意这儿一定要有return, return出createElement返回的Vnode。
return h(
'h'+this.level, // 参数1:标签名字
{attr: { title: this.title }},// 参数2
this.$slots.default, // 参数3: 子节点数组(虚拟节点) 标签之间的内容,需要使用默认插槽来获取
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
然后我们再来进阶来试一试:
当用户使用组件时,
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
我们希望渲染成:
<!-- 阿里矢量图使用方式 -->
<h1 :title='title'>
<svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
{{title}}
</h1>
最终代码为:
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
<!-- <h3 :title='title'>
<svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
{{title}}
</h3> -->
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
props: ['level', 'title', 'icon'],
render(h){
let children = [];
// 思路: 第一步,把用户传入的icon,生成<svg class="icon"><use xlink:href="#icon-icon名"></use></svg>添加到children数组中
// 第二步: 把默认插槽内容this.$slots.default放到children数组中
// 第三步:h函数参数3就替换为children数组
// 第一步: 生成svg,添加图标 同样是调用h函数生成
const svgVnode = h(
'svg',
{ class: 'icon' }, // 添加固定类名为icon 详见官网createElement参数2
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${this.icon}_C_`}})] // 参数3: 子节点数组(虚拟节点),svg还有个子级use,所以再调用h方法生成use,需要注意的是需要是数组,所以将返回的vnode放到一个数组中
);
children = [svgVnode, ...this.$slots.default];
return h(
'h'+this.level, // 参数1:标签名字
{attrs: { title: this.title }},// 参数2
children, // 参数3: 子节点数组(虚拟节点) 标签之间的内容,需要使用默认插槽来获取
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
模板语法是如何实现的
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
之前的例子中原本代码如下:
<!-- 宿主容器(根节点) -->
<div id="app">
<ul>
<!-- class绑定 -->
<li v-for="item in goodList"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
<!-- style绑定 -->
<!-- <li v-for="item in goodList"
:style="{backgroundColor: (selected === item)?'#ddd':'transparent'}" @click="selectedCourse = item">{{item}}</li> -->
</ul>
</div>
<script src="vueJs所在路径"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
selected: ''
}
},
});
</script>
然后我们去输出vue替我们生成的渲染函数 :
执行代码: console.log(vm.$options.render)
我们看到输出信息:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
})
然后我们基于这一个点,改写为渲染函数版本。
<!-- 宿主容器(根节点) -->
<div id="app"></div>
// 创建vue实例
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
selected: ''
}
},
methods: {},
render() {
with(this){
return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
}
})
我们可以看到,结果是一样的。
结论:Vue通过它的编译器将模板编译成渲染函数,在数据发生变化的时候再次执行渲染函数,通过对比两次执行结果得出要做的dom操作,模板中的神奇魔法得以实现。
函数式组件
没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。
修改上一个例子为函数式组件:
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
functional: true, // 1. functional设置为true,标示是函数式组件
props: ['level', 'title', 'icon'],
// 在函数式组件中,没有this
// 所以render函数,提供第二个参数作为上下文
render(h, context){
// 之前从this上拿取'level', 'title', 'icon',就要变化了
// 2. 从context.props上去拿取
const { level, title, icon } = context.props;
let children = [];
const svgVnode = h(
'svg',
{ class: 'icon' },
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})]
);
// 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
children = [svgVnode, ...context.children];
return h(
'h'+level,
{attrs: { title: title }},
children,
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
// 定义一个混入对象
let myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
Vue.component('mycomponent', { mixins: [myMixin] })
插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一
个可选的选项对象.
const MyPlugin = {
install (Vue, options) {
Vue.component('my-head', {...})
}
}
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(MyPlugin)
}
例如把上面的标题组件,封装成插件。
首先新建个js文件,存放插件代码:
const MyPlugin = {
// 插件需要install方法
install (Vue, options) {
Vue.component('my-head',{
functional: true, // 1. functional设置为true,标示是函数式组件
props: ['level', 'title', 'icon'],
// 在函数式组件中,没有this
// 所以render函数,提供第二个参数作为上下文
render(h, context){
// 之前从this上拿取'level', 'title', 'icon',就要变化了
// 2. 从context.props上去拿取
const { level, title, icon } = context.props;
let children = [];
const svgVnode = h(
'svg',
{ class: 'icon' },
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})]
);
// 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
children = [svgVnode, ...context.children];
return h(
'h'+level,
{attrs: { title: title }},
children,
)
}
});
}
}
// 判断当前环境 并且判断是否已经存在Vue
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(MyPlugin);
}
然后在页面上,直接使用插件即可。
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script src="./plugins/head.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>