你也许不知道的Vuejs - 组件式开发

you-may-not-know-vuejs.png

by yugasun from https://yugasun.com/post/you-may-not-know-vuejs-4.html
本文可全文转载,但需要保留原作者和出处。

初识组件

组件(Component)绝对是 Vue 最强大的功能之一。它可以扩展HTML元素,封装可复用代码。从较高层面讲,可以理解组件为自定义的HTML元素,Vue 的编译器为它添加了特殊强大的功能。所有的 Vue 组件同时也都是 Vue 的实例,因此可以接受相同的选项对象(除了一些特有的选项)并提供相同的生命周期函数。

再来回顾下 你也许不知道的Vuejs - 花式渲染目标元素 中的代码:

<div id="app1">
  <helloworld/>
</div>
Vue.component("helloworld", {
  template: "<h1>{{ msg }}</h1>",
  data () {
    return {
      msg: "Hello Vue.js!"
    }
  }
})
var app1 = new Vue({
  el: "#app1"
})

上面通过 Vue.component 注册了一个全局组件,然后在 div#app 元素内通过 <helloworld/> 标签直接使用。可以看出,这里就是相当于自定义了一个 HTML 元素 helloworld,它的功能就是输出一个内容为 msgh1 标签。这就是一个基本全局组件的定义方式,当然你也可以注册为局部组件:

var app2 = new Vue({
  el: "#app2",
  components: {
    'helloworld': {
      template: "<h1>{{ msg }}</h1>",
      data () {
        return {
          msg: "Hello Vue.js!"
        }
      }
    }
  }
})

无论是全局或者局部注册组件,它跟上一篇中的指令注册是非常相似的。局部注册组件就是在创建 Vue 实例的时候添加一个 components 对象属性,它的键值对就是一个自定义组件,键是组件名,值是创建组建的配置对象参数。当然也可以将组件定义放到单独的文件,然后通过引入的方式,然后添加到components属性中,这个在单文件组件中会具体讲到。

组件间通信

既然说到组件,就不得不说组件间通信了,实际开发中,我们经常需要在不同组件间传递/共享数据,所以实现组件间通信是非常重要的。

组件间关系可以总结为 父子组件非父子组件,自然通信方式也就是这两种了。

父子组件间通信

image

如上图,在 Vue 中,父子组件的关系可以总结为 props向下传递,事件向上传递。也就是父组件通过 prop 给子组件下发数据,子组件通过 $emit 事件, 给父组件发送数据。先来看个例子:

<div id="app3">
  当前输入内容:{{ text }}<br>
  <com-input :text="text" v-on:change="handleChange"/>
</div>
Vue.component('com-input', {
  props: {
    text: {
      type: String,
      default: "请输入"
    }
  },
  template: '<input v-on:change="handleChange" v-model="msg"/>',
  data () {
    return {
      // 这里定义为 input 的 v-model绑定值
      msg: this.text
    }
  },
  methods: {
    // 当input值变化时,执行函数,通过 $emit change 事件,
    // 父级组件通过 v-on:change 来监听此事件,执行相关操作
    handleChange (e) {
      this.$emit('change', this.msg)
    }
  }
})
var app3 = new Vue({
  el: "#app3",
  data () {
    return {
      text: 'Hello Vue.js'
    }
  },
  methods: {
    handleChange (val) {
      this.text = val
    }
  }
})

原理解析:上面的代码中,先通过 Vue.component 定义了 com-input 组件,给它添加了 props 属性,用来接收父级通过属性传递的属性数据 text,这里 text 是个对象,含有 type - 属性值类型default - 默认值 两个属性。当然 props 也可以为所有从父级接受的属性数组,有关 props 基础知识请直接阅读 官方文档。然后将初始值赋值给了 data 中的 msg,该子组件的模板是个 input,通过 v-model 实现 input的值msg 的双向绑定,当input值变化时,通过 this.$emit('change', this.msg),发出 change 事件,同时将当前值作为监听器回调参数,这样父级组件就可以通过 v-on:change 来监听此事件,获取修改后的值,执行相关操作了。

虽然这段代码同时实现了上述图片中的 父 -> 子子 -> 父 通信流程,但是代码还是比较繁琐的,单纯实现单个数据的循环传递,就需要父子组件同时监听改变事件,执行监听回调函数,是不是太麻烦了。要是能直接修改 props 中的 text 值就好了,实践证明,这是不行的,因为直接修改,会报下面错误(注意只有引入 vue.js 文件才会出现,因为 vue.min.js 文件移除了 [Vue warn] 错误提示功能):

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "text"

这个问题,Vue 作者早就想到了,那就是使用 .sync 修饰符。早在 1.x 版本中此功能是一直存在的,但是作者认为它破坏了 单向数据流 的原则,所以 2.0 发布后,就移除了该修饰符,但是后来发现在实际开发中,有很多相关需求, 于是在 2.3.0+ 版本后,又重新引入了 .sync 修饰符,不过内部实现是跟 1.x 版本有区别的,它并没有破坏 单向数据流 原则,实际上内部就是帮我们实现了父级组件监听和修改相关属性值的操作。

使用 .sync 修改后的代码如下:

<div id="app4">
  当前输入内容:{{ text }}<br>
  <com-input2 v-bind:text.sync="text"/>
</div>
Vue.component('com-input2', {
  props: {
    text: {
      type: String,
      default: "请输入"
    }
  },
  template: '<input v-on:change="handleChange" v-model="msg"/>',
  data () {
    return {
      msg: this.text
    }
  },
  methods: {
    handleChange (e) {
      this.$emit('update:text', this.msg)
    }
  }
})
var app4 = new Vue({
  el: "#app4",
  data () {
    return {
      text: 'Hello Vue.js'
    }
  }
})

这次我们只是将子组件的 $emit 事件名修改为 update:text,并删除了父级组件 v-on:change 监听和相关监听回调,并在模板中 v-bind:text 后面添加了 .sync 修饰符,这样就是实现了相同的功能,代码确实精简了很多。实际上 Vue 在编译含有 .sync 修饰符的 v-bind 指令时,会自动实现监听 update 事件的相关代码,也就是:

<com-input2 v-bind:text.sync="text"/>

会被扩展为:

<com-input2 v-bind:text="text" v-on:update="val => text = val"/>

注意:val => text = val 是箭头函数,关于箭头函数的介绍可以看这里:箭头函数

这样一解析就很好理解了,全部是我们上一节讲到的内容。

非父子组件间通信

如果是两个非父子组件,并且有共同的父级组件,那么它拆解为 子 -> 父 -> 子 的过程,这个就完全可以使用 父子组件间通信 方法实现。如果是多个组件或者不同父组件的组件间通信,这时我们可以借助创建空的 Vue 实例作为事件总线,通过 发布订阅模式 进行数据传递。 代码如下:

<div id="app5">
  组件A: <com-a></com-a><br>
  组件B: <com-b></com-b>
</div>
var bus = new Vue()
Vue.component('com-a', {
  template: '<input v-on:change="handleChange" v-model="msg"/>',
  data () {
    return {
      msg: 'Hello Vue.js'
    }
  },
  methods: {
    handleChange() {
      bus.$emit('a-change', this.msg)
    }
  },
  created () {
    var me = this
    bus.$on('b-change', function (msg) {
      me.msg = msg
    })
  }
})
Vue.component('com-b', {
  template: '<input v-on:change="handleChange" v-model="msg"/>',
  data () {
    return {
      msg: 'Hello Vue.js'
    }
  },
  methods: {
    handleChange() {
      bus.$emit('b-change', this.msg)
    }
  },
  created () {
    var me = this
    bus.$on('a-change', function (msg) {
      me.msg = msg
    })
  }
})
var app5 = new Vue({
  el: '#app5'
})

熟悉 发布订阅模式 的同学,应该很容易理解上面这段代码,创建的全局空 Vue 实例 bus 就是用来充当中央事件总线,所有的事件都经过它来触发和传播。

思路解析:在组件 com-a 中,当 input 值发生改变时,通过 bus.$emit('a-change', this.msg) 来触发修改事件,并将其更新后的值做为参数传递,组件 com-b 通过 bus.$on('a-change', xxx) 来监听,进行值更新操作,组件 com-b 也是相同原理。

当然在复杂情况下,我们应该考虑使用专门的 状态管理模式,比如 vuex,这个将在后续的文章中讲到。

动态组件

Vue 中还提供了 component 元素,允许我们在实际开发中,通过修改其 is 属性值,来动态切换组件。这个在某些应用场景非常实用,笔者曾经有个需求就是,需要根据参数 type 来绘制不同类型的图表,而我的所有图表类型都已经装成了一个独立的组件,所以我只需要依据此特性,通过参数 type 来动态修改元素 component 的属性 is 为对应的组件名称即可。

下面来看示例代码:

<div id="app6">
  <button v-on:click="changeType">改变组件</button><br>
  <component v-bind:is="currentComp"></component>
</div>
var app6 = new Vue({
  el: '#app6',
  data () {
    return {
      type: 'a'
    }
  },
  computed: {
    currentComp () {
      return this.type === 'a' ? 'com-a' : 'com-b';
    }
  },
  components: {
    'com-a': {
      template: '<h1>我是组件a</h1>'
    },
    'com-b': {
      template: '<h1>我是组件b</h1>'
    }
  },
  methods: {
    changeType () {
      this.type = this.type === 'a' ? 'b' : 'a';
    }
  }
})

运行上面代码,点击改变组件按钮,就可以轻松的实现组件 com-acom-b 的动态切换了,是不是很酷?赶紧动手尝试下吧。

总结

组件作为 Vue 中最强大的功能之一,其特性当然不止上面所提到的,感兴趣的同学可以到官方文档中阅读了解。学会封装可重用的代码,增加代码可复用性,是个需要长期学习和总结的过程,这个就需要我们在不断的项目开发中尝试和总结。本节也是关于 Vue 基础知识的最后一节,当然还有很多 Vue 的基础知识,文章中并未提到,因为官方文档已经非常详细了,篇幅有限,也不再一一介绍。请至少将官方文档仔细阅读一篇,以便理解后面的项目实战开发。从下一篇开始,我将进行实际项目开发讲解,尽请期待~

源码在此

专题目录

You-May-Not-Know-Vuejs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,046评论 0 29
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,204评论 0 6
  • 1.安装 可以简单地在页面引入Vue.js作为独立版本,Vue即被注册为全局变量,可以在页面使用了。 如果希望搭建...
    Awey阅读 11,002评论 4 129
  • 我们需要时间站在镜子前静静地欣赏自己的裸体。 面对镜中的赤裸自己时,你不会为你的肉体感到羞耻或负罪,因为它就在你面...
    长马阅读 1,286评论 1 12
  • 1.老欧轻轻地把酒店的房门关上时,我的心顿感不是滋味,我牙槽里那颗坏牙开始隐隐作痛。薄薄的一扇门,隔离了两重世界,...
    叶舟轻阅读 1,124评论 1 21