初识创建Vue实例流程

73 阅读2分钟

这篇文章,是用来记录阅读源码过程中,对创建Vue应用流程的了解,从初始化,解析,挂载做一个整体上的了解。只有了解整体的流程,每个步骤做了什么事情有一定的了解后,源码阅读才会更轻松。

你将了解

  1. 创建Vue应用大致流程
  2. 异步方法为什么要在created及之后调用
  3. 为什么在data中可以使用inject透传的值

_init初始化

其实在认识Vue构造函数时,已经知道,创建Vue实例vm,只执行了_init方法一行代码。这个方法是被initMixin函数添加到Vue.prototype上的。initMixin定义在src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    // 将本身代理到_renderProxy上
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
      // 初始化生命周期
    initLifecycle(vm)
      // 初始化事件中心
    initEvents(vm)
      // 初始话render函数
    initRender(vm)
    callHook(vm, 'beforeCreate')
      // 初始化inject
    initInjections(vm) // resolve injections before data/props
    // 初始化状态,props,methods,data...
    initState(vm)
      // 初始化provide
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
      // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

_init方法还是很清晰的,大致流程如下:

  1. 合并配置
  2. 开发环境,将_renderProxy代理到vm上,就是让其拥有提示的能力。
  3. 初始化生命周期,定义一堆属性,记录实例vm的状态(如,父子关系,生命周期的状态)。
  4. 初始化事件中心(自定义事件)
  5. 执行beforeCreate生命周期钩子函数
  6. 初始化render函数,添加创建vnode的方法_c$createElement到实例vm
  7. 初始化inject(在data,props之前,方便在data和props使用)
  8. 初始化状态(data,props,methods...)
  9. 初始化provide
  10. 执行created生命周期钩子函数
  11. 挂载

小结

  1. 为什么beforeCreate不能操作数据?这里就能找到原因。调用beforeCreate钩子函数时datamethods还没有准备好。
  2. 为什么dataprops可以使用inject透传过来的值?原因就是injectdataprops先初始化。

同时需要注意,options到底是什么?编写*.vue文件时,都会返回一个对象,在vue-loader编译之后转化为这里的options,如下

<template>
</template>
<script>
export default {
    name: 'xxx',
    data(){
        return {}
    }
}
</script>

挂载

最后如果options配置了el属性,那么就会调用$mounted把实例挂载到el节点上。在上一篇中,知道$mounted定义在src/platforms/web/runtime/index.js并在web/entry-runtime-with-compiler.js进行重写,使其拥有编译的能力。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    // 组件
    if (template) {
        // 指定的是模板的id
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

web/entry-runtime-with-compiler.js中重写$mounted,其核心就是找到组件的模板,使用compileToFunctions对其template进行编译最后生成render函数,最后调用原来的mount也就是原来的$mount方法方法挂载。定义在src/platforms/web/runtime/index.js,实际上就是调用mountComponent,这个方法定义在src/core/instance/lifecycle.js中。

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  // 创建渲染函数
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent的核心就是创建vm的渲染函数。每次渲染都会调用_render方法生成对应的vnode,再调用_update方法生成真正的节点,并添加在页面中。