Vue 原始碼解讀(7)—— Hook Event

李永寧發表於2022-03-01

前言

Hook Event(鉤子事件)相信很多 Vue 開發者都沒有使用過,甚至沒聽過,畢竟 Vue 官方文件中也沒有提及。

Vue 提供了一些生命週期鉤子函式,供開發者在特定的邏輯點新增額外的處理邏輯,比如:在元件掛載階段提供了 beforeMountmounted 兩個生命週期鉤子,供開發者在元件掛載階段執行額外的邏輯處理,比如為元件準備渲染所需的資料。

那這個 Hook Event —— 鉤子事件,其中也有鉤子的意思,和 Vue 的生命週期鉤子函式有什麼關係呢?它又有什麼用呢?這就是這邊文章要解答的問題。

目標

  • 理解什麼是 Hook Event ?明白其使用場景

  • 深入理解 Hook Event 的實現原理

什麼是 Hook Event ?

Hook Event 是 Vue 的自定義事件結合生命週期鉤子實現的一種從元件外部為元件注入額外生命週期方法的功能。

使用場景

假設現在有這麼一個第三方的業務元件,邏輯很簡單,就在 mounted 生命週期中呼叫介面獲取資料,然後將資料渲染到頁面上。

<template>
  <div class="wrapper">
    <ul>
      <li v-for="item in arr" :key="JSON.stringify(item)">
        {{ item }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      arr: []
    }
  },
  async mounted() {
    // 呼叫介面獲取元件渲染的資料
    const { data: { data } } = await this.$axios.get('/api/getList')
    this.arr.push(...data)
  }
}
</script>

然後在使用的發現這個元件有些瑕疵,比如最簡單的,介面等待時間可能比較長,我想在 mounted 生命週期開始執行的時候在控制檯輸出一個 loading ... 字串,增強使用者體驗。

這個需求該怎麼實現呢?

有兩個辦法:第一個比較麻煩,修改原始碼;而第二種方式則簡單多了,就是我們今天介紹的 Hook Event,從元件外面為元件注入額外的生命週期方法。

<template>
  <div class="wrapper">
    <comp @hook:mounted="hookMounted" />
  </div>
</template>

<script>
// 這就是上面的那個第三方業務元件
import Comp from '@/components/Comp.vue'

export default {
  components: {
    Comp
  },
  methods: {
    hookMounted() {
      console.log('loading ...')
    }
  }
}
</script>

這時候你再重新整理頁面就會發現業務元件在請求資料的時候,會在控制檯輸出一個 loading ... 字串。

作用

Hook Event 有什麼作用?

通過 Hook Event 可以從元件外部為元件注入額外的生命週期方法。

實現原理

知道了 Hook Event 的使用場景和作用,接下來就從原始碼去找它的實現原理,做到 “知其然,亦知其所以然”。

前面說過,Hook Event 是 Vue 的自定義事件結合生命週期鉤子函式實現的一種功能,所以我們就去看生命週期相關的程式碼,比如:我們知道,Vue 的生命週期函式是通過一個叫 callHook 的方法來執行的

callHook

/src/core/instance/lifecycle.js

/**
 * callHook(vm, 'mounted')
 * 執行例項指定的生命週期鉤子函式
 * 如果例項設定有對應的 Hook Event,比如:<comp @hook:mounted="method" />,執行完生命週期函式之後,觸發該事件的執行
 * @param {*} vm 元件例項
 * @param {*} hook 生命週期鉤子函式
 */
export function callHook (vm: Component, hook: string) {
  // 在執行生命週期鉤子函式期間禁止依賴收集
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  // 從例項配置物件中獲取指定鉤子函式,比如 mounted
  const handlers = vm.$options[hook]
  // mounted hook
  const info = `${hook} hook`
  if (handlers) {
    // 通過 invokeWithErrorHandler 執行生命週期鉤子
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  // Hook Event,如果設定了 Hook Event,比如 <comp @hook:mounted="method" />,則通過 $emit 觸發該事件
  // vm._hasHookEvent 標識元件是否有 hook event,這是在 vm.$on 中處理元件自定義事件時設定的
  if (vm._hasHookEvent) {
    // vm.$emit('hook:mounted')
    vm.$emit('hook:' + hook)
  }
  // 關閉依賴收集
  popTarget()
}

invokeWithErrorHandling

/src/core/util/error.js

/**
 * 通用函式,執行指定函式 handler
 * 傳遞進來的函式會被用 try catch 包裹,進行異常捕獲處理
 */
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    // 執行傳遞進來的函式 handler,並將執行結果返回
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

vm.$on

/src/core/instance/events.js

/**
 * 監聽例項上的自定義事件,vm._event = { eventName: [fn1, ...], ... }
 * @param {*} event 單個的事件名稱或者有多個事件名組成的陣列
 * @param {*} fn 當 event 被觸發時執行的回撥函式
 * @returns 
 */
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    // event 是有多個事件名組成的陣列,則遍歷這些事件,依次遞迴呼叫 $on
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    // 將註冊的事件和回撥以鍵值對的形式儲存到 vm._event 物件中 vm._event = { eventName: [fn1, ...] }
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // hookEvent,提供從外部為元件例項注入宣告週期方法的機會
    // 比如從元件外部為元件的 mounted 方法注入額外的邏輯
    // 該能力是結合 callhook 方法實現的
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

總結

  • 面試官 問:什麼是 Hook Event?

    Hook Event 是 Vue 的自定義事件結合生命週期鉤子實現的一種從元件外部為元件注入額外生命週期方法的功能。


  • 面試官 問:Hook Event 是如果實現的?

    <comp @hook:lifecycleMethod="method" />
    
    • 處理元件自定義事件的時候(vm.$on) 如果發現元件有 hook:xx 格式的事件(xx 為 Vue 的生命週期函式),則將 vm._hasHookEvent 置為 true,表示該元件有 Hook Event

    • 在元件生命週期方法被觸發的時候,內部會通過 callHook 方法來執行這些生命週期函式,在生命週期函式執行之後,如果發現 vm._hasHookEvent 為 true,則表示當前元件有 Hook Event,通過 vm.$emit('hook:xx') 觸發 Hook Event 的執行

    這就是 Hook Event 的實現原理。

連結

感謝各位的:關注點贊收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 關注點贊收藏評論

新視訊和文章會第一時間在微信公眾號傳送,歡迎關注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

相關文章