Vue 组件学习 (2):slot 内容分发,编译作用域,动态切换组件

4,680 阅读9分钟
原文链接: blog.csdn.net

组件这内容太多了,被迫分成2部分。上节最后讲了props进行父子组件通讯,这里接着来。

  1. 父子组件通信

1.1 Vue组件是树形结构,必然要涉及到怎么查找父节点,根节点,子节点。子组件可以用 this.$parent 访问它的父组件。根实例的后代可以用 this.$root访问它。父组件有一个数组 this.$children,包含它所有的子元素。

1.2 Vue也提供了自定义事件,用于组件树通信,与原生的Dom事件是不一样的,不能混为一谈。

每个 Vue 实例都是一个事件触发器:

使用 $on() 监听事件;

使用 $emit() 在它上面触发事件;

使用 $dispatch() 派发事件,事件沿着父链冒泡;

使用 $broadcast() 广播事件,事件向下传导给所有的后代。

ps:Vue 事件在冒泡过程中第一次触发回调之后自动停止冒泡,除非回调明确返回 true

1.3 原文里的栗子,赶脚很标准

<!-- 子组件模板 -->
<template id="child-template">
  <input v-model="msg">
  <button v-on:click="notify">Dispatch Event</button>
</template>

<!-- 父组件模板 -->
<div id="events-example">
  <p>Messages: {{ messages | json }}</p>
  <child></child>
</div>
<script>
// 注册子组件
// 将当前消息派发出去
Vue.component('child', {
  template: '#child-template',
  data: function () {
    return { msg: 'hello' }
  },
  methods: {
    notify: function () {
      if (this.msg.trim()) {
        this.$dispatch('child-msg', this.msg)
        this.msg = ''
      }
    }
  }
})

// 初始化父组件
// 将收到消息时将事件推入一个数组
var parent = new Vue({
  el: '#events-example',
  data: {
    messages: []
  },
  // 在创建实例时 `events` 选项简单地调用 `$on`
  events: {
    'child-msg': function (msg) {
      // 事件回调内的 `this` 自动绑定到注册它的实例上
      this.messages.push(msg)
    }
  }
})
</script>

刚好用这个栗子梳理下前面学的那些,
当button被点击,会触发Vue指令v-on的click动作,click里面是提前指定的method:notify。notify被执行,this.$dispatch(‘child-msg’, this.msg)向上派发事件event:child-msg,此时parent实例监听到该事件,执行方法将msg放到messages数组中,messages数组发生变化刷新界面,html这里<p>Messages: {{ messages | json }}</p>因为设置了过滤器json,会将messages按照json语法输出。恩,棒棒哒!

1.4 子组件可以用 v-on 监听自定义事件:

<child v-on:child-msg="handleIt"></child>

这样就很清楚了:当子组件触发了 “child-msg” 事件,父组件的 handleIt 方法将被调用。所有影响父组件状态的代码放到父组件的 handleIt 方法中;子组件只关注触发事件。

这里看的我又是一脸懵逼,确定没有漏掉什么?上网找了另一个栗子

<div id="app"> 
父组件: 
<button>点击向下传播broadcast</button> 
<br/> 
子组件1: 
<!--绑定写在这里,可以多个绑定同一个,或者不同绑定不同的,但不能一个绑定多个--> 
<children v-on:test="parent" @test2="another"></children> 
</div> 
<script> 
var vm = new Vue({ 
    el: '#app', 
    data: { 
        val: 1 
    }, 
    methods: { 
        parent: function (arg) { 
        console.log(arg); 
        console.log("the first method with test event"); 
    }, 
    another: function () { 
        console.log("another method"); 
    }, 
    components: { 
        children: { //这个无返回值,不会继续派发 
            props: ['test'], 
            template: "<button @click='childClick'>children1</button></br><button @click='childClick2'>children1</button>", 
            methods: { 
                childClick: function () { 
                this.$emit("test", 'the argument for dispatch'); 
                }, 
                childClick2: function () { 
                    this.$emit("test2"); 
                } 
            }, 
            events: { 
                test: function () { 
                console.log("test"); 
                }, 
                test2: function () { 
                console.log("test2"); 
                } 
            } 
        } 
    } 
}); 
</script>

这个结构虽然与现在看到的不一样,但是也能看到监听自定义事件是怎么回事。
<children v-on:test="parent" @test2="another"></children> children上监听两个自定义事件,test和test2,这两个事件直接属于子组件。同时渲染的时候,会替换children组件为其模板内容:

<button @click='childClick'>children1</button></br>
<button @click='childClick2'>children2</button>

点击 children1触发子组件方法childClick,此时this.$emit(“test”, ‘the argument for dispatch’); 响应事件test,然后被子组件本身监听到,然后执行事件test,console.log(“test”); 如果把子组件的methods和events移到父组件,就是本节说的那种情况了,所有影响父组件状态的代码放到父组件的方法中;子组件只关注触发事件。

1.5 尽管有 props 和 events,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 v-ref 为子组件指定一个索引 ID。例如:

<div id="parent">
  <user-profile v-ref:profile></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile

v-ref 和 v-for 一起用时,ref 是一个数组或对象,包含相应的子组件。

国庆归来接着写。

2.slot分发内容
这个东西看官方原文,看的我一愣一愣的,真心感觉漏了啥是不是没说明,各种看不懂,经过多次验证终于明白。

编译作用域的问题暂时跳过,先看看slot。

2.1 先来看看slot是怎么用的,什么叫分发内容。

<template id="slot-template">
    <div>
          <h1>This is my component!</h1>
         <slot>
           如果没有分发内容则显示我。
         </slot>
    </div>
</template>

<div id="div2">
    <child2>
         <p>This is some original content</p>
    </child2>
<div>

<script>

    Vue.component('child2',{
        template:'#slot-template'
    });

    new Vue({
        el:"#div2"
    });
</script>

先梳理下模板组件这些概念,隔了一周有点忘了。这里创建了一个child2的组件,这个组件使用id=slot-template的模板,这个模板在id=div2的块中使用。
这里多了slot的内容,先看看运行结果,会被渲染成为:

<div id="div2">
    <div>
          <h1>This is my component!</h1>
         <p>This is some original content</p>
    </div>
<div>

所以可以看出slot就相当于一个模板中的占位。把组件中的内容<p>This is some original content</p>替换给slot。

2.2 这时候突然能够理解编译作用域的问题,原文是这样的:

<child-component>
  {{ msg }}
</child-component>
msg 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件

跟这里的child2的情况是一样的,child2作为子组件,其中的内容替换了slot,岂不是变成了父组件的数据?

再来看看它后面的例子:

一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:

<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。

这里看的也是懵逼,其实是因为不明白什么是父组件,什么是子组件。那child-component肯定是子组件没错,父组件是什么呢?父组件的就是child-component的上一层。还记得本文1.3里面的那个栗子吗?parent就是父组件,child就是子组件,所以相当于这种结构:

<parent>
     <child v-show="someChildProperty"><child>
</parent>

当child使用v-show指令时看似,是在子组件中使用someChildProperty。然而并不是,代码是写在<parent>中的,它的编译作用域是属于父组件的,也就是父组件调用了someChildProperty。如果someChildProperty是一个子组件属性,这时候必然是不阔以的。

所以原文提供了正确的姿势:

Vue.component('child-component', {
  // 有效,因为是在正确的作用域内
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

看似两种写法一样,其实不一样,someChildProperty是在{}中被调用的,编译作用域属于子组件,这是阔以的。所以再来理解下slot为什么叫内容分发,child里写的内容最后发给了parent父组件。

2.3 具名slot,其实就是给slot命名。命名的目的就是为了根据名字来分发内容,哪个内容是发给哪个slot的,看下原文的栗子:

<div>
  <slot name="one"></slot>
  <slot></slot>
  <slot name="two"></slot>
</div>
//子组件(ps:原文这里是父组件模板,它瞄的不对头啊)
<multi-insertion>
  <p slot="one">One</p>
  <p slot="two">Two</p>
  <p>Default A</p>
</multi-insertion>
渲染结果为:

<div>
  <p slot="one">One</p>
  <p>Default A</p>
  <p slot="two">Two</p>
</div>

multi-insertion是一个组件,这个组件有三条内容,其中slot=”one”说明了第一条内容<p slot="one">One</p>到时候要替换父组件中名为one的slot。two情况类似,最后一条未命名,指定给未命名的slot,所以渲染结果就是这样。这就是内容分发。

3.动态切换组件。看见这个东东感觉有点略屌啊,岂不是说在一个位置动态切换内容就靠它了。
3.1看看怎么用:

<div id="div3">
    <component :is="currentView">
  <!-- 组件在 vm.currentview 变化时改变 -->
    </component>
</div>

<script>
    new Vue({
        el:"#div3",
        data: {
         currentView: 'component1'
        },
        components:{
            component1:{template:"<p>hello1</p>"},
            component2:{template:"<p>hello2</p>"},
            component3:{template:"<p>hello3</p>"}
        }
    });  
</script>

这里:is相当于v-bind:is。is属性在前面用过,还记得不:

<tr is="my-component"></tr>

相当于:

<tr><my-component></my-component></tr>

只不过当时没有用v-bind,v-bind 指令用于响应地更新 HTML 特性,所以因为要切换组件所以用上。

渲染结果为:

<div id="div3">
    <p>hello1</p>
</div>

这里采用了一种匿名的方式创建组件,也可以改成我们习惯的方式,但是这种写法好像更简单。为了动态切换组件我们可以添加一下代码:

 var i = 1;
    function tip(){
        i = i%3+1;
        div3Vue.currentView='component'+i;
    }
    setInterval(tip,2000);

写了个全局变量i,每2秒切换一次组件,这样就可以动态变化内容了。
但是全局变量要少用,这时候我们想起了最早看的闭包,改一下写法:

 var tip = function(){
        var i = 1;
        return function(){
            i = i%3+1;
            div3Vue.currentView='component'+i;
        };
    }();

    setInterval(tip,2000);

3.2 keep-alive

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:

<component :is="currentView" keep-alive>
  <!-- 非活动组件将被缓存 -->
</component>

3.3 activate回调(ps:原文叫钩子实在听不懂)
在切换组件时,切入组件在切入前可能需要进行一些异步操作。为了控制组件切换时长,给切入组件添加 activate 回调:

Vue.component(‘activate-example’, {
activate: function (done) {
var self = this
loadDataAsync(function (data) {
self.someData = data
done()
})
}
})
所谓回调,大概是这么一种概念。经常老妈要出门,锅里又煮着东西,这个时候她会交给你一个任务,{水开了关下火},这时候你就会执行这个任务。执行时机:水开,执行方法:关下火。为什么叫做回调呢?把这个过程抽象成代码:
老妈类里面写着水开的时候调用以下方法
function a(){
关下火;
}
这个时候你发现水开了,就会去执行该方法,a方法并不是你写的,但是具体时间到了就会去执行这个方法,所以回到老妈类去查找这个方法具体是什么。如果写的是”放点盐”,这个时候你执行的就变成了放点盐。

所以看看这里的activate,很明显这里注册了个组件activate-example,同时添加了个回调方法activate。故名思议,执行时机:activate(这个组件存活时),执行内容:就是后面的function了。这用在切换组件里特别有用,比如刚才的component1,component2,component3,你想让他们在切换的同时做点其他事情,比如弹窗,就可以这么写

components:{
            component1:{
                template:"<p>hello1</p>",
                activate:function(){
                    alert("我是1");
                }
            },
            component2:{template:"<p>hello2</p>"},
            component3:{template:"<p>hello3</p>"}
        }

这时,只要切换到了component1时就会弹窗了。
注意 activate回调只作用于动态组件切换或静态组件初始化渲染的过程中,不作用于使用实例方法手工插入的过程中。

3.4 transition-mode

transition-mode 特性用于指定两个动态组件之间如何过渡。

在默认情况下,进入与离开平滑地过渡。这个特性可以指定另外两种模式:

in-out:新组件先过渡进入,等它的过渡完成之后当前组件过渡出去。

out-in:当前组件先过渡出去,等它的过渡完成之后新组件过渡进入。

示例:

<!-- 先淡出再淡入 -->
<component
  :is="view"
  transition="fade"
  transition-mode="out-in">
</component>
.fade-transition {
  transition: opacity .3s ease;
}
.fade-enter, .fade-leave {
  opacity: 0;
}

所以可以用这个做内容切换时的动画