Vue 进阶(01)组件深入

本文详细介绍了Vue.js组件的注册、Prop属性、自定义事件、插槽等内容,并探讨了动态组件、异步组件及组件的一些边界情况处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入了解组件

一、组件注册

1、组件名 & 注册

  • 组件名最好全部小写,并且用连字符隔开,比如:test-component
  • 关于全局注册和局部注册的内容参考基础部分

二、Prop属性

  • prop 大小写
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
<!-- 组件中这么写 -->
props: ['postTitle']
  • prop类型
//常规数组写法
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
//指定具体类型的对象写法
props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}
传递静态或动态 prop
  • 不用 v-bind 就只是一个字符串,用了就是一个变量或者一个表达式,一般都是要用 v-bind 的
  • 传递 数字、布尔、数组、对象 都是遵从上面的规则的,传递一个对象的所有属性的写法比较特殊
<!-- 静态,就是只一个字符串 -->
<blog-post title="My journey with Vue"></blog-post>
<!-- 动态要么是一个变量值,要么就是一个 js 表达式 -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- 传递一个对象的所有属性 -->
post: {
  id: 1,
  title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
<!-- 二者是等价的 -->
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>
单向数据流
  • 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行
  • 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态
Prop验证
  • 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator 函数中是不可用的
  • 不满足验证的条件的话,控制台会输出警告信息
Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})
非Prop属性
  • <bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
  • 这里的 bootstrap-date-input是一个第三方的组件,data-date-picker是一个属性,但是这个属性并没有在组件的 props 里面定义,data-date-picker="activated"特性就会自动添加到 <bootstrap-date-input>的根元素上
合并替换已有属性
  • 组件
<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>
  • 组件的 template
<input type="date" class="form-control">
  • 外部传入的属性大部分会替换掉内部属性,比如传入一个 type=text的话,template里面的type属性就会被替换掉,
  • 但是style和class是比较特殊的,这两个属性,内部和外部的值会进行合并
禁用属性继承
  • 有了inheritAttrs: false$attrs,可以手动决定这些特性会被赋予哪个元素,在使用基础组件的时候更像是使用原始的 HTML 元素
<h2>禁用属性继承</h2>
<div id="input-cus">
    <base-input label="haha" value="default1" cus-sttr="cusattr" v-model="username">
    </base-input>
    <br>
    <span>{{username}}</span>
</div>
Vue.component('base-input', {
    inheritAttrs: false,
    props: ['label', 'value'],
    template: `
        <label>
          {{ label }}
          <input
            v-bind="$attrs"
            v-bind:value="value"
            v-on:input="$emit('input', $event.target.value)"
          >
        </label>
      `
});
var input_cus = new Vue({
   el:"#input-cus",
   data:{
       username:"haha"
   }
});

三、自定义事件

  • 事件名推荐全部小写,否则可能出现无法监听的情况

1、自定义组件v-model

<div id="event1">
    <base-checkbox v-model="lovingVue"></base-checkbox>
    <br>
    <span>{{lovingVue}}</span>
</div>
  • 单选框、复选框等类型的输入控件需要使用 model 选项,因为他们的 value 有別的用途
Vue.component('base-checkbox', {
    model: {
        prop: 'checked',
        event: 'change'
    },
    props: {
        checked: Boolean
    },
    template: `
<input
  type="checkbox"
  v-bind:checked="checked"
  v-on:change="$emit('change', $event.target.checked)"
>
`
});
new Vue({
    el:"#event1",
    data:{
        lovingVue:false
    }
});

2、将原生事件绑定到组件

使用 native 修饰符
<h2>原生事件绑定native</h2>
<div id="event1">
    <base-input v-on:focus.native="onFocus"></base-input>
</div>

<script>
    Vue.component('base-input', {
        props:['value'],
        template: `
              <input
                v-bind="$attrs"
                v-bind:value="value"
                v-on:input="$emit('input', $event.target.value)"
              >
        `
    });
    new Vue({
        el:"#event1",
        methods:{
            onFocus:function (){
                alert("test onFocus");
            }
        }
    });
</script>
使用 $listeners 属性
  • 上面的例子中如果模板的最外层的元素不是input的话而是label的话,就只能这样来实现了
<div id="event1">
    <base-input v-on:focus="onFocus" v-model="modelv"></base-input>
    {{modelv}}
</div>

<script>
    Vue.component('base-input', {
        inheritAttrs: false,
        props: ['label', 'value'],
        computed: {
            inputListeners: function () {
                var vm = this;
                // `Object.assign` 将所有的对象合并为一个新对象
                return Object.assign({},
                    this.$listeners,
                    {
                        input: function (event) {
                            vm.$emit('input', event.target.value)
                        }
                    }
                )
            }
        },
        template: `
            <label>
              {{ label }}
              <input
                v-bind="$attrs"
                v-bind:value="value"
                v-on="inputListeners"
              >
            </label>
        `
    });

    new Vue({
        el:"#event1",
        data:{
            modelv:"test-vmodel"
        },
        methods:{
            onFocus:function () {
                alert('focus me');
            }
        }
    });
</script>

1、父组件可以使用 props 把数据传给子组件
2、子组件可以使用 $emit 触发父组件的自定义事件

3、sync 修饰符

  • 正常情况组件的属性都是单向下行传递的
  • 使用 sync 修饰符可以触发事件进行更新的方法来间接实现数据的双向绑定
<!--设置一个属性-->
<text-document v-bind:title.sync="doc.title"></text-document>
<!--设置多个属性-->
<text-document v-bind.sync="doc"></text-document>

四、插槽

  • slot 插槽可以分发的内容:文本、html元素、组件
  • 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
  • slot插槽可以设置后备内容(类似于变量的默认值):父级组件中不提供内容的时候默认值就是这个了
<button type="submit">
  <slot>Submit</slot>
</button>
  • 要分发多个内容的时候可以使用有名字的 slot:v-slot 指令只能使用在 template 上
<div id="lay1">
    <base-layout>
        <template v-slot:header>
            <h1>Here might be a page title</h1>
        </template>

        <template v-slot:default>
            <p>A paragraph for the main content.</p>
            <p>And another one.</p>
        </template>

        <template v-slot:footer>
            <p>Here's some contact info</p>
        </template>
    </base-layout>
</div>
<script>
   Vue.component('base-layout',{
       template:`
           <div class="container">
              <header>
                <slot name="header"></slot>
              </header>
              <main>
                <slot></slot>
              </main>
              <footer>
                <slot name="footer"></slot>
              </footer>
            </div>
       `
   });

   new Vue({
       el:'#lay1'
   })
</script>

实例属性在父组件和子组件中都是可以访问,之所以组件注册的时候在根节点定义

1、作用域插槽

  • 提供的组件带有一个可从子组件获取数据的可复用的插槽
  • 在 2.5.0+,slot-scope 不再限制在 <template>元素上使用,而可以用在插槽内的任何元素或组件上
<h2>作用域插槽</h2>
<div id="div1">
    <!--<todo-list :todos="todos"></todo-list>-->
    <todo-list v-bind:todos="todos">
        <template slot-scope="{ todo }">
            <span v-if="todo.isComplete"></span>
            {{ todo.text }}
        </template>
    </todo-list>
</div>

<script>
    Vue.component('todo-list', {
        props: ['todos'],
        template:
            `
        <ul>
          <li
            v-for="todo in todos"
            v-bind:key="todo.id"
          >
            <slot v-bind:todo="todo">
            {{ todo.text }}
            </slot>
          </li>
        </ul>
        `

    });

    new Vue({
        el: "#div1",
        data: {
            todos: [
                {id: 1, text: 'test1',isComplete:true},
                {id: 2, text: 'test2',isComplete:true},
                {id: 3, text: 'test3',isComplete:false},
                {id: 4, text: 'test4',isComplete:true}
            ]
        }
    })
</script>

五、动态组件 & 异步组件

1、动态组件

  • 就是前面讲到的使用is来进行组件切换的
  • 这里只是用keep-alive的标签将component包裹起来将失活的组件缓存起来,再次切换的时候进行激活,避免组件的重复渲染
<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

2、异步组件

  • 以一个工厂函数的方式定义组件,这个工厂函数会异步解析组件定义
  • 在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染
  • 工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用
  • 可以调用 reject(reason) 来表示加载失败
  • 这里的 setTimeout 是为了演示用的,实际使用如何获取组件需要自己定义
<h2>异步组件</h2>
<div id="div2">
    <async-example></async-example>
</div>
<script>
    Vue.component('async-example', function (resolve, reject) {
        setTimeout(function () {
            // 向 `resolve` 回调传递组件定义
            resolve({
                template: '<div>I am async!</div>'
            })
        }, 5000)
    });
    new Vue({
        el:'#div2'
    });
</script>

六、处理边界情况

1、访问元素 & 组件

访问根实例
// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
访问父级组件实例
  • google-map-markers访问google-mapmap属性难以确定到底有多少层的包裹(要写几个$parent
<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>
var map = this.$parent.map || this.$parent.$parent.map
访问子组件实例或子元素
依赖注入

2、程序化的事件侦听器

侦听事件的三个方法
  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

$emit、$on, 和 $offdispatchEvent、addEventListener 和 removeEventListener 并不是一回事

一个具体的示例
  • 原来的代码
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
  // Pikaday 是一个第三方日期选择器的库
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
  this.picker.destroy()
}
  • 修改后的代码
<script src="https://unpkg.com/pickaday@1.7.0"></script>

<div id="app">
    <input ref="dateInput" v-model="date" type="date">
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            date: null
        },
        mounted: function () {
            var picker = new Pikaday({
                field: this.$refs.dateInput,
                format: 'YYYY-MM-DD'
            });

            this.$once('hook:beforeDestroy', function () {
                picker.destroy()
            })
        }
    })
</script>

3、循环引用

递归组件
  • 这样的递归调用要避免,递归一定要有结束条件
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
组件之间的循环引用
<div id="file">
    <tree-folder :folder="folder"></tree-folder>
</div>

<script>
    Vue.component('tree-folder',{
        props:['folder'],
        template:`
            <p>
              <span>{{ folder.name }}</span>
              <tree-folder-contents :children="folder.children"/>
            </p>
        `
    });
    Vue.component('tree-folder-contents',{
        props:['children'],
        template:`
            <ul>
              <li v-for="child in children">
                <tree-folder v-if="child.children" :folder="child"/>
                <span v-else>{{ child.name }}</span>
              </li>
            </ul>
        `
    });
    new Vue({
        el:'#file',
        data:{
           folder:{
               name:'folder1',
               children:[
                   {name:'file1',children:null,content:'file1-content'},
                   {
                       name:'folder2',content:null,children:[
                           {name:'file2',children:null,content:'file2-content'},
                           {name:'file3',children:null,content:'file3-content'},
                           {name:'file4',children:null,content:'file4-content'}
                       ]
                   }
               ]
           }
        }
    })
</script>

4、模板定义的替代品

  • 内联模板
<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>
  • X-Templates
<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
<script >
  Vue.component('hello-world', {
  template: '#hello-world-template'
})
</script>
  • 总结:最好还是使用 template 选项来定义模板,以上的二者都有一定的弊端

5、控制更新

强制更新:
  • 得益于 vue 的响应式系统,一般情况是不需要强制更新的,
  • 如果需要的话很可能是自己的代码有疏漏,比如没有留意到数组或对象的变更检测注意事项,或者依赖了一个未被 Vue 的响应式系统追踪的状态
  • 非要强制更新的话就用 vm.$forceUpdate(),这个方法是迫使 vue 实例重新渲染
通过 v-once 创建低开销的静态组件
  • 组件包含了大量静态内容,可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来
  • 这个东西也是有弊端的,不要随意滥用
Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值