深入剖析Vue原始碼 - 揭祕Vue的事件機制

不做祖國的韭菜發表於2019-08-19

這個系列講到這裡,Vue基本核心的東西已經分析完,但是Vue之所以強大,離不開它提供給使用者的一些實用功能,開發者可以更偏向於業務邏輯而非基本功能的實現。例如,在日常開發中,我們將@click=***用得飛起,但是我們是否思考,Vue如何在後面為我們的模板做事件相關的處理,並且我們經常利用元件的自定義事件去實現父子間的通訊,那這個事件和和原生dom事件又有不同的地方嗎,能夠實現通訊的原理又是什麼,帶著疑惑,我們深入原始碼展開分析。

9.1. 模板編譯

Vue在掛載例項前,有相當多的工作是進行模板的編譯,將template模板進行編譯,解析成AST樹,再轉換成render函式,而有了render函式後才會進入例項掛載過程。對於事件而言,我們經常使用v-on或者@在模板上繫結事件。因此對事件的第一步處理,就是在編譯階段對事件指令做收集處理。

從一個簡單的用法分析編譯階段收集的資訊:

<div id="app">
    <div v-on:click.stop="doThis">點選</div>
    <span>{{count}}</span>
</div>
<script>
var vm = new Vue({
    el: '#app',
    data() {
        return {
            count: 1
        }
    },
    methods: {
        doThis() {
            ++this.count
        }
    }
})
</script>
複製程式碼

我們之前在將模板編譯的時候大致說過編譯的流程,模板編譯的入口是在var ast = parse(template.trim(), options);中,parse通過拆分模板字串,將其解析為一個AST樹,其中對於屬性的處理,在processAttr中,由於分支較多,我們只分析例子中的流程。

var dirRE = /^v-|^@|^:/;

function processAttrs (el) {
    var list = el.attrsList;
    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
    for (i = 0, l = list.length; i < l; i++) {
      name = rawName = list[i].name; // v-on:click
      value = list[i].value; // doThis
      if (dirRE.test(name)) { // 匹配v-或者@開頭的指令
        el.hasBindings = true;
        modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')
        if (modifiers) {
          name = name.replace(modifierRE, '');
        }
        if (bindRE.test(name)) { // v-bind分支
          // ...留到v-bind指令時分析
        } else if (onRE.test(name)) { // v-on分支
          name = name.replace(onRE, ''); // 拿到真正的事件click
          isDynamic = dynamicArgRE.test(name);// 動態事件繫結
          if (isDynamic) {
            name = name.slice(1, -1);
          }
          addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
        } else { // normal directives
         // 其他指令相關邏輯
      } else {}
    }
  }
複製程式碼

processAttrs的邏輯雖然較多,但是理解起來較為簡單,var dirRE = /^v-|^@|^:/;是匹配事件相關的正則,命中匹配的記過會得到事件指令相關內容,包括事件本身,事件回撥以及事件修飾符。最終通過addHandler方法,為AST樹新增事件相關的屬性。而addHandler還有一個重要功能是對事件修飾符進行特殊處理。

// el是當前解析的AST樹
function addHandler (el,name,value,modifiers,important,warn,range,dynamic) {
    modifiers = modifiers || emptyObject;
    // passive 和 prevent不能同時使用,可以參照官方文件說明
    if (
      warn &&
      modifiers.prevent && modifiers.passive
    ) {
      warn(
        'passive and prevent can\'t be used together. ' +
        'Passive handler can\'t prevent default event.',
        range
      );
    }
    // 這部分的邏輯會對特殊的修飾符做字串拼接的處理,以備後續的使用
    if (modifiers.right) {
      if (dynamic) {
        name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";
      } else if (name === 'click') {
        name = 'contextmenu';
        delete modifiers.right;
      }
    } else if (modifiers.middle) {
      if (dynamic) {
        name = "(" + name + ")==='click'?'mouseup':(" + name + ")";
      } else if (name === 'click') {
        name = 'mouseup';
      }
    }
    if (modifiers.capture) {
      delete modifiers.capture;
      name = prependModifierMarker('!', name, dynamic);
    }
    if (modifiers.once) {
      delete modifiers.once;
      name = prependModifierMarker('~', name, dynamic);
    }
    /* istanbul ignore if */
    if (modifiers.passive) {
      delete modifiers.passive;
      name = prependModifierMarker('&', name, dynamic);
    }
    // events 用來記錄繫結的事件
    var events;
    if (modifiers.native) {
      delete modifiers.native;
      events = el.nativeEvents || (el.nativeEvents = {});
    } else {
      events = el.events || (el.events = {});
    }

    var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);
    if (modifiers !== emptyObject) {
      newHandler.modifiers = modifiers;
    }

    var handlers = events[name];
    /* istanbul ignore if */
    // 繫結的事件可以多個,回撥也可以多個,最終會合併到陣列中
    if (Array.isArray(handlers)) {
      important ? handlers.unshift(newHandler) : handlers.push(newHandler);
    } else if (handlers) {
      events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
    } else {
      events[name] = newHandler;
    }
    el.plain = false;
  }
複製程式碼

修飾符的處理會改變最終字串的拼接結果,我們看最終轉換的AST樹:

深入剖析Vue原始碼 - 揭祕Vue的事件機制

9.2. 程式碼生成

模板編譯的最後一步是根據解析完的AST樹生成對應平臺的渲染函式,也就是render函式的生成過程, 對應var code = generate(ast, options);

function generate (ast,options) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"), // with函式
      staticRenderFns: state.staticRenderFns
    }
  }
複製程式碼

其中核心處理在getElement中,getElement函式會根據不同指令型別處理不同的分支,對於普通模板的編譯會進入genData函式中處理,同樣分析只針對事件相關的處理,從前面解析出的AST樹明顯看出,AST樹中多了events的屬性,genHandlers函式會為event屬性做邏輯處理。

function genData (el, state) {
    var data = '{';

    // directives first.
    // directives may mutate the el's other properties before they are generated.
    var dirs = genDirectives(el, state);
    if (dirs) { data += dirs + ','; }
    //其他處理
    ···

    // event handlers
    if (el.events) {
      data += (genHandlers(el.events, false)) + ",";
    }

    ···

    return data
  }
複製程式碼

genHandlers的邏輯,會遍歷解析好的AST樹,拿到event物件屬性,並根據屬性上的事件物件拼接成字串。

function genHandlers (events,isNative) {
    var prefix = isNative ? 'nativeOn:' : 'on:';
    var staticHandlers = "";
    var dynamicHandlers = "";
    // 遍歷ast樹解析好的event物件
    for (var name in events) {
      //genHandler本質上是將事件物件轉換成可拼接的字串
      var handlerCode = genHandler(events[name]);
      if (events[name] && events[name].dynamic) {
        dynamicHandlers += name + "," + handlerCode + ",";
      } else {
        staticHandlers += "\"" + name + "\":" + handlerCode + ",";
      }
    }
    staticHandlers = "{" + (staticHandlers.slice(0, -1)) + "}";
    if (dynamicHandlers) {
      return prefix + "_d(" + staticHandlers + ",[" + (dynamicHandlers.slice(0, -1)) + "])"
    } else {
      return prefix + staticHandlers
    }
  }
// 事件模板書寫匹配
var isMethodPath = simplePathRE.test(handler.value); // doThis
var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}
var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)


function genHandler (handler) {
    if (!handler) {
      return 'function(){}'
    }
    // 事件繫結可以多個,多個在解析ast樹時會以陣列的形式存在,如果有多個則會遞迴呼叫getHandler方法返回陣列。
    if (Array.isArray(handler)) {
      return ("[" + (handler.map(function (handler) { return genHandler(handler); }).join(',')) + "]")
    }
    // value: doThis 可以有三種方式
    var isMethodPath = simplePathRE.test(handler.value); // doThis
    var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}
    var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)

    // 沒有任何修飾符
    if (!handler.modifiers) {
      // 符合函式定義規範,則直接返回撥用函式名 doThis
      if (isMethodPath || isFunctionExpression) {
        return handler.value
      }
      // 不符合則通過function函式封裝返回
      return ("function($event){" + (isFunctionInvocation ? ("return " + (handler.value)) : handler.value) + "}") // inline statement
    } else {
    // 包含修飾符的場景
    }
  }
複製程式碼

模板中事件的寫法有三種,分別對應上訴上個正則匹配的內容。

    1. <div @click="doThis"></div>
    1. <div @click="doThis($event)"></div>
    1. <div @click="()=>{}"></div> <div @click="function(){}"></div>

上述對事件物件的轉換,如果事件不帶任何修飾符,並且滿足正確的模板寫法,則直接返回撥用事件名,如果不滿足,則有可能是<div @click="console.log(11)"></div>的寫法,此時會封裝到function($event){}中。

包含修飾符的場景較多,我們單獨列出分析。以上文中的例子說明,modifiers: { stop: true }會拿到stop對應需要新增的邏輯指令碼'$event.stopPropagation();',並將它新增到函式字串中返回。

function genHandler() {
  // ···
  } else {
    var code = '';
    var genModifierCode = '';
    var keys = [];
    // 遍歷modifiers上記錄的修飾符
    for (var key in handler.modifiers) {
      if (modifierCode[key]) {
        // 根據修飾符新增對應js的程式碼
        genModifierCode += modifierCode[key];
        // left/right
        if (keyCodes[key]) {
          keys.push(key);
        }
        // 針對exact的處理
      } 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)")
        : isFunctionInvocation
          ? ("return " + (handler.value))
          : handler.value;
    return ("function($event){" + code + handlerCode + "}")
  }
}
var modifierCode = {
  stop: '$event.stopPropagation();',
  prevent: '$event.preventDefault();',
  self: genGuard("$event.target !== $event.currentTarget"),
  ctrl: genGuard("!$event.ctrlKey"),
  shift: genGuard("!$event.shiftKey"),
  alt: genGuard("!$event.altKey"),
  meta: genGuard("!$event.metaKey"),
  left: genGuard("'button' in $event && $event.button !== 0"),
  middle: genGuard("'button' in $event && $event.button !== 1"),
  right: genGuard("'button' in $event && $event.button !== 2")
};
複製程式碼

經過這一轉換後,生成with封裝的render函式如下:

"_c('div',{attrs:{"id":"app"}},[_c('div',{on:{"click":function($event){$event.stopPropagation();return doThis($event)}}},[_v("點選")]),_v(" "),_c('span',[_v(_s(count))])])"
複製程式碼

9.3. 事件繫結

前面花了大量的篇幅介紹了模板上的事件標記在構建AST樹上是怎麼處理,並且如何根據構建的AST樹返回正確的render渲染函式,但是真正事件繫結還是離不開繫結註冊事件。這一個階段就是發生在元件掛載的階段。 有了render函式,自然可以生成例項掛載需要的Vnode樹,並且會進行patchVnode的環節進行真實節點的構建,如果發現過程已經遺忘,可以回顧以往章節。 Vnode樹的構建過程和之前介紹的內容沒有明顯的區別,所以這個過程就不做贅述,最終生成的vnode如下:

深入剖析Vue原始碼 - 揭祕Vue的事件機制

有了Vnode,接下來會遍歷子節點遞迴呼叫createElm為每個子節點建立真實的DOM,由於Vnode中有data屬性,在建立真實DOM時會進行註冊相關鉤子的過程,其中一個就是註冊事件相關處理。

function createElm() {
  ···
  // 針對指令的處理
   if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
}


function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](emptyNode, vnode);
  }
  i = vnode.data.hook; // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) { i.create(emptyNode, vnode); }
    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  }
}

var events = {
  create: updateDOMListeners,
  update: updateDOMListeners
};
複製程式碼

我們經常會在template模板中定義v-on事件,v-bind動態屬性,v-text動態指令等,和v-on事件指令一樣,他們都會在編譯階段和Vnode生成階段建立data屬性,因此invokeCreateHooks就是一個模板指令處理的任務,他分別針對不同的指令為真實階段建立不同的任務。針對事件,這裡會呼叫updateDOMListeners對真實的DOM節點註冊事件任務。

function updateDOMListeners (oldVnode, vnode) {
  // on是事件指令的標誌
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  // 新舊節點不同的事件繫結解綁
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  // 拿到需要新增事件的真實DOM節點
  target$1 = vnode.elm;
  // normalizeEvents是對事件相容性的處理
  normalizeEvents(on);
  updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
  target$1 = undefined;
}
複製程式碼

其中normalizeEvents是針對v-model的處理,例如在IE下不支援change事件,只能用input事件代替。

updateListeners的邏輯也很簡單,它會遍歷on事件對新節點事件繫結註冊事件,對舊節點移除事件監聽,它即要處理原生DOM事件的新增和移除,也要處理自定義事件的新增和移除,關於自定義事件,後續內容再分析。

function updateListeners (on,oldOn,add,remove$$1,createOnceHandler,vm) {
    var name, def$$1, cur, old, event;
    // 遍歷事件
    for (name in on) {
      def$$1 = cur = on[name];
      old = oldOn[name];
      event = normalizeEvent(name);
      if (isUndef(cur)) {
        // 事件名非法的報錯處理
        warn(
          "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
          vm
        );
      } else if (isUndef(old)) {
        // 舊節點不存在
        if (isUndef(cur.fns)) {
          // createFunInvoker返回事件最終執行的回撥函式
          cur = on[name] = createFnInvoker(cur, vm);
        }
        // 只觸發一次的事件
        if (isTrue(event.once)) {
          cur = on[name] = createOnceHandler(event.name, cur, event.capture);
        }
        // 執行真正註冊事件的執行函式
        add(event.name, cur, event.capture, event.passive, event.params);
      } 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);
      }
    }
  }
複製程式碼

在初始構建例項時,舊節點是不存在的,此時會呼叫createFnInvoker函式對事件回撥函式做一層封裝,由於單個事件的回撥可以有多個,因此createFnInvoker的作用是對單個,多個回撥事件統一封裝處理,返回一個當事件觸發時真正執行的匿名函式。

function createFnInvoker (fns, vm) {
  // 當事件觸發時,執行invoker方法,方法執行fns
  function invoker () {
    var arguments$1 = arguments;

    var fns = invoker.fns;
    // fns是多個回撥函式組成的陣列
    if (Array.isArray(fns)) {
      var cloned = fns.slice();
      for (var i = 0; i < cloned.length; i++) {
        // 遍歷執行真正的回撥函式
        invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
      }
    } else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
    }
  }
  invoker.fns = fns;
  // 返回最終事件執行的回撥函式
  return invoker
}
複製程式碼

其中invokeWithErrorHandling會執行定義好的回撥函式,這裡做了同步非同步回撥的錯誤處理。try-catch用於同步回撥捕獲異常錯誤,Promise.catch用於捕獲非同步任務返回錯誤。

function invokeWithErrorHandling (handler,context,args,vm,info) {
    var res;
    try {
      res = args ? handler.apply(context, args) : handler.call(context);
      if (res && !res._isVue && isPromise(res)) {
        // issue #9511
        // reassign to res to avoid catch triggering multiple times when nested calls
        // 當生命週期鉤子函式內部執行返回promise物件是,如果捕獲異常,則會對異常資訊做一層包裝返回
        res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
      }
    } catch (e) {
      handleError(e, vm, info);
    }
    return res
  }
複製程式碼

如果事件只觸發一次(即使用了once修飾符),則呼叫createOnceHandler匿名,在執行完回撥之後,移除事件繫結。

function createOnceHandler (event, handler, capture) {
    var _target = target$1; 
    return function onceHandler () {
      //呼叫事件回撥
      var res = handler.apply(null, arguments);
      if (res !== null) {
        // 移除事件繫結
        remove$2(event, onceHandler, capture, _target);
      }
    }
  }
複製程式碼

addremove是真正在DOM上繫結事件和解綁事件的過程,它的實現也是利用了原生DOMaddEventListener,removeEventListener api

function add (name,handler,capture,passive){
  ···
  target$1.addEventListener(name,handler,
      supportsPassive
        ? { capture: capture, passive: passive }
        : capture);
}
function remove (name,handler,capture,_target) {
  (_target || target$1).removeEventListener(
    name,
    handler._wrapper || handler,
    capture
  );
}
複製程式碼

另外事件的解綁除了發生在只觸發一次的事件,也發生在元件更新patchVnode過程,具體不展開分析,可以參考之前介紹元件更新的內容研究updateListeners的過程。

9.4. 自定義事件

Vue如何處理原生的Dom事件基本流程已經講完,然而針對事件還有一個重要的概念不可忽略,那就是元件的自定義事件。我們知道父子元件可以利用事件進行通訊,子元件通過vm.$emit向父元件分發事件,父元件通過v-on:(event)接收資訊並處理回撥。因此針對自定義事件在原始碼中自然有不同的處理邏輯。我們先通過簡單的例子展開。

<script>
    var child = {
      template: `<div @click="emitToParent">點選傳遞資訊給父元件</div>`,
      methods: {
        emitToParent() {
          this.$emit('myevent', 1)
        }
      }
    }
    new Vue({
      el: '#app',
      components: {
        child
      },
      template: `<div id="app"><child @myevent="myevent" @click.native="nativeClick"></child></div>`,
      methods: {
        myevent(num) {
          console.log(num)
        },
        nativeClick() {
          console.log('nativeClick')
        }
      }
    })
  </script>
複製程式碼

從例子中可以看出,普通節點只能使用原生DOM事件,而元件上卻可以使用自定義的事件和原生的DOM事件,並且通過native修飾符區分,有了原生DOM對於事件處理的基礎,接下來我們看看自定義事件有什麼特別之處。

9.4.1 模板編譯

回過頭來看看事件的模板編譯,在生成AST樹階段,之前分析說過addHandler方法會對事件的修飾符做不同的處理,當遇到native修飾符時,事件相關屬性方法會新增到nativeEvents屬性中。 下圖是child生成的AST樹:

深入剖析Vue原始碼 - 揭祕Vue的事件機制

9.4.2 程式碼生成

不管是元件還是普通標籤,事件處理程式碼都在genData的過程中,和之前分析原生事件一致,genHandlers用來處理事件物件並拼接成字串。

function genData() {
  ···
  if (el.events) {
    data += (genHandlers(el.events, false)) + ",";
  }
  if (el.nativeEvents) {
    data += (genHandlers(el.nativeEvents, true)) + ",";
  }
}
複製程式碼

getHandlers的邏輯前面已經講過,處理元件原生事件和自定義事件的區別在isNative選項上,我們看最終生成的程式碼為:

with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{on:{"myevent":myevent},nativeOn:{"click":function($event){return nativeClick($event)}}})],1)}
複製程式碼

有了render函式接下來會根據它建立Vnode例項,其中遇到元件佔位符節點時會建立子元件Vnode, 此時為on,nativeOn做了一層特殊的轉換,將nativeOn賦值給on,這樣後續的處理方式和普通節點一致。另外,將on賦值給listeners,在建立VNode時以元件配置componentOptions傳入。

 // 建立子元件過程
function createComponent (){
  ···
  var listeners = data.on;
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;
  ···

  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 },
    asyncFactory
  );

  return vnode
}
複製程式碼
9.4.3 子元件例項

接下來是通過Vnode生成真實節點的過程,這個過程遇到子Vnode會例項化子元件例項。例項化子類構造器的過程又回到之前文章分析的初始化選項配置的過程,在系列最開始的時候分析Vue.prototype.init的過程,跳過了元件初始化的流程,其中針對自定義事件的處理的關鍵如下

Vue.prototype._init = function(options) {
  ···
  // 針對子元件的事件處理邏輯
  if (options && options._isComponent) {
    // 初始化內部元件
    initInternalComponent(vm, options);
  } else {
    // 選項合併,將合併後的選項賦值給例項的$options屬性
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
  }
  // 初始化事件處理
  initEvents(vm);
}
function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  ···
  opts._parentListeners = vnodeComponentOptions.listeners;
  ···
}
複製程式碼

此時,子元件拿到了父佔位符節點定義的@myevent="myevent"事件。接下來進行子元件的初始化事件處理,此時vm.$options._parentListeners會拿到父元件自定義的事件。而帶有自定義事件的元件會執行updateComponentListeners函式。

function initEvents (vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    // 帶有自定義事件屬性的例項
    updateComponentListeners(vm, listeners);
  }
}
複製程式碼

之後又回到了之前分析的updateListeners過程,和原生DOM事件不同的是,自定義事件的新增移除的方法不同。

var target = vm;

function add (event, fn) {
  target.$on(event, fn);
}

function remove$1 (event, fn) {
  target.$off(event, fn);
} 

function updateComponentListeners (vm,listeners,oldListeners) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
  target = undefined;
}

複製程式碼
9.4.4 事件API

我們回頭來看看Vue在引入階段對事件的處理還做了哪些初始化操作。Vue在例項上用一個_events屬性存貯管理事件的派發和更新,暴露出$on, $once, $off, $emit方法給外部管理事件和派發執行事件。

  eventsMixin(Vue); // 定義事件相關函式

  function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    // $on方法用來監聽事件,執行回撥
    Vue.prototype.$on = function (event, fn) {
      var vm = this;
      // event支援陣列形式。
      if (Array.isArray(event)) {
        for (var i = 0, l = event.length; i < l; i++) {
          vm.$on(event[i], fn);
        }
      } else {
        // _events陣列中記錄需要監聽的事件以及事件觸發的回撥
        (vm._events[event] || (vm._events[event] = [])).push(fn);
        if (hookRE.test(event)) {
          vm._hasHookEvent = true;
        }
      }
      return vm
    };
    // $once方法用來監聽一次事件,執行回撥
    Vue.prototype.$once = function (event, fn) {
      var vm = this;
      // 對fn做一層包裝,先解除繫結再執行fn回撥
      function on () {
        vm.$off(event, on);
        fn.apply(vm, arguments);
      }
      on.fn = fn;
      vm.$on(event, on);
      return vm
    };
    // $off方法用來解除事件監聽
    Vue.prototype.$off = function (event, fn) {
      var vm = this;
      // 如果$off方法沒有傳遞任何引數時,將_events屬性清空。
      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++) {
          vm.$off(event[i$1], fn);
        }
        return vm
      }
      var cbs = vm._events[event];
      if (!cbs) {
        return vm
      }
      if (!fn) {
        vm._events[event] = null;
        return vm
      }
      // specific handler
      var cb;
      var i = cbs.length;
      while (i--) {
        cb = cbs[i];
        if (cb === fn || cb.fn === fn) {
          // 將監聽的事件回撥移除
          cbs.splice(i, 1);
          break
        }
      }
      return vm
    };
    // $emit方法用來觸發事件,執行回撥
    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 + "\"."
          );
        }
      }
      var cbs = vm._events[event];
      // 找到已經監聽事件的回撥,執行
      if (cbs) {
        cbs = cbs.length > 1 ? toArray(cbs) : cbs;
        var args = toArray(arguments, 1);
        var info = "event handler for \"" + event + "\"";
        for (var i = 0, l = cbs.length; i < l; i++) {
          invokeWithErrorHandling(cbs[i], vm, args, vm, info);
        }
      }
      return vm
    };
  }
複製程式碼

有了這些事件api,自定義事件的新增移除理解起來也簡單很多。元件通過this.$emit在元件例項中派發了事件,而在這之前,元件已經將需要監聽的事件以及回撥新增到例項的_events屬性中,觸發事件時便可以直接執行監聽事件的回撥。

最後,我們換一個角度理解父子元件通訊,元件自定義事件的觸發和監聽本質上都是在當前的元件例項中進行,之所以能產生父子元件通訊的效果是因為事件監聽的回撥函式寫在了父元件中。

9.5 小結

事件是我們日常開發中必不可少的功能點,Vue在應用層暴露了@,v-on的指令供開發者在模板中繫結事件。事件指令在模板編譯階段會以屬性的形式存在,而在真實節點渲染階段會根據事件屬性去繫結相關的事件。對於元件的事件而言,我們可以利用事件進行子父元件間的通訊,他本質上是在同個子元件內部維護了一個事件匯流排,從分析結果可以看出,之所以有子父元件通訊的效果,原因僅僅是因為回撥函式寫在了父元件中。


相關文章