Vue多个组件到底new Vue了几次?

问题来源:时常在思考,vue每个组件实例大体一致,但是组件都是导出的一个组件构造对象,并没有看到组件new Vue成一个实例的一个操作,在Vue中每个组件都是一个vue实例,那么多个vue组件是不是会new Vue多次呢?伴随这个问题我百度了很多,有的说一个vue文件/组件就会new一次,有多少个就new多少次,还有的说vue作为个单页面应用,只new一次...。啊这我到底该听哪个?随着这个答案不确定,还是自己去解答吧!

1、我们可以看到唯一的一次new Vue是在main.js中,在new Vue以后调用的是this._init方法。


//vue初始化
Vue.prototype._init = function (options?: Object) {
  //vue实例: 在new Vue函数后的this._init,所以this就是Vue实例
  const vm: Component = this
  // a uid
  //每个vue实例都有一个uid,并且是依次递增的,类似于key避免重复
  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._isVue = true
  // 处理组件配置项
  if (options && options._isComponent) {
    /**
     * 每个子组件初始化走这里,只做一些性能优化
     * 将组件配置项对象的一些深层次属性放到 vue.$options 选项中,以提高代码的执行效率
     */
    initInternalComponent(vm, options)
  } else {
    /**
     * 初始化根组件走这里,合并 vue 的全局配置到根组件的局部配置,比如Vue.component 注册的全局组件会合并到 根实例的 components 选项
     * 至于每个子组件的选项合并则发生在两个地方:
     *  1、Vue.component 方法注册的全局组件在注册时做了选择合并
     *  2、{ components: { xx } } 方式注册的局部组件在执行编译器生产的 render 函数进行了选项合并,包括根组件中的 components 配置
     * @type {Object}
     */
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor), //vm的构造器:Vue函数对象,也就是全局配置对象
      options || {}, //vm上的属性
      vm
    )
  }
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  // expose real self
  vm._self = vm
  // 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
  // 也可以理解为找出组件实例的生命周期所在位置
  initLifecycle(vm)
  /**
   * 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
   * 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
   */
  initEvents(vm)
  // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
  initRender(vm)
  // 调用 beforeCreate 钩子函数
  callHook(vm, 'beforeCreate')
  // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
  initInjections(vm) // resolve injections before data/props
  // 数据响应式的重点,处理 props、methods、data、computed、watch
  initState(vm)
  // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
  initProvide(vm) // resolve provide after data/props
  // 调用 created 钩子函数
  callHook(vm, '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属性,则自动调用$mount方法,也就是有了el属性就不需要手动调用$mount,反之没有el则必须手动调用$mount
  if (vm.$options.el) {
    //调用$mount方法,进入挂载阶段
    vm.$mount(vm.$options.el)
  }
}

组件的属性合并、数据响应式等都是在这个函数里面完成的,那么组件每次导出的都是export default { ...属性 },那组件是在哪初始化的?

2、为了理解这个问题我试着去找在哪一步有处理子组件的。当分析组件嵌套的生命周期时,我们可以看到子组件开始创建是在父组件beforeMount之后

image

3、那么推断与render函数有关,于是找到_init函数内的initRender(vm)函数,我们可以看到函数内部得到了$createElement,即h渲染函数。在父组件开始渲染的时候会触发这个$createElement,也就是执行createElement

export function initRender (vm: Component) {
  //省略
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  //省略
}

4、createElement函数返回一个_createElement,这里就直接贴_createElement的代码了。


export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  //...省略
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      //...省略
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 仔细看这一步,当判断当前实例有components时,就执行createComponent
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      //...省略
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  //...省略
}

5、接着往下走,createComponent做了什么操作,主要是以下代码,先拿到baseCtor也就是Vue这个构造函数,然后判断这个子组件export的是不是一个对象,是的话执行Vue.extend把这个子组件的构造对象传入进去

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

6、那么Vue身上的extend是从哪里来的,又做了什么。看以下代码可以看到extedn方法导出了一个Sub构造函数,并且继承自Vue构造函数的,然后最后执行baseCtor.extend等于调用了Sub方法也就是this._init做子组件的初始化创建。

Vue.extend = function (extendOptions: Object): Function {
  //...省略
  // 定义 Sub 构造函数,和 Vue 构造函数一样
  const Sub = function VueComponent (options) {
    // 初始化
    this._init(options)
  }
  //...省略
  return Sub
}

总结:Vue作为个单页面实际只new了一次,后续组件都是通过this._init创建的实例。所以关于父子组件的生命周期也大体明白,这也是为什么父组件的beforeMount后,就到了子组件的创建环节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容