跳至主要內容

vue组件

技术中心大约 13 分钟

vue组件

组件是可复用的 Vue 实例,且带有一个名字。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把组件作为自定义元素来使用。

因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。

组件注册

在组件注册的时候需要调用Vue.component( id, [definition] )方法, 第一个参数 'id' 就是组件的名称,第二个参数是组件对象。

你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

定义组件名的方式有两种:

  • 使用 kebab-case
    Vue.component('my-component-name', { /* ... */ })
    
    当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>。
  • 使用 PascalCase
    Vue.component('MyComponentName', { /* ... */ })
    
    当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name> 和 <MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

推荐使用 kebab-case 方式

全局组件

通过全局注册的组件就是全局组件,在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。

Vue.component('global-components1', {
    template: '<div>我是全局组件</div>'
})
// 如果使用的是运行时版本的Vue会报错:
You are using the runtime-only 
build of Vue where the template compiler is not available.
Either pre-compile the templates into render functions, 
or use the compiler-included build.

// 解决方法:
const component = Vue.extend({
    render(h) {
        return h('div', {}, ['我是全局组件1'])
    }
})
Vue.component('global-components1', component)

// Vue.extend:
// 使用基础 Vue 构造器,创建一个“子类”。
可以帮我们创建一个基础的Vue实例

全局组件还可以通过Vue插件的方式来引入,通过 Vue.use() 写插件

局部组件

通过一个普通的 JavaScript 对象来定义组件:

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

然后在 components 选项中定义你想要使用的组件:

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

对于 components 对象中的每个属性来说:

  • 属性就是上面提到的注册组件的id, 也是自定义元素名称
  • 属性值就是组件对象

如果是通过 Babel 和 webpack 使用 ES2015 模块,则可以通过import来引入:

<template>
  <div id="app">
    <my-page></my-page>
  </div>
</template>

<script>
 // 组件的挂载元素 el 就是组件中 template 中的根元素,
 // 所以在Vue2的组件中根元素必须要求有且只有一个
import MyPage from './my-page/my-page.vue'

export default {
  name: 'App',
  components: {
    'my-page': MyPage
  }
}
</script>

注意在 ES2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是

ComponentA: ComponentA 的缩写,即这个变量名同时是:

  • 用在模板中的自定义元素的名称
  • 包含了这个组件选项的变量名
import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

组件的组成

  • name: 组件名
  • computed: 计算属性
  • data:一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
  • props: 父组件传递过来的参数
  • methods: 组件中自定义的方法
  • watch: 监测Vue实例上的数据变动
  • 生命周期

组件生命周期

Vue的初始化过程中,可以传入很多的函数,这些函数会在不同的时间点被调用。那么这些回调函数就称为Vue的生命周期回调函数。

生命周期是同步执行的

  • 第一步Vue初始化的时候会初始化一些事件:onon、off、$emit等等

    以及一些生命周期: 这里的生命周期指例如,该组件的父亲是谁,儿子是谁等相关信息

  • 然后就会调用 beforeCreate :组件进入开始创建前,然后就可以进行初始化注入和校验

    • 检验我们的组件所需的参数是否正确
    • 帮我们把数据进行响应式处理
    • 初始化props、watch等等
  • 进入 created: Vue实例创建完成了

    • 把数据准备好了
    • 判断当前是否指定了 el元素 元素,前面我们也讲过了如果没有el的话,就没法把东西放到页面上
    • 有el就看看有没有template
    • 没有el就看有没有 $mount(el)
    • 将template编译成render函数
    • 将el外部的HTML编译成template, 在编译成render函数
  • beforeMount: 拿到了render函数,vue就会进入beforeMount回调, 进行挂载之前

    • 在内部创建一个 vm.$el, 并渲染好
    • 把渲染好的结果替换掉 el
  • mounted: 渲染完成

  • beforeUpdate: 在数据有变化,要进行更新视图之前,就会调用beforeUpdate方法

    • 进行vue dom-diff操作
    • 虚拟dom重新渲染完成,并更新
  • updated: 更新完成

如果调用了 vm.$destory(): 移除所有的watcher,事件的解绑,实例的销毁

  • beforeDestroy: 可以在真正被销毁前做一些需要做的事情

    • 解除定时器
    • 解除自定义事件监听
  • destroyed: 整个Vue实例已经销毁完成了,无法再访问到该实例了

vue生命周期
vue生命周期

总结:

常用到的生命周期函数:

  • created: 数据、事件、监听都准备好了,如果在初始化的时候要进行数据的修改,或者异步方法的调用,比如调用后端接口,这个时候就可以在这个生命周期回调函数中,

    但是不能在这个时候操作dom, 因为这个时候组件还没有创建好,$el,更没有把渲染好的dom替换到真实dom上去

  • mounted: 除开可以做created中可以做的事情以外,dom这个时候也已经准备好了,这个时候可以去操作dom了

  • beforeDestroy 在vue实例销毁之前,做一些善后工作:

    • 解除定时器
    • 解除自定义事件监听 等

父子嵌套组件的渲染顺序:

  • 父组件先进行beforeCreate
  • 父组件created
  • 父组件beforeMount
  • 调用父组件的render方法,这个时候就回去渲染子组件,因为要把渲染好的子组件通过render方法和父组件的元素一起渲染到el上
  • 子组件的beforeCreate、created 、beforeMount 、mounted
  • 父组件mounted,渲染挂载完成

关于组件的总结:

组件: 组件其实就是一个对象

组件的实例化过程就是通过当前传入的对象,创建出一个实例

(通过这个对象创建出一个Vue的构造函数,并且实例化)

为什么要组件化?

  • 可以方便维护代码,我们可以抽离实现组件复用

  • 可以组件级别更新,每个组件都有自己的watcher(依赖收集),不像传统页面那样整个页面都需要更新

  • 能抽离成单个组件就尽量抽离,能减少更新 (独立)

组件初始化的时候就传了一个对象,为什么就能正常生成Vue实例了呢?

Vue.component('global-components1', {
    render(h) {
        return h('div', {}, ['我是全局组件1'])
    }
})

在内部会调用 Vue.extend() 方法

Vue.extend() 作用:

如果返回的是一个对象,会返回当前这个对象的构造函数,即构造一个子类

 let compont1 = Vue.extend({
      render(h) {
        return h('div', {}, [this.msg])
      }, 
      data () {
          return {
              msg: 'helloNew'
          }
      }
  })
  
  console.log(compont1) // 返回的是一个子的构造器
  
  // 手动挂载组件
  const dom1 = new compont1.$mount().$el
  document.bodt.appendChild(dom1)
  Vue.compontent('compont1', compont1)
  
  // Vue.component() 会调用 Vue.extend()方法
  

为什么组件的data必须是个函数?
如果不是函数的话,那么就会形成多个组件公用一坨数据,组件之间就不是相互独立的了。
为了防止数据之间的数据相互引用,必须通过data返回一个函数

上面这么写组件很麻烦,

我们想要把一个组件所需要的所有东西:模板,js,样式

都写在一个文件里。

所以我们需要一个能解析这个文件的东西,我们可以通过vue-cli脚手架配置专门用于解析.vue文件的vue-loader,这样就能成功解析这个文件了。

    <template>
     <div>hello</div>
    </template>
    
    // 主要是通过 vue-loader 去解析这个template
    // vue-loader => 找.vue文件
    // 在编译的时候通过 vue-template-compiler api来实现的
    // 然后在运行的时候已经把template里面的内容变成render函数了
    
    <script>
        export default {
            
        }
        
        // 上面这个对象就是
        Vue.extend({
            
        })
       // 里面传递的对象
    </script>
    
    <style>
    
    </style>
    
    
    // 使用
    import compontent from 'xxx'
    conosle.log(compontent) // 就是一个组件对象
    // 然后会调用组件的 render 方法进行渲染

组件通信

数据传递关系:

  • 父传子
  • 子传父
  • 平级传递(兄弟组件)
  • 跨级通信 (毫不相关联的组件)

props

// parent.vue
<template>
    <div>
        parent
        <Son1 :money="moneny"></Son1>
     </div>
</template>

<script>
    import Son1 from './son1.vue'
    export default {
        name: 'Parent',
        components: {
            Son1
        },
        data() {
            return {
                moneny: 100
            }
        }
    }
</script>
<style>
</style>

// son.vue
<template>
    <div>
        son1
        <div>父亲给的钱: {{money}}</div>
    </div>
</template>

<script>
    export default {
        name: 'Son1',
        props: {
            money: {
                type: Number,
                default: 0
            },
        },
    }
</script>

<style>
</style>
  • 子组件不能更改父组件中的数据,因为属性不是响应式的

那子想要改父的数据呢?

// parent.vue:
<Son1 :money="moneny" :change-money="changeMoney"></Son1>
methods: {
    changeMoney(value) {
        console.log('儿子更改了后的钱数: ', value)
        this.moneny = value
    }
}

// son.vue: 
 props: {
    money: {
        type: Number,
        default: 0
    },
    ChangeMoney: {
        type: Function,
        default: () => {}
    }
}

<button @click.stop="ChangeMoney(500)">更改父亲的钱数</button>

// props既可以传属性,也可以传递方法,
// 然后可以在子调用父的函数方法来更新数据
// 父组件数据更新了会重新渲染子组件

// 单向数据流,数据都是从上到下的

事件法

$emit() 与 $on()

 <!-- 方式2 -->
 // parent.vue
 <Son1 :money="moneny" @change-money="changeMoney"></Son1>
 methods: {
    changeMoney(value) {
        console.log('儿子更改了后的钱数: ', value)
        this.moneny = value
    }
}

// son.vue
<!-- 方式2 -->
<button @click.stop="childMoney(300)">通过事件更改父亲的钱数</button>
methods: {
    childMoney(value) {
        this.$emit('change-money', value)
    }
}

// 这里触发的事件不是原生的事件
// 通过事件绑定的方式传递shuju

跨级传递数据

// parent.vue中:
<Son2></Son2>

// son2.vue中:
<GrandSon></GrandSon>

现在有个需求是把parent中的数据直接传递给GrandSon

一层一层的props 传递

一层一层的props 传递 (非常麻烦, 要是层数特别多,不易于维护,性能也差)

provide 与 inject

// parent.vue
<Son2></Son2>

export default {
    name: 'Parent',
    provide () { // 提供者  上下文
        return {parent: this} // 直接将这个父组件暴露出去
    },
    components: {
        Son2
    },
    data() {
        return {
            moneny: 100
        }
    }
}

// grandson.vue
<div>
    grandson
    <div>爷爷的钱: {{parent.moneny}}</div>
</div>

<script>
    export default {
        inject: ['parent']
    }
</script>

provide 与 inject 是响应式的,当祖先值变了 都会更新

注意:provide 与 inject的问题:

  • 会造成单向数据流的混乱,你不知道数据从哪儿来的

自己实现工具库的时候可以使用这个方法

  • 不能改祖先组件传进来的数据,只能使用
    要改的话,还是调用祖先组件的方法,然后通过祖先组件自己里面的方法来修改,然后再自动的向下更新

$attrs 与 $listeners

表示所有属性和方法的集合,可以使用v-bind 或者 v-on传递

// app.vue
<Test a="1" b="2" c="3" d="4" @click="testClick" @mousedown="testMouseDown"></Test>

// test.vue
<template>
    <div>
        test.vue: 
        {{$attrs}}
        <!-- 将所有属性都传递给子组件 -->
        <aa v-bind="$attrs"></aa>
        <!-- 除开传属性也可以传方法 -->
        <!-- <aa v-bind="$attr" @click="aaClick" @mousedown="aaMouseDown"></aa> -->
        <!-- <aa v-bind="$attr" v-on="$listeners"></aa> -->
    </div>
</template>

<script>
    import aa from './aa.vue'
    // 这个组件只是一个过渡的组件,他不需要使用这些属性
    // 如果在props里面使用了, attr里就会减少
    // attr是响应式的
    export default {
        inheritAttrs: false, // 这些属性,虽然没用,但是不希望增加到dom元素上
        props: ['a', 'b'],
        components: {
            aa
        },
        methods: {
            aaClick() {
                console.log('test.vue 里面的click回调')
            },
            aaMouseDown() {
                console.log('test.vue 里面的mousedown回调')
            }
        },
    }
</script>

<style>
</style>

// aa.vue
<template>
    <div>
        aa.vue
    {{$attrs}}
    <!-- <button @click="$listeners.click">aa点击</button> -->
   </div>
</template>

<script>
    export default {
        mounted() {
            console.log(this.$attrs)
            console.log(this.$listeners)
        }
    }
</script>

<style>
</style>

ref 和 $ref

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:

<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>

通过$ref可以获取vue实例,这样就可以拿到该实例的数据和调用齐方法,这也算是通信的一种方式。适用于父子组件和兄弟组件

eventBus

事件总线,也叫事件中转,通过一个中间组件来发送和接收事件

可以让两个毫不相关联的组件进行通信

  • 先写一个eventBus 组件

eventBus.js:

import Vue from 'vue'
export default new Vue()
  • 需要收发事件的页面都引入 eventBus
import eventBus from '@/eventbus/index.js'
  • 事件触发
 eventBus.$emit('play-warn-voice', data)
  • 事件监听
mounted() {
	eventBus.$on('play-warn-voice', _type => { // 监听播放警情音频的事件
		this.type = _type;
		this.playAudio();
	});
},
beforeDestroy() {
	eventBus.$off('play-warn-voice');
}

缺点:

  • 事件发生者只能单向广播,无法获得接收者对事件的处理结果, 不可控因素太高;
  • 事件混乱
  • 导致事件发送者和接收者都依赖耦合事件类,一旦这个类出现什么问题,所有的组件都受到影响了

尽量少的用到eventBus

Vuex

通过Vuex状态管理也可以存储数据和调用方法更新数据,并且数据更新以后所有用到该数据的地方都将自动更新,这也是通信方式的一种。后续会对Vuex进行详细的介绍。

组件库

Vue组件库就是Vue.js对一些组件和功能进行封装并暴露出相关的API供使用者调用,同时拥有一定的UI样式,通过组件库我们可以快速高效的进行开发,不用所有东西都从头开始造轮子。

组件库的两大核心:

  • 全局引用

    引入一次就可以在任何组件里面使用组件库的所有组件和方法

    打包体积较大,加载较慢

  • 按需引用

    在需要使用到某个组件和方法的地方再单独对该组件或方法进行引入然后使用

    打包体积也会较小,首页加载更快,使用该组件库的组件打开的时候才会去加载。

常见开源vue组件库

  • Element UI
  • IViewUI
  • Ant Design of Vue
  • HeyUI

iviewui

IViewUI官方文档open in new window

全局引入

import ViewUI from 'view-design';
import 'view-design/dist/styles/iview.css';

Vue.use(ViewUI);

按需引入
借助插件 babel-plugin-import可以实现按需加载组件,减少文件体积。

首先安装,并在文件 .babelrc 中配置:

npm install babel-plugin-import --save-dev

// .babelrc
{
  "plugins": [["import", {
    "libraryName": "view-design",
    "libraryDirectory": "src/components"
  }]]
}

然后这样按需引入组件,就可以减小体积了:

import { Button, Table } from 'view-design';
Vue.component('Button', Button);
Vue.component('Table', Table);