文档章节

合格前端系列第二弹-Vue组件开发续篇

qiangdada
 qiangdada
发布于 2017/04/30 00:34
字数 2817
阅读 4395
收藏 158
点赞 2
评论 6

之前我写过一篇有关vue组件开发的文章,这次将是对上次的一次拓展。其中也会有vue部分源码的解析,接下来直接上正文吧。

一、父子组件之间的通信

总所周知,如果进行组件开发的话,必定存在组件通信的问题,具体通信如何进行的呢,我借用一张vue官网的图

图中很明显可以看到,Parent组件通过props向下传递数据(props down),Child组件通过events向上传递消息(events up)。具体通信机制,请转链接 https://vuejs.org/v2/guide/components.html。那么如果不是父子组件关系,而是slot节点之间的关系,又该如何进行通信呢。下面的内容会带着大家一步一步解惑。

二、组件的开发

接下里的例子我将模拟element-ui中的dropdown下拉菜单组件,对组件开发进行详细的解剖。进行开发前我们先看一下element-ui中的dropdown组件实现了哪些功能(具体功能转链接http://element.eleme.io/#/zh-CN/component/dropdown),这里我们挑选一些不会涉及到调用其他element-ui组件的功能,接下来,希望小伙伴们跟着我一起慢慢实现一个属于自己的element-ui组件吧。

1、组件设计

如上图所示,整个dropdown组件分成了三个组件模块,最外层的dropdown,下拉菜单dropdown-menu,以及下拉列表dropdown-list。至于为何这样设计,主要是为了该组件的cover范围可以大,可以适用各种场景。

我们先看下实现功能后每个组件对应的template内容
a、dropdown组件

<style media="screen">
.v-dropdown {
    display: inline-block;
    position: relative;
    color: #48576a;
    font-size: 14px;
}
</style>
<template>
<div class="v-dropdown"
    :trigger="trigger"
    :visible="visible"
    :hideOnClick="hideOnClick"
    v-clickoutside="hide"
>
    <slot></slot>
</div>
</template>

b、dropdown-menu组件

<style media="screen">
.v-dropdown-menu {
    margin: 5px 0;
    background-color: #fff;
    border: 1px solid #d1dbe5;
    box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.12);
    padding: 6px 0;
    z-index: 10;
    position: absolute;
    top: 20px;
    left: 0;
    min-width: 100px;
}
</style>
<template>
<ul class="v-dropdown-menu" v-show="visible">
    <slot></slot>
</ul>
</template>

c、dropdown-list组件

<style media="screen">
ul, li {
    list-style: none;
}
.v-dropdown-menu_list {
    cursor: pointer;
}
</style>
<template>
<li class="v-dropdown-menu_list"
    @click="handleClick"
    :command="command"
>
    <slot></slot>
</li>
</template>

如上,大家也可以看出来,dropdown组件负责一个全局的控制,他通过向dropdown-menu组件传递visible属性控制着其消失与显示,对于dropdown-list组件点击事件的回调与否的控制则是通过$emit监听dropdown组件中是否存在自定义事件command。

2、组件功能的实现

a、dropdown-menu消失与显示

首先我们实现一个基本功能,通过hover或者click事件控制dropdown-menu组件的显示与否,这里我们需要给dropdown组件绑定两个属性,一个是visible,一个是trigger。

<template>
<div class="v-dropdown"
    :trigger="trigger"
    :visible="visible"
>
    <slot></slot>
</div>
</template>

这里我们需要先重写两个方法,一个是broadcast(向下传递),一个是dispatch(向上传递),后面的传递也基本基于这两种方法。具体的mixins方法emitter.js如下

/**
 * [broadcast 上下传递]
 * @param  {[type]} componentName [组件别名]
 * @param  {[type]} eventName     [事件别名]
 * @param  {[type]} params        [事件回调参数]
 */
function broadcast(componentName, eventName, params) {
    // 遍历当前实例的children节点
    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]));
        }
    });
}
/**
 * [dispatch 向上传递]
 * @param  {[type]} componentName [组件别名]
 * @param  {[type]} eventName     [事件别名]
 * @param  {[type]} params        [事件回调参数]
 */
function dispatch(componentName, eventName, params) {
    var parent = this.$parent || this.$root;
    var name = parent.$options.name;
    // 向上找目标父节点,如果上一级父节点不符合,则继续往上查询
    while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
            name = parent.$options.name;
        }
    }
    // 找到目标父节点后,触发事件别名对应的回调,并将参数传入
    if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
    }
}
export default {
    methods: {
        broadcast(componentName, eventName, params) {
            broadcast.apply(this, [componentName, eventName, params]);
        },
        dispatch(componentName, eventName, params) {
            dispatch.apply(this, [componentName, eventName, params]);
        }
    }
};

好了对于事件传递的mixins方法我们已经写好了,接下来我们需要做的就是通过在dropdown-menu组件中注册好visible事件,代码如下

<template>
<ul class="v-dropdown-menu" v-show="visible">
    <slot></slot>
</ul>
</template>

<script>
export default {
    name: 'VDropdownMenu',
    componentName: 'VDropdownMenu',
    // 组件create的时候进行事件注册
    created () {
        this.$on('visible', val => {
            this.visible = val;
        });
    }
};
</script>

对于dropdown组件,则是通过watch visible属性,如果visible属性发生改变则将visible属性的最新值传递给dropdown-menu组件并触发其回调。而对于visible属性的控制,具体如下

<template>
<div class="v-dropdown"
    :trigger="trigger"
    :visible="visible"
    :hideOnClick="hideOnClick"
    v-clickoutside="hide"
>
    <slot></slot>
</div>
</template>

<script>
// vue自带指令,点击节点以外地方,并触发回调
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Emitter from 'element-ui/src/mixins/emitter';
export default {
    name: 'VDropdown',
    componentName: 'VDropdown',
    mixins: [Emitter],
    // 注册指令
    directives: { Clickoutside },
    props: {
        trigger: {
            type: String,
            default: 'hover'
        },
        hideOnClick: {
            type: Boolean,
            default: true
        }
    },
    data () {
        return {
            timeout: null,
            visible: false
        }
    },
    methods: {
        // 显示
        show () {
            let that = this;
            clearTimeout(this.timeout);
            this.timeout = setTimeout(function () {
                that.visible = true;
            }, 150);
        },
        // 隐藏
        hide () {
            let that = this;
            clearTimeout(this.timeout);
            this.timeout = setTimeout(function () {
                that.visible = false;
            }, 150);
        },
        // click事件的处理
        handleClick () {
            this.visible = !this.visible;
        },
        initEvent () {
            let {trigger, show, hide, handleClick} = this;
            // 触发事件的elm节点
            let triggerElm = this.$slots.default[0].elm;
            // hover事件处理
            if (trigger === 'hover') {
                triggerElm.addEventListener('mouseenter', show);
                triggerElm.addEventListener('mouseleave', hide);
            }
            // click事件处理
            else if (trigger === 'click') {
                triggerElm.addEventListener('click', handleClick);
            }
        }
    },
    watch: {
        // 向下传递,即VDropdownMenu组件传递visible属性并触发其回调
        visible (val) {
            this.broadcast('VDropdownMenu', 'visible', val);
        }
    },
    mounted () {
        this.initEvent();
    }
};
</script>

写到这里dropdown-menu的消失与显示的功能则已实现。

b、dropdown-list点击事件command指令的实现

在这里,我们需要实现的则是对于dropdown-list组件的拓展功能的实现,试想,如果我需要在点击dropdown-list的时候做一些自定义的事件,该如何实现呢?那么接下来我们要做的就是给人提供一个对外的指令接口command,$emit监测到command指令的时候触发其自定义的事件回调。

首先我们看看dropdown-list进行的操作,具体如下

<template>
<li class="v-dropdown-menu_list"
    @click="handleClick"
    :command="command"
>
    <slot></slot>
</li>
</template>

<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
    name: 'VDropdownList',
    mixins: [Emitter],
    props: {
        command: String
    },
    methods: {
        // 点击dropdown-list时,向上传递,即监听VDropdown的 menu-list-click自定义事件并触发其回调
        handleClick (e) {
            this.dispatch('VDropdown', 'menu-list-click', [this.command, this]);
        }
    }
};
</script>

对于dropdown组件中,需要做的事情便是在组件渲染完成后通过$on注册 'menu-list-click'事件,如下

this.$on('menu-list-click', this.handleMenuListClick);

需要被触发的回调如下

handleMenuListClick (command, instance) {
    // 点击list后是否隐藏menu,属性通过hideOnClick控制
    if (this.hideOnClick) {
        this.visible = false;
    }
    // 监听command指令,并触发其回调
    this.$emit('command', command, instance);
}

调用如下

<template>
    <v-dropdown trigger="click" @command="commandHandle" :hide-on-click="false">
        <span class="drop-down_link">下拉菜单</span>
        <v-dropdown-menu>
            <v-dropdown-list command="a">下拉列表1</v-dropdown-list>
            <v-dropdown-list command="b">下拉列表2</v-dropdown-list>
            <v-dropdown-list command="c"><h4>下拉列表3</h4></v-dropdown-list>
        </v-dropdown-menu>
    </v-dropdown>
</template>
<script>
export default {
    methods: {
        commandHandle(command) {
            console.log(command);
        }
    }
}
</script>

执行结果如下(点击每个列表)

到这里,点击dropdown-list触发的事件回调也就完成了。我们需要完成的属于自己的dropdown组件也算是完成了。

三、vue部分源码解析

我们看到上面的代码可以看出,对于组件之间的消息与事件的传递我们是通过$on,$emit完成的。当然我们看文档还知道,vue还提供了$once$off的实例方法(API链接:https://vuejs.org/v2/api/#vm-on)。那么对于$on$once$off$emit,vue的作者又是如何实现的呢。

其实从上面$on,$emit实现的功能来看,我们就能看出,对于$on,他就像一个发布者,只负责发布消息。而$emit则相当于订阅者,监听发布者发布的消息。而$once则只发布一次消息,$off则取消发布的消息。想要了解观察者模式(发布-订阅者模式)的小伙伴请先转链接http://www.sxrczx.com/docs/js/2355128.html

下面我将直接将源码及我写好的注释放给大家,具体如下

var hookRE = /^hook:/;
/**
 * [$on 事件注册]
 * @param  {[type]}   event [注册事件别名]
 * @param  {Function} fn    [注册事件对应回调]
 */
Vue.prototype.$on = function (event, fn) {
  var this$1 = this;

  var vm = this;
  // 遍历需要发布的消息是否是数组,如果是,则循环注册
  if (Array.isArray(event)) {
    for (var i = 0, l = event.length; i < l; i++) {
      this$1.$on(event[i], fn);
    }
  // 如果不是则单次注册
  } else {
    // 默认值 vm._events = Object.create(null); 通过数组的push()将注册事件回调保存在vm._events[event]中
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    if (hookRE.test(event)) {
      // 默认值vm._hasHookEvent = false
      vm._hasHookEvent = true;
    }
  }
  return vm
};
/**
 * [$once 仅注册一次事件]
 * @param  {[type]}   event [注册事件别名]
 * @param  {Function} fn    [注册事件对应回调]
 */
Vue.prototype.$once = function (event, fn) {
  var vm = this;
  // 定义 on()函数进行事件监听并移除,同时作为$on() 函数的回调执行
  function on () {
    // 移除事件
    vm.$off(event, on);
    // 执行回调,进行事件监听
    fn.apply(vm, arguments);
  }
  on.fn = fn;
  vm.$on(event, on);
  return vm
};
/**
 * [$off 事件移除]
 * @param  {[type]}   event [注册事件别名]
 * @param  {Function} fn    [注册事件对应回调]
 */
Vue.prototype.$off = function (event, fn) {
  var this$1 = this;

  var vm = this;
  // 移除所有的事件监听器
  if (!arguments.length) {
    vm._events = Object.create(null);
    return vm
  }
  // 如果事件别名是数组,则循环将数组中对应的所有事件别名对应的监听器移除
  if (Array.isArray(event)) {
    for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
      this$1.$off(event[i$1], fn);
    }
    return vm
  }
  // specific event
  var cbs = vm._events[event];
  if (!cbs) {
    return vm
  }
  // 如果只传了事件别名一个参数,则移除该事件对应的所有监听器
  if (arguments.length === 1) {
    vm._events[event] = null;
    return vm
  }
  // 参数中既传了事件别名,还传了回调
  var cb;
  var i = cbs.length;
  // 遍历事件对应的所有监听器,即 cbs = vm._events[event]
  while (i--) {
    cb = cbs[i];
    // 如果找到目标监听器,则通过splice移除数组中的监听器,并通过break终止循环
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1);
      break
    }
  }
  return vm
};
/**
 * [$emit 触发事件]
 * @param  {[type]} event [事件别名]
 */
Vue.prototype.$emit = function (event) {
  var vm = this;
  {
    var lowerCaseEvent = event.toLowerCase();
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        "Event \"" + lowerCaseEvent + "\" is emitted in component " +
        (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
        "Note that HTML attributes are case-insensitive and you cannot use " +
        "v-on to listen to camelCase events when using in-DOM templates. " +
        "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
      );
    }
  }
  // 定义cbs接收 vm._events[event]
  var cbs = vm._events[event];
  if (cbs) {
    // 通过判断cbs缓存的监听器个数,确保cbs为数组,以便下面的循环执行
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    var args = toArray(arguments, 1);
    // 遍历数组cbs,循环执行数组cbs中的方法
    for (var i = 0, l = cbs.length; i < l; i++) {
      cbs[i].apply(vm, args);
    }
  }
  return vm
};

 

OK,到这里,这篇博客也该谢幕了,相信看到这里,小伙伴们应该也能写出属于自己的element-ui组件,并且理解了vue是如何进行事件的注册,及事件的回调触发的。对于博客中我实现的dropdown组件,后期我会做下整理并上传到github。小伙伴们关注走一波,后续动弹更精彩哦!

© 著作权归作者所有

共有 人打赏支持
qiangdada
粉丝 419
博文 27
码字总数 79982
作品 0
徐汇
前端工程师
加载中

评论(6)

断风格男丶
断风格男丶
刚刚入手VUE 感觉还行 轻快
qiangdada
qiangdada
代码已托管github,地址-https://github.com/xuqiang521/dropdown
qiangdada
qiangdada
这。。。评论我就不发表意见吧:joy:
智远
智远
团队不靠谱,主流框架抄个遍,一群刷存在感的杂耍艺人在搞。
一刀
一刀

引用来自“智远”的评论

远离vue

@智远 为啥?
智远
智远
远离vue
VUE从零开始系列(上手),呆萌小白上手VUE

第三章 上手 前言 抱歉这回拖得时间有点长,不过我尽量保证抽出时间来坚持写下去,另本人水平有限,欢迎大家指正和拍砖。在上一章,大家对整个项目结构应该有所了解了,那我们就开始着手开始...

在这别动_我去买橘子 ⋅ 05/14 ⋅ 0

使用 Mpvue 开发微信小程序的最佳实践

本文由 授权转发,转载需与GitChat联系。 原文链接 本文作者:美团点评 胡成全 前言 小程序面世一年多以来,给前端开发带来了巨大的影响,有移动应用的地方,就有小程序的踪迹。经过一年多的...

乌骑凤 ⋅ 05/17 ⋅ 0

Vue.js视频教程

Vue.js 1.0 免费中文视频教程在线观看和网盘下载地址收集 原文地址:http://phpecshop.blog.51cto.com/6296699/1834208 NideShop:基于Node.js+MySQL开发的高仿网易严选开源B2C商城(微信小...

ch10mmt ⋅ 2016/08/04 ⋅ 0

Vuebnb:一个用vue.js和Laravel构建的全栈应用

今年我一直在写一本新书叫全栈Vue网站开发:Vue.js,Vuex和Laravel。它会在Packt出版社在2018年初出版。 这本书是围绕着一个案例研究项目,Vuebnb,简单克隆Airbnb。在这篇文章中,我会把它如...

笔阁 ⋅ 04/16 ⋅ 0

面试官: 你为什么使用前端框架?

面试官系列(5): 你为什么使用前端框架? 往期 面试官系列(1): 如何实现深克隆 面试官系列(2): Event Bus的实现 面试官系列(3): 前端路由的实现 面试官系列(4): 基于Proxy 数据劫持的双向绑定优...

寻找海蓝96 ⋅ 06/06 ⋅ 0

基于手淘 flexible 的 Vue 组件:Toast -- 显示框

vue-flexible-components 基于手淘 flexible.js 的 Vue 组件 前言: 目前手头的移动端Vue项目是用手淘的 lib-flexible 作适配的,并用 px2rem 来自动转换成rem。关于lib-flexible和px2rem的配...

冰扬 ⋅ 2017/12/26 ⋅ 0

mpvue: vuejs和小程序碰撞出来的火花

微信自推出小程序以来,热度一直居高不下,各大公司开始专门开发小程序,但是小程序自定义的wxml和wxss和自己定义的语法,让被三大框架统治的前端江湖头疼不易,因为需要专门为小程序开发一...

蜗牛老湿 ⋅ 05/18 ⋅ 0

前端-优雅的VueJS

Vue.js轻松实现页面后退时,还原滚动位置 前言 从Vue.js 2.x发布之后,陆陆续续做了七八个项目,摸索出来了一套自己的状态管理模式,我将之称为Vuet。它以规则来驱动状态更新,它带来的是开发...

掘金官方 ⋅ 01/08 ⋅ 0

WEB前端学习:vue图标组件Vue-Awesome,让你快速方便的使用font-awesome图标

Web前端开发工程师是一个很新的职业,是从事Web前端开发工作的工程师。主要进行网站开发,优化,完善的工作。网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行...

web前端小辰 ⋅ 06/04 ⋅ 0

西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分

最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理。 情景再现...

闰土大叔 ⋅ 04/25 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Sqoop

1.Sqoop: 《=》 SQL to Hadoop 背景 1)场景:数据在RDBMS中,我们如何使用Hive或者Hadoop来进行数据分析呢? 1) RDBMS ==> Hadoop(广义) 2) Hadoop ==> RDBMS 2)原来可以通过MapReduce I...

GordonNemo ⋅ 51分钟前 ⋅ 0

全量构建和增量构建的区别

1.全量构建每次更新时都需要更新整个数据集,增量构建只对需要更新的时间范围进行更新,所以计算量会较小。 2.全量构建查询时不需要合并不同Segment,增量构建查询时需要合并不同Segment的结...

无精疯 ⋅ 今天 ⋅ 0

如何将S/4HANA系统存储的图片文件用Java程序保存到本地

我在S/4HANA的事务码MM02里为Material维护图片文件作为附件: 通过如下简单的ABAP代码即可将图片文件的二进制内容读取出来: REPORT zgos_api.DATA ls_appl_object TYPE gos_s_obj.DA...

JerryWang_SAP ⋅ 今天 ⋅ 0

云计算的选择悖论如何对待?

导读 人们都希望在工作和生活中有所选择。但心理学家的调查研究表明,在多种选项中进行选择并不一定会使人们更快乐,甚至不会产生更好的决策。心理学家Barry Schwartz称之为“选择悖论”。云...

问题终结者 ⋅ 今天 ⋅ 0

637. Average of Levels in Binary Tree - LeetCode

Question 637. Average of Levels in Binary Tree Solution 思路:定义一个map,层数作为key,value保存每层的元素个数和所有元素的和,遍历这个树,把map里面填值,遍历结束后,再遍历这个map,把每...

yysue ⋅ 今天 ⋅ 0

IDEA配置和使用

版本控制 svn IDEA版本控制工具不能使用 VCS-->Enable Version Control Integration File-->Settings-->Plugins 搜索Subversion,勾选SVN和Git插件 删除.idea文件夹重新生成项目 安装SVN客户......

bithup ⋅ 今天 ⋅ 0

PE格式第三讲扩展,VA,RVA,FA的概念

作者:IBinary 出处:http://www.cnblogs.com/iBinary/ 版权所有,欢迎保留原文链接进行转载:) 一丶VA概念 VA (virtual Address) 虚拟地址的意思 ,比如随便打开一个PE,找下它的虚拟地址 这边...

simpower ⋅ 今天 ⋅ 0

180623-SpringBoot之logback配置文件

SpringBoot配置logback 项目的日志配置属于比较常见的case了,之前接触和使用的都是Spring结合xml的方式,引入几个依赖,然后写个 logback.xml 配置文件即可,那么在SpringBoot中可以怎么做?...

小灰灰Blog ⋅ 今天 ⋅ 0

冒泡排序

原理:比较两个相邻的元素,将值大的元素交换至右端。 思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第...

人觉非常君 ⋅ 今天 ⋅ 0

Vagrant setup

安装软件 brew cask install virtualboxbrew cask install vagrant 创建project mkdir -p mst/vmcd mst/vmvagrant init hashicorp/precise64vagrant up hashicorp/precise64是一个box......

遥借东风 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部