经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
来源:cnblogs  作者:大沙漠  时间:2019/6/28 10:06:58  对本文有异议

我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现

组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事件需要加一个.native修饰符。

子组件里通过过this.$emit()将自定义事件以及需要发出的数据通过以下代码发送出去,第一个参数是自定义事件的名称,后面的参数是依次想要发送出去的数据,例如:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  6. <title>Document</title>
  7. </head>
  8. <body>
  9. <div id="d"><com @myclick="MyClick" @mouseenter.native="Enter"></com></div>
  10. <script>
    Vue.config.productionTip=false;
  11. Vue.config.devtools=false;
  12. Vue.component('com',{
  13. template:'<button @click="childclick">Click</button>',
  14. methods:{
  15. childclick:function(){this.$emit('myclick','gege','123')} //子组件的事件,通过this.$emit触发父组件的myclick事件
  16. }
  17. })
  18. debugger
  19. var app = new Vue({
  20. el:'#d',
  21. methods:{
  22. MyClick:function(){console.log('parent MyClick method:',arguments)}, //响应子组件的事件函数
  23. Enter:function(){console.log("MouseEnter")} //子组件的原生DOM事件
  24. }
  25. })
  26. </script>
  27. </body>
  28. </html>

子组件就是一个按钮,渲染如下:

我们给整个组件绑定了两个事件,一个DOM原生的mouseenter事件和自定义的MyClick组件事件,当鼠标移动到按钮上时,打印出:MouseEnter,如下:

当点击按钮时输出子组件传递过来的信息,如下:

自定义事件其实是存储在组件实例的_events属性上的,我们在控制台输入console.log(app.$children[0]["_events"])就可以打印出来,如下:

myclick就是我们自定义的事件对象

 

 源码分析


 父组件在解析模板时会执行processAttrs()函数,会在AST对象上增加一个events和nativeevents属性,如下

  1. function processAttrs (el) { //第9526行 对属性进行解析
  2. var list = el.attrsList;
  3. var i, l, name, rawName, value, modifiers, isProp;
  4. for (i = 0, l = list.length; i < l; i++) { //遍历每个属性名
  5. name = rawName = list[i].name;
  6. value = list[i].value;
  7. if (dirRE.test(name)) {
  8. // mark element as dynamic
  9. el.hasBindings = true;
  10. // modifiers
  11. modifiers = parseModifiers(name);
  12. if (modifiers) {
  13. name = name.replace(modifierRE, '');
  14. }
  15. if (bindRE.test(name)) { // v-bind
  16. /**/
  17. } else if (onRE.test(name)) { // v-on //如果name以@或v-on:开头,表示绑定了事件
  18. name = name.replace(onRE, '');
  19. addHandler(el, name, value, modifiers, false, warn$2); 调用addHandler()函数将事件相关信息保存到el.eventsnativeEvents里面
  20. } else { // normal directives
  21. /**/
  22. }
  23. } else {
  24. /**/
  25. }
  26. }
  27. }
  28. function addHandler ( //第6573行 给el这个AST对象增加event或nativeEvents
  29. el,
  30. name,
  31. value,
  32. modifiers,
  33. important,
  34. warn
  35. ) {
  36. modifiers = modifiers || emptyObject;
  37. /**/
  38.  
  39. var events;
  40. if (modifiers.native) { //如果存在native修饰符,则保存到el.nativeEvents里面
  41. delete modifiers.native;
  42. events = el.nativeEvents || (el.nativeEvents = {});
  43. } else { //否则保存到el.events里面
  44. events = el.events || (el.events = {});
  45. }
  46. /**/
  47. var handlers = events[name]; //尝试获取已经存在的该事件对象
  48. /* istanbul ignore if */
  49. if (Array.isArray(handlers)) { //如果是数组,表示已经插入了两次了,则再把newHandler添加进去
  50. important ? handlers.unshift(newHandler) : handlers.push(newHandler);
  51. } else if (handlers) { //如果handlers存在且不是数组,则表示只插入过一次,则把events[name]变为数组
  52. events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
  53. } else { //否则表示是第一次新增该事件,则值为对应的newHandler
  54. events[name] = newHandler;
  55. }
  56. el.plain = false;
  57. }

例子里执行完后AST对象里对应的信息如下:(AST可以这样认为:Vue把模板通过正则解析后以对象的形式表现出来)

接下来在generate生成rendre函数的时候会调用genHandlers函数根据不同修饰符等生成对应的属性(作为_c函数的第二个data参数一部分),如下:

  1. function genData$2 (el, state) { //第10274行 拼凑data值
  2. var data = '{';
  3. /**/
  4. // event handlers
  5. if (el.events) { //如果el有绑定事件(没有native修饰符时)
  6. data += (genHandlers(el.events, false, state.warn)) + ",";
  7. }
  8. if (el.nativeEvents) { //如果el有绑定事件(native修饰符时)
  9. data += (genHandlers(el.nativeEvents, true, state.warn)) + ",";
  10. }
  11. /**/
  12. return data
  13. }

genHandlers会根据参数2的值将事件存储在nativeOn或on属性里,如下:

  1. function genHandlers ( //第9992行 拼凑事件的data函数
  2. events,
  3. isNative,
  4. warn
  5. ) {
  6. var res = isNative ? 'nativeOn:{' : 'on:{'; //如果参数isNative为true则设置res为:nativeOn:{,否则为:on:{
  7. for (var name in events) {
  8. res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
  9. }
  10. return res.slice(0, -1) + '}'
  11. }

例子里执行到这里时等于:

 _render将rendre函数转换为VNode时候会调用createComponent()函数创建组件占位符VNode,此时会有

  1. function createComponent ( //第4182行
  2. Ctor,
  3. data,
  4. context,
  5. children,
  6. tag
  7. ) {
  8. /**/
  9. var listeners = data.on; //对自定义事件(没有native修饰符)的处理,则保存到listeners里面,一会儿存到占位符VNode的配置信息里
  10. // replace with listeners with .native modifier
  11. // so it gets processed during parent component patch.
  12. data.on = data.nativeOn; //对原生DOM事件,则保存到data.on里面,这样等该DOM渲染成功后会执行event模块的初始化,就会绑定对应的函数了
  13.  
  14. /**/
      var name = Ctor.options.name || tag;
      var vnode = new VNode(
        ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
        data, undefined, undefined, undefined, context,
        { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },        //自定义事件作为listeners属性存储在组件Vnode的配置参数里了
        asyncFactory
      );

      // Weex specific: invoke recycle-list optimized @render function for
      // extracting cell-slot template.
      // https://github.com/Hanks10100/weex-native-directive/tree/master/component
      /* istanbul ignore if */
      return vnode
  15. }

原生事件存储在on属性上,后面介绍v-on指令时再详细介绍,对于自定义事件存储在组件Vnode配置参数的listeners属性里了。

当组件实例化的时候执行_init()时首先执行initInternalComponent()函数,该函数会获取listeners属性,如下:

  1. function initInternalComponent (vm, options) { //第4632行 初始化子组件
  2. var opts = vm.$options = Object.create(vm.constructor.options);
  3. // doing this because it's faster than dynamic enumeration.
  4. var parentVnode = options._parentVnode; //该组件的占位符VNode
  5. opts.parent = options.parent;
  6. opts._parentVnode = parentVnode;
  7. opts._parentElm = options._parentElm;
  8. opts._refElm = options._refElm;
  9. var vnodeComponentOptions = parentVnode.componentOptions; //占位符VNode初始化传入的配置信息
  10. opts.propsData = vnodeComponentOptions.propsData;
  11. opts._parentListeners = vnodeComponentOptions.listeners; //将组件的自定义事件保存到_parentListeners属性里面
  12. opts._renderChildren = vnodeComponentOptions.children;
  13. opts._componentTag = vnodeComponentOptions.tag;
  14. if (options.render) {
  15. opts.render = options.render;
  16. opts.staticRenderFns = options.staticRenderFns;
  17. }
  18. }

 回到_init函数,接着执行initEvents()函数,该函数会初始化组件的自定义事件,如下:

  1. function initEvents (vm) { //第2412行 初始化自定义事件
  2. vm._events = Object.create(null);
  3. vm._hasHookEvent = false;
  4. // init parent attached events
  5. var listeners = vm.$options._parentListeners; //获取占位符VNode上的自定义事件
  6. if (listeners) {
  7. updateComponentListeners(vm, listeners); //执行updateComponentListeners()新增事件
  8. }
  9. }

  updateComponentListeners函数用于新增/更新组件的事件,如下:

  1. function add (event, fn, once) { //第2424行
  2. if (once) {
  3. target.$once(event, fn); //自定义事件最终调用$once绑定事件的
  4. } else {
  5. target.$on(event, fn);
  6. }
  7. }
  8. function remove$1 (event, fn) {
  9. target.$off(event, fn);
  10. }
  11. function updateComponentListeners ( //第2436行
  12. vm,
  13. listeners,
  14. oldListeners
  15. ) {
  16. target = vm;
  17. updateListeners(listeners, oldListeners || {}, add, remove$1, vm); //调用updateListeners()更新DOM事件,传入add函数
  18. target = undefined;
  19. }

updateListeners内部会调用add()函数,这里用了一个优化措施,实际上我们绑定的是Vue内部的createFnInvoker函数,该函数会遍历传给updateListeners的函数,依次执行。

add()最终执行的是$on()函数,该函数定义如下:

  1. Vue.prototype.$on = function (event, fn) { //第2448行 自定义事件的新增 event:函数名 fn:对应的函数
  2. var this$1 = this;
  3. var vm = this;
  4. if (Array.isArray(event)) { //如果event是一个数组
  5. for (var i = 0, l = event.length; i < l; i++) { //则遍历该数组
  6. this$1.$on(event[i], fn); //依次调用this$1.$on
  7. }
  8. } else { //如果不是数组
  9. (vm._events[event] || (vm._events[event] = [])).push(fn); //则将事件保存到ev._event上
  10. // optimize hook:event cost by using a boolean flag marked at registration
  11. // instead of a hash lookup
  12. if (hookRE.test(event)) { //如果事件名以hook:开头
  13. vm._hasHookEvent = true; //则设置vm._hasHookEvent为true,这样生命周期函数执行时也会执行这些函数
  14. }
  15. }
  16. return vm
  17. };

从这里可以看到自定义事件其实是保存到组件实例的_events属性上的

当子组件通过$emit触发当前实例上的事件时,会从_events上拿到对应的自定义事件并执行,如下:

  1. Vue.prototype.$emit = function (event) { //第2518行 子组件内部通过$emit()函数执行到这里
  2. var vm = this;
  3. {
  4. var lowerCaseEvent = event.toLowerCase(); //先将事件名转换为小写
  5. if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { //如果lowerCaseEvent不等于event则报错(即事件名只能是小写)
  6. tip(
  7. "Event \"" + lowerCaseEvent + "\" is emitted in component " +
  8. (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
  9. "Note that HTML attributes are case-insensitive and you cannot use " +
  10. "v-on to listen to camelCase events when using in-DOM templates. " +
  11. "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
  12. );
  13. }
  14. }
  15. var cbs = vm._events[event]; //从_events属性里获取对应的函数数组
  16. if (cbs) {
  17. cbs = cbs.length > 1 ? toArray(cbs) : cbs; //获取所有函数
  18. var args = toArray(arguments, 1); //去掉第一个参数,后面的都作为事件的参数
  19. for (var i = 0, l = cbs.length; i < l; i++) { //遍历cbs
  20. try {
  21. cbs[i].apply(vm, args); //依次执行每个函数,值为子组件的vm实例
  22. } catch (e) {
  23. handleError(e, vm, ("event handler for \"" + event + "\""));
  24. }
  25. }
  26. }
  27. return vm
  28. };

大致流程跑完了,有点繁琐,多调试一下就好了。

原文链接:http://www.cnblogs.com/greatdesert/p/11091402.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号