1. 程式人生 > 其它 >vue 原始碼詳解(三): 渲染初始化 initRender 、生命週期的呼叫 callHook 、異常處理機制

vue 原始碼詳解(三): 渲染初始化 initRender 、生命週期的呼叫 callHook 、異常處理機制

vue 原始碼詳解(三): 渲染初始化 initRender 、生命週期的呼叫 callHook 、異常處理機制

1 渲染初始化做了什麼

Vue 例項上初始化了一些渲染需要用的屬性和方法:

  1. 將元件的插槽編譯成虛擬節點 DOM 樹, 以列表的形式掛載到 vm 例項,初始化作用域插槽為空物件;
  2. 將模板的編譯函式(把模板編譯成虛擬 DOM 樹)掛載到 vm_c$createElement 屬性;
  3. 最後把父元件傳遞過來的 $attrs$listeners 定義成響應式的。

$attrs$listeners 在高階元件中用的比較多, 可能普通的同學很少用到。後面我會單獨寫一篇文章來介紹$attrs

$listeners 的用法。

// node_modules\vue\src\core\instance\render.js
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree 子元件的虛擬 DOM 樹的根節點
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 父元件在父元件虛擬 DOM 樹中的佔位節點
  const renderContext = parentVnode && parentVnode.context
  /*
      resolveSlots (
        children: ?Array<VNode>,
        context: ?Component
      ): { [key: string]: Array<VNode> }  
  */
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

2 生命週期的呼叫 callHook

完成渲染的初始化, vm 開始呼叫 beforeCreate 這個生命週期。

使用者使用的 beforeCreatecreated 等鉤子在 Vue 中是以陣列的形式儲存的,可以看成是一個任務佇列。 即每個生命週期鉤子函式都是 beforeCreate : [fn1, fn2, fn3, ... , fnEnd] 這種結構, 當呼叫 callHook(vm, 'beforeCreate') 時, 以當前元件的 vmthis 上下文依次執行生命週期鉤子函式中的每一個函式。 每個生命週期鉤子都是一個任務佇列的原因是, 舉個例子, 比如我們的元件已經寫了一個 beforeCreate

生命週期鉤子, 但是可以通過 Vue.mixin 繼續向當前例項增加 beforeCreate 鉤子。

#7573 disable dep collection when invoking lifecycle hooks 翻譯過來是, 當觸發生命週期鉤子時, 禁止依賴收集。 通過 pushTargetpopTarget 兩個函式完成。 pushTarget 將當前依賴項置空, 並向依賴列表推入一個空的依賴, 等到 beforeCreate 中任務佇列執行完畢,再通過 popTarget 將剛才加入的空依賴刪除。至於什麼是依賴和收集依賴, 放在狀態初始化的部分吧。

callHook(vm, 'beforeCreate') 呼叫後, const handlers = vm.$options[hook] 即讀取到了當前 vm 例項上的任務佇列,然後通過 for 迴圈依次傳遞給 invokeWithErrorHandling(handlers[i], vm, null, vm, info) 進行處理, 呼叫 invokeWithErrorHandling 的好處是如果發生異常, 則會統一報錯處理。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
// node_modules\vue\src\core\observer\dep.js
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

3 異常處理機制

Vue 有一套異常處理機制, 所有的異常都在這裡處理。

Vue 中的異常處理機制有個特點, 就是一旦有一個元件報錯,Vue 會收集當前元件到根元件上所有的異常處理函式, 並從子元件開始, 層層觸發, 直至執行完成全域性異常處理; 如果使用者不想層層上報, 可以通過配置某個元件上的 errorCaptured 返回布林型別的值 false 即可。下面是從組建中擷取的一段程式碼,用以演示如何停止錯誤繼續上報上層元件:

export default {
  data() {
    return {
      // ... 屬性列表
    }
  }
  errorCaptured(cur, err, vm, info) {
    console.log(cur, err, vm, info)
    return false // 返回布林型別的值 `false` 即可終止異常繼續上報, 並且不再觸發全域性的異常處理函式
  },
}

Vue 的全域性 api 中有個 Vue.config 在這裡可以配置 Vue 的行為特性, 可以通過 Vue.config.errorHandler 配置異常處理函式, 也可以在呼叫 new Vue() 時通過 errorCaptured 傳遞, 還可以通過 Vue.mixin 將錯誤處理混入到當前元件。執行時先執行 vm.$options.errorCaptured 上的異常處理函式, 然後根據 errorCaptured 的返回值是否與布林值 false嚴格相等來決定是否執行 Vue.config.errorHandler 異常處理函式, 實際運用中這兩個配置其中一個即可。 我們可以根據異常型別,確定是否將資訊展示給使用者、是否將異常提交給伺服器等操作。下面是一個簡單的示例:

Vue.config.errorHandler = (cur, err, vm, info)=> {
  console.log(cur, err, vm, info)
  alert(2)
}
new Vue({
  errorCaptured(cur, err, vm, info) {
    console.log(cur, err, vm, info)
    alert(1)
  },
  router,
  store,
  render: h => h(App)
}).$mount('#app')

呼叫宣告週期的鉤子,是通過 callHook(vm, 'beforeCreate') 進行呼叫的, 而 callHook 最終都呼叫了 invokeWithErrorHandling 這個函式, 以 callHook(vm, 'beforeCreate') 為例, 在遍歷執行 beforeCreate 中的任務佇列時, 每個任務函式都會被傳遞到 invokeWithErrorHandling 這個函式中。

export function invokeWithErrorHandling (
  handler: Function, // 生命週期中的任務函式
  context: any, // 任務函式 `handlers[i]` 執行時的上下文
  args: null | any[], // 任務函式 `handlers[i]`執行時的引數, 以陣列的形式傳入, 因為最終通過 apply 呼叫
  vm: any, // 當前元件的例項物件
  info: string // 拋給使用者的異常資訊的描述文字
) {
  // 生命週期處理
}

invokeWithErrorHandling(handlers[i], vm, null, vm, info) 這個呼叫為例,第一個引數 handlers[i] 即任務函式; 第二個引數 vm 表示任務函式 handlers[i] 執行時的上下文, 也就是函式執行時 this 指向的物件,對於生命週期函式而言, this 全都指向當前元件; 第三個引數 null 表示任務函式 handlers[i] 執行時,沒有引數; 第四個引數 vm 表示當前元件的例項; 第五個引數表示異常發生時丟擲給使用者的異常資訊。

invokeWithErrorHandling 的核心處理是 res = args ? handler.apply(context, args) : handler.call(context) ,若呼叫成功, 則直接返回當前任務函式的返回值 res ; 否則呼叫 handleError(e, vm, info) 函式處理異常。

接下來繼續看 handleError 的邏輯。 Deactivate deps tracking while processing error handler to avoid possible infinite rendering. 翻譯過來的意思是 在執行異常處理函式時, 不再追蹤 deps 的變化,以避免發生無限次數渲染的情況, 處理方法與觸發生命週期函式時的處理方法一直, 也是通過 pushTarget, popTarget 這兩個函式處理。

然後,從當前元件開始,逐級查詢父元件,直至查詢到根元件, 對於所有被查到的上層元件, 都會讀取其 $options.errorCaptured 中配置的異常處理函式。
處理過程為 :

  • hooks[i].call(cur, err, vm, info) ,
  • 如果在這一步又發生了異常則呼叫通過 Vue.config 配置的 errorHandler 函式;
    • 如果呼叫成功並且返回 false 則異常處理終止, 不再呼叫全域性的異常處理函式 globalHandleError
    • 如果呼叫成功, 且返回值不與 false 嚴格相等(原始碼中通過 === 判斷的), 則繼續呼叫全域性的異常處理函式 globalHandleError
    • 如果呼叫 globalHandleError 時發生異常, 則通過預設的處理函式 logError 進行處理, 通過 console.error 將異常資訊輸出到控制檯。
// node_modules\vue\src\core\util\error.js
/* @flow */

import config from '../config'
import { warn } from './debug'
import { inBrowser, inWeex } from './env'
import { isPromise } from 'shared/util'
import { pushTarget, popTarget } from '../observer/dep'

export function handleError (err: Error, vm: any, info: string) {
  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
  // See: https://github.com/vuejs/vuex/issues/1505
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}

// invokeWithErrorHandling(handlers[i], vm, null, vm, info)
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    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
}

function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // if the user intentionally throws the original error in the handler,
      // do not log it twice
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  logError(err, vm, info)
}

function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

Vue 支援的可配置選項:

// node_modules\vue\src\core\config.js
/* @flow */
import {
  no,
  noop,
  identity
} from 'shared/util'

import { LIFECYCLE_HOOKS } from 'shared/constants'

export type Config = {
  // user
  optionMergeStrategies: { [key: string]: Function };
  silent: boolean;
  productionTip: boolean;
  performance: boolean;
  devtools: boolean;
  errorHandler: ?(err: Error, vm: Component, info: string) => void;
  warnHandler: ?(msg: string, vm: Component, trace: string) => void;
  ignoredElements: Array<string | RegExp>;
  keyCodes: { [key: string]: number | Array<number> };

  // platform
  isReservedTag: (x?: string) => boolean;
  isReservedAttr: (x?: string) => boolean;
  parsePlatformTagName: (x: string) => string;
  isUnknownElement: (x?: string) => boolean;
  getTagNamespace: (x?: string) => string | void;
  mustUseProp: (tag: string, type: ?string, name: string) => boolean;

  // private
  async: boolean;

  // legacy
  _lifecycleHooks: Array<string>;
};

export default ({
  /**
   * Option merge strategies (used in core/util/options)
   */
  // $flow-disable-line
  optionMergeStrategies: Object.create(null),

  /**
   * Whether to suppress warnings.
   */
  silent: false,

  /**
   * Show production mode tip message on boot?
   */
  productionTip: process.env.NODE_ENV !== 'production',

  /**
   * Whether to enable devtools
   */
  devtools: process.env.NODE_ENV !== 'production',

  /**
   * Whether to record perf
   */
  performance: false,

  /**
   * Error handler for watcher errors
   */
  errorHandler: null,

  /**
   * Warn handler for watcher warns
   */
  warnHandler: null,

  /**
   * Ignore certain custom elements
   */
  ignoredElements: [],

  /**
   * Custom user key aliases for v-on
   */
  // $flow-disable-line
  keyCodes: Object.create(null),

  /**
   * Check if a tag is reserved so that it cannot be registered as a
   * component. This is platform-dependent and may be overwritten.
   */
  isReservedTag: no,

  /**
   * Check if an attribute is reserved so that it cannot be used as a component
   * prop. This is platform-dependent and may be overwritten.
   */
  isReservedAttr: no,

  /**
   * Check if a tag is an unknown element.
   * Platform-dependent.
   */
  isUnknownElement: no,

  /**
   * Get the namespace of an element
   */
  getTagNamespace: noop,

  /**
   * Parse the real tag name for the specific platform.
   */
  parsePlatformTagName: identity,

  /**
   * Check if an attribute must be bound using property, e.g. value
   * Platform-dependent.
   */
  mustUseProp: no,

  /**
   * Perform updates asynchronously. Intended to be used by Vue Test Utils
   * This will significantly reduce performance if set to false.
   */
  async: true,

  /**
   * Exposed for legacy reasons
   */
  _lifecycleHooks: LIFECYCLE_HOOKS
}: Config)