Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue2.x源码解析系列二: Vue组件初始化过程概要 #23

Open
lihongxun945 opened this issue Jul 26, 2018 · 2 comments
Open

vue2.x源码解析系列二: Vue组件初始化过程概要 #23

lihongxun945 opened this issue Jul 26, 2018 · 2 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Jul 26, 2018

这里分析的是当前(2018/07/25)最新版 V2.5.16 的源码,如果你想一遍看一遍参阅源码,请务必记得切换到此版本,不然可能存在微小的差异。


大家都知道,我们的应用是一个由Vue组件构成的一棵树,其中每一个节点都是一个 Vue 组件。我们的每一个Vue组件是如何被创建出来的,创建的过程经历了哪些步骤呢?把这些都搞清楚,那么我们对Vue的整个原理将会有很深入的理解。

从入口函数开始,有比较复杂的引用关系,为了方便大家理解,我画了一张图可以直观地看出他们之间的关系:

modules

创建Vue实例的两步

我们创建一个Vue实例,只需要两行代码:

import Vue from ‘vue'
new Vue(options)

而这两步分别经历了一个比较复杂的构建过程:

  1. 创建类:创建一个 Vue 构造函数,以及他的一系列原型方法和类方法
  2. 创建实例:创建一个 Vue 实例,初始化他的数据,事件,模板等
    下面我们分别解析这两个阶段,其中每个阶段 又分为好多个 步骤

第一阶段:创建Vue类

第一阶段是要创建一个Vue类,因为我们这里用的是原型而不是ES6中的class声明,所以拆成了三步来实现:

  1. 创建一个构造函数 Vue
  2. Vue.prototype 上创建一系列实例属性方法,比如 this.$data
  3. Vue 上创建一些全局方法,比如 Vue.use 可以注册插件

我们导入 Vue 构造函数 import Vue from ‘vue’ 的时候(new Vue(options) 之前),会生成一个Vue的构造函数,这个构造函数本身很简单,但是他上面会添加一系列的实例方法和一些全局方法,让我们跟着代码来依次看看如何一步步构造一个 Vue 类的,我们要明白每一步大致是做什么的,但是这里先不深究,因为我们会在接下来几章具体讲解每一步都做了什么,这里我们先有一个大致的概念即可。

我们看代码先从入口开始,这是我们在浏览器环境最常用的一个入口,也就是我们 import Vue 的时候直接导入的,它很简单,直接返回了 从 platforms/web/runtime/index/js 中得到的 Vue 构造函数,具体代码如下:

platforms/web/entry-runtime.js

import Vue from './runtime/index'
export default Vue

可以看到,这里不是 Vue 构造函数的定义地方,而是返回了从下面一步得到的Vue构造函数,但是做了一些平台相关的操作,比如内置 directives 注册等。这里就会有人问了,为什么不直接定义一个构造函数,而是这样不停的传递呢?因为 vue 有不同的运行环境,而每一个环境又有带不带 compiler 等不同版本,所以环境的不同以及版本的不同都会导致 Vue 类会有一些差异,那么这里会通过不同的步骤来处理这些差异,而所有的环境版本都要用到的核心代码是相同的,因此这些相同的代码就统一到 core/中了。

完整代码和我加的注释如下:
platforms/web/runtime/index.js

import Vue from 'core/index'
import config from 'core/config'
// 省略

import platformDirectives from './directives/index'
import platformComponents from './components/index'

//这里都是web平台相关的一些配置
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
// 省略

// 注册指令和组件,这里的 directives 和 components 也是web平台上的,是内置的指令和组件,其实很少
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives) // 内置的directives只有两个,`v-show` 和 `v-model`
extend(Vue.options.components, platformComponents) // 内置的组件也很少,只有`keepAlive`, `transition`和 `transitionGroup`

// 如果不是浏览器,就不进行 `patch` 操作了
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 如果有 `el` 且在浏览器中,则进行 `mount` 操作
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// 省略devtool相关代码

export default Vue

上面的代码终于把平台和配置相关的逻辑都处理完了,我们可以进入到了 core 目录,这里是Vue组件的核心代码,我们首先进入 core/index文件,发现 Vue 构造函数也不是在这里定义的。不过这里有一点值得注意的就是,这里调用了一个 initGlobalAPI 函数,这个函数是添加一些全局属性方法到 Vue 上,也就是类方法,而不是实例方法。具体他是做什么的我们后面再讲

core/index.js

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

initGlobalAPI(Vue) // 这个函数添加了一些类方法属性

// 省略一些ssr相关的内容
// 省略

Vue.version = '__VERSION__'

export default Vue

core/instance/index.js 这里才是真正的创建了 Vue 构造函数的地方,虽然代码也很简单,就是创建了一个构造函数,然后通过mixin把一堆实例方法添加上去。

core/instance/index.js 完整代码如下:

//  省略import语句
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

下面我们分成两段来讲解这些代码分别干了什么。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 构造函数有用的只有这一行代码,是不是很简单,至于这一行代码具体做了什么,在第二阶段我们详细讲解。
}

这里才是真正的Vue构造函数,注意其实很简单,忽略在开发模式下的警告外,只执行了一行代码 this._init(options)。可想而知,Vue初始化必定有很多工作要做,比如数据的响应化、事件的绑定等,在第二阶段我们会详细讲解这个函数到底做了什么。这里我们暂且跳过它。

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

上面这五个函数其实都是在Vue.prototype上添加了一些属性方法,让我们先找一个看看具体的代码,比如initMixin 就是添加 _init 函数,没错正是我们构造函数中调用的那个 this._init(options) 哦,它里面主要是调用其他的几个初始化方法,因为比较简单,我们直接看代码:

core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  // 就是这里,添加了一个方法
  Vue.prototype._init = function (options?: Object) {
    // 省略,这部分我们会在第二阶段讲解
  }
}

另外的几个同样都是在 Vue.prototype 上添加了一些方法,这里暂时先不一个个贴代码,总结一下如下:

  1. core/instance/state.js,主要是添加了 $data,$props,$watch,$set,$delete 几个属性和方法
  2. core/instance/events.js,主要是添加了 $on,$off,$once,$emit 三个方法
  3. core/instance/lifecycle.js,主要添加了 _update, $forceUpdate, $destroy 三个方法
  4. core/instance/renderMixin.js,主要添加了 $nextTick_render 两个方法以及一大堆renderHelpers

还记得我们跳过的在core/index.js中 添加 globalAPI的代码吗,前面的代码都是在 Vue.prototype 上添加实例属性,让我们回到 core/index 文件,这一步需要在 Vue 上添加一些全局属性方法。前面讲到过,是通过 initGlobalAPI 来添加的,那么我们直接看看这个函数的样子:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  // 省略

  // 这里添加了一个`Vue.config` 对象,至于在哪里会用到,后面会讲
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  
  //一般我们用实例方法而不是这三个类方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
  
  // 注意这里,循环出来的结果其实是三个 `components`,`directives`, `filters`,这里先创建了空对象作为容器,后面如果有对应的插件就会放进来。
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  // 内置组件只有一个,就是 `keepAlive`
  extend(Vue.options.components, builtInComponents)

  initUse(Vue) // 添加了 Vue.use 方法,可以注册插件
  initMixin(Vue) //添加了Vue.mixin 方法
  initExtend(Vue) // 添加了 Vue.extend 方法

  // 这一步是注册了 `Vue.component` ,`Vue.directive` 和 `Vue.filter` 三个方法,上面不是有 `Vue.options.components` 等空对象吗,这三个方法的作用就是把注册的组件放入对应的容器中。
  initAssetRegisters(Vue)
}

至此,我们就构建出了一个 Vue 类,这个类上的方法都已经添加完毕。这里再次强调一遍,这个阶段只是添加方法而不是执行他们,具体执行他们是要到第二阶段的。总结一下,我们创建的Vue类都包含了哪些内容:

//构造函数
function Vue () {
  this._init()
}

//全局config对象,我们几乎不会用到
Vue.config = {
  keyCodes,
  _lifecycleHooks: ['beforeCreate', 'created', ...]
}

// 默认的options配置,我们每个组件都会继承这个配置。
Vue.options = {
  beforeCreate, // 比如 vue-router 就会注册这个回调,因此会每一个组件继承
  components, // 前面提到了,默认组件有三个 `KeepAlive`,`transition`, `transitionGroup`,这里注册的组件就是全局组件,因为任何一个组件中不用声明就能用了。所以全局组件的原理就是这么简单
  directives, // 默认只有 `v-show` 和 `v-model`
  filters // 不推荐使用了
}

//一些全局方法
Vue.use // 注册插件
Vue.component // 注册组件
Vue.directive // 注册指令
Vue.nextTick //下一个tick执行函数
Vue.set/delete // 数据的修改操作
Vue.mixin // 混入mixin用的

//Vue.prototype 上有几种不同作用的方法

//由initMixin 添加的 `_init` 方法,是Vue实例初始化的入口方法,会调用其他的功能初始话函数
Vue.prototype._init

// 由 initState 添加的三个用来进行数据操作的方法
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$watch

// 由initEvents添加的事件方法
Vue.prototype.$on
Vue.prototype.$off
Vue.prototype.$one
Vue.prototype.$emit

// 由 lifecycle添加的生命周期相关的方法
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy

//在 platform 中添加的生命周期方法
Vue.prototype.$mount

// 由renderMixin添加的`$nextTick` 和 `_render` 以及一堆renderHelper
Vue.prototype.$nextTick
Vue.prototype._render
Vue.prototype._b
Vue.prototype._e
//...

上述就是我们的 Vue 类的全部了,有一些特别细小的点暂时没有列出来,如果你在后面看代码的时候,发现有哪个函数不知道在哪定义的,可以参考这里。那么让我们进入第二个阶段:创建实例阶段

第二阶段:创建 Vue 实例

我们通过 new Vue(options) 来创建一个实例,实例的创建,肯定是从构造函数开始的,然后会进行一系列的初始化操作,我们依次看一下创建过程都进行了什么初始化操作:

core/instance/index.js, 构造函数本身只进行了一个操作,就是调用 this._init(options) 进行初始化,这个在前面也提到过,这里就不贴代码了。

core/instance/init.js 中会进行真正的初始化操作,让我们详细看一下这个函数具体都做了些什么。
先看看它的完整代码:

 Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  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
  // merge options
  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 {
    vm.$options = mergeOptions(
      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)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  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)
  }

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

我们来一段一段看看上面的代码分别作了什么。

    const vm: Component = this // vm 就是this的一个别名而已
    // a uid
    vm._uid = uid++ // 唯一自增ID

    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)
    }

这段代码首先生成了一个全局唯一的id。然后如果是非生产环境并且开启了 performance,那么会调用 mark 进行performance标记,这段代码就是开发模式下收集性能数据的,因为和Vue本身的运行原理无关,我们先跳过。

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 
    // TODO
    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 {
      // mergeOptions 本身比较简单,就是做了一个合并操作
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

上面这段代码,暂时先不用管_isComponent,暂时只需要知道我们自己开发的时候使用的组件,都不是 _isComponent,所以我们会进入到 else语句中。这里主要是进行了 options的合并,最终生成了一个 $options 属性。下一章我们会详细讲解 options 合并的时候都做了什么,这里我们只需要暂时知道,他是把构造函数上的options和我们创建组件时传入的配置 options 进行了一个合并就可以了。正是由于合并了这个全局的 options 所以我们在可以直接在组件中使用全局的 directives

  /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

这段代码可能看起来比较奇怪,这个 renderProxy 是干嘛的呢,其实就是定义了在 render 函数渲染模板的时候,访问属性的时候的一个代理,可以看到生产环境下就是自己。
开发环境下作了一个什么操作呢?暂时不用关心,反正知道渲染模板的时候上下文就是 vm 也就是 this 就行了。如果有兴趣可以看看非生产环境,作了一些友好的报错提醒等。
这里只需要记住,在生产环境下,模板渲染的上下文就是 vm就行了。

  // expose real self
    vm._self = vm

    initLifecycle(vm) // 做了一些生命周期的初始化工作,初始化了很多变量,最主要是设置了父子组件的引用关系,也就是设置了 `$parent` 和 `$children`的值
    initEvents(vm) // 注册事件,注意这里注册的不是自己的,而是父组件的。因为很明显父组件的监听器才会注册到孩子身上。
    initRender(vm) // 做一些 render 的准备工作,比如处理父子继承关系等,并没有真的开始 render
    callHook(vm, 'beforeCreate') // 准备工作完成,接下来进入 `create` 阶段
    initInjections(vm) // resolve injections before data/props
    initState(vm) // `data`, `props`, `computed` 等都是在这里初始化的,常见的面试考点比如`Vue是如何实现数据响应化的` 答案就在这个函数中寻找
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created') // 至此 `create` 阶段完成

这一段代码承担了组件初始化的大部分工作。我直接把每一步的作用写在注释里面了。 把这几个函数都弄懂,那么我们也就差不多弄懂了Vue的整个工作原理,而我们接下来的几篇文章,其实都是从这几个函数中的某一个开始的。

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

开始mount,注意这里如果是我们的options中指定了 el 才会在这里进行 $mount,而一般情况下,我们是不设置 el 而是通过直接调用 $mount("#app") 来触发的。比如一般我们都是这样的:

new Vue({
  router,
  store,
  i18n,
  render: h => h(App)
}).$mount('#app')

以上就是Vue实例的初始化过程。因为在 create 阶段和 $mount 阶段都很复杂,所以后面会分几个章节来分别详细讲解。下一篇,让我们从最神秘的数据响应化说起。

下一篇:Vue2.x源码解析系列三:Options配置的处理

@eltonchan
Copy link

👍

@afan1123
Copy link

afan1123 commented Dec 3, 2021

666

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants