Vue2 初始化实例过程源码解析(一)

246 阅读2分钟
本文,我将会讲述从打包入口开始,了解整个项目的初始化实例流程。

第一步,当然是先把vue项目打包到本地啦。需要注意的是,vue项目使用rollup打包,可以提前安装

https://github.com/vuejs/vue // 项目地址
npm i rollup -g // 安装rollup

为方便调试,我们在指令中开启sourcemap,然后执行命令npm run dev即可。

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"

完成了前置准备工作之后,我们进入到项目中来看。 首先看到上述指令中的-c scripts/config.jsTARGET:web-full-dev,分别代表的是配置文件以及输入文件配置项,也就是以下信息,此时,我们可以找到相应的入口文件及各项配置。

// Runtime+compiler development build (Browser)
'web-full-dev': {
    // resolve函数负责解析别名,具体使用不再赘述
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
},

让我们进入到此文件中,可以看到它只在当前做了一件事情--扩展$mount函数,实现render方法

// web/entry-runtime-with-compiler.js
// Vue实例创建过程需要继续向上寻找
import Vue from './runtime/index'
// 扩展mount函数,生成render并挂载到options
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // query以获取当前元素节点
  el = el && query(el)
  const options = this.$options
  // resolve template/el and convert to render function
  // 获取当前组件render函数并挂载到options上
  if (!options.render) {
    let template = options.template
    // 如果已有template
    if (template) {
      if (typeof template === 'string') {
        // 如果是一个id
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        //如果template是一个元素
        template = template.innerHTML
      } else {
        // 报错信息
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果template不存在,元素存在
      template = getOuterHTML(el)
    }
    // 此时已经获取到template
    if (template) {
      // template => render,这是vue的编译环节,主要功能为将template编译成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 挂载到options下以便调用
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 扩展后,我们仍需要实现原函数
  return mount.call(this, el, hydrating)
}

根据导入信息继续往上到./runtime/index,该文件依然没有完成Vue的构建,它完成了以下两件事:1. patch方法的安装 2. $mount方法的实现。

./runtime/index
import Vue from 'core/index'
import { mountComponent } from 'core/instance/lifecycle'
import { patch } from './patch'

// 安装patch方法,此为树的构建方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// $mount方法的实现
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // mount方法实质上就是mountComponent,此为挂载环节,这是Vue实例化的最后一步
  return mountComponent(this, el, hydrating)
}

再往上,我们到了core/index。core包是Vue的核心包。

// ./core/index
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'

// 完成了全局API的挂载,包括use/component/directive/filter/set/delete/nextTick等
initGlobalAPI(Vue)

全局化不是我们的重点,我们接着往上。

// ./instance/index
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue) // 实现Vue._init
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

终于,我们找到了Vue实例构建的地址,可是我们看到,它只调用了一个_init方法,找不到任何引用的痕迹。别急,初始化方法就藏在下面的几个混入方法中。让我们进入的initMixin函数中一探究竟。

./init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++

    vm._isVue = true
    // 合并选项。在这里我们会把所有之前挂载到options上的内容合并到vm.$options上
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
    vm._self = vm
    // 创建生命周期相关内容,和生命周期钩子不同,这里代表的是组件自身的生命。包括$parent,$root,$children,$refs以及一些相关参数,其中还有关于抽象组件(keep-alive)的父级绑定哦!
    initLifecycle(vm)
    // 创建组件相关的事件选项,获取到父组件的listeners
    initEvents(vm)
    // 进行了插槽以及$createElement函数(h函数)的挂载,还完成了$attrs,$listeners的响应式处理
    initRender(vm)
    // 调用钩子函数
    callHook(vm, 'beforeCreate')
    // 初始化inject
    initInjections(vm) // resolve injections before data/props
    // 在这里对整个Vue项目的数据进行了初始化,关键点在于响应式的处理
    initState(vm)
    // 初始化provide
    initProvide(vm) // resolve provide after data/props
    // 调用钩子函数
    callHook(vm, 'created')
    
    // 如果选项中已有el元素,将直接进行挂载,无需外部调用。
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

我们从打包入口进入,一步步往上查找,在过程中,得知了render函数的实现、patch方法的安装、$mount方法的实现(以上方法在编译,更新阶段也起到了重要作用),最后我们查找到了实现Vue初始化的方法,以及了解了该方法中实现的大致内容与方法。