认识组件化

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化是Vue.js中的重要思想。它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。任何的应用都会被抽象成一颗组件树。

注册组件

组件的使用分成三个步骤:创建组件构造器(在vue2.x中取消)、注册组件、使用组件。
创建组件的流程.png

组件其他补充

全局组件和局部组件

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

<div id="app1">
  <my-btn></my-btn>
</div>

/* 两个都被渲染出来 */
<div id="app2">
  <my-btn></my-btn>
</div>
<script src="../js/vue.js"></script>
<script>
  const cpn = Vue.extend({
    template: `<div>哈哈哈</div>`
  });
  Vue.component("my-btn", cpn);
  const app1 = new Vue({
    el: "#app1",
    data: {},
  });
  const app2 = new Vue({
    el: "#app2",
    data: {}
  })
</script>
//哈哈哈
//哈哈哈

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

<div id="app1">
  <my-btn></my-btn>
</div>

/*app2 没有被渲染出来*/
<div id="app2">
  <my-btn></my-btn>
</div>
<script src="../js/vue.js"></script>
<script>
  const cpn = Vue.extend({
    template: `<p>哈哈哈</p>`
  });
  const app1 = new Vue({
    el: "#app1",
    data: {},
    components: {
      "my-btn": cpn,
    }
  });
  const app2 = new Vue({
    el: "#app2",
    data: {}
  })
</script>

父组件和子组件

组件和组件之间存在层级关系。而其中一种非常重要的关系就是父子组件的关系。

<div id="app">
  <cpnc1></cpnc1>
</div>
<script src="../js/vue.js"></script>
<script>
  const cpn2 = Vue.extend({
    template: `
    <p>我是cpn2组件</p>
    `
  });
  const cpn1 = Vue.extend({
    template: `
    <div id="app1">
        <h2>我是cpn1组件</h2>
        <cpnc2></cpnc2>
    </div>
    `,
    components: {
      cpnc2: cpn2,
    }
  });
  const app = new Vue({
    el: "#app",
    data: {
      message: "hello vue"
    },
    methods: {},
    components: {
      cpnc1: cpn1,
    }
  })
</script>

注意:无法在Vue实例中直接使用子组件cpn2。

注册组件语法糖

在上面注册组件的方式,可能会有些繁琐。Vue为了简化这个过程,提供了注册的语法糖。主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。

<body>

<div id="app">
  <cpn1></cpn1>
  <cpn2></cpn2>
</div>

<script src="../js/vue.js"></script>
<script>
  // 2.注册组件
  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>

</body>

模板的分离写法

通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。
Vue提供了两种方案来定义HTML模块内容:使用<script>标签、使用<template>标签


<div id="app">
  <cpn1></cpn1>
</div>

<!--1. script标签-->
<script type="text/x-template" id="cpn1">
  <div>
    <h2>cpn1</h2>
  </div>
</script>
<!--2. template-->
<template id="cpn1">
  <div>
    <h2>cpn1</h2>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "hello vue"
    },
    methods: {},
    components: {
      "cpn1": {
        template: "#cpn1"
      }
    }
  })
</script>

组件数据存放

组件是一个单独功能模块的封装:这个模块有属于自己的HTML模板,也应该有属性自己的数据data。组件对象也有一个data属性,这个data属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据。
为什么data在组件中必须是一个函数?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。返回函数,目的是让每一个组件都有一个属于自己的状态。
在根组件data不需要为函数是因为根组件只有一个。

说明代码:

//示例1
function foo() {
  return {
    name: "tom",
    age: 22
  }
}
let obj1 = foo();
let obj2 = foo();
obj1.name = 'Jack';
console.log(obj2.name);//tom

//示例2
let obj = {
  name: "tom",
  age: 22
};
function bar() {
  return obj;
}
let obj1 = bar();
let obj2 = bar();
obj1.name = "Jack";
console.log(obj2.name);//Jack

父子组件通信

子组件是不能引用父组件或者Vue实例的数据的。如何进行父子组件间的通信呢?Vue官方提到:
在父组件中通过props向子组件传递数据;
在子组件中通过事件向父组件发送消息。
父子组件通信.png

父级向子级传递

在子组件中,使用选项props来声明需要从父级接收到的数据。
因为v-bind不支持驼峰标识,如果在子组件props使用驼峰标识(cMess),那么在使用子组件的绑定时应使用(c-mess)。
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称。
方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。(在实际开发中使用对象的形式较多)
父给子传值.png
除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。StringNumberBooleanArrayObjectDatFunctionSymbol。验证也支持自定义的类型。

<div id="app">
  <c-cpn :ccolor="color" :chmessage="message"></c-cpn>
</div>
<template id="cpn">
  <div>
    <h2>{{cmessage}}</h2>
    <ul>
      <li v-for="item in ccolor">{{item}}</li>
    </ul>
    <h2>{{chmessage}}</h2>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const ccpn = {
    template: '#cpn',
    data() {
      return {
        cmessage: "fo"
      }
    },
    // props: ["ccolor", "chmessage"],
    props: {
      chmessage: {
        type: String,
        default: "hello",
        required: true,
      },
      ccolor: {
        type: Array,
        default() {
          return [];
        }
      }
    }
  };
  const app = new Vue({
      el: "#app",
      data: {
        message: "hello vue",
        color: ["red", "black", "yellow"]
      },
      methods: {},
      components: {
        "c-cpn": ccpn,
      }
    }
  )
</script>

子级向父级传递

当子组件需要向父组件传递数据时,就要用到自定义事件了。之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
自定义事件的流程:
在子组件中,通过$emit()来触发事件。在父组件中,通过v-on来监听子组件事件。
在当页面应用,子组件向父组件通信中所传递的事件名称不能是驼峰式。若要修改子组件中通过父子件传来的值,不应该在子组件中直接操作,而应通过父组件。
父子通信.png

父子组件的访问

有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
父组件访问子组件:使用$children$refs。子组件访问父组件:使用$parent

$children

先来看下$children的访问。this.$children是一个数组类型,它包含所有子组件对象。
children.png
我们这里通过一个遍历,取出所有子组件的message状态。
$children的缺陷:
通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs

$refs

$refs的使用:
$refsref指令通常是一起使用的。首先,我们通过ref给某一个子组件绑定一个特定的ID。其次,通过this.$refs.ID就可以访问到该组件了。

<div id="app">
  <cpn></cpn>
  <cpn ref="a2"></cpn>
  <cpn></cpn>
  <button @click="getChildInfo">获取子组件信息</button>
</div>
<template id="cpn">
  <div>
    我是子组件
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const cpn = {
    template: "#cpn",
    data() {
      return {}
    }
  };
  const app = new Vue({
    el: "#app",
    data: {},
    methods: {
      getChildInfo() {
        console.log(this.$refs.a2);
      }
    },
    components: {
      cpn,
    }
  })
</script>
$parent

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

此外,还可以通过$root访问根组件。

非父子组件通信

使用Vuex

监听属性watch

可以通过 watch 来响应数据的变化。
里面的函数名必须为data中定义的属性,里面可以传两个值:newValue,oldValue。

<div id="app">
  <cpn @num1-change="num1Change" @num2-change="num2Change" :pnum1="num1" :pnum2="num2"></cpn>
</div>
<template id="cpn">
  <div>
    <p>
      父数据1的值:{{pnum1}}
      <input type="text" v-model="cnum1">
      子组件2的值:{{cnum1}}
    </p>
    <p>
      父数据2的值:{{pnum2}}
      <input type="text" v-model="cnum2">
      子组件2的值:{{cnum2}}
    </p>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  //子组件
  const cpn = {
    template: "#cpn",
    data() {
      return {
        cnum1: this.pnum1,
        cnum2: this.pnum2,
      }
    },
    props: {
      pnum1: Number,
      pnum2: Number
    },
    methods: {},
    watch: {
      cnum1(newValue) {
        this.cnum2 = newValue * 2;
        this.$emit("num1-change", newValue);
      },
      cnum2(newValue) {
        this.cnum1 = newValue / 2;
        this.$emit("num2-change", newValue);
      }
    }
  };
  //根组件
  const app = new Vue({
    el: "#app",
    data: {
      num1: 2,
      num2: 4,
    },
    methods: {
      num1Change(value) {
        this.num1 = value;
      },
      num2Change(value) {
        this.num2 = value;
      }
    },
    components: {
      cpn,
    }
  })
</script>

watch 默认是浅监听。(只监听表层的变化。)对于引用类型(对象数组)的浅监听:只能监听自身一层,他的子层及以下改变监听不到。watch 如何是深度监听?在属性中设置:deep: true
注意:watch 监听引用类型时,是拿不到oldVal。因为指针相同,新值旧值指向同一个堆的地址。此时已经指向了新的val。

data() {
    return {
        msg: "这是首页",
        counter: 0,
        info:{
            city:"beijing"
        }
    }
},
watch: {
    //值类型,可正常拿到oldVal 和 newVal
    counter: function (newVal, oldVal) {
        alert('计数器值的变化 :' + oldVal + ' 变为 ' + newVal + '!');
    },
    //引用类型,拿不到oldVal,因为指针相同,指向同一个堆的地址。此时已经指向了新的val
    info: {
        handler(oval, nval) {
            console.log("watch info", oval, nval);
        },
        deep: true //深度监听
    }
}

插槽slot

使用插槽的原因

组件的插槽:
组件的插槽也是为了让我们封装的组件更加具有扩展性。让使用者可以决定组件内部的一些内容到底展示什么。
例子:移动网站中的导航栏。移动开发中,几乎每个页面都有导航栏。导航栏我们必然会封装成一个插件,比如nav-bar组件。一旦有了这个组件,我们就可以在多个页面中复用了。但是,每个页面的导航并不是一样的?
如何封装这类组件
抽取共性,保留不同。
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。是搜索框,还是文字,还是菜单。由调用者自己来决定。

slot基本使用

在子组件中,使用特殊的元素<slot>就可以为子组件开启一个插槽。该插槽插入什么内容取决于父组件如何使用。
1.插槽的基本使用 <slot></slot>
2.插槽的默认值 <slot>button</slot>
3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素。

slot.png

具名插槽slot

当子组件的功能复杂时,子组件的插槽可能并非是一个。比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
这个时候,我们就需要给插槽起一个名字。
如何使用具名插槽呢?
非常简单,只要给slot元素一个name属性即可<slot name='myslot'></slot>给出一个案例:这里我们先不对导航组件做非常复杂的封装,先了解具名插槽的用法。

<div id="app">
  <cpn>
    <h4 slot="middle">haha</h4>
  </cpn>
</div>
<template id="cpn">
  <div>
    <slot>左边</slot>
    <slot name="middle">中间</slot>
    <slot>右边</slot>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {},
    methods: {},
    components: {
      cpn: {
        template: "#cpn",
      }
    }
  })
</script>
//左边
//haha
//右边

作用域插槽

编译作用域

先看一个例子:考虑下面的代码是否最终是可以渲染出来的:
编译作用域.png
答案:最终可以渲染出来,也就是使用的是Vue实例的属性。
解释:官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。而在使用<my-cpn v-show="isShow"></my-cpn>的时候,整个组件的使用过程是相当于在父组件中出现的。那么他的作用域就是父组件,使用的属性也是属于父组件的属性。因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。

作用域插槽

父组件对子组件展示数据的方式不满意,他要以自己方式展示,就需要从子组件中获取数据。父组件替换插槽的标签,但是内容由子组件来提供。
作用域插槽1.png
作用域插槽2.png

说明:
子组件中:data也可以其他名称,但是不能使用驼峰式。
在vue2.5.x以上的版本中,<template>可以替换为其他标签


梁柱
135 声望12 粉丝

« 上一篇
Vue基础语法
下一篇 »
4.Webpack详解