Vue源码阅读(5):new Vue 底层发生了什么

 我的开源库:

在讲正文之前,最好先了解一下 new 操作符的运行机制

在 Vue源码阅读之路(4) 中,我们知道,Vue 函数定义在 src/core/instance/index.js 文件中,先看一下 Vue 函数的定义。

1,src/core/instance/index.js

function Vue (options) {
  // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话,
  // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  this._init(options)
}

// 下面函数的作用是:往 Vue 的原型上写入原型函数,这些函数是给 Vue 的实例使用的
// 这些函数分为两类:一类是 Vue 内部使用的,特征是函数名以 '_' 开头;
//                 还有一类是给用户使用的,特征是函数名以 '$' 开头,这些函数可以在 Vue 的官方文档中看到;
// 写入 vm._init
initMixin(Vue)
// 写入 vm.$set、vm.$delete、vm.$watch
stateMixin(Vue)
// 写入 vm.$on、vm.$once、vm.$off、vm.$emit
eventsMixin(Vue)
// 写入 vm._update、vm.$forceUpdate、vm.$destroy
lifecycleMixin(Vue)
// 写入 vm.$nextTick、vm._render
renderMixin(Vue)

export default Vue

首先判断用户有没有正确的使用 Vue 函数,正确的使用方式是使用 new 关键词进行调用。如果没有使用 new 调用并且是非生产环境的话,则打印出相应的警告。

然后调用 Vue 函数实例的 _init 方法,该方法定义在 Vue.prototype 中。我们可以看到 Vue 函数定义的下面,以 Vue 为参数执行了一系列的函数,这些函数的作用是向 Vue.prototype 写入原型方法,这些方法是给 Vue 函数的实例使用的。上面说的 _init 方法就被定义在 initMixin() 中。

2,src/core/instance/init.js ==> initMixin

export function initMixin (Vue: Class<Component>) {
  // _init 方法会在 new Vue() 的时候调用,看下面的代码:
  // function Vue (options) {
  //   this._init(options)
  // }
  Vue.prototype._init = function (options?: Object) {
    // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
    const vm: Component = this
    // 赋值唯一的 id
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    // 一个标记,用于防止 vm 变成响应式的数据
    vm._isVue = true
    // 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
      // 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
      // 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
      // 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
      vm.$options = mergeOptions(
        // resolveConstructorOptions 函数的返回值是 Vue 的 options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化与生命周期有关的内容
    initLifecycle(vm)
    // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法。
    initEvents(vm)
    // 初始化与渲染有关的内容
    initRender(vm)
    // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
    // 执行 beforeCreate 生命周期函数
    callHook(vm, 'beforeCreate')
    // 解析初始化当前组件的 inject
    initInjections(vm) // resolve injections before data/props
    // 初始化 state,包括 props、methods、data、computed、watch
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // 在 created 回调函数中,可以访问到实例中的数据
    // 执行 created 回调函数
    callHook(vm, 'created')
    // beforeCreate 和 created 生命周期的区别是:能否访问到实例中的变量

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

主要做了以下几件事:

  • 合并配置,合并当前组件的 option 和保存在 Vue 中的全局 option。
  • 通过调用 initLifecycle、initEvents、initRender、initInjections、initState 和 initProvide 进行生命周期的初始化、事件的初始化、渲染的初始化、inject 的初始化、状态的初始化和 provide 的初始化。这些在 vm 中初始化的内容会在后续的操作中使用到。
  • 在适当的时机,通过调用 callHook 执行生命周期函数。 
  • 检查是否配置了 el 属性,如果配置了的话,就进行挂载渲染真实 DOM 的操作。

2-1,callHook 的实现

// 执行 Vue 实例(vm)中的生命周期函数
// 内容也很简单,就是从 vm.$options 中取出指定生命周期的回调函数数组
// 然后遍历执行数组中的函数
export function callHook (vm: Component, hook: string) {
  // 取出回调函数数组
  const handlers = vm.$options[hook]
  if (handlers) {
    // 遍历执行每一个函数
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
}

生命周期函数都保存在 $options 中,存储形式如下所示:

vm.$options = {
  // 数组中保存生命周期函数  
  created: [],
  mounted: [],
  ......
}

callHook 首先从 vm.$options 中取出对应生命周期的函数数组,然后遍历数组,执行各个生命周期函数。

也许你会有疑问?组件的某一个特定的生命周期函数不是只有一个吗?为什么还用数组进行存储?这是因为我们可以借助 Vue.mixin() 或者 mixins 配置选项混入其他的生命周期函数,所以组件的某一个生命周期的函数有可能有多个,所以需要用数组存储生命周期函数,然后遍历数组执行函数。

2-2,initState 的实现

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化计算属性
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化监听属性
  // nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
  if (opts.watch && opts.watch !== nativeWatch) {
    // 进行侦听属性的初始化过程
    initWatch(vm, opts.watch)
  }
}

从上往下依次初始化 props、methods、data、computed、watch。至于初始化的具体细节,会在后续具体讲到这个特性的时候,再具体分析。

3,总结

这篇博客主要总结分析 new Vue 的大致流程,其中具体的操作暂不做分析。在后续的博客中,如果讲到了某个特性,再进行细致的解析。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值