根據調試工具看Vue源碼之組件通訊(一)

根據調試工具看Vue源碼之組件通訊(一)## 根據調試工具看Vue源碼之組件通訊(一)

在平時的業務開發中,相信在座的各位沒少用過組件通訊。然而,對於一些新手/業務熟手來講,不懂技術原理每每知其然而不知其因此然,用得一臉懵逼。看完本文能夠幫助你瞭解 Vue組件的通訊方式及原理,從而進一步加深對 Vue的理解,遠離 CV工程師的行列。

Vue經常使用的組件通訊方式

  • 經過$emit在子組件傳參給父組件,同時觸發對應的父組件函數,以此達到父子組件通訊的目的
  • 經過eventbus$emit$on方法傳遞數據,以此實現父子組件/兄弟組件之間的通訊
  • 經過Vuex將頁面數據劃分模塊,更好更方便的管理數據

父子組件通訊原理

🌰示例代碼:

// 父組件
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld
      msg="Welcome to Your Vue.js App"
      @test="test"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'app',
  methods: {
    test (param) {
      debugger
      console.log('param-->', param);
    }
  },
  components: {
    HelloWorld
  }
}
</script>
// 子組件
<template>
  <div class="wrapper">
    <button @click="test">按鈕</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  methods: {
    test () {
      debugger
      this.$emit('test', '666')
    }
  }
}
</script>

咱們能夠看到,父子組件的test方法中各打了一個debuggerjavascript

運行程序,進入第一個斷點

Vue.prototype.$emit = function (event) {
  var vm = this;
  ...
  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
};

看完上面的代碼咱們知道,vm._events[event]拿到了一個方法,而後調用invokeWithErrorHandling。固然,vm._events[event]的方法應該是從template上拿到的,接下來咱們能夠帶着這幾個疑問繼續往下看:前端

  • vm._events是何時賦值的?
  • invokeWithErrorHandling方法是怎麼執行的?

vm._events是何時賦值的?

在子組件的test方法中打下一個斷點,選中調用堆棧中的最後一個之後能夠看到add$1函數,在這裏再下一個斷點,從新刷新頁面之後斷點停在了add$1這個函數上,同時調用堆棧列表刷新,大概有這些:vue

  • add$1
  • updateListeners
  • updateDomListeners
  • invokeCreateHooks
  • createElm
  • ...

試探性的點進updateListeners之後,咱們看到:java

function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  createOnceHandler,
  vm
) {
  var name, def$$1, cur, old, event;
  // 看到這裏初步猜想會遍歷全部的方法
  // 在chrome的斷點下能夠看到一個click屬性,這裏不知道爲何沒有test方法
  for (name in on) {
    def$$1 = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    // 判斷當前的方法的調用器(invoker)是不是undefined,在開發環境下則會有報錯提示
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } else if (isUndef(old)) { // 判斷以前是否已存在
      if (isUndef(cur.fns)) { // 判斷實際上調用的函數是不是undefined
        cur = on[name] = createFnInvoker(cur, vm);
      }
      if (isTrue(event.once)) { // 多是掛載在一次性節點上,這裏也作出判斷
        cur = on[name] = createOnceHandler(event.name, cur, event.capture);
      }
      // 斷點沒打在這裏以前,event.name一直是「click」
      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);
    }
  }
}

整理完上面這個函數的邏輯之後,將斷點打在add上,刷新頁面後斷點停在這裏,步進這個函數:node

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

顯然target是全局變量,可是這裏先不深究。再次步進以後能夠看到斷點停在這裏:chrome

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

可見父子組件通訊過程當中,儘管$on對開發者不可見,可是最終仍是要走$on函數,這裏感受跟使用eventbus大同小異。數組

至此,剛纔提出的第一個疑問已經解決:)微信

invokeWithErrorHandling方法是怎麼執行的?

在一開始的基礎上,直接步進invokeWithErrorHandling方法:app

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
      res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
    }
  } catch (e) {
    handleError(e, vm, info);
  }
  return res
}

最後從新梳理下父子組件通訊的實現邏輯:dom

  • 賦值vm._events[event]
  1. 頁面初始化時,Vue調用updateListeners函數(固然,在那以前會生成虛擬dom,也就是vnode

這裏暫不深究),在函數裏面調用createFnInvoker方法,給模板上的方法再套一層調用器(invoker)

  1. 調用target.$on方法
  2. 遞歸處理event爲數組的狀況
  3. vm._events[event]賦值
  • invokeWithErrorHandling方法是怎麼執行的?
  1. 判斷是否有參數,而後分狀況調用
  2. 處理異步函數的狀況

⚠️注意:因爲Vue會在方法上再封裝一層調用器(invoker),因此在在調用堆棧這裏每每會出現兩個invokeWithErrorHandling方法

掃描下方的二維碼或搜索「tony老師的前端補習班」關注個人微信公衆號,那麼就能夠第一時間收到個人最新文章。

相關文章
相關標籤/搜索