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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容