Vue基础 —— 组件

Vue

# 组件间通信方式

大多数情况下,组件都不是孤立存在的,他们需要共同协作构成一个复杂的业务功能,在Vue中,为不同的组件关系提供了不同的通信规则。

vue中提倡单向数据流,这是为了保证数据流向的简洁性,使程序更易于理解。但对于一些边界情况,vue也提供了隐性的通信方式,这些通信方式会打破单向数据流的原则,应该谨慎使用。

常见的组件间通信的方式有:

# 父子组件通信

# Props:父组件给子组件传值 (最简单的一种方式)

  • 父组件中给子组件通过相应属性传值
  • 子组件通过props接受数据

通过 prop 向下传递,通过事件向上传递是一个 vue 项目最理想的通信状态。

注意

  1. 不应该在一个子组件内部改变 prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换。
  2. 第二,如果 props 传递的是引用类型(对象或者数组),在子组件中改变这个对象或数组,父组件的状态会也会做相应的更新,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 .sync。
<!--子组件-->
<template>
  <div>
    <h1>Props Down Child</h1>
    <h2>{{ title }}</h2>
  </div>
</template>

<script>
export default {
  // 子组件中通过props来接收父组件传的值
  // props可以是数组也可以是对象
  // 如果想约定传值的类型用对象,这里title定了是string类型,如果传number类型会报错
  // props: ['title'],
  props: {
    title: String
  }
}
</script>

<style>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--父组件-->
<template>
  <div>
    <h1>Props Down Parent</h1>
    <!--2. 使用子组件的使用通过属性给子组件传值,这里也可以是表达式,绑定data中的成员-->
    <child title="My journey with Vue"></child>
  </div>
</template>

<script>
import child from './01-Child'
export default {
 // 1. 注册子组件
  components: {
    child
  }
}
</script>

<style>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# $emit\$event\$on: 子组件给父组件传值

  • 子组件通过自定义事件,用$emit触发的时候携带参数给父组件传值
  • 父组件通过$on注册子组件内部触发的事件,并接收传递的数据,行内可以通过$event获取事件传递的参数 (事件处理函数中是不这么使用的)
<!--子组件-->
<template>
  <div>
    <h1 :style="{ fontSize: fontSize + 'em' }">Props Down Child</h1>
    <button @click="handler">文字增大</button>
  </div>
</template>

<script>
export default {
 // 通过props接收父组件传的默认字体大小
  props: {
    fontSize: Number
  },
  methods: {
    handler () {
    // 当点击按钮的时候,触发自定义事件enlargeText放大字体,让字体放大0.1
    // this是当前子组件对象,this.$emit这个是由子组件触发的自定义事件,当注册事件的时候要给子组件注册该事件
      this.$emit('enlargeText', 0.1)
    }
  }
}
</script>

<style></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--父组件-->
<template>
  <div>
  <!--父组件将fontSize进行绑定-->
    <h1 :style="{ fontSize: hFontSize + 'em'}">Event Up Parent</h1>

    这里的文字不需要变化
    <!--使用子组件,通过v-on给子组件设置了自定义方法enlargeText-->
    <child :fontSize="hFontSize" v-on:enlargeText="enlargeText"></child>
    <child :fontSize="hFontSize" v-on:enlargeText="enlargeText"></child>
    <!--还有一种在行内获取值的方式,获取自定义事件传递数据的时候,直接通过$event获取,这个值是触发事件传递的0.1-->
    <child :fontSize="hFontSize" v-on:enlargeText="hFontSize += $event"></child>
  </div>
</template>

<script>
import child from './02-Child'
export default {
  components: {
    child
  },
  data () {
    return {
      hFontSize: 1
    }
  },
  methods: {
  // 子组件把值传递给了父组件,父组件通过参数接收到了值,进行运算
    enlargeText (size) {
      this.hFontSize += size
    }
  }
}
</script>

<style></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# v-model 指令

v-model 是用来在表单控件或者组件上创建双向绑定的,他的本质是 v-bind 和 v-on 的语法糖,在一个组件上使用 v-model,默认会为组件绑定名为 value 的 prop 和名为 input 的事件。

当我们组件中的某一个 prop 需要实现上面所说的”双向绑定“时,v-model 就能大显身手了。有了它,就不需要自己手动在组件上绑定监听当前实例上的自定义事件,会使代码更简洁。

下面以一个 input 组件实现的核心代码,介绍下 v-model 的应用。

<!--父组件-->
<template>
    <base-input v-model="input"></base-input>
</template>
<script>
    export default {
        data() {
            return {
                input: ''
            }
        },
    }
</script>


<!--子组件-->
<template>
    <input type="text" :value="currentValue"  @input="handleInput">
</template>
<script>
    export default {
        data() {
            return {
                currentValue: this.value === undefined || this.value === null ? ''
            }
        },
        props: {
            value: [String, Number],
        },
        methods: {
            handleInput(event) {
                const value = event.target.value;
                this.$emit('input', value);
            },
        },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

有时,在某些特定的控件中名为 value 的属性会有特殊的含义,这时可以通过 model 选项来回避这种冲突。

# .sync 修饰符

背景知识

.sync 修饰符在 vue 1.x 的版本中就已经提供,1.x 版本中,当子组件改变了一个带有 .sync 的 prop 的值时,会将这个值同步到父组件中的值。这样使用起来十分方便,但问题也十分明显,这样破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

于是乎,在vue 2.0中移除了 .sync。但是在实际的应用中,.sync 是有它的应用场景的,所以在 vue 2.3 版本中,又迎来了全新的 .sync

新的 .sync 修饰符所实现的已经不再是真正的双向绑定,它的本质和 v-model 类似,只是一种缩写。

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
1
2
3
4

上面的代码,使用 .sync 就可以写成

<text-document v-bind:title.sync="doc.title"></text-document>
1

这样,在子组件中,就可以通过下面代码来实现对这个 prop 重新赋值的意图了。

this.$emit('update:title', newTitle)
1

v-model 和 .sync 对比

.sync 从功能上看和 v-model 十分相似,都是为了实现数据的“双向绑定”。本质上,也都不是真正的双向绑定,而是语法糖

相比较之下:

  • .sync 更加灵活,它可以给多个 prop 使用,而 v-model 在一个组件中只能有一个。
  • 从语义上来看,v-model 绑定的值是指这个组件的绑定值,比如 input 组件,select 组件,日期时间选择组件,颜色选择器组件,这些组件所绑定的值使用 v-model 比较合适。
  • 其他情况,没有这种语义,个人认为使用 .sync 更好。

# ref

ref 特性可以为子组件赋予一个 ID 引用,通过这个 ID 引用可以直接访问这个子组件的实例。当父组件中需要主动获取子组件中的数据或者方法时,可以使用 $ref 来获取。

<!--父组件-->
<template>
    <base-input ref="baseInput"></base-input>
</template>
<script>
    export default {
        methods: {
        focusInput: function () {
            this.$refs.usernameInput.focus()
        }
    }
}
</script>

<!--子组件-->
<template>
    <input ref="input">
</template>
<script>
    export default {
    methods: {
        focus: function () {
            this.$refs.input.focus()
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

使用ref时,有两点需要注意

  1. $refs 是作为渲染结果被创建的,所以在初始渲染的时候它还不存在,此时无法无法访问。
  2. $refs 不是响应式的,只能拿到获取它的那一刻子组件实例的状态,所以要避免在模板和计算属性中使用它。

ref的两个作用

  1. 在普通HTML标签上使用ref,用$refs获取到的是DOM对象
  2. 在组件标签上使用ref,用$refs获取到的是组件实例
<!--子组件,一个简单的自定义组件,功能是能够获取焦点的自定义文本框。-->
<template>
  <div>
    <h1>ref Child</h1>
    <!--这个input标签上设置了ref属性-->
    <input ref="input" type="text" v-model="value">
  </div>
</template>

<script>
export default {
  data () {
    return {
      value: ''
    }
  },
  methods: {
    focus () {
      // 通过this.$refs.input获取input的DOM对象,并调用其focus方法让文本框获取焦点
      this.$refs.input.focus()
    }
  }
}
</script>

<style></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--父组件,-->
<template>
  <div>
    <h1>ref Parent</h1>
    <!--在子组件的标签上设置了ref-->
    <child ref="c"></child>
  </div>
</template>

<script>
import child from './04-Child'
export default {
  components: {
    child
  },
  mounted () {
    // 这里想要拿到子组件的话,必须要等组件渲染完毕,所以这里在mounted函数下
    // 这里通过this.$refs.c就是子组件对象,拿到这个对象就可以访问其属性和方法
    // 这里调用子组件方法让其内部获取焦点
    this.$refs.c.focus()
    // 通过value属性给文本框赋值
    this.$refs.c.value = 'hello input'
  }
}
</script>

<style>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# $parent 和 $children

$parent 属性可以用来从一个子组件访问父组件的实例,$children 属性 可以获取当前实例的直接子组件。

看起来使用 $parent 比使用prop传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop 那样需要提前定义好。但使用 $parent 会导致父组件数据变更后,很难去定位这个变更是从哪里发起的,所以在绝大多数情况下,不推荐使用。

在有些场景下,两个组件之间可能是父子关系,也可能是更多层嵌套的祖孙关系,这时就可以使用 $parent。

下面是 element ui 中的组件 el-radio-group 和 组件 el-radio 使用示例:

<template>
  <el-radio-group v-model="radio1">
    <el-radio :label="3">备选项</el-radio>
    <component-1>
        <el-radio :label="3">备选项</el-radio>
    </component-1>
  </el-radio-group>
</template>

<script>
  export default {
    data () {
      return {
        radio2: 3
      };
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在 el-radio-group 和 组件 el-radio 通信中, 组件 el-radio 的 value 值需要和 el-radio-group的 v-model 的值进行“绑定”,我们就可以在 el-radio 内借助 $parent 来访问到 el-radio-group 的实例,来获取到 el-radio-group 中 v-model 绑定的值。

下面是获取 el-radio 组件中获取 el-radio-group 实例的源码:

// el-radio组件
    let parent = this.$parent;
    while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
        } else {
            this._radioGroup = parent; // this._radioGroup 为组件 el-radio-group 的实例
        }
    }
1
2
3
4
5
6
7
8
9

# 非父子组件通信

# $attrs 和 $listeners

当要和一个嵌套很深的组件进行通信时,如果使用 prop 和 events 就会显的十分繁琐,中间的组件只起到了一个中转站的作用,像下面这样:

<!--父组件-->
  <parent-component :message="message">我是父组件</parent-component>
<!--子组件-->
  <child-component :message="message">我是子组件</child-component>
<!--孙子组件-->
  <grand-child-component :message="message">我是孙子组件</grand-child-component>
1
2
3
4
5
6

当要传递的数据很多时,就需要在中间的每个组件都重复写很多遍,反过来从后代组件向祖先组件使用 events 传递也会有同样的问题。使用 $attrs 和 $listeners 就可以简化这样的写法。

$attrs 会包含父组件中没有被 prop 接收的所有属性(不包含class 和 style 属性),可以通过 v-bind="$attrs" 直接将这些属性传入内部组件。

$listeners 会包含所有父组件中的 v-on 事件监听器 (不包含 .native 修饰器的) ,可以通过 v-on="$listeners" 传入内部组件。

下面以父组件和孙子组件的通信为例介绍它们的使用:

<!--父组件 parent.vue-->
<template>
    <child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
    inheritAttrs: false,
    data() {
        return {
            name: '通信',
            message: 'Hi',
        }
    },
    methods: {
        sayHello(mes) {
            console.log('mes', mes) // => "hello"
        },
    },
}
</script>
<!--子组件 child.vue-->
<template>
    <grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
    data() {
        return {}
    },
    props: {
        name,
    },
}
</script>
<!--孙子组件 grand-child.vue-->
<template>
</template>
<script>
export default {
    created() {
        this.$emit('sayHello', 'hello')
    },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# provide 和 inject

provide 和 inject 需要在一起使用,它可以使一个祖先组件向其所有子孙后代注入一个依赖,可以指定想要提供给后代组件的数据/方法,不论组件层次有多深,都能够使用。

<!--祖先组件-->
<script>
export default {
    provide: {
        author: 'yushihu',
    },
    data() {},
}
</script>
<!--子孙组件-->
<script>
export default {
    inject: ['author'],
    created() {
        console.log('author', this.author) // => yushihu
    },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

provide 和 inject 绑定不是响应的,它被设计是为组件库和高阶组件服务的,平常业务中的代码不建议使用。

# dispatch 和 broadcast

vue 在2.0版本就已经移除了 $dispatch 和 $broadcast,因为这种基于组件树结构的事件流方式会在组件结构扩展的过程中会变得越来越难维护。但在某些不使用 vuex 的情况下,仍然有使用它们的场景。所以 element ui 和 iview 等开源组件库中对 broadcast 和 dispatch 方法进行了重写,并通过 mixin 的方式植入到每个组件中。

实现 dispatch 和 broadcast 主要利用我们上面已经说过的 $parent 和 $children。

//element ui 中重写 broadcast 的源码
function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;
    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11

broadcast 方法的作用是向后代组件传值,它会遍历所有的后代组件,如果后代组件的 componentName 与当前的组件名一致,则触发 $emit 事件,将数据 params 传给它。

 //element ui 中重写 dispatch 的源码
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

dispatch 的作用是向祖先组件传值,它会一直寻找父组件,直到找到组件名和当前传入的组件名一致的祖先组件,就会触发其身上的 $emit 事件,将数据传给它。这个寻找对应的父组件的过程和文章前面讲解 $parent 的例子类似。

# eventBus

对于比较小型的项目,没有必要引入 vuex 的情况下,可以使用 eventBus。

说明

  • 也是使用自定义事件的方式,但是因为没有父子关系,所以不能通过子组件触发传值,所以这里需要使用eventBus,即创建一个公共的Vue实例,这个实例的作用是作为事件总线,或者事件中心。
  • eventBus:创建一个Vue实例,这个实例并不是用来展示内容,所以没有传递任何的选项,我们使用他的目的是使用$emit$on,用来触发和注册事件。
  • 触发事件组件:通过$emit触发事件,并传递参数
  • 注册事件组件:通过$on注册事件,接收参数进行处理

例子一:

//eventBus.js
import Vue from 'vue';
export default new Vue();


<!--组件A-->
<script>
import Bus from 'eventBus.js';
export default {
    methods: {
        sayHello() {
            Bus.$emit('sayHello', 'hello');
        }
    }
}
</script>


<!--组件B-->
<script>
import Bus from 'eventBus.js';
export default {
    created() {
        Bus.$on('sayHello', target => {
            console.log(target);  // => 'hello'
        });
    }
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

例子二:

// eventbus.js
import Vue from 'vue'
export default new Vue()
1
2
3
<!--组件一:触发事件-->
<template>
  <div>
    <h1>Event Bus Sibling01</h1>
    <div class="number" @click="sub">-</div>
    <input type="text" style="width: 30px; text-align: center" :value="value">
    <div class="number" @click="add">+</div>
  </div>
</template>

<script>
// 这个组件要触发事件,将事件中心导入
import bus from './eventbus'

export default {
// props参数num
  props: {
    num: Number
  },
  // 因为props的值不能随便改动,所以传递给value
  created () {
    this.value = this.num
  },
  data () {
    return {
      value: -1
    }
  },
  methods: {
  // 减值操作,判断不能为0
    sub () {
      if (this.value > 1) {
        this.value--
        // 触发bus的自定义事件numchange,并把value当参数传递出去
        bus.$emit('numchange', this.value)
      }
    },
    // 加值操作,和减值类似
    add () {
      this.value++
      bus.$emit('numchange', this.value)
    }
  }
}
</script>

<style>
.number {
  display: inline-block;
  cursor: pointer;
  width: 20px;
  text-align: center;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!--组件二:定义-->
<template>
  <div>
    <h1>Event Bus Sibling02</h1>

    <div>{{ msg }}</div>
  </div>
</template>

<script>
// 因为要注册事件,所以将事件中心导入
import bus from './eventbus'
export default {
  data () {
    return {
      msg: ''
    }
  },
  created () {
    // 通过bus注册了numchange事件,事件处理函数中接收事件触发时候传递的参数,进行展示
    bus.$on('numchange', (value) => {
      this.msg = `您选择了${value}件商品`
    })
  }
}
</script>

<style>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!--App.vue-->
<template>
  <div id="app">
    <h1>不相关组件传值</h1>
    <sibling0301 :num="num"></sibling0301>
    <sibling0302></sibling0302>
  </div>
</template>

<script>
import sibling0301 from './components/03-event-bus/03-Sibling-01'
import sibling0302 from './components/03-event-bus/03-Sibling-02'

export default {
  name: 'App',
  components: {
    sibling0301,
    sibling0302,
  },
  data () {
    return {
      num: 1
    }
  }
}
</script>

<style></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 通过 $root 访问根实例

通过 $root,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data,就可以实现组件间的数据共享。

//main.js 根实例
new Vue({
    el: '#app',
    store,
    router,
    // 根实例的 data 属性,维护通用的数据
    data: function () {
        return {
            author: ''
        }
    },
    components: { App },
    template: '<App/>',
});


<!--组件A-->
<script>
export default {
    created() {
        this.$root.author = '于是乎'
    }
}
</script>


<!--组件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。

# Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。对一个中大型单页应用来说是不二之选。

# 唠唠叨叨

  • $root$parent$children$ref,通过这几种属性获取根组件成员,实现组件之间的通信。但这些都是不被推荐的方式。只有当项目很小,或者在开发自定义组件的时候,才会使用到。如果是大型项目的话,还是推荐使用Vuex管理状态。

如果滥用的话可以造成状态管理的混乱。

更新时间: 2022-02-06 00:16