vue 源碼詳解(三): 渲染初始化 initRender 、生命周期的調(diào)用 callHook 、異常處理機(jī)制

vue 源碼詳解(三): 渲染初始化 initRender 、生命周期的調(diào)用 callHook 、異常處理機(jī)制

1 渲染初始化做了什么

Vue 實(shí)例上初始化了一些渲染需要用的屬性和方法:

  1. 將組件的插槽編譯成虛擬節(jié)點(diǎn) DOM 樹, 以列表的形式掛載到 vm 實(shí)例,初始化作用域插槽為空對象;
  2. 將模板的編譯函數(shù)(把模板編譯成虛擬 DOM 樹)掛載到 vm_c$createElement 屬性;
  3. 最后把父組件傳遞過來的 $attrs$listeners 定義成響應(yīng)式的。

$attrs$listeners 在高階組件中用的比較多, 可能普通的同學(xué)很少用到。后面我會單獨(dú)寫一篇文章來介紹$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 樹的根節(jié)點(diǎn)
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 父組件在父組件虛擬 DOM 樹中的占位節(jié)點(diǎn)
  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 生命周期的調(diào)用 callHook

完成渲染的初始化, vm 開始調(diào)用 beforeCreate 這個(gè)生命周期。

用戶使用的 beforeCreate 、 created 等鉤子在 Vue 中是以數(shù)組的形式保存的,可以看成是一個(gè)任務(wù)隊(duì)列。 即每個(gè)生命周期鉤子函數(shù)都是 beforeCreate : [fn1, fn2, fn3, ... , fnEnd] 這種結(jié)構(gòu), 當(dāng)調(diào)用 callHook(vm, 'beforeCreate') 時(shí), 以當(dāng)前組件的 vmthis 上下文依次執(zhí)行生命周期鉤子函數(shù)中的每一個(gè)函數(shù)。 每個(gè)生命周期鉤子都是一個(gè)任務(wù)隊(duì)列的原因是, 舉個(gè)例子, 比如我們的組件已經(jīng)寫了一個(gè) beforeCreate 生命周期鉤子, 但是可以通過 Vue.mixin 繼續(xù)向當(dāng)前實(shí)例增加 beforeCreate 鉤子。

#7573 disable dep collection when invoking lifecycle hooks 翻譯過來是, 當(dāng)觸發(fā)生命周期鉤子時(shí), 禁止依賴收集。 通過 pushTarget 、 popTarget 兩個(gè)函數(shù)完成。 pushTarget 將當(dāng)前依賴項(xiàng)置空, 并向依賴列表推入一個(gè)空的依賴, 等到 beforeCreate 中任務(wù)隊(duì)列運(yùn)行完畢,再通過 popTarget 將剛才加入的空依賴刪除。至于什么是依賴和收集依賴, 放在狀態(tài)初始化的部分吧。

callHook(vm, 'beforeCreate') 調(diào)用后, const handlers = vm.$options[hook] 即讀取到了當(dāng)前 vm 實(shí)例上的任務(wù)隊(duì)列,然后通過 for 循環(huán)依次傳遞給 invokeWithErrorHandling(handlers[i], vm, null, vm, info) 進(jìn)行處理, 調(diào)用 invokeWithErrorHandling 的好處是如果發(fā)生異常, 則會統(tǒng)一報(bào)錯(cuò)處理。

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 異常處理機(jī)制

Vue 有一套異常處理機(jī)制, 所有的異常都在這里處理。

Vue 中的異常處理機(jī)制有個(gè)特點(diǎn), 就是一旦有一個(gè)組件報(bào)錯(cuò),Vue 會收集當(dāng)前組件到根組件上所有的異常處理函數(shù), 并從子組件開始, 層層觸發(fā), 直至執(zhí)行完成全局異常處理; 如果用戶不想層層上報(bào), 可以通過配置某個(gè)組件上的 errorCaptured 返回布爾類型的值 false 即可。下面是從組建中截取的一段代碼,用以演示如何停止錯(cuò)誤繼續(xù)上報(bào)上層組件:

export default {
  data() {
    return {
      // ... 屬性列表
    }
  }
  errorCaptured(cur, err, vm, info) {
    console.log(cur, err, vm, info)
    return false // 返回布爾類型的值 `false` 即可終止異常繼續(xù)上報(bào), 并且不再觸發(fā)全局的異常處理函數(shù)
  },
}

Vue 的全局 api 中有個(gè) Vue.config 在這里可以配置 Vue 的行為特性, 可以通過 Vue.config.errorHandler 配置異常處理函數(shù), 也可以在調(diào)用 new Vue() 時(shí)通過 errorCaptured 傳遞, 還可以通過 Vue.mixin 將錯(cuò)誤處理混入到當(dāng)前組件。執(zhí)行時(shí)先執(zhí)行 vm.$options.errorCaptured 上的異常處理函數(shù), 然后根據(jù) errorCaptured 的返回值是否與布爾值 false嚴(yán)格相等來決定是否執(zhí)行 Vue.config.errorHandler 異常處理函數(shù), 實(shí)際運(yùn)用中這兩個(gè)配置其中一個(gè)即可。 我們可以根據(jù)異常類型,確定是否將信息展示給用戶、是否將異常提交給服務(wù)器等操作。下面是一個(gè)簡單的示例:

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')

調(diào)用聲明周期的鉤子,是通過 callHook(vm, 'beforeCreate') 進(jìn)行調(diào)用的, 而 callHook 最終都調(diào)用了 invokeWithErrorHandling 這個(gè)函數(shù), 以 callHook(vm, 'beforeCreate') 為例, 在遍歷執(zhí)行 beforeCreate 中的任務(wù)隊(duì)列時(shí), 每個(gè)任務(wù)函數(shù)都會被傳遞到 invokeWithErrorHandling 這個(gè)函數(shù)中。

export function invokeWithErrorHandling (
  handler: Function, // 生命周期中的任務(wù)函數(shù)
  context: any, // 任務(wù)函數(shù) `handlers[i]` 執(zhí)行時(shí)的上下文
  args: null | any[], // 任務(wù)函數(shù) `handlers[i]`執(zhí)行時(shí)的參數(shù), 以數(shù)組的形式傳入, 因?yàn)樽罱K通過 apply 調(diào)用
  vm: any, // 當(dāng)前組件的實(shí)例對象
  info: string // 拋給用戶的異常信息的描述文本
) {
  // 生命周期處理
}

invokeWithErrorHandling(handlers[i], vm, null, vm, info) 這個(gè)調(diào)用為例,第一個(gè)參數(shù) handlers[i] 即任務(wù)函數(shù); 第二個(gè)參數(shù) vm 表示任務(wù)函數(shù) handlers[i] 執(zhí)行時(shí)的上下文, 也就是函數(shù)執(zhí)行時(shí) this 指向的對象,對于生命周期函數(shù)而言, this 全都指向當(dāng)前組件; 第三個(gè)參數(shù) null 表示任務(wù)函數(shù) handlers[i] 執(zhí)行時(shí),沒有參數(shù); 第四個(gè)參數(shù) vm 表示當(dāng)前組件的實(shí)例; 第五個(gè)參數(shù)表示異常發(fā)生時(shí)拋出給用戶的異常信息。

invokeWithErrorHandling 的核心處理是 res = args ? handler.apply(context, args) : handler.call(context) ,若調(diào)用成功, 則直接返回當(dāng)前任務(wù)函數(shù)的返回值 res ; 否則調(diào)用 handleError(e, vm, info) 函數(shù)處理異常。

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

然后,從當(dāng)前組件開始,逐級查找父組件,直至查找到根組件, 對于所有被查到的上層組件, 都會讀取其 $options.errorCaptured 中配置的異常處理函數(shù)。
處理過程為 :

  • hooks[i].call(cur, err, vm, info) ,
  • 如果在這一步又發(fā)生了異常則調(diào)用通過 Vue.config 配置的 errorHandler 函數(shù);
    • 如果調(diào)用成功并且返回 false 則異常處理終止, 不再調(diào)用全局的異常處理函數(shù) globalHandleError;
    • 如果調(diào)用成功, 且返回值不與 false 嚴(yán)格相等(源碼中通過 === 判斷的), 則繼續(xù)調(diào)用全局的異常處理函數(shù) globalHandleError
    • 如果調(diào)用 globalHandleError 時(shí)發(fā)生異常, 則通過默認(rèn)的處理函數(shù) logError 進(jìn)行處理, 通過 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 支持的可配置選項(xiàng):

// 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)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容