JackCin's blog JackCin's blog
首页
  • 页面

    • Html
    • CSS
  • 核心

    • JavaScript基础
    • JavaScript高级
  • 框架

    • Vue
  • jQuery
  • Node
  • Ajax
Linux
  • 操作系统
  • 数据结构与算法
  • 51单片机
  • CC2530
  • 网站
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

JackCin

前端小菜鸡(✪ω✪)
首页
  • 页面

    • Html
    • CSS
  • 核心

    • JavaScript基础
    • JavaScript高级
  • 框架

    • Vue
  • jQuery
  • Node
  • Ajax
Linux
  • 操作系统
  • 数据结构与算法
  • 51单片机
  • CC2530
  • 网站
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 页面

  • 核心

  • 框架

    • Vue

      • 初识 Vue
      • Vue 基础语法
      • 组件化开发
        • 一、认识组件化
          • 1、 什么是组件化?
          • 2、 Vue组件化思想
        • 二、注册组件
          • 1、 注册组件的基本步骤
          • 2、 注册组件步骤解析
        • 三、组件其他补充
          • 1、 组件其他补充
          • 2、父组件和子组件
          • 3、注册组件语法糖
          • 4、 模板的分离写法
          • 5、 组件数据存放
          • 5.1 组件可以访问Vue实例数据吗?
          • 5.2 组件数据的存放
          • 5.3 为什么 data 是一个函数呢?
          • 6、父子组件的通信(数据的传递)
          • 7、父级向子级传递---props
          • 7.1props基本用法
          • 7.2 props数据验证——对象写法
          • 8、 子级向父级传递---自定义事件
          • 8.1 子级向父级传递
          • 8.2 父传子--结合双向绑定案例
          • 8.3 父传子--结合双向绑定案例(watch实现)
          • 9、父子组件的访问
          • 9.1 父子组件的访问方式: $children(父访问子)
          • 9.2 父子组件的访问方式: $refs(父访问子)
          • 3.父子组件的访问方式: $parent(子访问父)、$root(根组件)
          • 4.非父子组件通信
          • 10、插槽 slot
          • 10.1.为什么使用slot
          • 10.2 如何封装这类组件呢?slot
          • 10.3 slot基本使用
          • 10.4 具名插槽 slot
          • 10.5(个人补充)vue2.6之后具名插槽的写法
          • 10.6 编译作用域
          • 10.7 作用域插槽:准备
          • 10.8 作用域插槽:使用
          • 10.9(个人补充)vue2.6后作用域插槽的写法
      • 前端模块化
      • Vue CLI 脚手架
      • vue-route
      • Promise
      • Vuex
      • 网络模块封装
  • 前端
  • 框架
  • Vue
JackCin
2022-11-13
目录

组件化开发

# 一、认识组件化

# 1、 什么是组件化?

  • 人面对复杂问题的处理方式:
    • 任何一个人处理信息的逻辑能力都是有限的
    • 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
    • 但是,我们人有一种天生的能力,就是将问题进行拆解。
    • 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
  • 组件化也是类似的思想:
  • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
  • 但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。

img

# 2、 Vue组件化思想

  • 组件化是 Vue.js 中的重要思想
    • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    • 任何的应用都会被抽象成一颗组件树。

img

  • 组件化思想的应用:
    • 有了组件化的思想,我们在之后的开发中就要充分的利用它。
    • 尽可能的将页面拆分成一个个小的、可复用的组件。 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
  • 所以,组件是Vue开发中,非常重要的一个篇章,要认真学习。

# 二、注册组件

# 1、 注册组件的基本步骤

  • 组件的使用分成三个步骤:
    • 创建组件构造器
    • 注册组件
    • 使用组件

img

  • 我们来看看通过代码如何注册组件
  • 查看运行结果:
    • 和直接使用一个div看起来并没有什么区别。
    • 但是我们可以设想,如果很多地方都要显示这样的信息,我们是不是就可以直接使用<my-cpn></my-cpn>来完成呢?
  • 组件的基本使用(全局组件)
 
  <!-- 
  组件的使用分成三个步骤:
    1.创建组件构造器
    2.注册组件
    3.使用组件。
 -->
  <div id="app">
    <!--3.在Vue实例范围内使用组件-->
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
    <my-cpn></my-cpn>
<!-- 如果组件中间没有内容,也可以写成单标签 <my-cpn/> -->
    <div>
      <div>
        <my-cpn></my-cpn>
      </div>
    </div>
  </div>
 
  <my-cpn></my-cpn>
 
  <script src="../js/vue.js"></script>
  <script>
    // 1.创建组件构造器对象  extend() 没有s
    const cpnC = Vue.extend({
      // 自定义组件的模板 使用到组件的地方,要显示的HTML代码
      // *最外需要一个div包裹
      template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容, 哈哈哈哈</p>
        <p>我是内容, 呵呵呵呵</p>
      </div>`
    })
 
    // 2.注册组件(全局注册) 
    // 需要传递两个参数:
    // 1、注册组件的标签名 (必须加引号)
    // 2、组件构造器
 
    Vue.component('my-cpn', cpnC)
 
 
 
    // 以上两步需要在Vue实例创建之前
 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 2、 注册组件步骤解析

  • 这里的步骤都代表什么含义呢?

    1. Vue.extend():
    • 调用Vue.extend()创建的是一个组件构造器。
    • 通常在创建组件构造器时,传入template代表我们自定义组件的模板。
    • 该模板就是在使用到组件的地方,要显示的HTML代码。
    • 事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。
    1. Vue.component():
    • 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
    • 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器
    1. 组件必须挂载在某个Vue实例下,否则它不会生效。(见下图)
    • 我们来看下面我使用了三次<my-cpn></my-cpn>
    • 而第三次其实并没有生效:

img

# 关于Vue.extend() 的补充:

(46条消息) Vue中 Vue.extend() 详解及使用_前端 贾公子的博客-CSDN博客_vue.extend (opens new window)

# 利用extend实现的关于toast的案例:
  • 这个案例可以让我们不用去每个组件里重复引用去使用我们想要的组件

    • 实现原理是直接将我们要使用的vue组件挂载到 prototype 上

    以下是 Toast组件的vue代码:(Toast.vue)

<template>
  <div class="toast" v-show="isShow">
    <div>{{ message }}</div>
  </div>
</template>

<script>
export default {
  name: "Toast",
  data() {
    return {
      message: "",
      isShow: false,
    };
  },
  methods: {
    show(message, duration = 2000) {
      this.isShow = true;
      this.message = message;

      setTimeout(() => {
        this.isShow = false;
        this.message = "";
      }, duration);
    },
  },
};
</script>

<style>
.toast {
  position: fixed;
  top: 50%;
  right: 50%;
  padding: 8px 10px;
  transform: translate(50%, 50%);
  color: #fff;
  background-color: rgba(0, 0, 0, 0.75);
  z-index: 999;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

Toast组件的index代码:(Toast.index)

//这里的引用路径这样写就只是因为我把 Toast.vue 文件和 Toasat.index 文件放到了同一个目录下
import Toast from './Toast'
const obj = {}

// 我们使用 install 函数时,默认会传入 Vue
//这里必须写传入Vue,因为我们下面需要用到Vue
obj.install = function (Vue) {
    console.log('执行了obj的install', Vue)

    // 1.创建组件构造器
    const toastContrustor = Vue.extend(Toast)
    // //2. new的方式,根据组件构造器,可以创建出一个组件对象
    const toast = new toastContrustor()

    // extend 创建的是 Vue 构造器,而不是我们平时常写的组件实例,所以不能用下面这种写法直接写
    // const toast = Vue.extend(Toast)

    // 3. 将组件对象手动挂载到某一个元素上
    //我们创建的组件必须要先挂载到某个元素上,这样才能将组件内的模板替换(渲染)掉挂载的元素,如果没有把组件挂载到某个元素上,那 toast就不会渲染,我们也就没办法利用$el来获得toast组件中的元素
    toast.$mount(document.createElement('div'))
    
    // 4. toast.$el 对应的就是div
    //这里用 $el是为了获取 toast组件里的dom (toast.$el 拿到的是toast组件里的根元素)
    document.body.appendChild(toast.$el)
    // 5.将toast挂载到 Vue的原型上
    Vue.prototype.$toast = toast


}

export default obj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

在项目的mian.js文件里导入并安装 Toast 组件:

import toast from 'components/common/toast'
// 安装 toast 插件
// 使用Vue.use()方法进行安装,安装会默认执行要安装插件的 install 方法 
Vue.use(toast)

new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10

​ 补: vue 中 $el与$refs 区别 - 简书 (jianshu.com) (opens new window)

​ (46条消息) vue中$ref和$el的区别?小智玩前端的博客-CSDN博客$el和$ref (opens new window)

按照上面这样写后我们就把 Toast组件挂载到了 Vue 原型上,这样的话,这个组件就已经存在于我们挂载的 app 组件上了,不过我们这里写的是不显示这个组件,所以我们就看不到

(具体调用看项目的Detail.vue 220行和)

# 三、组件其他补充

# 1、 组件其他补充

  • 当我们通过调用Vue.component()注册组件时,组件的注册是全局的
    • 这意味着该组件可以在任意Vue示例下使用。

img

  • 如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件

img

 
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>
 
<div id="app2">
  <cpn></cpn>
</div>
 
<script src="../js/vue.js"></script>
<script>
  // 1.创建组件构造器
  const cpnC = Vue.extend({
    template: `
      <div>
        <h2>我是全局组件</h2>
        <p>我是内容,哈哈哈哈啊</p>
      </div>
    `
  })
 
  // 2.注册组件(component注册的是全局组件, 意味着可以在多个Vue的实例下面使用,比如app,和app2都可以使用cpn)
 
  // Vue.component('cpn', cpnC); // '标签名一定要加引号'
 
  // 疑问: 怎么注册的组件才是局部组件了? 挂载在某个实例中
 
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      // 使用组件时的标签名:组件构造器
      // 'cpn': cpnC  局部组件的标签名有无引号都可以
      cpn: cpnC
    }
  })
 
  const app2 = new Vue({
    el: '#app2'
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • 补充:组件名命名规范
  • 1.全部小写,使用短横线-连接
    • 必须在引用这个自定义元素时使用 短横线分隔命名,
    • 例如 <my-component-name>
  • 2.或驼峰命名 引用这个自定义元素时两种命名法都可以使用。 也就是说 <my-component-name> 和 <MyComponentName> 都是可接受的

# 2、父组件和子组件

  • 在前面我们看到了组件树:
    • 组件和组件之间存在层级关系
    • 而其中一种非常重要的关系就是父子组件的关系
  • 我们来看通过代码如何组成的这种层级关系:

img

  • 父子组件错误用法:以子标签的形式在Vue实例中使用
    • 因为当子组件注册到父组件的components时,Vue会编译好父组件的模块
    • 该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)
    • <child-cpn></child-cpn>是只能在父组件中被识别的。
    • 类似这种用法,<child-cpn></child-cpn>是会被浏览器忽略的。
  • 父子组件正确用法:在父组件的 components 注册,在template 中使用子组件标签 代码:
 
  <div id="app">
    <cpn2></cpn2>
    <!-- 父子组件错误用法:以子标签的形式在Vue实例中使用 -->
    <!--<cpn1></cpn1>-->
    <!-- 正确写法 在 template 中使用 <cpn1></cpn1> -->
  </div>
 
  <script src="../js/vue.js"></script>
  <script>
    // 1.创建第一个组件构造器(子组件)
    const cpnC1 = Vue.extend({
      template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
    })
 
 
    // 2.创建第二个组件构造器(父组件)
    const cpnC2 = Vue.extend({
      // 2.2 在 template 中使用 <cpn1></cpn1>
      template: `
      <div>
        <h2>我是标题2</h2>
        <p>我是内容, 呵呵呵呵</p>
        <cpn1></cpn1>
      </div>
    `,
      // 2.1 在components注册子组件cpn1
      components: {
        cpn1: cpnC1
      }
    })
 
    // root组件
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn2: cpnC2
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 3、注册组件语法糖

  • 在上面注册组件的方式,可能会有些繁琐。
    • Vue为了简化这个过程,提供了注册的语法糖。
    • 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
  • 语法糖注册全局组件和局部组件:
 
  <div id="app">
    <cpn1></cpn1>
    <cpn2></cpn2>
  </div>
 
  <script src="../js/vue.js"></script>
  <script>
    // 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替
    // 1.全局组件注册的语法糖
    // * Vue.component标签名必须加引号
    Vue.component('cpn1', {
      template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
    })
 
    // 2.注册局部组件的语法糖
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        'cpn2': {
          template: `
          <div>
            <h2>我是标题2</h2>
            <p>我是内容, 呵呵呵</p>
          </div>
        }
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 4、 模板的分离写法

  • 刚才,我们通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。

  • 如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。

  • Vue提供了两种方案来定义HTML模块内容:

  • (1) 使用<script>标签

    img

  • (2) 使用<template>标签

img

 
  <div id="app">
    <cpn></cpn>
    <cpn></cpn>
    <cpn></cpn>
  </div>
 
  <!--1.script标签, 注意:类型必须是text/x-template 然后给它设置一个id -->
  <script type="text/x-template" id="cpn">
    <div>
      <h2>我是标题</h2>
      <p>我是内容,哈哈哈</p>
    </div>
  </script>
 
  <!--2.template标签-->
  <template id="cpn">
    <div>
      <h2>我是标题</h2>
      <p>我是内容,呵呵呵</p>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    // 1.注册一个全局组件
    Vue.component('cpn', {
      template: '#cpn' // 需要加上选择器
    })
 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • 补充
    • vue为什么要求组件模板只能有一个根元素 (opens new window)
    • vue中template的作用及使用 (opens new window)

# 5、 组件数据存放

# 5.1 组件可以访问Vue实例数据吗?

  • 组件是一个单独功能模块的封装:
    • 这个模块有属于自己的HTML模板,也应该有属于自己的数据data。
  • 组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
    • 我们先来测试一下,组件中能不能直接访问Vue实例中的data

img

  • 我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿。
  • 结论:Vue组件应该有自己保存数据的地方。

# 5.2 组件数据的存放

  • 组件自己的数据存放在哪里呢?
    • 组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
    • 只是这个data属性必须是一个函数
    • 而且这个函数返回一个对象,对象内部保存着数据

img

 
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>
 
<template id="cpn">
  <div>
    <h2>{{title}}</h2>
    <p>我是内容,呵呵呵</p>
  </div>
</template>
 
<script src="../js/vue.js"></script>
<script>
 
  // 1.注册一个全局组件
  Vue.component('cpn', {
    template: '#cpn',
    data() {
      return {
        title: 'abc'
      }
    }
  })

// 组件是不能直接访问Vue实例中的data数据
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      // title: '我是标题'
    }
  })
 
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 5.3 为什么 data 是一个函数呢?

  • 为什么data在组件中必须是一个函数呢?
    • 首先,如果不是一个函数,Vue直接就会报错。
    • 其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。

img

 <!--组件实例对象-->
  <div id="app">
    <cpn></cpn>
    <cpn></cpn>
    <cpn></cpn>
  </div>
 
  <template id="cpn">
    <div>
      <h2>当前计数: {{counter}}</h2>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  </template>
  <script src="../js/vue.js"></script>
  <script>
    // 1.注册组件
    const obj = {
      counter: 0
    }
    Vue.component('cpn', {
      template: '#cpn',
      //这样写就可以正常,因为我们每次创建一个新的组件就会调用一次data()函数,而data函数再返回一个新的对象,这样就保证了每个组件内的数据有了独立性
      // data() {
      //   return {
      //     counter: 0
      //   }
      // },
      data() {
        // 会一起改变
        return obj
      },
      methods: {
        increment() {
          this.counter++
        },
        decrement() {
          this.counter--
        }
      }
    })
 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  </script>
  <script>
    const object = {
      name: 'why',
      age: 18
    }
 
 
    // function abc() {
    // 共用一个对象
    //   return object;
    // }
 
    function abc() {
      // 每调用一次函数都返回一个新的对象
      return {
        name: 'why',
        age: 18
      }
    }
 
    let obj1 = abc();
    let obj2 = abc();
    let obj3 = abc();
 
    obj1.name = 'kobe'
    console.log(obj2);
    console.log(obj3);
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
  • 补充(结合网络资料)
    • Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了;
    • JavaScript只有函数构成作用域 ( 注意理解作用域,只有函数{}构成作用域,对象的{}以及if(){}都不构成作用域) ,data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。
    • 组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
    • 而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。
    • 所以说vue组件的data必须是函数。这都是因为js的特性带来的,跟vue本身设计无关。

# 6、父子组件的通信(数据的传递)

  • 在上一个小节中,我们提到了子组件是不能引用父组件或者Vue实例的数据的。
  • 但是,在开发中,往往一些数据确实需要从上层传递到下层:
    • 比如在一个页面中,我们从服务器请求到了很多的数据。
    • 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
    • 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
  • 如何进行父子组件间的通信呢?Vue官方提到
    • 通过 props 向子组件传递数据(父传子)
    • 通过事件向父组件发送消息(子传父)

img

  • 在下面的代码中,我直接将Vue实例当做父组件,并且其中包含子组件来简化代码。
  • 真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的。

# 7、父级向子级传递---props

# 7.1props基本用法

  • 在组件中,使用选项props来声明需要从父级接收到的数据。
  • props的值有两种方式:
    • 方式一:字符串数组,数组中的字符串就是传递时的名称。
    • 方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
  • 我们先来看一个最简单的props传递:

img

<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
 
<body>
 
  <div id="app">
    <!--<cpn v-bind:cmovies="movies"></cpn>-->
    
    <!-- 没有v-bind直接这样写是给prop传递一个静态的值,也就是说movies不是一个变量而是一个字符串 -->
    <!--<cpn cmovies="movies" cmessage="message"></cpn>-->
 
    <!-- 步骤2 通过:cmessage="message" 将data中的数据传给子组件props -->
    <cpn :cmessage="message" :cmovies="movies"></cpn>
  </div>
 
 
  <!-- 子组件 -->
  <template id="cpn">
    <div>
      <!-- 步骤3 将props中的值显示在子组件中 -->
      <ul>
        <li v-for="item in cmovies">{{item}}</li>
      </ul>
      <h2>{{cmessage}}</h2>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    // 父传子: props
 
    // -------子组件------
    const cpn = {
      template: '#cpn',
 
   // 子组件通过prop接收  我们能够在组件实例中访问这个值,就像访问 data 中的值一样
 
     /* ***步骤1*** 在 子组件 定义props */
     // ****方式1:字符串数组,数组中的字符串就是传递时的名称(之后要引用的变量名)
     // props: ['cmovies', 'cmessage'], // 不要把元素当成字符串,把它当成数组
      data() {
        return {}
      }
    }
 
 
    // -----父组件-----
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊',
        movies: ['海王', '海贼王', '海尔兄弟']
      },
      components: {
        cpn
      }
    })
  </script>
 
<!-- 
  步骤:
  1.在子组件里写props
  2.在子组件的标签加上v-bind  <cpn v-bind:props里定义的名称="父组件data数据名称"></cpn>
  3.将props中的值显示在子组件中
 -->
 
  <!-- 
  工厂函数
    1,它是一个函数。
    2,它用来创建对象。
    3 ,它像工厂一样,“生产”出来的函数都是“标准件”(拥有同样的属性)
 -->
 
</body>
 
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 7.2 props数据验证——对象写法

  • 在前面,我们的props选项是使用一个数组。
  • 我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。
  • 验证都支持哪些数据类型呢?
    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol
  • 当我们有自定义构造函数时,验证也支持自定义的类型

img

img

// ***方式2:对象,对象可以设置传递时的类型,也可以设置默认值等->当需要对props进行类型等验证时
      props: {
        // 1.类型限制
        // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
        // cmovies: Array,
        // cmessage: String,
 
        // 多个可能的类型
        // propB: [String, Number],
 
        // 2.提供一些默认值, 以及必传值
        cmessage: {
          type: String,
          default: 'aaaaaaaa',
          required: true // 必填的字符串
        },
        // 类型是对象或者数组时, 默认值必须是一个工厂函数
        cmovies: {
          type: Array,
          default () {
            return {
              message: 'hello'
            }
          }
        },
        // 自定义验证函数
        propF: {
          //  validator 验证器
          validator: function (value) {
            // 这个值必须匹配下列字符串中的一个
          return ['success', 'warning', 'danger'].indexOf(value) !== -1
          }
        }
 
      },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 7.3 props中的驼峰标识

​ HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

 
  <div id="app">
    <!-- v-bind 不支持驼峰 需要换成 -  -->
    <cpn :c-info="info" :child-my-message="message" v-bind:class></cpn>
  </div>
 
  <template id="cpn">
    <div>
      <h2>{{cInfo}}</h2>
      <h2>{{childMyMessage}}</h2>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const cpn = {
      template: '#cpn',
      props: {
        // 在这里使用驼峰 :c-info="info" 那里(传入)要用 -
        cInfo: {
          type: Object,
          default () {
            return {}
          }
        },
        childMyMessage: {
          type: String,
          default: ''
        }
      }
    }
 
    const app = new Vue({
      el: '#app',
      data: {
        info: {
          name: 'why',
          age: 18,
          height: 1.88
        },
        message: 'aaaaaa'
      },
      components: {
        cpn
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  • 补充
    • Vue官网单向数据流 (opens new window)

# 8、 子级向父级传递---自定义事件

# 8.1 子级向父级传递

  • props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
  • 我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。
  • 什么时候需要自定义事件呢?
    • 当子组件需要向父组件传递数据时,就要用到自定义事件了。
    • 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
  • 自定义事件的流程:
    • 在子组件中,通过**$emit()**来触发事件。
    • 在父组件中,通过v-on来监听子组件事件。
  • 我们来看一个简单的例子:
    • 我们之前做过一个两个按钮+1和-1,点击后修改counter。
    • 我们整个操作的过程还是在子组件中完成,但是之后的展示交给父组件。
    • 这样,我们就需要将子组件中的counter,传给父组件的某个属性,比如total。

img

 
  <!--父组件模板-->
  <div id="app">
    <!-- 3.在父组件子标签中,通过v-on来监听子组件事件 并添加一个响应该事件的处理方法 -->
    <cpn @item-click="cpnClick"></cpn>
  </div>
 
  <!--子组件模板-->
  <template id="cpn">
    <div>
      <!-- 1.在子组件中创建一个按钮,给按钮绑定一个点击事件 -->
      <button v-for="item in categories" @click="btnClick(item)">
        {{item.name}}
      </button>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    // 子传父 自定义事件
 
    // 子组件 
    const cpn = {
      template: '#cpn',
      data() {
        return {
          categories: [{
              id: 'aaa',
              name: '热门推荐'
            },
            {
              id: 'bbb',
              name: '手机数码'
            },
            {
              id: 'ccc',
              name: '家用家电'
            },
            {
              id: 'ddd',
              name: '电脑办公'
            },
          ]
        }
      },
      methods: {
        btnClick(item) {
          // 发射事件: 自定义事件
          // 2.在子组件中,通过$emit()来触发事件
          this.$emit('item-click', item)
          // 注意!!!!这里的$emit事件名不要写成驼峰!!!脚手架里可以,会先编译成一个组件对象render函数
        }
      }
    }
 
    // 父组件 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn
      },
      methods: {
        cpnClick(item) { // 这里的参数是接收子组件传过来的数据的
          console.log('cpnClick', item);
          
        }
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# 8.2 父传子--结合双向绑定案例

  • 需求

    • 子组件input 绑定v-model,input改变,props里的number1、number2跟着改变,vue实例data里的num1,num2也跟着变
    • 子组件 data 的 dnumber1一改变,dnumber2就 *100,dnumber2一改变,dumber/100
  • 分析

  • 之前的v-model是绑定vue实例data里面的数据

  • 如果v-model绑定了props里的值,会报错(props里的值最好是通过父组件修改)

  • v-model不要绑定props里的值用data或computed代替把number1,number2分别赋值给data的dnumber1,dnumber2

代码:

<!-- 父组件 -->
  <div id="app">
    <h3>父组件</h3>
    <h3>-----num1----</h3>
    <h3>{{num1}}</h3>
    <h3> -----num2----</h3>
    <h3>{{num2}}</h3>
    <hr>
    <cpn :number1="num1" :number2="num2" @num1change="num1change" @num2change="num2change" />
  </div>
 
  <!-- 子组件 -->
  <template id="cpn">
    <div>
      <!-- 这样写会报错 应该是由父组件修改它,避免直接修改props的值 -->
      <!-- 
        <input v-model="number1" type="text" />
      <input v-model="number2" type="text" />
     -->
 
      <h3>子组件</h3>
 
      <h3> -----number1----</h3>
      <!-- 为什么props也会跟着一起变? -> number1绑定的是父组件num1 -->
      <h2>props:{{number1}}</h2>
      <h2>data:{{dnumber1}}</h2>
 
      <!--<input type="text" v-model="dnumber1">-->
      <!-- v-model的本质 用@input来传值 -->
      <input type="text" :value="dnumber1" @input="num1Input">
 
      <h3>-----number2----</h3>
 
      <h2>props:{{number2}}</h2>
      <h2>data:{{dnumber2}}</h2>
      <!--<input type="text" v-model="dnumber2">-->
      <input type="text" :value="dnumber2" @input="num2Input">
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    // 子组件
    const cpn = {
      template: '#cpn',
      props: {
        number1: Number,
        number2: Number
      },
      data() {
        return {
          dnumber1: this.number1,
          dnumber2: this.number2
        }
      },
      methods: {
        num1Input(event) {
          // 1.将input中的value赋值到dnumber中
          this.dnumber1 = event.target.value;
 
          // 2.为了让父组件可以修改值, 发出一个事件
          this.$emit('num1change', this.dnumber1)
 
          // 3.同时修饰dnumber2的值
          this.dnumber2 = this.dnumber1 * 100;
          this.$emit('num2change', this.dnumber2);
        },
        num2Input(event) {
          this.dnumber2 = event.target.value;
          this.$emit('num2change', this.dnumber2)
 
          // 同时修饰dnumber1的值
          this.dnumber1 = this.dnumber2 / 100;
          this.$emit('num1change', this.dnumber1);
        }
      }
    }
 
    // 父组件
    const app = new Vue({
      el: '#app',
      data: {
        num1: 1,
        num2: 0
      },
      methods: {
        num1change(value) {
          // value传过来的是string类型,需要转换成数字
          this.num1 = parseFloat(value)
        },
        num2change(value) {
          this.num2 = parseFloat(value)
        }
      },
      components: {
        cpn
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

# 8.3 父传子--结合双向绑定案例(watch实现)

 
<div id="app">
  <cpn :number1="num1"
       :number2="num2"
       @num1change="num1change"
       @num2change="num2change"/>
</div>
 
<template id="cpn">
  <div>
    <h2>props:{{number1}}</h2>
    <h2>data:{{dnumber1}}</h2>
    <input type="text" v-model="dnumber1">
    <h2>props:{{number2}}</h2>
    <h2>data:{{dnumber2}}</h2>
    <input type="text" v-model="dnumber2">
  </div>
</template>
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      num1: 1,
      num2: 0
    },
    methods: {
      num1change(value) {
        this.num1 = parseFloat(value)
      },
      num2change(value) {
        this.num2 = parseFloat(value)
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        props: {
          number1: Number,
          number2: Number,
          name: ''
        },
        data() {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        watch: {
          dnumber1(newValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue);
          },
          dnumber2(newValue) {
            this.number1 = newValue / 100;
            this.$emit('num2change', newValue);
          }
        }
      }
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 9、父子组件的访问

# 9.1 父子组件的访问方式: $children(父访问子)

  • 有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
    • 父组件访问子组件:使用**$children或$refs**
    • 子组件访问父组件:使用**$parent**
  • 我们先来看下$children的访问
    • this.$children是一个数组类型,它包含所有子组件对象。
    • 我们这里通过一个遍历,取出所有子组件的message状态。

img

# 9.2 父子组件的访问方式: $refs(父访问子)

  • children的缺陷:
    • 通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
    • 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
    • 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用 $refs
  • $refs的使用:
    • $refs和ref指令通常是一起使用的。
    • 首先,我们通过ref给某一个子组件绑定一个特定的ID。
    • 其次,通过this.$refs.ID就可以访问到该组件了。

父子组件的访问($refs)

 <div id="app">
    <cpn></cpn>
    <cpn></cpn>
 
    <my-cpn></my-cpn>
    <y-cpn></y-cpn>
 
    <cpn ref="aaa"></cpn>
    <button @click="btnClick">按钮</button>
  </div>
 
  <template id="cpn">
    <div>我是子组件</div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      methods: {
        btnClick() {
          // 1.$children
          // console.log(this.$children);
          this.$children[0].showMessage(); // 子组件的方法可以听过这种方式调用
          // this.$children是一个数组类型,它包含所有子组件对象。
 
          // for (let c of this.$children) {
          //   console.log(c.name);
          //   c.showMessage();
          // }
          // console.log(this.$children[3].name);
 
          // 明确获取其中一个特定的组件,这个时候就可以使用$refs
 
          // 2.$refs reference(引用)  => 对象类型, 默认是一个空的对象 ref='bbb'
          console.log(this.$refs.aaa.name);
        }
      },
      components: {
        cpn: {
          template: '#cpn',
          data() {
            return {
              name: '我是子组件的name'
            }
          },
          methods: {
            showMessage() {
              console.log('showMessage');
            }
          }
        },
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 3.父子组件的访问方式: $parent(子访问父)、$root(根组件)

  • 如果我们想在子组件中直接访问父组件,可以通过 $parent
  • 注意事项:
    • 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。
    • 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
    • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
    • 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。

img

 
<div id="app">
  <cpn></cpn>
</div>
 
<template id="cpn">
  <div>
    <h2>我是cpn组件</h2>
    <ccpn></ccpn>
  </div>
</template>
 
<template id="ccpn">
  <div>
    <h2>我是子组件</h2>
    <button @click="btnClick">按钮</button>
  </div>
</template>
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是cpn组件的name'
          }
        },
        components: {
          ccpn: {
            template: '#ccpn',
            methods: {
              btnClick() {
                // 1.访问父组件$parent
                // console.log(this.$parent);
                // console.log(this.$parent.name);
 
                // 2.访问根组件$root
                console.log(this.$root);
                console.log(this.$root.message);
              }
            }
          }
        }
      }
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 4.非父子组件通信

  • 刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
    • 非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。
  • 在Vue1.x的时候,可以通过$dispatch和$broadcast完成
    • $dispatch用于向上级派发事件
    • $broadcast用于向下级广播事件
    • 但是在Vue2.x都被取消了
  • 在Vue2.x中,有一种方案是通过中央事件总线,也就是一个中介来完成。
    • 但是这种方案和直接使用Vuex的状态管理方案还是逊色很多。
    • 并且Vuex提供了更多好用的功能,所以这里我们暂且不讨论这种方案,后续我们专门学习Vuex的状态管理。

# 10、插槽 slot

# 10.1.为什么使用slot

  • slot翻译为插槽:

    • 在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
    • 插槽的目的是让我们原来的设备具备更多的扩展性。
    • 比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
  • 组件的插槽:

    • 组件的插槽也是为了让我们封装的组件更加具有扩展性。
    • 让使用者可以决定组件内部的一些内容到底展示什么。
  • 栗子:移动网站中的导航栏。

    • 移动开发中,几乎每个页面都有导航栏。
    • 导航栏我们必然会封装成一个插件,比如nav-bar组件。
    • 一旦有了这个组件,我们就可以在多个页面中复用了。
  • 但是,每个页面的导航是一样的吗?No,我以京东M站为例

img

# 10.2 如何封装这类组件呢?slot

  • 如何去封装这类的组件呢?
    • 它们也很多区别,但是也有很多共性。
    • 如果,我们每一个单独去封装一个组件,显然不合适:比如每个页面都返回,这部分内容我们就要重复去封装。
    • 但是,如果我们封装成一个,好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等。
  • 如何封装合适呢?抽取共性,保留不同。
    • 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
    • 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
    • 是搜索框,还是文字,还是菜单。由调用者自己来决定。
  • 这就是为什么我们要学习组件中的插槽slot的原因。

# 10.3 slot基本使用

  • 了解了为什么用slot,我们再来谈谈如何使用slot?
    • 在子组件中,使用特殊的元素 <slot> 就可以为子组件开启一个插槽。
    • 该插槽插入什么内容取决于父组件如何使用。
  • 我们通过一个简单的例子,来给子组件定义一个插槽:
    • <slot> 中的内容表示,如果没有在该组件中插入任何其他内容,就默认显示该内容
    • 有了这个插槽后,父组件如何使用呢?
  • 简而言之,插槽就是预留的空间(位置),即占位符

插槽基本使用方法

 
 
  <!--
    插槽
      插槽,也就是slot,是组件的一块HTML模板,这块模板 显示不显示、以及 怎样显示 由父组件来决定
    
    单个插槽 | 默认插槽 | 匿名插槽
      可以放置在组件的任意位置 一个组件中只能有一个该类插槽
      
      1.插槽的基本使用 <slot></slot>
    
      2.插槽的默认值 <slot>button</slot>
    
      3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
-->
 
  <div id="app">
    <cpn></cpn>
 
    <cpn>
      <span>哈哈哈</span>
    </cpn>
 
    <cpn>
      <i>呵呵呵</i>
    </cpn>
 
    <cpn>
      <i>呵呵呵</i>
      <div>我是div元素</div>
      <p>我是p元素</p>
    </cpn>
 
    <cpn></cpn>
    <cpn></cpn>
 
    <!-- 如果不使用插槽,往组件标签里写东西是没有效果的 -->
    <cpn2>略略略</cpn2>
  </div>
 
 
  <template id="cpn">
    <div>
      <h2>我是组件</h2>
      <p>我是组件, 哈哈哈</p>
      <slot><button>按钮</button></slot>
      <!--<button>按钮</button>-->
    </div>
  </template>
  <template id="cpn2">
    <div>
      <h2>我是组件222</h2>
      <p>我是组件22222, 哈哈哈</p>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn: {
          template: '#cpn'
        },
        cpn2: {
          template: '#cpn2'
        }
 
      }
    })
  </script>
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 10.4 具名插槽 slot

提示

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在**文档中 (opens new window)**的 attribute。新语法的由来可查阅这份 RFC (opens new window)。

推荐文章:vue_插槽的理解和使用 (opens new window)

  • 当子组件的功能复杂时,子组件的插槽可能并非是一个。

    • 比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
    • 那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
    • 这个时候,我们就需要给插槽起一个名字
  • 如何使用具名插槽呢?

    • 非常简单,只要给slot元素一个name属性即可 <slot name='myslot'></slot>
  • 我们来给出一个案例:

    • 这里我们先不对导航组件做非常复杂的封装,先了解具名插槽的用法。

具名插槽的基本使用

# 代码(vue2.5版本):
<!-- 
  具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,
  而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中。
  具名插槽就可以有很多个,只要名字(name属性)不同就可以了
 -->
  <div id="app">
 
    <!-- 这个只能替换没有名字的插槽-->
    <cpn><span>没有名字的替换</span></cpn>
    <!-- vue2.5的写法  -->
    <!-- 下面这两个还是会显示没有名字的插槽的默认内容 -->
    <cpn><span slot="center">标题</span></cpn>
    <cpn><button slot="left">返回</button></cpn>
  </div>
 
 
  <template id="cpn">
    <div>
      <!-- 没有名字的插槽 -->
      <slot>没有名字的插槽默认内容</slot>
 
 
      <slot name="left"><span>左边</span></slot>
      <slot name="center"><span>中间</span></slot>
      <slot name="right"><span>右边</span></slot>
 
 
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn: {
          template: '#cpn'
        }
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 10.5(个人补充)vue2.6之后具名插槽的写法

   <!-- 
  注意 v-slot 只能添加在 < template > 上,只有当被提供的内容只有默认插槽时,
  组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件
 -->
    <div id="app">
        <!-- 默认 -->
        <cpn></cpn>
        <hr>
        <!-- 使用v-slot替换 -->
        <cpn>
            <!-- vscode 快捷语法 vslot-named -->
            <template v-slot:left>
                <span>返回</span>
            </template>
        </cpn>
        <cpn>
            <!--任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。或者可以给他起名default-->
            <!--<template v-slot:default>
                    我是内容
                </template>-->
 
            <template v-slot:center>
                <span>标题</span>
            </template>
            <span>替换没有名字的插槽</span>
        </cpn>
        <cpn>
        <!-- 
        跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。
        例如 v-slot:header 可以被重写为 #header:,前提是必须要有插槽名!!!
        -->
            <template #right>
                <span>替换后的右边</span>
            </template>
        </cpn>
    </div>
    <template id="cpn">
        <div>
            <slot name="left"><span>左边</span></slot>
            <slot name="center"><span>中间</span></slot>
            <slot name="right"><span>右边</span></slot>
            <slot>默认插槽内容</slot>
        </div>
    </template>
    <!-- 需要重新引入一个vue2.6之后的版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        const cpn = {
            template: '#cpn'
        }
        const app = new Vue({
            el: '#app',
            data: {},
            components: {
                cpn
            }
        })
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 10.6 编译作用域

  • 在真正学习插槽之前,我们需要先理解一个概念:编译作用域。
  • 官方对于编译的作用域解析比较简单,我们自己来通过一个例子来理解这个概念:
  • 我们来考虑下面的代码是否最终是可以渲染出来的:
    • **<my-cpn v-show="isShow"></my-cpn>**中,我们使用了isShow属性。
    • isShow属性包含在组件中,也包含在Vue实例中。
  • 答案:最终可以渲染出来,也就是使用的是Vue实例的属性。
  • 为什么呢?
    • 官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
    • 而我们在使用 <my-cpn v-show="isShow"></my-cpn> 的时候,整个组件的使用过程是相当于在父组件中出现的。
    • 那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
    • 因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。

img

 <div id="app">
    <cpn v-show="isShow"></cpn>
    <cpn v-for="item in names"></cpn>
  </div>
 
  <template id="cpn">
    <div>
      <h2>我是子组件</h2>
      <p>我是内容, 哈哈哈</p>
      <button v-show="isShow">按钮</button>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊',
        isShow: true
      },
      components: {
        cpn: {
          template: '#cpn',
          data() {
            return {
              isShow: false
            }
          }
        },
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 10.7 作用域插槽:准备

  • 作用域插槽是slot一个比较难理解的点,而且官方文档说的又有点不清晰。

  • 这里,我们用一句话对其做一个总结,然后我们在后续的案例中来体会:

    • 父组件替换插槽的标签,但是内容由子组件来提供。
    • (样式父组件说了算,但内容可以显示子组件插槽绑定的data)
  • 我们先提一个需求:

    • 子组件中包括一组数据,比如:pLanguages: ['JavaScript', 'Python', 'Swift', 'Go', 'C++']
    • 需要在多个界面进行展示:
      • 某些界面是以水平方向一一展示的,
      • 某些界面是以列表形式展示的,
      • 某些界面直接展示一个数组
    • 内容在子组件,希望父组件告诉我们如何展示,怎么办呢?
      • 利用slot作用域插槽就可以了
  • 我们来看看子组件的定义: img

# 10.8 作用域插槽:使用

  • 在父组件使用我们的子组件时,从子组件中拿到数据:
    • 我们通过**<template slot-scope="slotProps">**获取到slotProps属性
    • 再通过slotProps.data就可以获取到刚才我们传入的data了

img

  • 目的:让插槽内容能够访问引用的组件中才有的数据
  • 常用场景(以下为常用的情况之一)
    • 如果子组件中的某一部分的数据,每个父组件都会有自己的一套对该数据的不同的呈现方式,这时就需要用到作用域插槽。

代码(vue2.6之前):

<!-- vue2.6之前写法 -->
  <div id="app">
    <!-- 以下三个组件 内容一样,样式不同 -->
    <cpn></cpn>
 
    <cpn>
      <!--slot-scope声明属性名 接收子组件传递的数据pLanguages-->
      <template slot-scope="slot">
        <!-- 在dom标签使用该数据,通常用插值表达式接收具体数据 -->
        <!--<span v-for="item in slot.data"> - {{item}}</span>-->
        <h3>{{slot}}</h3>
        <span>{{slot.data.join(' - ')}}</span>
      </template>
    </cpn>
 
 
    <cpn>
      <template slot-scope="slot">
        <!--<span v-for="item in slot.data">{{item}} * </span>-->
        <span>{{slot.data.join(' * ')}}</span>
      </template>
    </cpn>
 
    <!--<cpn></cpn>-->
  </div>
 
  <!-- 子组件 template -->
  <template id="cpn">
    <div>
      <!-- 作用域插槽要求,在slot上面绑定数据 -->
      <slot :data="pLanguages">
        <ul>
          <li v-for="item in pLanguages">{{item}}</li>
        </ul>
      </slot>
      <hr>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn: {
          template: '#cpn',
          data() {
            return {
              pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
            }
          }
        }
      }
    })
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 10.9(个人补充)vue2.6后作用域插槽的写法

 
    <div id="app">
        <!-- 默认 -->
        <h4>默认</h4>
        <cpn></cpn>
        <hr>
        <h4>替换样式</h4>
        <cpn>
            <!-- 具名插槽和作用域插槽混用 -->
            <template v-slot:slot1='props1'>
                <!-- 
                <span>
                    {{props1}}
                </span> -->
 
                <span>{{props1.data1.join('-')}}</span>
                <h3>
                    {{props1.msg}}
                </h3>
            </template>
            <template v-slot:slot2="props2">
                <h2 style="color: red;">
                    {{props2.data2}}
                </h2>
            </template>
        </cpn>
        <!--
            当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。
            这样我们就可以把 v-slot 直接用在组件上,但是不能和具名插槽混用
            -->
        <cpn v-slot="props3">
            <template>
                <h3 style="color: blue;">
                    {{props3.data3}}
                </h3>
            </template>
        </cpn>
    </div>
    <template id="cpn">
        <div>
            <!-- 可以传多个值 所有的值会包含在一个对象中 在父组件中v-slot=""中定义名字接收 -->
            <slot :data1='movies' :msg='message' name='slot1'>
                <ul>
                    <li v-for="(item, index) in movies" :key="index">
                        {{item}}
                    </li>
                </ul>
            </slot>
            <slot :data2='name' name='slot2'>
                {{name}}
            </slot>
            <slot :data3='defult'>默认插槽</slot>
        </div>
    </template>
    <!-- 需要重新引入一个vue2.6之后的版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        const cpn = {
            template: '#cpn',
            data() {
                return {
                    movies: ['战狼', '鬼吹灯', '盗墓笔记'],
                    message: '你好呀',
                    name: 'yangyanyan',
                    defult: '我是默认的数据'
                }
            },
        }
        const app = new Vue({
            el: '#app',
            data: {},
            components: {
                cpn
            }
        })
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

#

编辑 (opens new window)
上次更新: 2023/09/12, 14:55:44
Vue 基础语法
前端模块化

← Vue 基础语法 前端模块化→

最近更新
01
51单片机及补充知识
09-13
02
独立按键
09-13
03
LCD1602液晶显示器
09-13
更多文章>
Theme by Vdoing | Copyright © 2019-2023 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式