Vue 07 组件通信和高级用法

Vue 07 组件通信和高级用法

组件通信综述 子组件向父组件传递数据:v-on监听自定义事件 子组件向父组件传递数据:v-model绑定 非父子组件通信 使用slot 父组件向子组件传递数据:props使用字符串数组 作用域插槽 访问slot 组件高级用法 异步更新DOM后执行代码 X-Template

  1. 组件通信综述
  2. 子组件向父组件传递数据:v-on监听自定义事件
  3. 子组件向父组件传递数据:v-model绑定
  4. 非父子组件通信
  5. 使用slot
  6. 父组件向子组件传递数据:props使用字符串数组
  7. 作用域插槽
  8. 访问slot
  9. 组件高级用法
  10. 异步更新DOM后执行代码
  11. X-Template
  12. 手动挂载实例

组件通信

父组件单向将数据传递给子组件只是通信的一种方式。实际上组件通信可以分为:
  1. 父子组件通信
  2. 兄弟组件通信
  3. 跨级组件通信
这些还都分为单向和双向。在组件中学习的props只是父组件传数据给子组件的方式。还有很多传递数据的方式。

子组件向父组件传递数据

Vue提供了子组件用$emit()来触发事件,父组件$on()来监听子组件的事件。 在实际开发中,可以在子组件的标签上使用v-on来监听子组件触发的自定义事件
<div id="app">
    <p>传给子组件的是{{count}}</p>
    <my-comp :child-count="count" @event-from-child="fatherFunction"></my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                props:["childCount"],
                template: "<div>" +
                    "<button @click='myFunction1'>++</button>" +
                    "<button @click='myFunction2'>--</button></div>",
                methods: {
                    myFunction1: function () {
                        this.initCount++;
                        this.$emit('event-from-child', this.initCount);
                    },
                    myFunction2: function () {
                        this.initCount--;
                        this.$emit('event-from-child', this.initCount);
                    }
                },
                data: function () {
                    return {
                        initCount: this.childCount
                    }
                }
            }
        },
        data: {
            message: [1, 2, 3, 4, 5],
            count: 0
        },
        methods: {
            fatherFunction(value) {
                console.log("从子组件传来的数据是:" + value);
            }
        }
    })
</script>
注意这里红色的几行。先看子组件,子组件从父组件接收了count属性,并用一个initCount存放了这个属性。这样做到数据和父组件隔离。 此后子组件两个按钮定义了两个事件,一个减少自己的initCount,一个增加自己的initCount,然后使用了特殊的$emit(自定义事件名称,数据)方法来生成一个自定义事件。 父组件将这个自定义事件的名称写在子组件的元素标签内,对其监听并用自己的fatherFunction去处理。 fatherFunction参数直接就是子组件传递来的数据。子组件的$emit可以传递多个数据参数,在父组件的事件内也提供同样多的参数进行接收即可。 通过自定义事件,就可以实现子组件向父组件传递数据。 除了直接监听自定义事件之外,还可以监听原生的DOM事件,在上边的例子里如果还想监听子组件内所有的按钮被按下的事件,可以增加如下代码:
<div id="app">
    <p>传给子组件的是{{count}}</p>
    <my-comp :child-count="count" @event-from-child="fatherFunction" @click.native="clickhandler"></my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                props: ["childCount"],
                template: "<div>" +
                    "<button @click='myFunction1'>++</button>" +
                    "<button @click='myFunction2'>--</button></div>",
                methods: {
                    myFunction1: function () {
                        console.log(this.initCount);
                        this.$emit('event-from-child', this.initCount, this.initCount + 10, this.initCount + 100);
                        this.initCount++;
                    },
                    myFunction2: function () {
                        console.log(this.initCount);
                        this.$emit('event-from-child', this.initCount, this.initCount + 10, this.initCount + 100);
                        this.initCount--;
                    }
                },
                data: function () {
                    return {
                        initCount: this.childCount
                    }
                }
            }
        },
        data: {
            message: [1, 2, 3, 4, 5],
            count: 0
        },
        methods: {
            fatherFunction(value, value1, value2) {
                console.log("从子组件传来的数据是:" + value);
                console.log("从子组件传来的数据是:" + value1);
                console.log("从子组件传来的数据是:" + value2);
            },
            clickhandler:function () {
                console.log("我被按了");
            }
        }
    })
</script>

v-model监听事件

v-model其实是一个语法糖,会自动监听input事件并从中获得数据,本质上还是监听自定义事件。看一个简单的例子
<div id="app">
    <p v-if="total">从子组件传来的数据是{{total}}</p>
    <my-comp v-model="total"></my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                template: "<div><button @click='myFunction'>++</button></div>",
                methods: {
                    myFunction: function () {
                        this.count++;
                        this.$emit('input', this.count);
                    },
                },
                data: function () {
                    return {
                        count: 0
                    }
                }
            }
        },
        data: {
            total: 0
        },
        methods: {
            fatherFunction(value) {
                this.total = value;
                console.log("从子组件传来的数据是:" + value);
            },
        }
    })
</script>
这个东西其实就是一个语法糖,需要使用v-model语法糖,需要满足两个条件:
  1. v-model在子组件元素上绑定父组件一个data属性
  2. 子组件必须使用$emit产生名称为input的事件并传递一个value值
有了父子组件通信之后,很多想法就可以实现了,比如组件是一个表单,接受输入之后传给父组件。

非父子组件通信

很多时候可能还需要非父子组件之间通信,比如两个各渲染一块内容的组件通信。 这个时候推荐用一个专门的Vue实例,来做事件中介。在进阶的时候再来学习这个。 现在先来看两种方式:
  1. 父链
  2. 子组件索引
父链其实就是在组件中去直接访问父级组件和子组件,采用this.$parent就可以访问当前组件的父级组件,this.$children则可以访问子组件。 可以一层一层向上或者向下访问,可以取得对组件的完全控制。 尽管父链和子链是Vue提供的功能,但实际中最好不要去写直接操作父子组件的代码,这样会造成高耦合,如果是单纯的父子组件,最好还是使用之前介绍的通信方法。 一个父组件有多个子组件是很常见的,如果通过this.$children遍历,由于渲染顺序不固定,比较麻烦。Vue提供了子组件索引的方法,用ref属性再子组件标签上为子组件指定一个索引名称,之后就可以比较方便的通过this.$refs.xxx访问该子组件。
<div id="app">
    <button @click="clickhandler">从子组件获取消息</button>
    <my-comp ref="child"></my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                template: "<div><button>++</button></div>",
                data: function () {
                    return {
                        count: 0,
                        message: "子组件的内容"
                    }
                }
            }
        },
        data: {
            total: 0,
            message:""
        },
        methods: {
            clickhandler() {
                this.message = this.$refs.child.message;
                console.log("从子组件传来的数据是:" + this.message);
            },
        }
    })
</script>

使用单个slot

有的时候需要混合父组件与子组件的模板,就会用到slot,就是内容分发。 用一个形象的说明就是,一个子组件模板的一块区域是留给父组件的,像一个插槽一样,其实际渲染的部分,是父组件渲染出来的。 一个Vue组件对外交互的API,其实就是三块内容,之前学过了props,还有事件传递,现在还有第三个,就是slot。 在子组件内使用特殊的<slot>就可以为这个组件开启一个插槽,父组件可以把渲染的内容插进来。
<div id="app">
    <my-comp>
        <p>{{fathermessage}}</p>
    </my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                template: "<div><slot><p>子组件的默认内容</p></slot></div>",
                data: function () {
                    return {
                        count: 0,
                    }
                }
            }
        },
        data: {
            fathermessage: "父组件的信息",
        }
    })
</script>
在子组件元素中的内容,也就是<p>{{fathermessage}}</p>,是父组件插入给子组件的内容,注意这里的作用域是父组件而不是子组件。 在子组件的slot标签中的HTML元素,是父组件没有插入内容的时候,默认显示的内容。slot标签中的内容是子组件的作用域。 这个例子渲染之后的实际结果是父组件的信息,如果删除<p>{{fathermessage}}</p>,则会显示子组件的默认内容。

具名slot

在父组件的渲染区域,可以给元素上添加slot属性,用于指定一个命名的插槽。在组件内,则要通过slot标签的name属性来指定对应名称的插槽。 指定了名称之后,父组件渲染的元素就会插到子组件同名的插槽中。如果子组件有一个不带有name属性的slot标签,则父元素所有不具名的插入元素都会被收集到这个无名的slot标签中。
<div id="app">
    <my-comp>
        <p slot="slot1">这是插入给slot1插槽的内容</p>
        <p slot="slot2">这是插入给slot2插槽的内容</p>
        <p>这是父元素的不具名插槽的内容</p>
    </my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                template: "<div><slot name='slot2'></slot><slot name='slot1'></slot><slot></slot></div>",
                data: function () {
                    return {
                        count: 0,
                        message: "子组件的内容"
                    }
                }
            }
        },
        data: {
            total: 0,
            fathermessage: "父组件的信息",
            message: "",
        }
    })
</script>
可以看到父组件渲染的内容分别插入到了不同的插槽中,通过结果页面的顺序就可以看到。如果没有匿名的slot,则父组件所有不具名的slot渲染内容都会被抛弃。 可以看到内容分发API也是非常重要的。

作用域插槽

作用域插槽使用一个模板替换已经渲染的元素,先看一个例子:
<div id="app">
    <my-comp>
        <template slot-scope="props">
            <p>父组件渲染的内容</p>
            <p>{{props.msg}}</p>
        </template>
    </my-comp>
</div>

<script>
    var app = new Vue({
        el: "#app",
        components: {
            "my-comp": {
                template: "<div><slot msg='子组件渲染的内容'></slot></div>",
            }
        }
    })
</script>
这个作用域插槽在Vue 2.5之后,要使用slot-scope属性,这个属性的值是一个临时变量。 在template之内的父组件的渲染内容里,访问这个临时变量中,子组件在插槽上设置的同名属性,即可渲染成具体的值。 这个作用域插槽的意义何在呢?实际上可以把数据集中存放在父组件中,子组件只负责具体渲染。看一个常用的列表的例子:
<div id="app">
    <my-list :books="books">
        <!--        作用域插槽具名slot-->
        <template slot="book" slot-scope="props">
        <p>父组件自己想插入到每个插槽的内容</p>
        <!--            插入到子组件插槽中的li元素+子组件的书名的内容-->
            <li>{{props.bookName}}</li>
        </template>
    </my-list>
</div>

<script>
    Vue.component('my-list', {
        props: {
            books: {
                type: Array,
                default: function () {
                    return [];
                }
            }
        },
        template: '<ul><slot name="book" v-for="book in books" :book-name="book.name"></slot></ul>'
    });

    var app = new Vue({
        el: "#app",
        data: {
            books: [
                {
                    name: "Vue实战",
                },
                {
                    name: "JS Promise",
                },
                {
                    name: "TypeScript实战",
                },
            ]
        }
    })
</script>
在这个例子里,父子组件先通过props,父组件把books对象数组传递给子组件。子组件拿到了books对象数组后,实际上在模板里,使用v-for弄出了一堆slot,然后分别将每个slot的book-name属性(注意HTML标签和驼峰对应关系)设置上。父组件在template中插入插槽的是li加上每个插槽的book-name属性的渲染内容。 通过渲染结果可以看到,父组件的slot插入到了子组件的每一个同名的插槽之中。这实际上是一对多的分发,很有意思。 如果采用之前的例子,直接在父组件中使用v-for,则父组件内既有数据又负责渲染。有了插槽之后,结合props,父组件可以做到只分发数据而且控制渲染的位置,子组件负责具体渲染,有效解耦。 作用域插槽的使用场景就是既可以复用子组件的slot ,又可以使slot内容不一致。如果上例还在其他组件内使用,li标签的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。

访问slot

之前都是将内容分发到slot中进行渲染,如果想要便捷的获取渲染后的slot对应的元素,可以使用$slots.slotName来获取。不具名的slot对象都会存放在slot.default中。
<div id="app">
    <my-list>
        <template slot="slot1">
            <p>第一个插槽内容</p>
        </template>

        <template slot="slot2">
            <p>第二个插槽的内容</p>
        </template>

        <template>
            <p>不具名插槽的内容</p>
        </template>
    </my-list>
</div>

<script>
    Vue.component('my-list', {
        props: {
            books: {
                type: Array,
                default: function () {
                    return [];
                }
            }
        },
        template: '<p>' +
            '<slot name="slot1" >11</slot>' +
            '<slot name="slot2" >22</slot>' +
            '<slot>33</slot>' +
            '</p>',
        mounted:function () {
            var slot1 = this.$slots.slot1;
            var slot2 = this.$slots.slot2;
            var nonameslot = this.$slots.default;
            console.log(slot1);
            console.log(slot2);
            console.log(nonameslot);
        }
    });

    var app = new Vue({
        el: "#app",
    })
</script>
在生命周期刚挂载到HTML元素的时候,打印出了获取的插槽。是一些vnode对象。

组件高级用法

组件的高级用法一般不用在写业务逻辑,而是用于开发组件中。可以看一下。

组件的name递归调用自身

常用于开发级联的菜单,不知道有几层,所以很方便。不过必须给一个条件来限制最大递归数量。 看一个简单的例子:
<div id="app">
    <my-comp :count="1">

    </my-comp>
</div>

<script>
    Vue.component('my-comp', {
        name:'my-comp',
        props: {
            count: {
                type: Number,
                default: 1
            }
        },
        template: '<p>这是第{{count}}层递归' +
            '<my-comp :count="count+1" v-if="count<3"></my-comp>' +
            '</p>'
    });

    var app = new Vue({
        el: "#app",
    })
</script>
先给自己指定一个名称,这里名称和注册组件的名称一样,然后在自己的模板里就可以使用了。 每次先给count自增1,然后判断vi-if是否小于3,就可以作出递归的效果。每次相当于又添加了一个子组件,并给子组件传递了count自增之后的结果。 由这个例子还可以看出,在判断v-if和其他属性变动的时候,v-if是先于属性自增判断的。

内联模板

组件的模板是template属性中的字符串,会被解析为HTML然后再被Vue解析。 如果给组件添加inline-template属性,组件就会把组件标签其中的内容当成模板,而不是当成内容分发的slot插槽内容,这让组件更加灵活。
<div id="app">
    <my-comp inline-template>
        <div>
        <h1>内联模板标题</h1>
        <p>内联模板内容</p>
        </div>
    </my-comp>
</div>

<script>
    Vue.component('my-comp', {
        template: '<div>template属性中的内容</div>'
    });

    var app = new Vue({
        el: "#app",
    })
</script>
这个例子,子组件template属性完全不生效。 如果写成:
<div id="app">
    <my-comp inline-template>
        <h1>内联模板标题</h1>
        <p>内联模板内容</p>
    </my-comp>
</div>
就只能渲染出来第一个HTML元素,可见内联模板一样也要遵守template属性中的要求,必须用一个HTML元素包围所有要渲染的内容。

指定要渲染的组件

Vue提供了一个特殊的HTML元素component,和is属性搭配,用于指定要挂载的组件:
<div id="app">
    <component is="comp1"></component>
    <hr>
    <component is="comp2"></component>
</div>

<script>
    Vue.component('comp1', {
        template: '<div>comp1的内容</div>'
    });
    Vue.component('comp2', {
        template: '<div>comp2的内容</div>'
    });

    var app = new Vue({
        el: "#app",
    })
</script>

异步组件

Vue允许将组件定义成一个工厂函数,在需要的时候调用函数来返回组件进行渲染,还可以把结果缓存起来。 这里先简单看一个小例子:
<div id="app">
    <comp></comp>
</div>

<script>
    Vue.component('comp', function (resolve, reject) {
        window.setTimeout(function () {
            resolve({
                template: '<div>异步渲染的组件</div>'
            })
        })

    }, 2000);

    var app = new Vue({
        el: "#app",
    })
</script>
更复杂的就是将函数换成异步的promise等方式。这个暂时还不会。待以后再看。

异步更新DOM后执行代码

看一个例子:
<div id="app">
    <div id="div" v-if="showDiv">文本</div>
    <button @click="getText">获取内容</button>
</div>

<script>
    var app = new Vue({
        el: "#app",
        data: {
            showDiv: false,
        },
        methods: {
            getText: function () {
                this.showDiv = true;
                var text = document.getElementById("div").innerText;
                console.log(text);
            }
        }
    })
</script>
这个例子的意图很明显,就是在按下按钮的时候,将文本显示出来,同时将其中的文本输出到控制台。 不过在实际操作的时候,第一次按按钮的时候会报错如下:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'innerText' of null"

(found in <Root>)

TypeError: Cannot read property 'innerText' of null
    at Vue.getText (component.html?_ijt=ktcljrpg9soilp14ao257j93ta:26)
    at invokeWithErrorHandling (vue.js:1863)
    at HTMLButtonElement.invoker (vue.js:2188)
    at HTMLButtonElement.original._wrapper (vue.js:7541)
这说明在更新了false之后,其实Vue并没有立刻渲染出这个div,导致获取文本失败。这其实是因为Vue在异步更新队列,会先检测变化,再进行更新。在设置属性为true的时候,在下一个事件循环,Vue检测到属性有变化,才会去创建div,而此时异步的立刻去获取div就会报错。 解决的方法是使用一个$nextTick来在DOM更新完毕后再执行代码:
<div id="app">
    <div id="div" v-if="showDiv">文本</div>
    <button @click="getText">获取内容</button>
</div>

<script>
    var app = new Vue({
        el: "#app",
        data: {
            showDiv: false,
        },
        methods: {
            getText: function () {
                this.showDiv = true;
                this.$nextTick(function () {
                    var text = document.getElementById("div").innerText;
                    console.log(text);
                });
            }
        }
    })
</script>
Vue的理念是不要直接操作DOM,而是以数据驱动DOM。如果要使用第三方库,而第三方库是直接操作DOM的,就要使用这个功能。

X-Template

X-Templates是Vue为了组件的模板编写方便而提供的一个便捷的语法糖。 在前边的例子里,编写组件的模板都要通过字符串拼接,行数多了很麻烦。 Vue提供了一个在script标签上使用的type="text/x-template"属性:
<div id="app">
    <comp></comp>
</div>

<script>
    Vue.component("comp",{
        template: "#comp"
    });


    var app = new Vue({

    })
</script>
<script type="text/x-template" id="comp">
    <p>标签中的模板</p>
</script>
这里给script标签指定了type和id,在组件的template属性里,像用选择器一样选择script标签的id。 然后其中的所有内容就被当成了模板渲染,非常方便。
<div id="app">
    <comp></comp>
</div>

<script type="text/x-template" id="comp">
    <div>
        <p>标签中的模板</p>
        <p>{{ count }}</p>
    </div>
</script>

<script>
    Vue.component('comp', {
        template: "#comp",
        data: function () {
            return {
                count: 1
            }
        },
    });

    var app = new Vue({
        el: "#app"
    })
</script>
这里要注意的是,由于是在加载Vue的时候扫描DOM,所以放模板的script标签,要在使用这个模板的Vue代码之前出现。

手动挂载实例

如果一个Vue实例没有el属性或者尚未设置,这个Vue实例就还没有被挂载。 在一些特殊的情况下,可能要手动创建Vue实例然后再指定挂载,Vue提供了extend$mount两个方法手动挂载。

Vue.extend

这是一个基础的Vue实例构造器,接受一个包含vue实例所需要的属性的对象,然后会返回一个“类”,其实就是一个Vue实例的构造器。 然后可以手动使用$mount来挂载:
<div id="mount">
</div>

<script>
    vueobj = Vue.extend({
        template: "<p>新创建的Vue实例 {{name}}</p>",
        data: function () {
            return {
                name: "Jenny"
            }
        }
    })
</script>
现在有了一个Vue.extend返回的构造器,还有一个空的div。 可以在浏览器的调试界面内输入:
var target = new vueobj().$mount("#mount")
$mount也类似一个选择器,选择指定的id元素挂载Vue,输入完这行之后,原来是空的div就会出现内容了,即被挂载上了Vue实例,div的内容会被替换成模板内容。 还可以新创建Vue实例的时候传入el属性:new vueobj({el:"#mount"}) 另外还有一个挂载方式和这个不同,不是直接替换元素,而是把一个Vue实例挂到某个元素下边:
var target = new vueobj().$mount();
document.getElementById("mount").appendChild(target.$el)
这样是把Vue实例作为div的子元素挂载。 总结一下,以前一直没有学Vue的组件。看完之后,发现这个东西实际上就想一个Java类,可以在标签上注入所需要的基础数据,然后可以对其各种操作改变状态,同时其对应的DOM元素也会发生变化,还能够通过API来与父组件或者其他组件进行交互。这样就把一个统一渲染的页面,变成了一块一块背后由Vue实例控制的区域,这些区域之间可以互相传递数据,确实很不错。后边根据书上的两个例子写了组件,发现确实很有意思。
LICENSED UNDER CC BY-NC-SA 4.0
Comment