Vue 组件:$ref & 父子组件的数据传递 & 组件参数校验与非 props 特性 & 给组件绑定原生事件 & 非父子组件间的传值 & 在 Vue 中使用插槽 & 作用域插槽 & 动态组件

使用组件的细节点

<table>
	<tbody>
	  <row></row>
	  <row></row>
	  <row></row>
	</tbody>
</table>
<script>
Vue.component('row', {
  template: '<tr><td>This is a row</td></tr>'
})
var vm = new Vue({
  el: '#app'
})
</script>

我们发现 tr 不在 tbody 里面,原因是 HTML5 规定 table 下面必须是 tbody,tbody 下面必须是 tr。
所以我们需要在 tbody 下面写 tr,但是我们可以通过 is 属性来指明模版。

<table>
	<tbody>
	  <tr is="row"></tr>
	  <tr is="row"></tr>
	  <tr is="row"></tr>
	</tbody>
</table>
<script>
Vue.component('row', {
  template: '<tr><td>This is a row</td></tr>'
})
var vm = new Vue({
  el: '#app'
})
</script>

<div id="app">
  <table>
    <tbody>
      <tr is="row"></tr>
      <tr is="row"></tr>
      <tr is="row"></tr>
    </tbody>
  </table>
</div>
<script>
  Vue.component('row', {
    data() {
      return {
        content: 'This is a row'
      }
    },
    template: '<tr><td>{{content}}</td></tr>'
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

在子组件中定义 data 时,data 必须是一个函数,不能是一个对象。


ref 引用

我希望 div 被点击的时候,把 div 的内容打印出来。

<div id="app">
  <div ref="hello" @click="handleClick">hello world</div>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    methods: {
      handleClick() {
        console.log(this.$refs.hello.innerHTML); 
      }
    }
  })
</script>

$refs 指所有引用, .hello 就是 ref=“hello” 这个引用。

$ref 在组件中的使用:

「 计数器 」
<div id="app">
  <counter></counter>
</div>
<script>
  Vue.component('counter', {
    data() {
      return {
        number: 0
      }
    },
    template: '<div @click="handleClick">{{number}}</div>',
    methods: {
      handleClick() {
        this.number++;
      }
    }
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

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

父子组件的数据传递

父组件通过属性的形式向子组件传递数据。
子组件通过 props:[] 接收数据。

<div id="app">
  <counter :count="0"></counter>  <!-- 通过:count属性传值 -->
  <counter :count="1"></counter>

</div>
<script>
  var counter = {
    props: ['count'],
    template: '<div>{{count}}</div>'
  }
  var vm = new Vue({
    el: '#app',
    components: {
      counter: counter
    }
  })
</script>

Vue 单向数据流

父组件可以向子组件通过属性的形式传递参数,传递的数据可以随便进行修改。
但是子组件只能使用父组件传递过来的数据,不能修改父组件传递过来的数据。

一个父组件可以传值给多个子组件。一旦子组件接收的是引用形式的数据时,在子组件中改变了数据内容,会影响其它的子组件。

子组件使用父组件传递过来的数据时,不能修改。这个时候可以复制一份:(在 data 里定义一个 number,把父组件的值赋值给 number)

 <div id="app">
  <counter :count="0"></counter>
  <counter :count="1"></counter>
</div>
<script>
  var counter = {
    props: ['count'],
    data() {
      return {
        number: this.count
      }
    },
    template: '<div @click="handleClick">{{number}}</div>',
    methods: {
      handleClick() {
        this.number++;
      }
    }
  }
  var vm = new Vue({
    el: '#app',
    components: {
      counter: counter
    }
  })
</script>

子组件通过事件的形式向父组件传值。子组件被点击的时候,可以通过 this.$emit('change') 触发一个 change 事件。

<div id="app">
  <counter :count="0" @change="handleIncrease"></counter>
  <!-- @监听事件,名字可以随便写(比如可以写成 @inc),和下面 this.$emit 里的对应即可。 -->
  <counter :count="1" @change="handleIncrease"></counter>
  <div>total: {{total}}</div>
</div>
<script>
  var counter = {
    props: ['count'],
    data() {
      return {
        number: this.count
      }
    },
    template: '<div @click="handleClick">{{number}}</div>',
    methods: {
      handleClick() {
        this.number++;
        this.$emit('change', 1);
      }
    }
  }
  var vm = new Vue({
    el: '#app',
    data: {
      total: 1
    },
    components: {
      counter: counter
    },
    methods: {
      handleIncrease(step) {
        this.total += step;
      }
    }
  })
</script>

this.$emit('change', 1); 的第二个参数实际上和上面那句 this.number++; 相对应,代表增长的步长。比如,上面写成 this.number=this.number+2; 则可以写成 this.$emit('change', 2); 用以告诉父组件增加了多少。


组件参数校验与非 props 特性

组件参数校验

我们都知道父组件通过属性(content="hello world")向子组件传递参数,那组件的参数校验是什么?
父组件向子组件传递了一些内容,子组件有权对这些内容做一个约束。这些约束就可以叫做参数的校验。

比如:需求是传递过来的 content 必须是一个字符串。

<div id="app">
 <child :content="{}"></child> <!-- :content 有:时,后面跟的是js变量,没有:时,后面的值是字符串-->
</div>
<script>
  Vue.component('child', {
    props: {
      content: String
    },
    template: '<div>{{content}}</div>'
  })
  var vm = new Vue({
    el: '#app',
  })
</script>

如果 content 不是一个字符串(现在是{},一个对象),控制台就会报错。
在这里插入图片描述
参数校验可以是一个数组(有多个条件):

content: [Number, String]

也可以是一个对象:

<child content="hello"></child>
content: {
  type: String,               // 类型
  required: true,             // 是否必须存在
  default: 'default value'    // 默认值
  validator(value) {          // 自定义校验器
    return (value.length > 5)
  }
}

非 props 特性

props 特性指:当父组件使用子组件时,通过属性向子组件传值的时候,恰好子组件中声明了对父组件传递过来的属性的接收(props: )。父子组件有一个对应关系。
父组件传,子组件接。

非 props 特性,子组件中没有声明。没有接收父组件传的值。没办法使用。
父组件传,子组件不接。
声明了一个非 props 特性的时候,其属性会展示在子组件最外层的 dom 标签的 html 属性里面。

给组件绑定原生事件

<div id="app">
  <child @click="handleClick"></child>   <!-- 这里绑定的事件是监听的自定义事件(目前无效)-->
</div>
<script>
  Vue.component('child', {
    template: '<div @click="handleChildClick">Child</div>',  // 这里绑定的事件是监听的原生事件
    methods: {
      handleChildClick() {
        console.log("handleChildClick");
      }
    }
  })
  var vm = new Vue({
    el: '#app',
    methods: {
      handleClick() {
      	this.$emit('click')
        console.log("handleClick");  // (目前无效)
      }
    }
  })
</script>

监听的自定义事件如何被触发?子组件想触发父组件的事件的监听,需要调用 this.$emit() 方法。

<div id="app">
  <child @click="handleClick"></child>
</div>
<script>
  Vue.component('child', {
    template: '<div @click="handleChildClick">Child</div>',
    methods: {
      handleChildClick() {
        this.$emit('click');
        // console.log("handleChildClick");
      }
    }
  })
  var vm = new Vue({
    el: '#app',
    methods: {
      handleClick() {
        console.log("handleClick");
      }
    }
  })
</script>

还有什么更简单的方法吗?

<child @click.native="handleClick"></child>

可以在事件绑定的后面加上 .native 修饰符,监听原生事件。

非父子组件间的传值

利用总线机制解决非父子组件间的传值问题。
( Bus / 总线 / 发布订阅模式 / 观察者模式 )

比如:我想要点击上面的 div 时,把其内容改成 daze;点击下面的 div 时,把其内容改成 bule。

<div id="app">
  <child content="bule"></child>
  <child content="daze"></child>
</div>
<script>
  Vue.prototype.bus = new Vue()   //bus总线
  Vue.component('child', {
    data() {
      return {
        selfContent: this.content
      }
    },
    props: {
      content: String
    },
    template: '<div @click="handleClick">{{selfContent}}</div>',
    methods: {
      handleClick() {
        this.bus.$emit('change', this.selfContent);
      }
    },
    mounted() {
      var this_ = this;
      this.bus.$on('change', function (msg) {
        this_.selfContent = msg;
      })
    }
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

在 Vue 中使用插槽

<div id="app">
  <child content='<p>bule_daze</p>'></child>
</div>
<script>
  Vue.component('child', {
    props: ['content'],
    template: `<div>
                  <p>hello</p>
                  <div v-html="this.content">{{content}}</div>
                </div>`
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

如果我不想要 p 标签外面显示 div 标签,应该如何去做?

template 占位符在这里是不好用的。

而通过 content 传值,想直接使用 content 里面的 p 标签是有问题的,必须在外面包裹一个 div (<div v-html="this.content">);且当传递的内容很多时,代码不利于阅读。

当子组件有一部分内容是根据父组件传递过来的 DOM 进行显示时,我们可以利用插槽 slot

<div id="app">
  <child>
    <p>bule_daze</p>
  </child>
</div>
<script>
Vue.component('child', {
  props: ['content'],
  template: `<div>
                <p>hello</p>
                <slot></slot>
              </div>`,

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

默认值

slot 插槽还可以定义默认值:<slot>默认内容</slot>(当父组件使用子组件时不传递插槽的内容时,才会显示出默认内容。<child></child>

具名插槽

如果只写成 <slot></slot>,代表插槽中的所有内容。如果我希望 slot 的内容分别对应,可以利用具名插槽,即给插槽取个名字。

  • 父组件中 slot="header"
  • 子组件中 <slot name="header"></slot>
<div id="app">
  <my-content>
    <div class="header" slot="header">header</div>
    <div class="footer" slot="footer">footer</div>
  </my-content>
</div>
<script>
  Vue.component('my-content', {
    template: `<div>
                  <slot name="header"></slot>  
                  <div class="content">content</div>
                  <slot name="footer"></slot>
                </div>`
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

具名插槽也可设置默认值。

作用域插槽

<div id="app">
 <child></child>
</div>
<script>
  Vue.component('child', {
    data() {
      return {
        list: [1, 2, 3, 4]
      }
    },
    template: `<div>
                  <ul>
                    <li v-for="item of list">
                      {{item}}
                    </li>
                  </ul>
    		   </div>`
  })
  var vm = new Vue({
    el: '#app'
  })
</script>

现在有一个需求,child 组件有可能在很多的地方被调用。我们希望在不同的地方调用 child 组件的时候,列表怎样循环列表的样式不是由 child 组件控制的,而是外部告诉我们的。(即 template 里面需要把 li 去掉)

需要用到作用域插槽:

<div id="app">
  <child>
    <template slot-scope="props">
      <li>{{props.item}}-hello</li>
    </template>
  </child>
</div>
<script>
  Vue.component('child', {
    data() {
      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: '#app'
  })
</script>

父组件调用子组件的时候,给子组件传了一个插槽,这个插槽叫做作用域插槽。作用域插槽必须是一个以 <template></template> 开头和结尾的内容;且这个插槽要声明其要接收的数据都放在哪(slot-scope="props" 都放在 props 里面);还需要告诉子组件一个模版的信息,接到 props 应该如何展示(<li>{{props.item}}-hello</li>)。

使用情境

当子组件做循环时,或者某一部分的 DOM 结构应该由外部传递进来的时候,使用作用域插槽。

使用作用域插槽时,子组件可以向父组件的插槽里面传数据。父组件如果想要接收这个数据,必须在外层使用 template,同时通过 slot-scope 对应的属性的名字来接收传递过来的所有的数据。

动态组件与 v-once 指令

<div id="app">
  <child-one></child-one>
  <child-two></child-two>
  <button>change</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: '#app'
  })
</script>

现在想实现当点击 change 按钮的时候,一会 child-one 进行显示,一会 child-two 进行显示:

<div id="app">
  <child-one v-if="type === 'child-one'"></child-one>
  <child-two v-if="type === 'child-two'"></child-two>
  <button @click="handleBtnClick">change</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: '#app',
    data: {
      type: 'child-one'
    },
    methods: {
      handleBtnClick() {
        this.type = this.type === 'child-one' ? 'child-two' : 'child-one';
      }
    }
  })
</script>

动态组件

可以利用动态组件的方式来实现上述需求。

<div id="app">
  <component :is="type"></component>
  <button @click="handleBtnClick">change</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: '#app',
    data: {
      type: 'child-one'
    },
    methods: {
      handleBtnClick() {
        this.type = this.type === 'child-one' ? 'child-two' : 'child-one';
      }
    }
  })
</script>

<component></component> 是 Vue 里面自带的一个标签,指的就是动态组件。动态组件的意思就是根据属性 is 里面数据的变化自动加载不同的组件。

v-once

上面的动态组件在每次点击按钮切换的时候,Vue 底层会帮我们进行判断,第一个组件不用了,就会销毁第一个组件,创建第二个组件;当第二个组件不用了的时候,就会销毁第二个组件,创建第一个组件。

这种操作会耗费一定的性能。如果组件的内容每一次都一样,我们可以使用 v-once 指令:

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

第一次被渲染的时候,会保存至内存中;当再次使用的时候不需要重新创建,而是直接从内存中拿。
v-once 可以提高静态内容的展示效率

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章