目录
十、非父子组件通信刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
一、组件化基本概念
什么是组件化?组件化有什么作用?
人面对复杂问题的处理方式:
一个人处理信息的逻辑能力都是有限的,所以,当遇到一个非常复杂的问题时,我们不能一次性解决的情况下,就需要将问题进行拆分成一个个小一些的问题。通过解决这些拆分后的小问题,再将其合并,你会发现大的问题也会迎刃而解。
组件化也是类似的思想:
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且该页面的代码量以及可阅读性都会大大减小,对于后续的管理、扩展、更新等 都有很大影响。但如果,我们把一个页面拆分成多个很小的功能块,每个功能块仅仅只是完成属于自己这部分独立的功能,那么之后我们在管理维护时,就只需要找对应的功能模块即可。整个页面的管理和维护就变得非常容易了。
Vue组件化思想:
组件化是Vue.js中的重要思想 。它提供了一种抽象,让我们将一个完整的应用变成一个个独立的可复用的小的组件。如果把完整的应用当作棵大树,那么这些小组件就是大树上的每个树枝,而我们的大树枝也可以继续拆分为一些小树枝(个人理解下,我们可以把最下面的那层组件当作叶子,成为树根的组成部分)还有一个根组件是我们的树根,用来联系这些子组件,最终就会形成一个组件树。所有应用都可以都会被抽象成一棵组件树。
组件化思想的应用:
有了组件化的思想,而且组件化思想对于我们的开发是及其有利的,因此,我们在开发中要充分利用组件化的思想。尽可能的将页面拆分成一个个小的,可复用的组件。这样我们的代码更加方便组织和管理,并且扩展性更强
二.组件的基本使用:
2.1.创建组件的构造器。
调用Vue.extend()方法创建组件构造器
Vue.extend():
1.调用Vue.extend()创建的是一个组件构造器。
2.通常在创建组件构造器时,传入template代表我们自定义组件的模板。该模板就是在使用到组件的地方,要显示的HTML代码。事实上,这种写法在Vue2.x的文芳中,几乎已经看达不到了,他会直接使用下面我们讲到的语法糖。但是在很多资料中,还是会提到这种方式
注意:
我们在创建组件构造器时,填写的template模板,其中需要使用我们键盘左上角的 ~ 键来将其包裹,而不是我们通常使用的 ' '
const cpnC = Vue.extend({
template:
`
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈哈</p>
<p>我是内容,呵呵呵呵</p>
</div>`
})
2.2.注册组件。
调用Vue.component()方法注册组件
Vue.component():
1.调用Vue.component()是将刚才创建的组件构造器注册一下,并且给它起一个组件的标签名称。我觉得我们可以这样理解,我们所谓的注册就是给这个组件起一个标签化的名字,是不是有思路了?没错,我觉得这其实就是我们的创建自定义HTML标签,而这个HTML标签代表的就是我们的组件里面的模板内容。通俗的说,如果我们不注册,我们创建的组件在要使用它的组件的信息中就相当于一个没有身份证的人,类似于没有中国国籍的美国人,在中国的公民信息中是不存在的,既然都不存在,使用也就无从说起。也就是说,我们创建组件创建是为了让其他组件也可以使用我们创建出来的组件,但是如果没有注册的话,意味着当其他组件想要使用我们创建的组件的时候,找不到我们创建的组件。这样我们创建组件就并无意义了,因此我们需要注册一个名字来代表我们创建的组件,使得其他组件也可以通过这个名字来使用我们创建的组件。
2.两个参数:1.注册组件的标签名 2.组件构造器
3.组件注册分为全局注册和局部注册两种
全局注册:意味着可以在多个Vue实例下面使用,该方式注册的组件就被称为全局组件
局部注册:局部注册,实质上就是在一个组件内部通过components来注册,此时我们注册的组件只在本组件使用,在别的组件中不能使用,该方式注册的组件就被称为是局部组件
//全局注册
Vue.component('my-cpn', cpnC)
const app = new Vue({
el: "#app",
data: {
message: "你好啊"
},
components: {
//局部注册:cpn是使用组件时的标签名
cpn: cpnC
}
})
2.3.使用组件。
在Vue实例的作用范围内使用组件
注意:
组件必须挂载在某个Vue实例下,否则它不会生效
<div id="app">
<!--3.使用组件-->
<my-cpn></my-cpn>
</div>
<my-cpn></my-cpn>//该组件不会被渲染,这是因为它是在挂载元素外部使用的
三、父组件和子组件
组件与组件之间存在层级关系
其中一种非常重要的关系就是父子组件的关系
Vue实例就可以看作是一个最大的根组件!!!!!!!!!!
父组件和子组件实际上就是在创建父组件组件构造器时通过components将子组件注册。此时,我们的父组件在创建模板的时候就可以直接使用子组件的模板来构建。
注意:
这样注册的组件是局部组件,不能在外面调用,也就是说,我们这样创建的子组件是不能直接在组件外面使用,只能在父组件中使用,除非我们在外部组件也注册了这个子组件模板。
//1.创建第一个组件
const cpnC1 = Vue.extend({
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容,哈哈哈哈</p>
</div>`
})
//2.直接创建第二个组件构造器(父组件)->render
const cpnC2 = Vue.extend({
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容,呵呵呵呵</p>
<cpn1></cpn1>
</div>`
,
components: {
cpn1: cpnC1
}
})
四、注册组件的语法糖写法
在Vue中,提供了注册组件的语法糖,可以简化我们组件的使用方式,主要是通过省去调用Vue.extend()的步骤,直接使用一个对象代替,我们可以在这个对象中编写我们的template模板。
通俗的讲,其实就是将我们的创建组件构造器这一步取消掉,将模板直接写到我们的第二步原来的组件构造器名称那里,再用{}包裹起来
1.语法糖全局注册
Vue.component('cpn1', {
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容,哈哈哈哈</p>
</div>`
})
2.语法糖局部注册
components: {
'cpn2': {
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容,呵呵呵呵</p>
</div>`
}
}
五、抽离模板
我们通过语法糖的形式讲我们的vue组件的第一步和第二步合在一起,但是这里我们的template模板在编写时仍然不符合我们平时的编程习惯。所以我们可以使用我们的抽离写法,将这个模板抽离出来编写,然后再把它挂载到对应的组件中去
5.1.模板编写方法
5.1.1使用script标签
直接使用<script>标签,将我们的模板写入到一个<script>标签中,并且把这个<script>标签的type设置text/x-template类型,最后别忘了给<script>标签添加一个id属性来作为名称(这里的名称可以代指为组件构造器中的模板【模板就是 ~ 键包裹的内容】的名称),此时<script>标签中的内容就是模板内容。
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>哈哈哈哈哈哈哈哈哈哈</p>
</div>
</script>
5.1.2.使用template标签
也可以使用<template>标签,我们的模板写入到一个<template>标签中,再给<template>标签添加一个id属性来作为名称(这里的名称可以代指为组件构造器中模板【模板就是 ~ 键包裹的内容】的名称),此时<template>标签中的内容就是模板内容。
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>呵呵呵呵呵</p>
</div>
</template>
5.2.注册组件
此时我们可以通过注册组件的方式来将组件注册为全局组件或局部组件,值得注意的是,我们只是将模板内容抽离出来了,因此,我们此时的id指的就是模板的内容。相应地,我们的注册组件的方式并不是像是基本使用方式一样,直接将标签中的id当作第二个参数。而是保持template不变,将后面的 ~ 包裹的内容替换成标签中的id(选择器)
5.2.1.抽离模板的全局注册方式
Vue.component('cpn', {
template: `#cpn`
})
5.2.2.抽离模板的局部注册方式
components: {
'cpn': {
template: `#cpn`
}
}
5.3.基于抽离的再次简化
我们也可以将我们创建的模板放到一个对象中,并通过一个变量来保存这个对象。这里的变量名就相当于我们组件的基本用法中第一步的组件构造器的名称。而且,我们的组件有时候也需要一些数据,这时我们就可以在这个对象中编写。此时,我们在注册组件的时候,我们的template就可以被省略,而是直接写成存储这个对象的变量名
const cpn = {
template: `#cpn`,
props: {
},
data() {
return {
}
}
}
const app = new Vue({
el: "#app",
components: {
'cpn': cpn,//普通写法
cpn//增强写法
}
})
六、组件可以访问Vue实例数据吗?
1.组件实质上就是一个单独的组件,是一个单独功能模块的封装。和根组件Vue实例一样,一个组件不仅仅有属于自己的HTML模板,同时也应该有属于自己的数据data等
2.组件中的数据是保存在哪里?顶层的Vue实例中吗?
组件对象也有一个data属性(也可以有,methods等属性)。只是这个data属性必须是一个函数,而且这个函数返回一个对象,对象内部保存的是我们需要数据。
Vue.component('cpn', {
template: `#cpn`,
data() {
return{
title: 'abc',
}
}
})
七、为什么组件中的data必须是一个函数?
首先,data中存放的其实是内存地址,而一个对象只有一个内存地址与他对应。
因此,当我们的data不是一个函数,而是一个对象时,那么当我们多次使用同一个组件时,这些组件本质上都是使用的同一个数据,而我们多次调用的也就是同一个组件。
反之,当我们的data是一个函数时,我们调用组件时则是通过这个函数来创建一个新的对象,因此虽然我们用了同一个组件,但是每个相同组件的data实际上内存地址不同,不会互相干扰 。
注意:
如果我们希望每个组件都使用同一个对象时,我们可以在外面创建一个函数用来返回我们所需要的值,再将这个值当作组件data的返回值。这样,我们的组件中的data实际上保存的是外面那个函数的返回值所在的内存地址。
在下面的例子中,我们可以把function当作我们的data
//采用这种方式,我们的obj1.obj2和obj3指向的都是函数的返回值所在的内存空间,
//修改其中一个的值,其他的也会跟着改变。
const obj = {
name: 'why',
age: 18
}
function abc() {
return obj;
}
let obj1 = abc();
let obj2 = abc();
let obj3 = abc();
obj1.name = 'kobe';
console.log(obj2);
console.log(obj3);
//采用这种方式时,我们发现,当我们修改obj1的name时,obj2和obj3不会跟着发生改变。
//这是因为,我们此时的pbj1,obj2和obj3实际上每次调用函数时,是创建的属于自己的对象,
//有自己的内存空间,因此可以做到互不打扰
function abc() {
return {
name: 'why',
age: 18
}
}
八、组件通信
8.1.组件通信的概念
子组件是不能引用父组件或者Vue实例的数据的。但是,如果我们各个组件自发地请求自己需要的数据,那么我们每个组件都需要进行网络请求,这会极大消耗性能,对服务器也会有很大的压力。而如果我们通过父组件请求大量数据,子组件向父组件请求自己需要的数据并将数据进行展示,那么我们的网络请求的次数就会大大减少;相应的,如果用户在子组件界面修改了数据,为了保持数据一致,我们子组件需要将数据发送到服务器,再有父组件请求数据进行数据更新,这样无疑是性能很差的,更别说,用户修改数据较为频繁的情况了。因此,我们需要让数据可以在父组件和子组件之间进行传递,这就是组件通信。
8.2.父子组件通信实现方式
组件通信的实现分为两部分:父传子和子传父
8.2.1.父组件向子组件传递
1.子组件通过props向父组件传递消息(props->properties【属性】)
在组件中,使用选项props来声明需要从父级接收到的数据。通俗的讲,就是子组件通过props告诉父组件自己需要哪些数据。
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称
props: ['cmovies', 'cmessage']
方式二:对象,对象可以设置传递时的类型,这样在使用对象语法时,就可以进行类型验证。也可以设置默认值等。
props: {
//基础的类型检查('null'匹配任何类型)
propA:Number,
//多个可能的类型
propB:[String, Number],
//必填的字符串
propC: {
type: String,
required: true
},
//带默认值的数字
propD: {
type: Number,
default: 100
},
//带默认值的对象
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
//自定义验证函数
propF: {
validator: function (value) {
//这个值必须可以匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
},
},
2.父组件将子组件需要的数据传递给子组件
父组件接受到请求后,通过v-bind来把子组件需要的数据发送给子组件。
注意:v-bind本身时不支持驼峰写法的,所以我们在需要驼峰写法的地方应该换一种方式,比如,itemClick换成item-click
<cpn :cmovies="movies" :cmessage="message"></cpn>
一个完整的子传父请求案例
//父组件使用子组件
<div id="app">
//通过v-bind将数据传入到子组件中
<cpn :cmovies="movies" :cmessage="message"></cpn>
<!--<cpn :cmovies="movies"></cpn>-->
</div>
//子组件模板
<template id="cpn">
<div>
<!--<p>{{cmovies}}</p>-->
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script src="../vue.js"></script>
<script>
//父传子:props
const cpn = {
template: `#cpn`,
//使用props来向父组件请求数据
//方式一:数组形式props
//props: ['cmovies', 'cmessage'],
//方式二:对象形式props
props: {
//1.类型的限制
//这种方式在输入变量名称的同时还能指定其类型
//cmovies: Array,
//cmessage: String,
//2.还可以提供一些默认值
cmessage: {
type: String,
default: 'aaaaaa',//默认值
//当某一个属性具有required时,我们在传递时就必须传递这个值
required: true
},
cmovies: {
type: Array,
//Vue2.5.17以下默认值这样写才可使用,以上就会报错(经过测试,现在已经消除了这个问题)
//default: [],
//2.5.17以上则需要将其写成函数形式才可以
/*default() {
return []
}*/
}
},
}
// 父组件
const app = new Vue({
el: "#app",
data: {
message: "你好啊",
movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
cpn
}
})
</script>
8.2.2.子组件向父组件通信
在子组件向父组件传递数据时,我们通常采用自定义事件的方式向父组件传递数据。此处也涉及到我们的v-on的另一种监听:监听组件之间的自定义事件。
自定义事件的流程:
首先,我们需要在子组件中通过$emit自定义事件来向父组件发出事件;
然后,我们在父组件中通过v-on来监听这个子组件事件。
1.通过自定义事件向父组件发送消息
//子组件模板,设置点击事件,当该事件被触发时,将自定义事件发给父组件
<template id="cpn">
<div>
<button v-for="item in categories"
@click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
//在子组件对象中添加发出自定义事件的方法
methods: {
btnClick(item) {
//发射事件:自定义事件
this.$emit('item-click', item);
}
}
2.父组件通过v-on来接受子组件传过来的事件并进行操作
//父组件模板:父组件在监听到子组件传出的自定义事件后,调用cpnClick方法
<div id="app">
//通过v-on来接受子组件发出的自定义事件
<cpn @item-click="cpnClick"></cpn>
</div>
//在父组件中定义接收到事件后要执行的方法
methods: {
cpnClick(item) {
console.log('btnClick', item);
}
}
九、组件访问
有时候我们需要父组件去直接访问子组件的某一个数据或是子组件去直接访问父组件的某一个数据。这就是组件访问
组件访问也分为两种情况,父访问子和子访问父
9.1.父组件访问子组件
父组件访问子组件,可以通过$children或者$refs(reference引用)来访问
$children
1.我们可以在父组件中直接使用this.$children来访问子组件,他会返回一个数组类型,其中包含所有的子组件,我们可以通过下标的方式来精准找到某一个子组件,并使用其中的数据、方法等;
this.$children[0].showMessage();
2.也可以通过遍历来使用每一个子组件中的数据、方法等
for (let c of this.$children){
console.log(c.name);
c.showMessage();
}
可以发现,当我们通过$children来访问子组件中的数据的时候,如果我们的子组件发生改变,那么我们所有使用下标方式访问数据的地方的下标都要做出相应的改变,因此,$children这种方式不够灵活。
$refs
相较而言,$refs更为灵活。使用$refs则是需要在子组件中添加一个ref属性,比如ref ="aaa",此时,我们就可以通过this.$refs.aaa来访问到这个子组件中的数据、方法等。
//子组件添加ref属性
<cpn ref="aaa"></cpn>
//父组件通过this.$refs调用子组件中的属性、方法、数据等
console.log(this.$refs.aaa.name);
9.1.子组件访问父组件
子组件访问父组件,可以通过$parent来访问,如果是访问根组件时,也可以通过$root来访问。
注意:
此时我不禁有一个深思,那就是如果一个组件是全局组件注册的,且这个组件中有一个调用父组件的情况,如果有两个组件都使用了这个组件,那么父组件是指的哪一个?
经过思考测试,我发现,即使有两个组件调用了该组件,那么对于两个父组件而言,他们的子组件是独立的,并不互相影响。
$parent
//因为子组件的父组件是确定的,只有一个,因此直接通过this.$parent调用即可
console.log(this.$parent);
console.log(this.$parent.name);
$root
//因为根组件只有一个,因此直接通过this.$root调用即可
console.log(this.$root);
console.log(this.$root.message);
十、非父子组件通信刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。那么二者之间时如何完成组件通信的呢?
在Vue1.x的时候,可以通过$dispatch和$broadcast完成。$dispatch用于向上级派发事件;$broadcast用于向下级广播事件。
在Vue2.x中取消了这两种都被取消了。在Vue2.x中,有一种方案是通过中央事件总线,也就是一个中介来完成。但是这种方案和直接使用Vuex的状态管理方案还是逊色很多。并且Vuex提供了更多好用的功能。因此我们后续通过vuex来进行探讨。
总结起来,一共有两种实现方式:事件总线和Vuex
十一、插槽slot的使用
11.1.编译作用域的概念
所谓的编译作用域,通俗的讲,就是代码的作用范围。
官方指出,父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。我的理解是,只看模板时属于谁,模板属于父组件,那么在模板中使用的所有的东西就都是用的父组件中的属性等,模板是子组件,那么就使用的子组件中的数据,即使,这个数据是子组件向父组件经过组件通信拿到的,那也是在子组件中有一个保存的值,组件通信只负责传递,实际使用,子组件还是使用的是子组件中定义的保存数据。
11.2.为什么要使用slot?
理解了编译作用域,那么就很容易理解为什么要使用插槽了。这是因为,我们的开发过程中,有些东西是有一定的共性的。如果我们不去思考这些共性,那么我们每一个页面都需要自己单独封装一个组件,这显然是浪费的;但是如果我们把所有共性的东西都封装成一个组件,在不同的地方引用,那么也是不合理的,因为不同的页面实际上是由一些不同的内容展示的,此时我们就需要使用slot来完成我们想要的效果。
比如淘宝和京东,这两款app的上方的搜索框,难道我们每开发一个类似的app都需要重新写一下同样的代码嘛?
11.3.怎么使用slot?
抽取共性,保留不同。最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。是搜索框,还是文字,还是菜单。由调用者自己来决定。
这时候我们就需要用到插槽了。插槽所起的作用就是预留一个位置。就像上面的搜索框,我们发现,他们的区别只是在于展现的内容不同,而实际上我们使用HTML标签所搭建的架构都是一样的左中右结构,此时,我们就可以将左中右各用一个slot,然后在父组件调用该子组件时,在告诉子组件,左中右各自需要展示什么东西。
11.3.1.插槽的作用
组件使用插槽是为了让我们封装的组件更加具有扩展性。让使用者可以决定组件内部的一些内容到底展示什么。
11.3.2.插槽的基本使用
首先,我们需要在子组件中设置一个插槽,让父组件在调用子组件是可以给子组件传递需要在插槽中显示的内容
<template id="cpn">
<div>
<h2>我是组件</h2>
<p>我是组件,哈哈哈哈</p>
<slot></slot>//子组件中放置一个插槽
</div>
</template>
然后,我们在父组件中添加需要在插槽中显示的内容给子组件
//父组件会根据输入的内容不同来显示不同的内容
<cpn><button>按钮</button></cpn>//插槽位置为一个文字为 按钮 的button
<cpn><span>哈哈哈</span></cpn>//插槽位置为 哈哈哈
<cpn><i>呵呵呵</i></cpn>//插槽位置为 呵呵呵(斜体)
注意:
1.插槽是可以使用默认值的
如果我们在子组件的插槽中写入一个默认标签的话,那么当父组件使用子组件时,没有告诉子组件插槽中需要填充什么内容的时候,就会将该默认内容进行展示。
2.在父组件中传递给子组件插槽内容时,如果没有指定插槽,即没有添加slot属性,那么要替换的内容就会将所有的非具名插槽全部替换成该内容
3.插槽的使用并不智能,其替换机制是,会将父组件在子组件中输入的要替换的内容,放在一起,当作一个元素,最后将该元素替换到插槽所在的位置。
如果我们一个子组件中有多个非具名插槽的时候,我们的父组件调用子组件时,传递给子组件的标签并不是一一对应的展示,而是把所有的标签当作一个元素,替换到每一个非具名插槽中。
11.3.3.具名插槽的使用
当我们将多个标签同时放到一个插槽时,此时我们的插槽会将多个标签当作一个元素被替换到slot标签中进行展示。
//此时插槽位置会将我们添加的三个标签中的内容按顺序都替换到插槽的位置
<cpn>
<i>呵呵呵</i>
<div>我是div元素</div>
<p>我是p元素</p>
</cpn>
如果我们一个子组件中有多个非具名插槽的时候,我们的父组件调用子组件时,传递给子组件的标签并不是一一对应的展示,而是把所有的标签当作一个元素,替换到每一个插槽中,也就是说,如果我们的子组件中有两个普通插槽slot的话,那么上面的那三行HTML会整整执行两次,这明显跟我们想要的效果是不一致的;如果只有一行HTML,那么也会将这一行代码执行两次,替换到两个插槽中。
我们的具名插槽就是为了解决这个问题,它可以让我们想要填充的内容填充到指定的地方去
首先,我们需要在子组件中添加具名插槽,实际上就是给普通插槽添加一个name属性
<template id="cpn">
<div>
//添加三个具名插槽
<slot name="left"><span>左边的</span></slot>
<slot name="center"><span>中间的</span></slot>
<slot name="right"><span>右边的</span></slot>
</div>
</template>
然后,我们在父组件中调用的时候需要通过slot属性来为我们的内容指定要替换的插槽
<cpn><span slot="center">标题</span></cpn>
注意:
当我们同时给多个具名插槽替换内容时,并不互相影响
11.4.作用域插槽
目的:父组件替换插槽的标签,但是内容由子组件来提供
在这里我们使用一个例子来解释:
子组件中包括一组数据,比如:plLanguages:['javaScript', 'Python', 'Swift', 'Go', 'C++'] ,但是这组数据需要在多个界面展示:
1.某些界面是以水平方向一一展示的
2.某些界面是以列表形式展示的
3.某些界面直接展示一个数组
也就是说,展示的内容不是由父组件来提供,而是在子组件,希望父组件告诉我们如何展示,这种情况下就需要用到slot作用域插槽。
该种方式在Vue3.x被弃用了但是没有删除,理解就好
首先,我们需要在子组件的data中写上要展示的数据,并且将数据通过:data传递给父组件,:data后面的参数就是我们子组件中需要传递给父组件展示的数据。
<template id="cpn">
<div>
<slot :date="pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
cpn: {
template: `#cpn`,
data() {
return {
pLanguages: ['javaScript', 'Python', 'C++', 'Java', 'C#', 'Go', 'Swift']
}
}
}
然后,父组件再通过slot-scope拿到slot中的数据并展示,值得注意的是,slot-scope后面的参数,是在父组件拿到子组件数据后,暂时存放的位置。不一定必须是slot。而这里的slot并不
等于pLanguages我们的slot.data才是我们要的数据,也就是pLanguages
<cpn>
<template slot-scope="slot">
<span v-for="item in slot.data">{{item}} - </span>
<span>{{slot.data.join(' - ')}}</span>
</template>
</cpn>
十二、为什么要有模块化?
模块由两个核心思想:导入和导出
在网页开发的早期,javaScript作为一个脚本语言,只是需要实现一些简单的表单验证或动画,当时的代码是写在一个<script>标签中即可满足。
随着ajax异步请求的出现,慢慢形成了前后端分离。客户端需要完成的事情越来越多,代码量也与日俱增。为了应对代码量的剧增,我们通常会将代码组织在多个js文件中,进行维护,但是这种维护当时,依然不能避免一些灾难性的问题。比如,全局变量同名的问题。
1.项目组长
2.小明
3.小红
项目开始时,项目组长,先创建了一个main.js写了一些公共代码
小明在创建的aaa.js中创建了一个flag的变量,并将其赋值为true;小红也在自己的bbb.js中创建了同名变量flag
此时,小明在创建的第二个mmm.js文件中想要再次使用flag变量时,就会因为小红将自己的变量值修改而出错
12.1.解决方案
12.1.1匿名函数闭包
我们可以通过匿名函数来解决这个全局变量重名的问题,但是这种解决方法也有一个问题,那就是,当我们想要在main.js根文件中使用这个变量时,会变得无法使用,因为通过匿名函数,此时的变量已经是一个局部变量了。这种方法,每个js文件会完全独立,不具备可复用性
12.1.2.使用模块化思想
我们可以把匿名函数中创建一个对象,将要暴露出去的变量整合到一起,放入对象中,最后将其作为返回值返回,当然了,既然有返回值,那么我们也需要一个参数来接收这些需要暴露的变量。
var moduleA = (function () {
//小明
//导出的对象
var obj = {
}
var name = '小明';
var age = 22;
function sum(num1, num2) {
return num1 + num2;
}
var flag = true;
if (flag) {
console.log(sum(10, 20));
}
//在对象中添加要暴露出去的变量
obj.flag = flag;
obj.sum = sum;
return obj;//同时我们还需要一个moduleA来接收数据
})()
之后,我们可以在别的文件中通过我们的moduleA来使用里面保存的变量、方法等。
(function () {
//小明
//1.想使用flag
if (moduleA.flag) {
console.log('小明是天才,哈哈哈哈');
}
//2.使用sum函数
console.log(moduleA.sum(40, 50));
})()
同样的,小红也可以在自己的项目中创建自己的对象来暴露自己的变量,而使用。此时,不同人员的变量就互不打扰,即使同名,他们也在不同的模块中了。
12.2.CommonJS
CommonJS的导出:使用module.exports来导出
//使用module.exports来导出
module.exports = {
flag: flag,//保存flag
sum, sum//保存sum
}
CommonJS的导入:通过require来导入
var a = require('aaa');//从aaa.js文件中导出,并存入变量a中
var flag = a.flag;//将a中的flag取出保存
var sum = a.sum;//将a中的sum取出保存
sum(20 , 30);
var {flag, sum} = require('aaa');//从aaa.js文件中取出,并将两者直接保存
sum(20, 50);
12.3.导出export的基本使用
export是用来导出我们的变量的。
1.将创建好的变量或函数通过export导出
let flag = true;
function sum(num1, num2) {
return num1 + num2;
}
if (flag) {
console.log(sum(20, 30));
}
//1.导出方式一
export {
flag, sum
}
2.在创建变量的时候就直接将其导出
export var num1 = 1000;
3.创建函数或类的时候就将其导出
export function mul(num1, num2) {
return num1 * num2;
}
export class Person {
run() {
console.log('在奔跑');
}
}
4.通过export default导出(注意通过该方式导出后,在导入的时候需要自己注册名字,这个名字代表的就是导出的东西。)
注意:export default在同一个模块中,不允许同时存在多个。
const address = '北京市';
export default address;
export default function (argument) {
console.log(argument);
}
12.4.导入import的使用
我们使用export指令导出了模块对外提供的接口,下面我们就可以通过import命令来加载对应的这个模块了。
//1.导入的{}中定义的变量
import {flag, sum} from "./aaa.js";
//2.直接导入export定义的变量
import {num1, height} from "./aaa.js";
//3.导入export的function
import {mul, Person} from "./aaa.js";
//4.导入export default中的内容
import addr from "./aaa.js"
//5.统一全部导入
import * as aaa from "./aaa.js";
注意:
当我们使用*全部导入的时候,不要忘记给*起一个别名,用于后续的使用。