欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue.js 源码分析(十六) 指令篇 v-on指令详解

程序员文章站 2023-01-01 14:31:15
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: 渲染结果为: 我们给测试按钮添加了一个mouseenter和click事件,鼠标移上去式控制台输出: 当点击时,输出为: Vue的事件绑定有很多种写法,例如: 可以看到v-on对应事件可以很多种格式的, ......

可以用 v-on 指令监听 dom 事件,并在触发时运行一些 javascript 代码,例如:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <button @click="show('click',$event)" @mouseenter="show('mouseenter',$event)">测试</button>
    </div>
    <script>
        vue.config.productiontip=false;
        vue.config.devtools=false;
        var app = new vue({
            el:'#app',
            methods:{ show(type,ev){console.log(type)} }
        })
    </script>
</body>
</html>

渲染结果为:

Vue.js 源码分析(十六) 指令篇 v-on指令详解

我们给测试按钮添加了一个mouseenter和click事件,鼠标移上去式控制台输出:

Vue.js 源码分析(十六) 指令篇 v-on指令详解

当点击时,输出为:

Vue.js 源码分析(十六) 指令篇 v-on指令详解

 vue的事件绑定有很多种写法,例如:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="app">
        <p>{{message}}</p> 
        <button @click="test1">test1</button>                       <!--事件可以对应一个方法-->
        <button @click="test2('test2',$event)">test2</button>          <!--方法还可以传递参数,$event表示原始的dom事件-->
        <button @click="message='test3'">test3</button>               <!--也可以是一个表达式-->
        <button @click="function(){message='test4'}">test4</button>    <!--也可以是一个函数-->
        <button @click="()=>{message='test5'}">test5</button>          <!--也可以是一个箭头函数-->
    </div>  
    <script>
        var app = new vue({
            el:'#app',
            data(){
                return {message:"hello vue"}
            },
            methods:{
                test1(){console.log('test1');},
                test2(text,ev){console.log(text);console.log(ev.type)}
            }
        })
    </script>
</body>
</html>

可以看到v-on对应事件可以很多种格式的,可以是当前vue实例的一个方法、一个表达式、一个函数,或者一个箭头函数

 

 源码分析


 以上面的第一个例子为例,vue将dom解析成ast对象时的时候执行到a节点时会执行processelement()函数,然后会执行processattrs()函数,该函数会遍历每个属性,然后用判断是否以:或v-bind:开头,如下:

function processattrs (el) {      //第9526行 对剩余的属性进行分析
  var list = el.attrslist;
  var i, l, name, rawname, value, modifiers, isprop;
  for (i = 0, l = list.length; i < l; i++) {    //遍历每个属性
    name = rawname = list[i].name;                //获取属性名
    value = list[i].value;                        //该属性对应的值
    if (dirre.test(name)) {                       //如果该属性以v-、@或:开头,表示这是vue内部指令
      // mark element as dynamic
      el.hasbindings = true;
      // modifiers
      modifiers = parsemodifiers(name);
      if (modifiers) {
        name = name.replace(modifierre, '');
      }
      if (bindre.test(name)) {                    //bindrd等于/^:|^v-bind:/ ,即该属性是v-bind指令时 例如:<a :href="url">你好</a>
        /*这里时v-bind指令对应的分支*/
      } else if (onre.test(name)) {               //onre等于/^@|^v-on:/,即该属性是v-on指令时
        name = name.replace(onre, '');                              //获取绑定的事件类型  比如@click,此时name等于click   v-on:click此时name也等于click
        addhandler(el, name, value, modifiers, false, warn$2);      //调用addhandler()函数将事件相关信息保存到el.events或nativeevents里面
      } else {                                  // normal directives
       /*自定义指令的分支*/
      }
    } else {                                      //存储普通属性的分支
      // literal attribute
      {
        var res = parsetext(value, delimiters);
        if (res) {
          warn$2(
            name + "=\"" + value + "\": " +
            'interpolation inside attributes has been removed. ' +
            'use v-bind or the colon shorthand instead. for example, ' +
            'instead of <div id="{{ val }}">, use <div :id="val">.'
          );
        }
      }
      addattr(el, name, json.stringify(value));
      // #6887 firefox doesn't update muted state if set via attribute
      // even immediately after element creation
      if (!el.component &&
          name === 'muted' &&
          platformmustuseprop(el.tag, el.attrsmap.type, name)) {
        addprop(el, name, 'true');
      }
    }
  }
}

addhandler()函数用于给对应的ast对象增加一个events属性,保存事件对应的信息,如下:

function addhandler (     //第6573行  给el这个ast对象增加event或nativeevents,用于记录事件的信息 
  el,
  name,
  value,
  modifiers,
  important,
  warn
) {
  modifiers = modifiers || emptyobject;
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (
    "development" !== 'production' && warn &&
    modifiers.prevent && modifiers.passive
  ) {
    warn(
      'passive and prevent can\'t be used together. ' +
      'passive handler can\'t prevent default event.'
    );
  }

  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture;
    name = '!' + name; // mark the event as captured
  }
  if (modifiers.once) {             //如果有once修饰符
    delete modifiers.once;
    name = '~' + name; // mark the event as once
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive;
    name = '&' + name; // mark the event as passive
  }

  // normalize click.right and click.middle since they don't actually fire
  // this is technically browser-specific, but at least for now browsers are
  // the only target envs that have right/middle clicks.
  if (name === 'click') {         //鼠标按键修饰符:如果是click事件,则根据modiflers进行修正
    if (modifiers.right) {
      name = 'contextmenu';
      delete modifiers.right;
    } else if (modifiers.middle) {
      name = 'mouseup';
    }
  }

  var events;
  if (modifiers.native) {       //如果存在native修饰符,则保存到el.nativeevents里面,对于组件的自定义事件执行到这里
    delete modifiers.native;
    events = el.nativeevents || (el.nativeevents = {});
  } else {                      //否则保存到el.events里面
    events = el.events || (el.events = {});
  }

  var newhandler = {
    value: value.trim()
  };
  if (modifiers !== emptyobject) {
    newhandler.modifiers = modifiers;
  }

  var handlers = events[name];          //尝试获取已经存在的该事件对象
  /* istanbul ignore if */
  if (array.isarray(handlers)) {        //如果是数组,表示已经插入了两次了,则再把newhandler添加进去
    important ? handlers.unshift(newhandler) : handlers.push(newhandler);
  } else if (handlers) {                 //如果handlers存在且不是数组,则表示只插入过一次,则把events[name]变为数组
    events[name] = important ? [newhandler, handlers] : [handlers, newhandler];
  } else {
    events[name] = newhandler;           //否则表示是第一次新增该事件,则值为对应的newhandler
  }

  el.plain = false;
}

例子里执行到这里这里后对应的ast等于:

Vue.js 源码分析(十六) 指令篇 v-on指令详解

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

 

function genhandlers (    //第9992行 拼凑事件的data函数
  events, 
  isnative,
  warn
) {
  var res = isnative ? 'nativeon:{' : 'on:{';     //如果参数isnative为true则设置res为:nativeon:{,否则为:on:{  ;对于组件来说isnative为true,原生事件来说是on
  for (var name in events) {                      //遍历events,拼凑结果
    res += "\"" + name + "\":" + (genhandler(name, events[name])) + ",";
  }
  return res.slice(0, -1) + '}'
}

genhandler会获取每个事件对应的代码,如下:

function genhandler (   //第10004行  name:事件名,比如:name handler:事件绑定的对象信息,比如:{value: "show", modifiers: {…}}
  name,
  handler
) {
  if (!handler) {
    return 'function(){}'
  }

  if (array.isarray(handler)) {   
    return ("[" + (handler.map(function (handler) { return genhandler(name, handler); }).join(',')) + "]")
  }

  var ismethodpath = simplepathre.test(handler.value);          //是否为简单的表达式,比如show、show_d、show1等
  var isfunctionexpression = fnexpre.test(handler.value);       //是否为函数表达式(箭头函数或function(){}格式的匿名函数)

  if (!handler.modifiers) {                                     //如果该事件的修饰符为空
    if (ismethodpath || isfunctionexpression) {                     //如果是简单表达式或者是函数表达式
      return handler.value                                              //则直接返回handler.value,比如:show
    }
    /* istanbul ignore if */
    return ("function($event){" + (handler.value) + "}") // inline statement  //否则返回带有一个$event变量的函数形式,比如:当value是个表达式时,例如:value=a+123,返回格式:function($event){a+123;}
  } else {                                                      //如果还存在修饰符(解析模板时有些修饰符被过滤掉了)
    var code = '';
    var genmodifiercode = '';
    var keys = [];
    for (var key in handler.modifiers) {                          //遍历每个修饰符,比如:prevent
      if (modifiercode[key]) {                                        //如果有在modifiercode里面定义   modifiercode是个数组,保存了一些内置修饰符对应的代码
        genmodifiercode += modifiercode[key];                            //则拼凑到genmodifiercode里面
        // left/right
        if (keycodes[key]) {
          keys.push(key);
        }
      } else if (key === 'exact') {
        var modifiers = (handler.modifiers);
        genmodifiercode += genguard(
          ['ctrl', 'shift', 'alt', 'meta']
            .filter(function (keymodifier) { return !modifiers[keymodifier]; })
            .map(function (keymodifier) { return ("$event." + keymodifier + "key"); })
            .join('||')
        );
      } else {
        keys.push(key);
      }
    }
    if (keys.length) {                                          //如果有按键
      code += genkeyfilter(keys);                                   //则拼凑按键
    }   
    // make sure modifiers like prevent and stop get executed after key filtering
    if (genmodifiercode) {
      code += genmodifiercode;
    }
    var handlercode = ismethodpath
      ? ("return " + (handler.value) + "($event)")
      : isfunctionexpression
        ? ("return (" + (handler.value) + ")($event)")
        : handler.value;
    /* istanbul ignore if */
    return ("function($event){" + code + handlercode + "}")
  }
}

例子里执行到这里后生成的render函数等于:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":function($event){show('click',$event)},"mouseenter":function($event){show('mouseenter',$event)}}},[_v("测试")])])}

其中和事件有关的如下:

on: {
    "click": function($event) {
        show('click', $event)
    },
    "mouseenter": function($event) {
        show('mouseenter', $event)
    }
}

最后在_watch渲染成真实的dom节点后,就会调用events模块的updatedomlisteners钩子函数,该函数会获取该vnode的on属性,依次遍历on对象里的每个元素,最后调用addeventlistener去绑定对应的事件

 

function updatedomlisteners (oldvnode, vnode) {     //第7083行 domn事件相关
  if (isundef(oldvnode.data.on) && isundef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};                                   //新node上的事件  例如:{click: ƒ ($event){}}
  var oldon = oldvnode.data.on || {};
  target$1 = vnode.elm;                                           //dom引用
  normalizeevents(on);                                            //处理v-model的
  updatelisteners(on, oldon, add$1, remove$2, vnode.context);     //调用updatelisteners做进一步处理
  target$1 = undefined;
}

updatelisteners()函数又会调用add$1函数去添加dom事件,如下:

function updatelisteners (      //第2036行 更新dom事件
  on,
  oldon,
  add,
  remove$$1,
  vm
) {
  var name, def, cur, old, event;
  for (name in on) {                  //遍历on,此时name就是对应的事件类型,比如:click
    def = cur = on[name];
    old = oldon[name];
    event = normalizeevent(name);
    /* istanbul ignore if */
    if (isundef(cur)) {
      "development" !== 'production' && warn(
        "invalid handler for event \"" + (event.name) + "\": got " + string(cur),
        vm
      );
    } else if (isundef(old)) {                //如果old没有定义,则表示这是一个创建事件
      if (isundef(cur.fns)) {
        cur = on[name] = createfninvoker(cur);
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params);   //调用add()绑定事件
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  for (name in oldon) {
    if (isundef(on[name])) {
      event = normalizeevent(name);
      remove$$1(event.name, oldon[name], event.capture);
    }
  }
}

updatelisteners里的add函数,也就是全局的add$1函数才是最终的添加事件函数,如下:

function add$1 (    //第7052行  绑定事件  event:事件名 handler:事件的函数 once$$1:是否只执行一次 capture:是否采用捕获状态 passive:可用于移动端性能提升
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  handler = withmacrotask(handler);
  if (once$$1) { handler = createoncehandler(handler, event, capture); }  //如果有设置了once$$1,则继续使用createoncehandler封装
  target$1.addeventlistener(                                              //调用原生的dom apiaddeventlistener添加对应的事件,2017年dom规范对addeventlistener()的第三个参数做了修订,可以是一个对象
    event,
    handler,
    supportspassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

我们看到vue内部添加dom事件最终也是通过addeventlistener()来添加的,说到底,vue只是把这些api进行了封装,使我们用起来更方便而已。