Vue.js(组件)

一、简介

  组件是可复用的 Vue 实例,且带有一个名字通常一个应用会以一棵嵌套的组件树的形式来组织:

  例如:你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

二、创建

  Vue根实例表示1个应用,一个应用有若干个组件拼装而成,推荐命名方式:组件变量名: 大驼峰 组件名: xx-xx | 大驼峰

2.1 定义

  组件模板分为:外部模板和字符模板(行间模板|inline-template)

  • 字符模板
// 常用
let 组件变量名 = {        
    template: '<header>组件</header>'    
}; 

//不常用
let 组件变量名 = Vue.extend({
    template:  '<header>组件</header>'  
})
  • 外部模板:
<!--常用-->
<template id="header">
    <div class="header">HomeHeader</div>
</template>

<!--不常用(模仿angular.js)-->
<script type="x-template" id='header'>
    <div class="header">HomeHeader</div>
</script>

  用常用的写法举个例子:

<div id="app">  
    <my-header></my-header>
</div>
// 抽离出来的组件模板
<template id="header">  
    <header>头部组件</header>
</template>

<script src="vue.js"></script>
<script> 
    const MyHeader = {     
        template: '#header'  
    }  
    new Vue({    
        el: '#app',    
        components: {      
        'my-header': MyHeader    
        }  
     })
</script>

  页面运行结果

头部组件

【注】

  • 组件名不可和html同名
  • 组件没有el选项,只有根实例存在el
  • 组件的模板一定要有根元素

2.2 注册(拼装)

2.2.1 全局

  全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

Vue.component('组件名',组件变量名);

  例如:

<div id="app">        
    <div>#app</div>        
    <my-header></my-header>        
    <my-main></my-main>        
    <my-footer></my-footer>    
</div>    
<div id="root">        
    <div>#root</div>        
    <my-header></my-header>        
    <my-main></my-main>        
    <my-footer></my-footer>    
</div>

<script type="text/javascript" src="./vue.js"</script><script>    
    // 定义组件    
    const Header = {        
        template: '<header>全局头部组件</header>'    
    };    
    const Main = {
        template: '<main>局部主体组件</main>'    
    };   
    const Footer = {       
        template: '<footer>全局尾部组件</footer>'    
    };
    
    // 全局注册组件——头部&&尾部
    Vue.component('my-header',Header); 
    Vue.component('my-footer',Footer);  
    
    // 第一个实例    
    new Vue({        
        el: '#app',        
        // 局部注册组件——内容        
        components: {            
            'my-main': Main        
        }    
     });  
    // 第二个实例    
    new Vue({        
        el: '#root',    
    })
</script>

  页面运行结果

#app
全局头部组件
局部主体组件
全局尾部组件
#root
全局头部组件
全局尾部组件

  控制台运行结果

[Vue warn]: Unknown custom element: <my-main> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

(found in <Root>)

  会报错,说没有注册组件

2.2.2 局部

  用一个选项component || components去局部注册

components:{
    组件名:组件变量名
}

2.3 调用

  闭合式和非闭合式去调用均可

<组件名></组件名>
<组件名/>

2.4 综合性案例

<style>
    main{width:200px;height:200px;border:1px solid red;}     
    .banner{width:50px;height:50px;border:1px solid 
blue;margin:5px;}
    .nav{width:50px;height:50px;border:1px solid 
yellow;margin:5px;}    
</style>
<div id="app">        
    <my-header></my-header>        
    <my-main></my-main>        
    <my-footer></my-footer>    
</div>

<script type="text/javascript" src="./vue.js"</script><script>    
    // 定义组件    
    const Header = {        
        template: '<header>头部组件</header>'    
    };   
    // 如果在外层组件定义会找不到该组件   
    // Main-Banner    
    const Banner = {        
        template: '<div class="banner">轮播图</div>'
    };   
    // Main-Nav    
    const Nav = {        
        template: '<div class="nav">导航</div>'    
    };    
    const Main = {        
        template: '<main>
                        <my-banner></my-banner>
                        <my-nav></my-nav>
                    </main>',        
        components:{            
            'my-banner': Banner,            
            'my-nav': Nav        
        }    
    };    
    const Footer = {        
        template: '<footer>尾部组件</footer>'    
    };    
    new Vue({        
        el: '#app',        
        components: {            
            'my-header': Header,           
            'my-main': Main,            
            'my-footer': Footer        
        }    
    });
</script>

  页面运行结果:
在这里插入图片描述
  综上所述,我们能够搭配出好几种组件的创建方式,可能还会混用,这样可能会使得代码看起来变得很乱,既不美观也不好维护,所以说,我们需要一个更好的选择——单文件组件(.vue):一个文件中包含了它的结构(template),样式(style)和行为(script),也就是这样的格式(模块化环境下的写法,Vue-cli中会提到):

<template>
    
    ......

</template>

<script>

    ......

</script>

<style>

    ......

</style>

  记得该开始学习的时候,考虑到可维护性,要把样式和行为单独的抽离成文件来引入,虽然方便了管理,但是不管是文件数量的增加还是引入文件,不免都会有点不方便。但是如果这样设计的话,不只方便管理,而且还不需要引入操作

三、通讯(传值)

  一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝,否则组件复用时,数据相互影响

3.1 逐层传递

3.1.1 父→子(推荐)

被动——适用场景:通用组件(紧耦合):子组件通过$parent可以抓取到整个父组件

主动——适用场景:电商(松耦合):props(推荐)
(1)父组件在调用子组件的地方添加一个自定义的属性,属性的值就是你要传递给子组件的值,如果属性的值是number类型,boolean类型,或者是变量,需要使用到绑定属性
(2)在子组件定义的地方添加一个选项props,props的值可以为数组以及对象
如果是数组, 元素则为父组件中自定义的属性名
如果是对象(为了检测数据的正确性,只能起到提示作用,并不会阻止程序的正常运行,因为类型的检测本就不是JS的强项),对象的形式有两种写法

  • 写法一:验证传递数据的有效性 ---- 可能父子组件不是一个人写的,如果数据类型不对,会报出警告,可以及时调整
props: {             
    test: String,             
    count: Number,             
    flag: Boolean,             
    tip: String           
}
  • 写法二:既要验证数据的类型,又要设定属性的默认值,如果默认值是对象或者是数组,值为一个函数
props: {    
    test: {      
        type: String,           // 数据类型
        default: '测试数据了',       // 默认值
        required: true,              // 是否必传
        validator: function(value){
            return value == 0       // 是否符合某种自定义的规则
        }
    }  
}  

(3)子组件中通过{{ 属性名 }}就可以访问到传递的数据
props的值为数组:

<div id="app">
    <my-content test = "测试" :count="100" :flag="true" :tip="tip"/>
</div>
<template id="content">    
    <div>
        我这里是内容区域 --- {{ test }} -- {{ count }} --- {{ flag }} --- {{ tip }}    
    </div>
</template>
<script src="vue.js"></script>
<script>    
const Content = {        
    template: '#content',        
    props: ['test', 'count', 'flag', 'tip']   
}    
new Vue({        
    el: '#app',        
    data: {            
        tip: '提示'        
    },        
    components: {            
        'my-content': Content        
    }    
})
</script>

  页面运行结果:

我这里是内容区域 --- 测试 -- 100 --- true --- 提示

  props的值为对象(第一种):

<div id="app">
    <my-content test = "测试" :count="100" :flag="true" :tip="tip"/>
</div>
<template id="content">    
    <div>
        我这里是内容区域 --- {{ test }} -- {{ count }} --- {{ flag }} --- {{ tip }}    
    </div>
</template>
<script src="vue.js"></script>
<script>    
const Content = {    
    template: '#content',   
    props: {        
        test: String,        
        count: Number,        
        flag: Boolean,        
        tip: String    
    }
}
new Vue({        
    el: '#app',        
    data: {            
        tip: '提示'        
    },        
    components: {            
        'my-content': Content        
    }    
})
</script>

  页面运行结果:

我这里是内容区域 --- 测试 -- 100 --- true --- 提示

  props的值为对象(第二种):

<div id="app">
    <my-content :count="100" :flag="true" :tip="tip"/>
</div>
<template id="content">    
    <div>
        我这里是内容区域 --- {{ test }}  
    </div>
</template>
<script src="vue.js"></script>
<script>    
const Content = {    
    template: '#content',    
    props: {        
        test: {            
            type: String,            
            default: '测试数据了'
        }    
    }
}
new Vue({        
    el: '#app',        
    data: {            
        tip: '提示'        
    },        
    components: {            
        'my-content': Content        
    }    
})
</script>

  页面运行结果:

我这里是内容区域 --- 测试数据了

  单向下行绑定: 父级 prop 的更新会向下流动到子组件中,但是反过来则不行
  在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态

3.1.2 子→父

被动——适用场景:通用组件(紧耦合)

  • $ref 引用dom元素

  $ref虽然也是抓取DOM元素,但是比直接抓取DOM元素的性能要好,它的使用方式是:给要抓取的DOM元素添加一个ref属性,值为引用这个DOM元素时要用到的标识,然后通过this.$ref.标识就可以抓取到这个DOM元素,而this.$refs则可以抓取到所有添加了ref属性的DOM元素

  • $children 父抓子

  父组件通过$children可以抓取到整个子组件

主动——适用场景:电商(松耦合):自定义事件(推荐)
(1)父组件调用子组件的地方,给他绑定一个自定义的事件, 事件不要加()

<my-content @myevent="getData"/>

(2)在父组件选项methods中实现此事件,默认参数为你将从子组件得到的值

methods: {       
    getData (val) { // val为从子组件中获取到的值
        console.log(val)       
    }     
}

(3)在子组件中,可以是生命周期钩子函数,也可以是组件自己的事件去触发 父组件中的自定义事件

this.$emit('myevent', 10000)

3.1.3 兄弟→兄弟

  利用父子关系的各种方式就可以完成

3.1.4 爷→孙

<中间层组件 v-bind="$attrs" v-on="$listeners"></..>

  $attrs 传承父层所有属性,如果中间层组件没有接受props,给c的是所有props
  $listeners 传承父层所有事件,如果中间层组件没有触发,给C的是所有自定义事件
  选项:inheritAttrs 是否允许把prop的值作为dom元素属性

3.1.5 路由(vue-router)

  详情见Vue.js(全家桶)

3.2 集中式管理(临时)

  不像数据库,前端文件存储(Cookie,localstrage/session),后端文件存储(writeFile)这些永久存储方式,这些临时存储在刷新浏览器的时候都会被清空

3.2.1 订阅发布模式

第三方库pubsub

3.2.2 公共总线(vue)——送外卖

公共总线(先找到一个外卖小哥)

const bus = new Vue()

接收端(客户凭着凭证从外卖小哥那拿到外卖):

bus.$on('拿到外卖的凭证(手机尾号)'function (val) {})

发送端(然后餐馆把美食交给外卖小哥,并注明送给谁):

bus.$emit('拿到外卖的凭证(手机尾号)', val)

移除操作(交易完毕后移除此订单,否则客户可以凭借此凭证继续拿到外卖)

bus.$off('拿到外卖的凭证(手机尾号)')

  通过上述例子,应该对公共总线的使用有了一个大致的了解,下面用专业术语来对其进行一个描述:

  • vm.$on( event, callback )
    监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
  • vm.$emit( event, […args] )
    触发当前实例上的事件。附加参数都会传给监听器回调
  • vm.$off( [event, callback] )
    移除自定义事件监听器。
    • 如果没有提供参数,则移除所有的事件监听器;
    • 如果只提供了事件,则移除该事件所有的监听器;
    • 如果同时提供了事件与回调,则只移除这个回调的监听器。

演示案例:

<body>  
    <div id="app">    
        <my-list></my-list>
        <my-count></my-count>  
    </div>
</body>
<template id="list">
    <ul>    
        <li>111<button @click="add">+1</button></li>    
        <li>222<button @click="add">+1</button></li>  
    </ul>
</template>
<template id="count">  
    <div>    总量是:{{ num }}  </div>
</template>
<script src="vue.js"></script>
<script> 
const bus = new Vue()
const List = {  
    template: '#list',  
    methods: {    
        add () {      
            bus.$emit('count-event', 1)    
        }  
    }
}
const Count = {  
    template: '#count',  
    data () {    
        return {      
            num: 0    
        }  
    },  
    mounted () {
        bus.$on('count-event', (val) => { // 此处一定要注意this指向,可以使用 =>      
            this.num += val    
        })  
    }
}
new Vue({  
    el: '#app', 
    components: {    
        'my-list': List,    
        'my-count': Count  
    }
})
</script>

  运行结果:操作页面当中两个按钮都能改变下方总量的值,说明实现了组件间的传值。
【注】使用公共总线时,需要注意生命周期,为什么这么说,这里涉及到两个坑:

  • $emit时,必须已经$on,否则将无法监听到事件也就是说对组件是有一定的同时存在的要求的(可以理解为客户不点外卖,店家还怎么送,送给谁)
  • $on()的触发次数逐渐累加,需要使用$off来清除

  这两个坑看貌似很容易避开,但实际没那么简单,因为一旦牵扯到多个组件间的数据传递,生命周期钩子函数的执行顺序就会变复杂,所以在哪个钩子函数中接收和发送是需要小心处理的。

3.2.3 $root(vue)

  子组件内部直接找到根实例,访问,操作根data数据

this.$root == vm

3.2.4 状态管理

  详情请见Vue.js(全家桶)

四、动态组件(自身不会渲染)

  动态组件就是组件动态化(数据化),在不同组件之间进行动态切换,Vue提供了一个不会渲染的组件:component,其实之前我们也遇到过一个自身不会渲染的组件,那就是template

<component is="组件名"></component>
<component :is="'组件名'"></component>
<component :is="数据"></component>

  演示案例:

<template>  
    <div class="test-is-comp">    
        <h3>动态组件</h3>    
        <button @click=" com = 'is-comp-a' ">a组件</button>    
        <button @click=" com = 'is-comp-b' ">b组件</button>
        <component :is=" com "></component>  
    </div>
</template>
<script>
import IsCompA from './isCompA'
import IsCompB from './isCompB'
export default {  
    data () {    
        return {      
            com:'is-comp-a'    
        }
    },  
    components: {    
        'is-comp-a': IsCompA,    
        'is-comp-b': IsCompB  
    }
}
</script>

  实现了一个类似于选项卡的效果

五、缓存组件(自身不会渲染)

  有动态组件的地方,可能就少不了缓存组件,用来对组件进行缓存,从而节省性能,因为被切换掉的组件(非当前显示的组件)会被移除,如果把被切换的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以用keep-alive包裹需要缓存的组件,不会触发卸载挂载,但会触发activated/deactivated,keep-alive不给属性时,默认内部出现过得组件,都会被缓存,默认缓存第0个组件
【注】:初始情况下,vm.$children属性中只有一个元素(first组件),点击按钮切换后,vm.$children属性中有两个元素,再次切换后,则有三个元素(三个子组件都保留在内存中)。之后无论如何切换,将一直保持有三个元素。activated(活动组件)和deactivated(非活动组件)是组件钩子,在被缓存时起效果

5.1 匹配规则

  匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称(父组件 components 选项的键值)。匿名组件不能被匹配

5.2 属性

  • :include=[‘组件名’,‘组件名2’] 加入一部分
    组件名为字符串或正则表达式。只有名称匹配的组件会被缓存。
  • :exclude=[‘组件名’,‘组件名2’] 排除一部分
    组件名为字符串或正则表达式。只有名称匹配的组件不会被缓存。
  • :max = 数字
    最多可以缓存多少组件实例
      最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被激活的实例会被销毁掉(卸载挂载)。

5.3 钩子函数

  在被keep-alive包含的组件/路由中,会多出两个生命周期的钩子函数:activated 与 deactivated。当组件在 <keep-alive> 内被切换,它的activated和deactivated这两个生命周期钩子函数将会被对应执行。被包裹在keep-alive中的组件的状态将会被保留,例如我们将某个列表类组件内容滑动到第100条位置,然后切换到其他组件,之后再次切换回到该组件,该组件的位置状态依旧会保持在第100条列表处。

activated(在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用)

  • 第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给 next 的回调函数之前调用:
    beforeMount=> 如果你是从别的路由/组件进来(组件销毁destroyed/或离开缓存deactivated)=>mounted=> activated 进入缓存组件 => 执行 beforeRouteEnter回调

  • 因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子:
    // beforeCreate created beforeMount mounted 都不会触发。
    所以之后的调用时机是:
    组件销毁destroyed/或离开缓存deactivated => activated 进入当前缓存组件 => 执行 beforeRouteEnter回调
    // 组件缓存或销毁,嵌套组件的销毁和缓存也在这里触发

deactivated(组件被停用(离开路由)时调用)

  • 因为使用了keep-alive后组件没被销毁,被缓存起来了,所以组件被停用(离开路由)时不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁)。这个钩子可以看作beforeDestroy的替代,如果你缓存了组件,要在组件销毁的的时候做一些事情,你可以放在这个钩子里。如果你离开了路由,会依次触发:
  • 组件内的离开当前路由钩子beforeRouteLeave => 路由前置守卫 beforeEach =>全局后置钩子afterEach => deactivated 离开缓存组件 => activated 进入缓存组件(如果你进入的也是缓存路由)
  • 如果离开的组件没有缓存的话 beforeDestroy会替换deactivated 如果进入的路由也没有缓存的话 全局后置钩子afterEach=>销毁 destroyed=> beforeCreate等
activated(){
    this.$emit('pass-data', "组件被添加")
},
deactivated(){
    this.$emit('pass-data', "组件被移除")
}

  可用如上所示代码添加这2个生命期钩子函数,然后通过自定义事件的方式,向外发送事件通知。

六、插槽组件(自身不会渲染)

  • 组件内部保留槽位:
    具名槽位:
    匿名槽位:

  • 调用组件插入内容:
    <template #槽名>
    内容 | <组件名>

    <template v-slot:槽名>
    内容

  父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译

发布了24 篇原创文章 · 获赞 43 · 访问量 9795

猜你喜欢

转载自blog.csdn.net/weixin_37844113/article/details/101627811