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,
        context: ?Component
      ): { [key: string]: Array }  
  */
  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;
  keyCodes: { [key: string]: number | Array };

  // 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;
};

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)

你可能感兴趣的:(vue 源码详解(三): 渲染初始化 initRender 、生命周期的调用 callHook 、异常处理机制)