Vue 深入理解组件(三)

一.组件使用的细节点

1.浏览器解析问题

 <div id="root">
           <table>
               <tbody>
                      <row></row>
                      <row></row>
                      <row></row>
               </tbody>
           </table>
     </div>

     <script>

           Vue.component("row",{
               template:"<tr><td>this is a row</td></tr>"
           })

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

在这里插入图片描述

  • 三个tr没有在tbody里面,而tbody要求必须放有tr
  • 可以用is属性,让这个tr等价于row
  • 这样解析就不会有问题
 <div id="root">
           <table>
               <tbody>
                      <tr is="row"></tr>
                      <tr is="row"></tr>
                      <tr is="row"></tr>
               </tbody>
           </table>
     </div>

     <script>

           Vue.component("row",{
               template:"<tr><td>this is a row</td></tr>"
           })

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

在这里插入图片描述

  • ul-li、select-option也是同样的解决方法

2.组件中的data

  • 在子组件中运用data,页面报错
<div id="root">
           <table>
               <tbody>
                  <tr is="row"></tr>
                  <tr is="row"></tr>
                  <tr is="row"></tr>
               </tbody>
           </table>
     </div>

     <script>

           Vue.component("row",{
               data:{
                 content:"this is a row"
               },
               template:"<tr><td>{{content}}</td></tr>"
           })

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

在这里插入图片描述

  • data应该是function 而不是对象
  • 在最外层的vue实例中定义data用对象没问题
  • 但是在非根组件中,比如子组件定义时只能用function,而不能用对象格式
<div id="root">
           <table>
               <tbody>
                  <tr is="row"></tr>
                  <tr is="row"></tr>
                  <tr is="row"></tr>
               </tbody>
           </table>
     </div>

     <script>

           Vue.component("row",{
               data:function(){
                   return {
                       content:"this is a row"
                   }
               },
               template:"<tr><td>{{content}}</td></tr>"
           })

           var vm = new Vue({
               el:"#root"
           })
     </script>
  • 子组件定义data,data只能是函数,而不能是对象
  • 一个子组件不想根组件只调用一次,它可能在不同的地方,比如row子组件调用三次
  • 每个子组件的数据不希望和其他的子组件产生冲突(每个子组件应该有自己的数据,比如上面的row是三套数据,而不是共享一个数据)
  • 通过一个函数返回一个对象的目的,让子组件拥有独立的数据存储,不会出现多个子组件相互影响的情况

3.vue中的ref引用

  • vue不建议我们在代码中操作dom
  • 但是复杂的动画效果,光靠vue数据绑定是不行的,所以有些必要情况下必须要操作dom
  • vue通过ref引用形式操作dom
<div id="root">
    <div ref="hello" @click="handleClick">hello world</div>
</div>

<script>

    Vue.component("row",{
        data:function(){
            return {
                content:"this is a row"
            }
        },
        template:"<tr><td>{{content}}</td></tr>"
    })

    var vm = new Vue({
        el:"#root",
        methods:{
            handleClick:function () {
                alert(this.$refs.hello.innerHTML)
            }
        }
    })
</script>
  • vue所有的引用下的名为hello的引用,对应的就是这个div的dom,指向的就是这个div的dom节点
  • 如果一个组件上加了ref,this.$refs.xxx就是对应这个组件的引用
  • 实现一个点击第一、二行都自增1,第三行显示第一行+第二行的和,这里就要用到ref引用
    在这里插入图片描述
 <div id="root">
            <counter ref="one" @change="handleChange"></counter>
            <counter ref="two" @change="handleChange"></counter>
            <div>{{total}}</div>
    </div>

    <script>
    
        Vue.component("counter",{
            template:"<div @click='handleClick'>{{number}}</div>",
            data:function(){
                return{
                    number:0
                }
            },
            methods:{
                handleClick:function () {
                     this.number++;
                     this.$emit("change")
                }
            }
        })
    
        var vm = new Vue({
            el:"#root",
            data:{
                total:0
            },
            methods:{
                handleChange:function () {
                     this.total = this.$refs.one.number + this.$refs.two.number
                }
            }
        })
    </script>

二.父子组件的间的数据传递

1.父组件向子组件数据传递

  • 此时count中的0和1都只是字符串
<div id="root">
    <counter count="0"></counter>
    <counter count="1"></counter>
</div>

<script>

    var counter = {
        template: "<div>0</div>"
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        }
    })
</script>
  • 加了冒号就是js表达式,也就是数字类型
<div id="root">
    <counter :count="0"></counter>
    <counter :count="1"></counter>
</div>

<script>

    var counter = {
        template: "<div>0</div>"
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        }
    })
</script>
  • 子组件要接收这个数据才能使用,所以用props属性来接收
<div id="root">
    <counter :count="0"></counter>
    <counter :count="1"></counter>
</div>

<script>

    var counter = {
        props:['count'],
        template: "<div>{{count}}</div>"
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        }
    })
</script>
  • 子组件累加的功能
<div id="root">
    <counter :count="0"></counter>
    <counter :count="1"></counter>
</div>

<script>

    var counter = {
        props:['count'],
        template: "<div @click='handleAdd'>{{count}}</div>",
        methods:{
            handleAdd:function () {
                 this.count++
            }
        }
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        }
    })
</script>

虽然可以实现,但是还是报了警告
在这里插入图片描述

  • 不能直接修改父组件传递过来的数据,vue有个单向数据流的概念,一旦接收的count不是基础类型数据,如果是对象形式,引用类型时,子组件改变了传递过来的内容,有可能接收的引用的数据还在被其他子组件所使用,这样子组件不仅影响了自己,还影响了其他子组件。所以不能改变父组件传递过来的数据
  • 我们可以拷贝一份来实现这个累加功能
<div id="root">
    <counter :count="0"></counter>
    <counter :count="1"></counter>
</div>

<script>

    var counter = {
        props:['count'],
        data:function(){
            return{
               number:this.count
            }
        },
        template: "<div @click='handleAdd'>{{number}}</div>",
        methods:{
            handleAdd:function () {
                 this.number++
            }
        }
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        }
    })
</script>

2.子组件向父组件数据传递

  • 简单说就是让子组件向外触发事件
  • 父组件监听这个事件
<div id="root">
    <counter :count="2" @change="handleIncrease"></counter>
    <counter :count="3" @change="handleIncrease"></counter>
    <div>{{total}}</div>
</div>

<script>

    var counter = {
        props:['count'],
        data:function(){
            return{
               number:this.count
            }
        },
        template: "<div @click='handleAdd'>{{number}}</div>",
        methods:{
            handleAdd:function () {
                 this.number = this.number + 2
                 this.$emit("change",2)
            }
        }
    }

    var vm = new Vue({
        el:"#root",
        components:{
            counter:counter
        },
        data:{
            total:5
        },
        methods:{
            handleIncrease:function (msg) {
                 this.total = this.total + msg
            }
        }
    })
</script>

三.组件参数校验与非props特性

1.组件参数校验

  • 平常情况下都这么写的
<div id="root">
     <counter count="123"></counter>
     <counter count="321"></counter>
</div>

<script>

    Vue.component("counter",{
        props:["count"],
        template:"<div>{{count}}</div>"
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 如果你要加参数校验,就要把props从数组改成对象
  • 要求count为字符串(加了冒号123就变成数字了,所以报错)
<div id="root">
     <counter count="hello"></counter>
     <counter :count="123"></counter>
</div>

<script>

    Vue.component("counter",{
        props:{
            count:String
        },
        template:"<div>{{count}}</div>"
    })

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

在这里插入图片描述

  • 可以设置成多种类型
<div id="root">
     <counter count="hello"></counter>
     <counter :count="123"></counter>
</div>

<script>

    Vue.component("counter",{
        props:{
            count:[String,Number]
        },
        template:"<div>{{count}}</div>"
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 此时传对象就会报错
<div id="root">
     <counter :count="{a:1}"></counter>
</div>
  • 数据后边也可以跟着对象(类型、是否必要、默认值)
<div id="root">
     <counter></counter>
</div>

<script>

    Vue.component("counter",{
        props:{
            count:{
                type:String,
                required:false,
                default:'哈哈'
            }
        },
        template:"<div>{{count}}</div>"
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 以及要求长度之类的validator对象(校验器)
<div id="root">
     <counter count="hello world"></counter>
</div>

<script>

    Vue.component("counter",{
        props:{
            count:{
                type:String,
                validator:function (value) {
                    return (value.length > 5)
                }
            }
        },
        template:"<div>{{count}}</div>"
    })

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

2.props特性

  • 父组件使用子组件时,通过属性子组件传值时,子组件申明了对父组件传递过来的数据的接收
  • 比如父组件传递content 给子组件,子组件恰好又申明了content
  • 属性传递不会在dom节点上显示的,子组件也能把传递数据通过插值表达式显示出来
<div id="root">
    <child content="hello world"></child>
</div>

<script>

    Vue.component("child",{
        props:{
            content:{
                type:String
            }
        },
        template:"<div>{{content}}</div>"
    })

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

3.非props特性

  • 父组件传递数据,子组件并没有申明接收父组件传递的内容
<div id="root">
    <child content="hello world"></child>
</div>

<script>

    Vue.component("child",{
        template:"<div>hello</div>"
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 虽然会报错,但是网页会显示dom属性
  • 实际情况下非props特性很少用到
    在这里插入图片描述

四.给组件绑定原生事件

  • 定义一个全局的子组件,按逻辑实例中methods写这个方法,但是会发现没有用
    <div id="root">
        <child @click="handleClick"></child>
    </div>

<script>

    Vue.component("child",{
        template:"<div>Child</div>",
        methods:{
            handleClick:function () {
                alert('1')
            }
        }
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 当给组件绑定事件的时候,这个事件绑定的是自定义的事件
  • 真正鼠标点击触发的事件并不是我绑定的这个click事件
  • 在组件中给元素绑定事件,这个才是原生事件,而Child绑定的click是自定义事件
  • 父组件这里自定义事件监听用this.$emit
  • 点击时,子组件会监听到div元素被点击了,然后执行这个自定义的事件,向外触发一个自定义click事件,父组件用子组件监听了这个事件,所以对应代码会执行 ,但是这样写代码很麻烦
    <div id="root">
        <child @btn="handleClick"></child>
    </div>

<script>

    Vue.component("child",{
        template:"<div @click='handleChildClick'>Child</div>",
        methods:{
            handleChildClick:function () {
                alert('handleChildClick')
                this.$emit("btn")
            }
        }
    })

    var vm = new Vue({
        el:"#root",
        methods:{
            handleClick:function(){
                alert('handleClick')
            }
        }
    })
</script>
  • 如果非要child监听原生的事件,可以加上.native修饰符就可以

    <div id="root">
        <child @click.native="handleChildClick"></child>
    </div>

<script>

    Vue.component("child",{
        template:"<div>Child</div>",
    })

    var vm = new Vue({
        el:"#root",
        methods:{
            handleChildClick:function () {
                alert('handleChildClick')
            }
        }
    })
</script>

五.非父子组件间的传值

1.非父子组件间的传值介绍

在这里插入图片描述

  • 假如我们拆分成3个组件
  • 第一层和第二层传递数据就是父子组件传值
    • 父组件通过props向子组件传值
    • 子组件通过向外触发事件向父组件传值
  • 那么万一是第一层和第三层传递数据呢?
    • 不能通过属性直接传递数据
    • 把第一层数据传给第二层然后在传给第三层…反过来也一样,但是这样就复杂了
      在这里插入图片描述
  • 万一是左边第3层像右边第3层传值呢?难道还一层一层传递?
  • Vue是轻量级视图框架,不是把事情变复杂的
  • 如果遇到复杂的数据传递,光靠vue框架是解决不了问题的,要引入一些设计模式之类的
  • 一般有两种解决方式(解决复杂的非父子组件传值)
    • 借助vue官方的数据层框架vuex(使用起来有难度)—后边结合项目来说
    • 使用bus格式来解决(vue称为总线机制/发布订阅模式/观察者模式)

2.总线机制

  • 我们通过组件间传值例子来讲解
  • 在vue实例挂载一个bus属性,bus属性又指向vue的实例
  • this.bus指的就是实例上挂载的bus,每个实例上都会有bus这个属性,而bus又是一个vue实例,所以它下面就会有$emit方法
<div id="root">
    <child content="hello"></child>
    <child content="world"></child>
</div>

<script>

    Vue.prototype.bus = new Vue()

    Vue.component("child",{
        props:{
            content:{
                type:String
            }
        }
        template:"<div @click='handleClick'>{{content}}</div>",
        methods:{
            handleClick:function () {
                this.bus.$emit('change',this.content)
            }
        }
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 接下来就让其他组件监听这个事件,可以借助生命周期钩子mounted(组件被挂载后执行的函数),$on监听bus触发的事件
<div id="root">
    <child content="hello"></child>
    <child content="world"></child>
</div>

<script>

    Vue.prototype.bus = new Vue()

    Vue.component("child",{
        props:{
            content:{
                type:String
            }
        }
        template:"<div @click='handleClick'>{{content}}</div>",
        methods:{
            handleClick:function () {
                this.bus.$emit('change',this.content)
            }
        },
        mounted:function(){
            var this_ = this
            this.bus.$on('change',function (msg) {
                  this_.content = msg //注意作用域
            })
        }
    })

    var vm = new Vue({
        el:"#root"
    })
</script>
  • 这样两个组件传值通过总线模式就搞定了,但是没有问题了吗?
  • 虽然实现,但是看浏览器还是会报警告(因为content是父组件传来的数据,在vue中要时刻有单项数据流的概念)
  • 可以用子组件拷贝父组件传来的数据的方法
<div id="root">
    <child content="hello"></child>
    <child content="world"></child>
</div>

<script>

    Vue.prototype.bus = new Vue()

    Vue.component("child",{
        props:{
            content:{
                type:String
            }
        },
        data:function(){
          return {
              selfContent : this.content
          }
        },
        template:"<div @click='handleClick'>{{selfContent}}</div>",
        methods:{
            handleClick:function () {
                this.bus.$emit('change',this.selfContent)
            }
        },
        mounted:function(){
            var this_ = this
            this.bus.$on('change',function (msg) {
                  this_.selfContent = msg //注意作用域
            })
        }
    })

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

六.在vue中使用插槽

1.插槽的使用场景

  • 子组件除了展示p标签外,还要展示一段内容,而这段内容不是子组件决定的,是父组件传递过来的
  • 以前的想法怎么传?可以通过属性形式向子组件传值
  • 但是你会发现做了转义
<div id="root">
      <child content='<p>world</p>'></child>
</div>

<script>
    Vue.component('child',{
        props:["content"],
        template:'<div><p>hello</p>  {{content}}</div>'
    })


    var vm = new Vue({
        el:"#root"
    })
</script>
  • 所以只能用模板占位符
<div id="root">
      <child content='<p>world</p>'></child>
</div>

<script>
    Vue.component('child',{
        props:["content"],
        template:'<div><p>hello</p>  <div v-html="this.content"></div></div>'
    })


    var vm = new Vue({
        el:"#root"
    })
</script>
  • 这种形式传递少可以,但传递一旦多了,显示没问题,但是代码很难阅读,插槽就是解决这个问题出来的

2.插槽的使用

  • 显示的就是父组件向子组件插入进来的这个p标签
<div id="root">
      <child>
          <h1>world</h1>
      </child>
</div>

<script>
    Vue.component('child',{
        template:'<div> <p>hello</p> <slot></slot> </div>'
    })


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

在这里插入图片描述

  • 也方便向子组件传递dom元素,同时子组件使用插槽内容也很简单
  • 也可以在slot标签内写默认内容

3.插槽的命名

  • 如果要实现header、content、footer用如下代码是不行的
  • 子组件全部都是插槽的内容,所以就会出现如下下图的样子,相当于调用了两次
  • 此时要给插槽命名
<div id="root">
        <body-content>
                 <div class="header">header</div>
                 <div class="footer">footer</div>
        </body-content>
</div>

<script>
    Vue.component('body-content',{
        template:'<div> <slot></slot>  <div class="content">content</div>  <slot></slot></div>'
    })


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

在这里插入图片描述

<div id="root">
        <body-content>
                 <div class="header" slot="header">header</div>
                 <div class="footer" slot="footer">footer</div>
        </body-content>
</div>

<script>
    Vue.component('body-content',{
        template:'<div> <slot name="header"></slot>  <div class="content">content</div>  <slot name="footer"></slot></div>'
    })


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

在这里插入图片描述

  • 当然也可以设置默认值(当没有同名的插槽)
<div id="root">
        <body-content>

                 <div class="footer" slot="footer">footer</div>
        </body-content>
</div>

<script>
    Vue.component('body-content',{
        template:'<div> <slot name="header"><h1>default slot</h1></slot>  <div class="content">content</div>  <slot name="footer"></slot></div>'
    })


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

在这里插入图片描述

七.作用域插槽

1.作用域插槽的使用

  • 循环遍历列表
<div id="root">
        <child></child>
</div>

<script>
    Vue.component('child',{
        data:function(){
            return {
                list:[1,2,3,4]
            }
        },
        template:'<div><ul>   <li v-for="item of list">{{item}}</li>        </ul></div>'
    })


    var vm = new Vue({
        el:"#root"
    })
</script>
  • 这个child组件会在很多地方调用,列表样式应该不是由child控制的,而是由外部决定的
<div id="root">
        <child>
             <template slot-scope="props">
                 <h1> {{props.item}} -- hello </h1>
             </template>
        </child>
</div>

<script>
    Vue.component('child',{
        data:function(){
            return {
                list:[1,2,3,4]
            }
        },
        template:'<div><ul>   <slot v-for="item of list" :item=item></slot>        </ul></div>'
    })


    var vm = new Vue({
        el:"#root"
    })
</script>
  • 当子组件用slot时,会往slot传递一个item数据,item数据放在slot-scope的属性之中
  • 这就不是由子组件决定的了,而是父组件调用子组件的时候给子组件去传递对应的模板,这就是作用域槽的使用
  • 父组件调用子组件时,给子组件传了一个插槽,这个插槽是作用域插槽,作用域插槽必须是template的,申明从子组件接收的数据slot-scope放在那里,告诉子组件模板信息
  • 当子组件做循环或者某一个部分dom结构由外部传递进来的时候用作用域插槽,子组件可以像父组件插槽传递数据
    ,父组件插槽向接收这个数据,必须外层申明一个template的,接收传递数据的放在那里,传递一个item过来 ,接收到这个item

八.动态组件与 v-once 指令

1.v-once指令

  • 实现一个动态互相展示的效果
  • 不过这种方式都是创建一次 销毁一次(重新),很消耗性能
<div id="root">
        <child-one v-if="type ==='child-one' "></child-one>
        <child-two v-if="type ==='child-two' "></child-two>
        <button @click="handleClick">交换</button>
</div>

<script>
    Vue.component('child-one',{
        template:'<div>child-one</div>'
    })

    Vue.component('child-two',{
        template:'<div>child-two</div>'
    })

    var vm = new Vue({
        el:"#root",
        data:{
            type:'child-one'
        },
        methods:{
            handleClick:function () {
                 this.type = (this.type === 'child-one'? 'child-two':'child-one')
            }
        }
    })
</script>
  • 可以用v-once指令优化
  • 第一次展示后都会放到内存里面,切换不会重新创建组件,直接去内存中拿的,性能更高
<div id="root">
    <child-one v-if="type ==='child-one' "></child-one>
    <child-two v-if="type ==='child-two' "></child-two>
        <button @click="handleClick">交换</button>
</div>

<script>
    Vue.component('child-one',{
        template:'<div v-once>child-one</div>'
    })

    Vue.component('child-two',{
        template:'<div v-once>child-two</div>'
    })

    var vm = new Vue({
        el:"#root",
        data:{
            type:'child-one'
        },
        methods:{
            handleClick:function () {
                 this.type = (this.type === 'child-one'? 'child-two':'child-one')
            }
        }
    })
</script>

2.动态组件

  • 也可以用动态组件的形式来展示这个效果
  • component是vue自带的,会根据type属性自带的变化加载不同的组件
<div id="root">
        <component :is="type"></component>
        <button @click="handleClick">交换</button>
</div>

<script>
    Vue.component('child-one',{
        template:'<div>child-one</div>'
    })

    Vue.component('child-two',{
        template:'<div>child-two</div>'
    })

    var vm = new Vue({
        el:"#root",
        data:{
            type:'child-one'
        },
        methods:{
            handleClick:function () {
                 this.type = (this.type === 'child-one'? 'child-two':'child-one')
            }
        }
    })
</script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章